fix(mileage): preserve anomalous odometer readings and show data quality
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -15,6 +15,7 @@ export interface MileageRow {
|
||||
vin: string;
|
||||
daily_km: string;
|
||||
total_km: string | null;
|
||||
mileage_anomaly?: string | null;
|
||||
source: string;
|
||||
source_protocol: OneOsProtocol | null;
|
||||
data_time: string | null;
|
||||
@@ -27,6 +28,7 @@ export interface DailyMileageRow {
|
||||
vin: string | null;
|
||||
date: string;
|
||||
daily_km: string | number | null;
|
||||
mileage_anomaly?: string | null;
|
||||
source: string | null;
|
||||
source_protocol: OneOsProtocol | null;
|
||||
data_time: string | null;
|
||||
@@ -105,7 +107,8 @@ export function toMileageRows(rows: OneOsDailyMileage[]): MileageRow[] {
|
||||
vin: row.vin,
|
||||
daily_km: String(row.dailyMileageKm ?? 0),
|
||||
total_km: row.totalMileageKm == null ? null : String(row.totalMileageKm),
|
||||
source: row.status === 'NORMAL' ? 'ONEOS_API' : 'NONE',
|
||||
source: row.status !== 'NO_DATA' ? 'ONEOS_API' : 'NONE',
|
||||
mileage_anomaly: row.status === 'DATA_ANOMALY' ? row.dataQuality || 'DATA_ANOMALY' : null,
|
||||
source_protocol: row.sourceProtocol,
|
||||
data_time: row.dataTime,
|
||||
calculated_at: row.calculatedAt,
|
||||
@@ -174,6 +177,7 @@ export function mergeMonitoringVehicles(
|
||||
plate: info.plate,
|
||||
vin: mileage?.vin || info.vin || '',
|
||||
dailyKm,
|
||||
mileageAnomaly: mileage?.mileage_anomaly || null,
|
||||
// Cumulative mileage is never backfilled from a different source here.
|
||||
totalKm: mileage?.total_km != null ? Number(mileage.total_km) : null,
|
||||
source,
|
||||
|
||||
@@ -164,7 +164,8 @@ export async function queryRangeMileage(
|
||||
vin: row.vin,
|
||||
date,
|
||||
daily_km: row.dailyMileageKm,
|
||||
source: row.status === 'NORMAL' ? 'ONEOS_API' : 'NONE',
|
||||
source: row.status !== 'NO_DATA' ? 'ONEOS_API' : 'NONE',
|
||||
mileage_anomaly: row.status === 'DATA_ANOMALY' ? row.dataQuality || 'DATA_ANOMALY' : null,
|
||||
source_protocol: row.sourceProtocol,
|
||||
data_time: row.dataTime,
|
||||
calculated_at: row.calculatedAt,
|
||||
@@ -216,6 +217,7 @@ export async function queryRangeMileage(
|
||||
vin: existing?.vin || row.vin || '',
|
||||
daily_km: String((Number(existing?.daily_km) || 0) + km),
|
||||
total_km: null,
|
||||
mileage_anomaly: existing?.mileage_anomaly || row.mileage_anomaly || null,
|
||||
source: existing?.source !== 'NONE' && existing?.source ? existing.source : (row.source || 'NONE'),
|
||||
source_protocol: row.source_protocol || existing?.source_protocol || null,
|
||||
data_time: row.data_time || existing?.data_time || null,
|
||||
|
||||
@@ -86,3 +86,15 @@ test('连续日期分组、车牌规范化和请求键保持稳定', () => {
|
||||
);
|
||||
assert.throws(() => assertMileageDate('2026/08/12'), /Invalid mileage date/);
|
||||
});
|
||||
|
||||
test('异常日里程不伪造为有效值,保留仪表累计和异常原因', () => {
|
||||
const [row] = normalizeOneOsRows([{
|
||||
plateNumber: '粤AGE4862', date: '2026-09-17', status: 'DATA_ANOMALY',
|
||||
dataQuality: 'ODOMETER_SOURCE_CHANGED', dailyMileageKm: null,
|
||||
totalMileageKm: 66852.4, sourceProtocol: 'GB32960',
|
||||
}], '2026-09-17');
|
||||
assert.equal(row.status, 'DATA_ANOMALY');
|
||||
assert.equal(row.dailyMileageKm, null);
|
||||
assert.equal(row.totalMileageKm, 66852.4);
|
||||
assert.equal(row.dataQuality, 'ODOMETER_SOURCE_CHANGED');
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ export interface OneOsDailyMileage {
|
||||
date: string;
|
||||
dailyMileageKm: number | null;
|
||||
totalMileageKm: number | null;
|
||||
status: 'NORMAL' | 'NO_DATA';
|
||||
status: 'NORMAL' | 'NO_DATA' | 'DATA_ANOMALY';
|
||||
dataQuality?: string;
|
||||
dataTime: string | null;
|
||||
calculatedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
@@ -44,7 +45,7 @@ export function normalizeOneOsRows(value: unknown, requestedDate: string): OneOs
|
||||
const vin = typeof row.vin === 'string' ? row.vin.trim() : '';
|
||||
const plateNumber = typeof row.plateNumber === 'string' ? row.plateNumber.trim() : '';
|
||||
const date = typeof row.date === 'string' ? row.date : requestedDate;
|
||||
const status = row.status === 'NORMAL' ? 'NORMAL' : 'NO_DATA';
|
||||
const status = row.status === 'NORMAL' || row.status === 'DATA_ANOMALY' ? row.status : 'NO_DATA';
|
||||
|
||||
// The aliases keep the BI compatible while the Open API formalizes its fields.
|
||||
const dataTime = stringField(row, 'dataTime', 'statisticTime', 'recordTime');
|
||||
@@ -60,8 +61,9 @@ export function normalizeOneOsRows(value: unknown, requestedDate: string): OneOs
|
||||
plateNumber,
|
||||
date,
|
||||
dailyMileageKm: nonNegativeMileage(row.dailyMileageKm, status),
|
||||
totalMileageKm: nonNegativeMileage(row.totalMileageKm, status),
|
||||
totalMileageKm: nonNegativeMileage(row.totalMileageKm, status === 'DATA_ANOMALY' ? 'NORMAL' : status),
|
||||
status,
|
||||
...(status === 'DATA_ANOMALY' ? { dataQuality: stringField(row, 'dataQuality') || 'DATA_ANOMALY' } : {}),
|
||||
dataTime,
|
||||
calculatedAt,
|
||||
updatedAt,
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface CachedVehicle {
|
||||
plate: string;
|
||||
vin: string;
|
||||
dailyKm: number;
|
||||
mileageAnomaly?: string | null;
|
||||
dailyMileage?: Record<string, number>;
|
||||
totalKm: number | null;
|
||||
source: string;
|
||||
|
||||
Reference in New Issue
Block a user