diff --git a/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx b/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
index 38ea06e..b183e93 100644
--- a/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
+++ b/src/modules/mileage/monitoring/components/FullscreenMonitoring.tsx
@@ -310,7 +310,7 @@ export default function FullscreenMonitoring({
{v.department || '-'} |
- {(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km> : 未对接}
+ {(v.isDataSynced || v.totalKm != null) ? <>{v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()} {v.mileageAnomaly ? '' : 'km'}> : 未对接}
|
diff --git a/src/modules/mileage/monitoring/components/VehicleList.tsx b/src/modules/mileage/monitoring/components/VehicleList.tsx
index 922523c..1bbb7c8 100644
--- a/src/modules/mileage/monitoring/components/VehicleList.tsx
+++ b/src/modules/mileage/monitoring/components/VehicleList.tsx
@@ -69,13 +69,13 @@ export default function VehicleList({
-
+
{v.plate}
- {v.isOnline ? '在线' : '离线'}
+ {v.mileageAnomaly ? '数据异常' : v.isOnline ? '在线' : '离线'}
+ {v.mileageAnomaly && {v.mileageAnomaly === 'ODOMETER_SOURCE_CHANGED' ? '里程来源变化' : '上游里程异常'} }
{statisticTime.label}
@@ -101,8 +102,8 @@ export default function VehicleList({
- {Math.max(0, v.dailyKm || 0).toLocaleString()}
- km
+ {v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()}
+ {v.mileageAnomaly ? '' : 'km'}
@@ -118,7 +119,7 @@ export default function VehicleList({
)}
区
- {(v.isDataSynced || v.totalKm != null) ? <>{Math.max(0, v.dailyKm || 0).toLocaleString()} km> : 未对接}
+ {(v.isDataSynced || v.totalKm != null) ? <>{v.mileageAnomaly ? '数据异常' : Math.max(0, v.dailyKm || 0).toLocaleString()} {v.mileageAnomaly ? '' : 'km'}> : 未对接}
diff --git a/src/modules/mileage/types.ts b/src/modules/mileage/types.ts
index 3ca395f..258739f 100644
--- a/src/modules/mileage/types.ts
+++ b/src/modules/mileage/types.ts
@@ -6,6 +6,7 @@ export interface MonitoringVehicle {
plate: string;
vin: string;
dailyKm: number;
+ mileageAnomaly?: string | null;
dailyMileage?: Record;
totalKm: number | null;
source: string;
diff --git a/src/server/routes/mileage/cache-model.ts b/src/server/routes/mileage/cache-model.ts
index 92fe861..25eacfc 100644
--- a/src/server/routes/mileage/cache-model.ts
+++ b/src/server/routes/mileage/cache-model.ts
@@ -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,
diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts
index fe3315c..2bf302a 100644
--- a/src/server/routes/mileage/cache.ts
+++ b/src/server/routes/mileage/cache.ts
@@ -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,
diff --git a/src/server/routes/mileage/oneos-model.test.ts b/src/server/routes/mileage/oneos-model.test.ts
index 7fa80de..11574cb 100644
--- a/src/server/routes/mileage/oneos-model.test.ts
+++ b/src/server/routes/mileage/oneos-model.test.ts
@@ -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');
+});
diff --git a/src/server/routes/mileage/oneos-model.ts b/src/server/routes/mileage/oneos-model.ts
index b4d28bc..53a12f6 100644
--- a/src/server/routes/mileage/oneos-model.ts
+++ b/src/server/routes/mileage/oneos-model.ts
@@ -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,
diff --git a/src/server/routes/mileage/types.ts b/src/server/routes/mileage/types.ts
index 4344325..4cb17dd 100644
--- a/src/server/routes/mileage/types.ts
+++ b/src/server/routes/mileage/types.ts
@@ -3,6 +3,7 @@ export interface CachedVehicle {
plate: string;
vin: string;
dailyKm: number;
+ mileageAnomaly?: string | null;
dailyMileage?: Record;
totalKm: number | null;
source: string;
|