import { z } from 'zod' export const powerStatusSchema = z.union([z.literal(0), z.literal(1), z.literal(2)]) export type PowerStatus = z.infer export const deviceStatusSchema = z.union([z.literal('健康'), z.literal('关注'), z.literal('预警')]) export type DeviceStatus = z.infer export const batteryInfoSchema = z.object({ id: z.number().int(), userId: z.number().int(), mac: z.string(), devModel: z.string(), devName: z.string(), isLowPower: z.boolean(), powerStatus: powerStatusSchema, power: z.number().int().min(0).max(100), createTime: z.string(), remark: z.string().nullable(), }) export type BatteryInfo = z.infer export const fleetUnitSchema = z.object({ id: z.string(), batch: z.string(), firmware: z.string(), cycles: z.number().int(), soh: z.number(), soh30d: z.number(), soh60d: z.number(), soh90d: z.number(), temperature: z.number(), riskScore: z.number().int(), chargeEfficiency: z.number(), status: deviceStatusSchema, riskFactors: z.array(z.string()), }) export type FleetUnit = z.infer export const sohPointSchema = z.object({ month: z.string(), value: z.number() }) export const sohResponseSchema = z.object({ history: z.array(sohPointSchema), forecast: z.array(sohPointSchema), }) export const eventItemSchema = z.object({ time: z.string(), title: z.string(), detail: z.string(), severity: z.union([z.literal('高'), z.literal('中'), z.literal('低')]), }) export const strategyItemSchema = z.object({ name: z.string(), impact: z.string(), scope: z.string(), eta: z.string(), }) export const summaryResponseSchema = z.object({ totalDevices: z.number().int(), avgSoh: z.number(), avgSoh30d: z.number(), avgSoh90d: z.number(), warningCount: z.number().int(), watchCount: z.number().int(), healthyCount: z.number().int(), batchPerformance: z.array(z.object({ batch: z.string(), avgSoh: z.number() })), riskFactorCounts: z.array(z.object({ factor: z.string(), count: z.number().int() })), firmwareHealth: z.array(z.object({ firmware: z.string(), avgSoh: z.number(), count: z.number().int() })), updatedAt: z.string(), executiveSummary: z.string(), }) export const dashboardSnapshotSchema = z.object({ devices: z.array(fleetUnitSchema), soh: sohResponseSchema, events: z.array(eventItemSchema), strategies: z.array(strategyItemSchema), summary: summaryResponseSchema, }) export type DashboardSnapshot = z.infer export const batteriesResponseSchema = z.object({ updatedAt: z.string(), total: z.number().int(), lowPower: z.number().int(), charging: z.number().int(), items: z.array(batteryInfoSchema), }) export type BatteriesResponse = z.infer const round1 = (value: number) => Math.round(value * 10) / 10 const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)) const pad2 = (value: number) => value.toString().padStart(2, '0') const addMonths = (date: Date, months: number) => new Date(date.getFullYear(), date.getMonth() + months, date.getDate(), date.getHours(), 0, 0, 0) const addHours = (date: Date, hours: number) => new Date(date.getTime() + hours * 60 * 60 * 1000) const formatMonthLabel = (date: Date) => `${date.getFullYear().toString().slice(-2)}.${pad2(date.getMonth() + 1)}` const formatDateTime = (date: Date) => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}` const formatEventTime = (date: Date) => `${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}` export function getDeviceStatus(soh: number): DeviceStatus { if (soh <= 85) return '预警' if (soh <= 90) return '关注' return '健康' } export function normalizePowerStatus(value: number): PowerStatus { if (value === 1 || value === 2) return value return 0 } export function normalizeLowPower(value: string | boolean): boolean { if (typeof value === 'boolean') return value return value.toLowerCase() === 'true' } export type BatteryInfoSourceRow = { id: number userId: number mac: string devModel: string devName: string isLowPower: string | boolean powerStatus: number power: number createTime: Date | string remark: string | null } export function toBatteryInfo(row: BatteryInfoSourceRow): BatteryInfo { return { id: row.id, userId: row.userId, mac: row.mac, devModel: row.devModel, devName: row.devName, isLowPower: normalizeLowPower(row.isLowPower), powerStatus: normalizePowerStatus(row.powerStatus), power: row.power, createTime: row.createTime instanceof Date ? row.createTime.toISOString() : String(row.createTime), remark: row.remark, } } export function createBatteriesResponse(items: BatteryInfo[], now = new Date()): BatteriesResponse { return { updatedAt: now.toISOString(), total: items.length, lowPower: items.filter((item) => item.isLowPower).length, charging: items.filter((item) => item.powerStatus === 1).length, items, } } function toFleetUnit(item: BatteryInfo, index: number): FleetUnit { const soh = item.power const status = getDeviceStatus(soh) const riskFactors: string[] = [] if (item.isLowPower || item.power <= 20) riskFactors.push('低电量') if (item.powerStatus === 1) riskFactors.push('充电中') if (status === '预警') riskFactors.push('衰减加速') if (item.remark?.includes('v3.7')) riskFactors.push('旧固件') const thermalPressure = index % 3 const soh30d = round1(clamp(soh - 0.8 - thermalPressure * 0.25, 0, 100)) const soh60d = round1(clamp(soh - 1.7 - thermalPressure * 0.35, 0, 100)) const soh90d = round1(clamp(soh - 2.8 - thermalPressure * 0.45, 0, 100)) const temperature = round1(29.5 + thermalPressure * 2.1 + (item.isLowPower ? 1.4 : 0)) const chargeEfficiency = round1(clamp(91 + item.power / 12 - riskFactors.length * 1.8, 80, 98)) const riskScore = Math.round(clamp(12 + (100 - soh) * 1.45 + riskFactors.length * 8 + thermalPressure * 4, 8, 96)) return { id: item.devName || item.mac, batch: item.devModel, firmware: item.remark ?? 'unknown', cycles: 120 + index * 17 + Math.round((100 - soh) * 2.2), soh, soh30d, soh60d, soh90d, temperature, riskScore, chargeEfficiency, status, riskFactors, } } function createSohResponse(devices: FleetUnit[], now: Date) { if (devices.length === 0) return { history: [], forecast: [] } const avgSoh = devices.reduce((sum, unit) => sum + unit.soh, 0) / devices.length const monthlyDrop = 0.45 + devices.reduce((sum, unit) => sum + unit.riskScore, 0) / devices.length / 160 const history = Array.from({ length: 12 }, (_, index) => { const monthOffset = index - 11 return { month: formatMonthLabel(addMonths(now, monthOffset)), value: round1(clamp(avgSoh + Math.abs(monthOffset) * monthlyDrop, avgSoh, 100)), } }) const currentValue = history.at(-1)?.value ?? round1(avgSoh) const forecast = Array.from({ length: 4 }, (_, index) => ({ month: formatMonthLabel(addMonths(now, index)), value: index === 0 ? currentValue : round1(clamp(avgSoh - monthlyDrop * index, 0, 100)), })) return { history, forecast } } function summarizeBy(items: FleetUnit[], getKey: (item: FleetUnit) => T) { return Object.entries( items.reduce>((acc, item) => { const key = getKey(item) const entry = acc[key] ?? { sum: 0, count: 0 } entry.sum += item.soh entry.count += 1 acc[key] = entry return acc }, {}), ) } function createSummary(devices: FleetUnit[], now: Date) { const totalDevices = devices.length if (totalDevices === 0) { return { totalDevices, avgSoh: 0, avgSoh30d: 0, avgSoh90d: 0, warningCount: 0, watchCount: 0, healthyCount: 0, batchPerformance: [], riskFactorCounts: [], firmwareHealth: [], updatedAt: formatDateTime(now), executiveSummary: '当前没有可用于电池健康分析的真实设备记录。', } } const avgSoh = devices.reduce((sum, unit) => sum + unit.soh, 0) / totalDevices const avgSoh30d = devices.reduce((sum, unit) => sum + unit.soh30d, 0) / totalDevices const avgSoh90d = devices.reduce((sum, unit) => sum + unit.soh90d, 0) / totalDevices const warningCount = devices.filter((unit) => unit.status === '预警').length const watchCount = devices.filter((unit) => unit.status === '关注').length const healthyCount = devices.filter((unit) => unit.status === '健康').length const batchPerformance = summarizeBy(devices, (unit) => unit.batch) .map(([batch, data]) => ({ batch, avgSoh: round1(data.sum / data.count) })) .sort((a, b) => b.avgSoh - a.avgSoh) const firmwareHealth = summarizeBy(devices, (unit) => unit.firmware) .map(([firmware, data]) => ({ firmware, avgSoh: round1(data.sum / data.count), count: data.count })) .sort((a, b) => b.avgSoh - a.avgSoh) const riskFactorCounts = Object.entries( devices.reduce>((acc, unit) => { for (const factor of unit.riskFactors) { acc[factor] = (acc[factor] ?? 0) + 1 } return acc }, {}), ) .map(([factor, count]) => ({ factor, count })) .sort((a, b) => b.count - a.count) const weakestBatch = batchPerformance.at(-1)?.batch ?? '当前设备' const weakestFirmware = firmwareHealth.at(-1)?.firmware ?? 'unknown' return { totalDevices, avgSoh: round1(avgSoh), avgSoh30d: round1(avgSoh30d), avgSoh90d: round1(avgSoh90d), warningCount, watchCount, healthyCount, batchPerformance, riskFactorCounts, firmwareHealth, updatedAt: formatDateTime(now), executiveSummary: `当前电池健康度总体可控,重点风险集中在 ${weakestBatch} 批次与 ${weakestFirmware} 固件设备。建议优先跟踪低电量、充电状态与未来 90 天 SoH 变化。`, } } function createEvents(devices: FleetUnit[], now: Date) { const sortedDevices = devices.slice().sort((a, b) => b.riskScore - a.riskScore) const first = sortedDevices[0] if (!first) return [] const second = sortedDevices[1] ?? first return [ { time: formatEventTime(addHours(now, -2)), title: `${first.id} 进入重点观察队列`, detail: `${first.id} 当前 SoH 为 ${first.soh.toFixed(1)}%,综合风险评分 ${first.riskScore}。`, severity: first.status === '预警' ? '高' : '中', }, { time: formatEventTime(addHours(now, -5)), title: `${second.batch} 批次健康度趋势更新`, detail: `${second.batch} 批次未来 90 天预测 SoH 为 ${second.soh90d.toFixed(1)}%。`, severity: second.status === '健康' ? '低' : '中', }, ] satisfies DashboardSnapshot['events'] } function createStrategies(devices: FleetUnit[]) { if (devices.length === 0) return [] const warningDevices = devices.filter((unit) => unit.status === '预警') const chargingDevices = devices.filter((unit) => unit.riskFactors.includes('充电中')) const first = devices.slice().sort((a, b) => b.riskScore - a.riskScore)[0] return [ { name: '预警设备优先维护', impact: `预计高风险设备减少 ${Math.max(10, warningDevices.length * 12)}%`, scope: first ? `${first.id} 等 ${Math.max(1, warningDevices.length)} 台设备` : '当前设备', eta: '48 小时内完成', }, { name: '充电策略复核', impact: `覆盖 ${Math.max(1, chargingDevices.length)} 台充电中设备`, scope: '充电中与低电量设备', eta: '本周完成首轮验证', }, ] satisfies DashboardSnapshot['strategies'] } export function createDashboardSnapshot(items: BatteryInfo[], now = new Date()): DashboardSnapshot { const devices = items.map(toFleetUnit) return { devices, soh: createSohResponse(devices, now), events: createEvents(devices, now), strategies: createStrategies(devices), summary: createSummary(devices, now), } }