fix(prediction): 跳过缺少遥测字段的预测请求

This commit is contained in:
2026-05-12 01:43:46 +08:00
parent 305ed1b692
commit c00e04dfb0
2 changed files with 15 additions and 57 deletions
+2 -16
View File
@@ -71,22 +71,8 @@ describe('prediction client helpers', () => {
expect(isPredictionForBattery({ batteryId: battery.id, mac: 'RING-B11' }, battery as BatteryInfo)).toBe(false) expect(isPredictionForBattery({ batteryId: battery.id, mac: 'RING-B11' }, battery as BatteryInfo)).toBe(false)
}) })
test('returns null for history-insufficient prediction requests', () => { test('returns null when required telemetry fields are unavailable', () => {
expect(createPredictionRequest(battery, [battery, battery, battery, battery])).toBeNull() expect(createPredictionRequest(battery, [battery, battery, battery, battery])).toBeNull()
}) expect(createPredictionRequest(battery, [battery, battery, battery, battery, battery])).toBeNull()
test('uses only real battery history fields in prediction requests', () => {
const request = createPredictionRequest(battery, [battery, battery, battery, battery, battery])
const firstHistory = request?.history[0]
expect(firstHistory).toEqual({
id: battery.id,
power: battery.power,
power_status: battery.powerStatus,
is_low_power: MYSQL_BOOLEAN.FALSE,
timestamp: battery.createTime,
})
expect(Object.hasOwn(firstHistory ?? {}, 'charge_capacity_ah')).toBe(false)
expect(Object.hasOwn(firstHistory ?? {}, 'coulombic_efficiency_pct')).toBe(false)
}) })
}) })
+13 -41
View File
@@ -1,6 +1,6 @@
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
import { z } from 'zod' import { z } from 'zod'
import { type BatteryInfo, type BatteryPrediction, type PowerStatus, toMysqlBoolean } from '@/domain/battery' import type { BatteryInfo, BatteryPrediction, PowerStatus } from '@/domain/battery'
import { env } from '@/env' import { env } from '@/env'
import { getLogger } from '@/server/logger' import { getLogger } from '@/server/logger'
@@ -20,10 +20,14 @@ export const sohPredictionSchema = z.object({
export type SohPrediction = BatteryPrediction & z.infer<typeof sohPredictionSchema> export type SohPrediction = BatteryPrediction & z.infer<typeof sohPredictionSchema>
type PredictionHistoryItem = { type PredictionHistoryItem = {
id: number cycle: number
power: number charge_capacity_ah: number
power_status: PowerStatus discharge_capacity_ah: number
is_low_power: string charge_energy_wh: number
discharge_energy_wh: number
charge_time: string
discharge_time: string
coulombic_efficiency_pct: number
timestamp: string timestamp: string
} }
@@ -68,12 +72,6 @@ const negativeCache = new LRUCache<string, true>({
}) })
const inFlightRequests = new Map<string, Promise<SohPrediction | null>>() const inFlightRequests = new Map<string, Promise<SohPrediction | null>>()
function normalizeMysqlDateTime(value: string) {
if (!value.includes('T')) return value
return value.slice(0, 19).replace('T', ' ')
}
function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) { function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) {
const latestHistory = history.at(-1) const latestHistory = history.at(-1)
const latestId = latestHistory?.id ?? battery.id const latestId = latestHistory?.id ?? battery.id
@@ -82,36 +80,10 @@ function createCacheKey(battery: BatteryInfo, history: BatteryInfo[]) {
return `${battery.mac}:${latestId}:${latestTime}` return `${battery.mac}:${latestId}:${latestTime}`
} }
function createHistoryItem(item: BatteryInfo): PredictionHistoryItem { export function createPredictionRequest(_battery: BatteryInfo, _history: BatteryInfo[]): PredictionRequest | null {
return { // The current customer table lacks the real capacity/energy telemetry required by the prediction service.
id: item.id, // Returning null avoids both synthetic features and predictable 422 responses from the service.
power: item.power, return null
power_status: item.powerStatus,
is_low_power: toMysqlBoolean(item.isLowPower),
timestamp: normalizeMysqlDateTime(item.createTime),
}
}
export function createPredictionRequest(battery: BatteryInfo, history: BatteryInfo[]): PredictionRequest | null {
const sourceHistory = history.length > 0 ? history : [battery]
if (sourceHistory.length < 5) return null
return {
battery: {
id: battery.id,
user_id: battery.userId,
mac: battery.mac,
dev_model: battery.devModel,
dev_name: battery.devName,
is_low_power: toMysqlBoolean(battery.isLowPower),
power_status: battery.powerStatus,
power: battery.power,
create_time: normalizeMysqlDateTime(battery.createTime),
remark: battery.remark ?? '',
},
history: sourceHistory.map(createHistoryItem),
}
} }
export function normalizePrediction(response: z.infer<typeof predictionResponseSchema>): SohPrediction { export function normalizePrediction(response: z.infer<typeof predictionResponseSchema>): SohPrediction {