From 8a7ce7eccf6e50fba7f31a56aaea089202b21fe1 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Fri, 7 Aug 2026 17:46:51 +0800 Subject: [PATCH] perf: reuse mileage target snapshots --- docs/bi-refactor-roadmap.md | 2 +- .../mileage/target-vehicle-cache.test.ts | 70 +++++++++++++++++++ .../routes/mileage/target-vehicle-cache.ts | 27 +++++++ src/server/routes/mileage/targets.ts | 19 +++-- 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 src/server/routes/mileage/target-vehicle-cache.test.ts create mode 100644 src/server/routes/mileage/target-vehicle-cache.ts diff --git a/docs/bi-refactor-roadmap.md b/docs/bi-refactor-roadmap.md index bd0580d..16f8a84 100644 --- a/docs/bi-refactor-roadmap.md +++ b/docs/bi-refactor-roadmap.md @@ -173,7 +173,7 @@ SUM(fee) / NULLIF(SUM(kwh), 0) - 已完成电能数据截至时间和实际趋势月展示。 - 电能日期下钻自动核对日汇总与订单全量合计的日期、车辆范围、电量和费用,并显式展示通过或差异状态。 - 已完成氢能结构化故障状态与重试体验。 -- 已完成里程今日仪表快照复用:普通查询命中分钟级快照,手动刷新生成一致的分页快照。 +- 已完成里程今日仪表快照复用:监控查询和考核车辆当前日明细命中同一分钟级快照,手动刷新生成一致的分页快照;历史日仍使用独立日期缓存。 - 已在各 BI 模块头部提供当前数据源的真实请求状态、失败率和最后成功/失败时间。 - 待恢复氢能数据库后进行总览、站点、客户三层对账。 - 待 ETC 同步后进行通行记录、账单和收款三层对账。 diff --git a/src/server/routes/mileage/target-vehicle-cache.test.ts b/src/server/routes/mileage/target-vehicle-cache.test.ts new file mode 100644 index 0000000..a2db364 --- /dev/null +++ b/src/server/routes/mileage/target-vehicle-cache.test.ts @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { targetVehicleMileageFromMonitoringCache } from './target-vehicle-cache.js'; +import type { CachedVehicle, MonitoringCache } from './types.js'; + +function vehicle(plate: string, overrides: Partial = {}): CachedVehicle { + return { + plate, + vin: '', + dailyKm: 12.3, + totalKm: 456.7, + source: 'ONEOS_API', + sourceProtocol: 'GB32960', + sourceCategory: 'INSTRUMENT', + dataTime: '2026-08-07T10:00:00+08:00', + calculatedAt: null, + updatedAt: null, + isOnline: true, + isDataSynced: true, + customer: null, + department: null, + manager: null, + managerId: null, + rentStatus: null, + entity: null, + project: null, + region: null, + brand: null, + targetNames: [], + yesterdayKm: 0, + ...overrides, + }; +} + +function cache(vehicles: CachedVehicle[]): MonitoringCache { + return { + vehicles, + stats: { totalToday: 0, totalAll: 0, vehicleCount: vehicles.length }, + filters: { + departments: [], customers: [], plates: [], projects: [], entities: [], + rentStatuses: [], platePrefixes: [], targetNames: [], regions: [], brands: [], + }, + targetPlatesMap: new Map(), + updatedAt: '2026-08-07T10:00:00+08:00', + }; +} + +test('reuses only the current-day monitoring snapshot and filters target plates', () => { + const monitoringCache = cache([ + vehicle('粤A1'), + vehicle('粤A2', { dailyKm: -1, totalKm: null, isOnline: false }), + ]); + + const result = targetVehicleMileageFromMonitoringCache( + monitoringCache, + '2026-08-07', + '2026-08-07', + ['粤A2'], + ); + assert.deepEqual(Array.from(result || []), [['粤A2', { + dailyKm: 0, + totalKm: null, + isOnline: false, + }]]); + assert.equal( + targetVehicleMileageFromMonitoringCache(monitoringCache, '2026-08-06', '2026-08-07', ['粤A2']), + null, + ); + assert.equal(targetVehicleMileageFromMonitoringCache(null, '2026-08-07', '2026-08-07', ['粤A2']), null); +}); diff --git a/src/server/routes/mileage/target-vehicle-cache.ts b/src/server/routes/mileage/target-vehicle-cache.ts new file mode 100644 index 0000000..a6d722e --- /dev/null +++ b/src/server/routes/mileage/target-vehicle-cache.ts @@ -0,0 +1,27 @@ +import type { MonitoringCache } from './types.js'; + +export interface TargetVehicleMileageSnapshot { + dailyKm: number; + totalKm: number | null; + isOnline: boolean; +} + +export function targetVehicleMileageFromMonitoringCache( + cache: MonitoringCache | null, + requestedDate: string, + today: string, + plates: string[], +): Map | null { + if (!cache || !requestedDate || requestedDate !== today) return null; + + const selected = new Set(plates); + return new Map( + cache.vehicles + .filter(vehicle => selected.has(vehicle.plate)) + .map(vehicle => [vehicle.plate, { + dailyKm: Math.max(0, Number(vehicle.dailyKm) || 0), + totalKm: vehicle.totalKm == null ? null : Math.max(0, Number(vehicle.totalKm) || 0), + isOnline: vehicle.isOnline, + }]), + ); +} diff --git a/src/server/routes/mileage/targets.ts b/src/server/routes/mileage/targets.ts index 3fcd94b..4b4efe5 100644 --- a/src/server/routes/mileage/targets.ts +++ b/src/server/routes/mileage/targets.ts @@ -4,6 +4,8 @@ import { getCache } from './cache.js'; import { fetchOneOsDailyMileage } from './oneos-api.js'; import { fetchVehicleInfoByPlates } from './vehicle-info.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js'; +import { shanghaiDate } from './monitoring-cache-policy.js'; +import { targetVehicleMileageFromMonitoringCache } from './target-vehicle-cache.js'; const app = new Hono(); @@ -252,11 +254,20 @@ app.get('/:id/vehicles', async (c) => { ) as [any[], unknown]; const plates: string[] = rows.map((r: any) => r.plate_number); - const infoMap = await fetchVehicleInfoByPlates(plates); + const cachedDateMileageMap = targetVehicleMileageFromMonitoringCache( + getCache(), + date, + shanghaiDate(), + plates, + ); + const [infoMap, mileageRows] = await Promise.all([ + fetchVehicleInfoByPlates(plates), + date && !cachedDateMileageMap ? fetchOneOsDailyMileage(date, plates) : Promise.resolve([]), + ]); - const dateMileageMap = new Map(); - if (date && plates.length > 0) { - const mileageRows = await fetchOneOsDailyMileage(date, plates); + const dateMileageMap = cachedDateMileageMap + || new Map(); + if (!cachedDateMileageMap && date && plates.length > 0) { for (const m of mileageRows) { const existing = dateMileageMap.get(m.plateNumber); const dailyKm = m.status === 'NORMAL' ? (Number(m.dailyMileageKm) || 0) : 0;