优化里程日期选择并修复区间累计里程缺失
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
shishengliang
2026-11-03 09:29:54 +08:00
parent a81ace14e4
commit 12324006db
13 changed files with 67 additions and 58 deletions
+1 -1
View File
@@ -127,7 +127,7 @@ export async function queryRangeMileage(
]);
const { summary, yesterdayRows } = cached.result;
return {
vehicles: buildSummaryMileage(summary, yesterdayRows, infoMap, targetRows, regionMap),
vehicles: buildSummaryMileage(summary, yesterdayRows, infoMap, targetRows, regionMap, cached.result.priorOdometers),
summary,
start: startDate, end: endDate,
queriedAt: cached.queriedAt,
@@ -54,11 +54,14 @@ test('summary integration preserves scope, valid partial mileage, trend nulls, r
assert.equal(first.vehicles[0].plate,'TEST1');assert.equal(first.stats.totalToday,50);
assert.equal(first.rangeDailyTotals?.[1].totalKm,null);
assert.equal(requests.filter(r=>r.url.endsWith('/summary/query')).length,1);
assert.equal(requests.filter(r=>r.url.endsWith('/range/query')).length,0);
assert.deepEqual(requests.filter(r=>r.url.endsWith('/range/query')).map(r=>r.body.plateNumbers), [['TEST0']]);
const second=await read('&page=2&limit=1&includeDailyTrend=0');assert.equal(second.vehicles[0].periodMileageKm,20);
const sorted=await read('&sortOrder=asc&includeDailyTrend=0');assert.equal(sorted.vehicles[0].periodMileageKm,0);
const exported=await read('&limit=9999&includeDailyTrend=0');
assert.equal(exported.vehicles.length,4);assert.equal(exported.vehicles[3].periodMileageKm,null);
const fallback = exported.vehicles.find(v => v.plate === 'TEST0')!;
assert.equal(fallback.totalKm,100);
assert.equal(fallback.totalKmFallbackDate,'2026-10-10');
assert.equal(exported.queriedAt,first.queriedAt);assert.ok(!exported.vehicles[0].dailyMileage);
assert.equal(requests.filter(r=>r.url.endsWith('/summary/query')).length,1);
user={...user,permissionLevel:'department'};
+20 -3
View File
@@ -1,5 +1,5 @@
import { fetchOneOsDailyMileage, type OneOsDailyMileage } from './oneos-api.js';
import { previousMileageDate } from './cache-model.js';
import { fetchOneOsDailyMileage, fetchOneOsMileageDates, type OneOsDailyMileage } from './oneos-api.js';
import { mileageDatesBetween, previousMileageDate } from './cache-model.js';
import { fetchMileageSummary, type MileageSummary } from './summary-api.js';
import type { OneOsProtocol } from './source-policy.js';
@@ -10,6 +10,7 @@ const MAX_ENTRIES = 4;
export interface RangeMileageSource {
summary: MileageSummary;
yesterdayRows: OneOsDailyMileage[];
priorOdometers?: Map<string, { date: string; km: number }>;
}
type Entry = { result: RangeMileageSource; queriedAt: string; expiresAt: number };
@@ -19,7 +20,23 @@ async function loadSource(start: string, end: string, protocols: OneOsProtocol[]
fetchMileageSummary(start, end, protocols, plates),
plates ? Promise.resolve([]) : fetchOneOsDailyMileage(previousMileageDate(start), undefined, protocols, refresh),
]);
return { summary, yesterdayRows };
// Plate-scoped summary requests are used only for filtered trends.
if (plates || start === end) return { summary, yesterdayRows };
const missing = summary.vehicles.filter(v => v.endDateTotalMileageKm === null && v.validDays + v.anomalyDays > 0);
if (!missing.length) return { summary, yesterdayRows };
const dates = mileageDatesBetween(start, previousMileageDate(end));
const history = await fetchOneOsMileageDates(dates, missing.map(v => v.plateNumber), protocols);
const priorOdometers = new Map<string, { date: string; km: number }>();
for (const date of dates.reverse()) {
for (const row of history.get(date) || []) {
const plate = row.plateNumber.toUpperCase();
const trusted = row.status === 'NORMAL' || row.dataQuality === 'BASELINE_MISSING' || row.dataQuality === 'ODOMETER_SOURCE_CHANGED';
if (!priorOdometers.has(plate) && trusted && row.dataQuality !== 'CARRIED_FORWARD' && row.totalMileageKm != null) {
priorOdometers.set(plate, { date, km: row.totalMileageKm });
}
}
}
return { summary, yesterdayRows, priorOdometers };
}
export function createMonitoringQueryCache(
+4 -2
View File
@@ -4,15 +4,17 @@ import { buildPlateTargetNamesMap, dailyMileageMap, mergeMonitoringVehicles, typ
import type { CachedVehicle, VehicleInfoRow } from './types.js';
import { sourceCategoryFromProtocol } from './source-policy.js';
export function buildSummaryMileage(summary: MileageSummary, yesterdayRows: OneOsDailyMileage[], infoMap: Map<string, VehicleInfoRow>, targets: TargetRow[], regions: Record<string, string>): CachedVehicle[] {
export function buildSummaryMileage(summary: MileageSummary, yesterdayRows: OneOsDailyMileage[], infoMap: Map<string, VehicleInfoRow>, targets: TargetRow[], regions: Record<string, string>, priorOdometers?: Map<string, { date: string; km: number }>): CachedVehicle[] {
const byPlate = new Map(summary.vehicles.map(v => [v.plateNumber, v]));
return mergeMonitoringVehicles([], infoMap, dailyMileageMap(yesterdayRows), buildPlateTargetNamesMap(targets), regions).map(vehicle => {
const row = byPlate.get(vehicle.plate.trim().toUpperCase());
if (!row) return { ...vehicle, periodMileageKm: null };
const prior = priorOdometers?.get(row.plateNumber);
const categories = new Set(row.sourceProtocols.map(sourceCategoryFromProtocol));
return { ...vehicle, vin: row.vin || vehicle.vin,
dailyKm: row.periodMileageKm ?? 0, periodMileageKm: row.periodMileageKm,
totalKm: row.endDateTotalMileageKm,
totalKm: row.endDateTotalMileageKm ?? prior?.km ?? null,
totalKmFallbackDate: prior?.date,
validDays: row.validDays, missingDays: row.missingDays, anomalyDays: row.anomalyDays,
qualityStatus: row.status, qualityReasons: row.qualityReasons, sourceProtocols: row.sourceProtocols,
mileageAnomaly: row.anomalyDays ? row.qualityReasons.join(', ') || 'DATA_ANOMALY' : null,
+1
View File
@@ -13,6 +13,7 @@ export interface CachedVehicle {
mileageAnomaly?: string | null;
dailyMileage?: Record<string, number | null>;
totalKm: number | null;
totalKmFallbackDate?: string;
source: string;
sourceProtocol: 'GB32960' | 'MQTT' | 'JT808' | null;
sourceCategory: 'INSTRUMENT' | 'GPS' | 'MIXED' | null;