feat(platform): diagnose source consistency
This commit is contained in:
@@ -251,6 +251,12 @@ func TestHandlerVehicleServiceIncludesSourceConsistency(t *testing.T) {
|
||||
if consistency.MileageDeltaKm <= 0 || consistency.SourceTimeDeltaSeconds <= 0 {
|
||||
t.Fatalf("sourceConsistency should expose mileage and source time deltas, got %+v", consistency)
|
||||
}
|
||||
if consistency.Status == "" || consistency.Severity == "" || consistency.Title == "" || consistency.Detail == "" {
|
||||
t.Fatalf("sourceConsistency should expose actionable diagnosis, got %+v", consistency)
|
||||
}
|
||||
if consistency.Status != "degraded" || consistency.Title != "部分来源离线" {
|
||||
t.Fatalf("sourceConsistency should diagnose partial source health, got %+v", consistency)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleServiceOverviewEndpoint(t *testing.T) {
|
||||
@@ -279,6 +285,9 @@ func TestHandlerVehicleServiceOverviewEndpoint(t *testing.T) {
|
||||
if body.Data.SourceConsistency == nil || body.Data.SourceConsistency.SourceCount != 2 || body.Data.SourceConsistency.OnlineSourceCount != 1 {
|
||||
t.Fatalf("overview endpoint should expose source consistency, got %+v", body.Data.SourceConsistency)
|
||||
}
|
||||
if body.Data.SourceConsistency.Status != "degraded" || body.Data.SourceConsistency.Title != "部分来源离线" {
|
||||
t.Fatalf("overview source consistency should expose diagnosis, got %+v", body.Data.SourceConsistency)
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), `"raw"`) || strings.Contains(rec.Body.String(), `"history"`) {
|
||||
t.Fatalf("overview endpoint should not return heavy detail pages: %s", rec.Body.String())
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ type VehicleSourceConsistency struct {
|
||||
MileageDeltaKm float64 `json:"mileageDeltaKm"`
|
||||
SourceTimeDeltaSeconds float64 `json:"sourceTimeDeltaSeconds"`
|
||||
Scope string `json:"scope"`
|
||||
Status string `json:"status"`
|
||||
Severity string `json:"severity"`
|
||||
Title string `json:"title"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
type VehicleServiceOverview struct {
|
||||
|
||||
@@ -376,9 +376,53 @@ func buildVehicleSourceConsistency(statuses []VehicleSourceStatus, realtime []Re
|
||||
if hasTime {
|
||||
consistency.SourceTimeDeltaSeconds = maxTime.Sub(minTime).Seconds()
|
||||
}
|
||||
applyVehicleSourceConsistencyDiagnosis(consistency)
|
||||
return consistency
|
||||
}
|
||||
|
||||
func applyVehicleSourceConsistencyDiagnosis(consistency *VehicleSourceConsistency) {
|
||||
if consistency == nil {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case consistency.SourceCount <= 0:
|
||||
consistency.Status = "no_source"
|
||||
consistency.Severity = "warning"
|
||||
consistency.Title = "暂无来源"
|
||||
consistency.Detail = "当前车辆没有可用于交叉校验的数据来源。"
|
||||
case consistency.SourceCount == 1:
|
||||
consistency.Status = "single_source"
|
||||
consistency.Severity = "warning"
|
||||
consistency.Title = "单一来源"
|
||||
consistency.Detail = "当前车辆只有一个数据来源,无法做跨来源一致性校验。"
|
||||
case consistency.OnlineSourceCount <= 0:
|
||||
consistency.Status = "offline"
|
||||
consistency.Severity = "error"
|
||||
consistency.Title = "全部来源离线"
|
||||
consistency.Detail = sourceCoverageDetail(consistency.SourceCount, consistency.OnlineSourceCount, "无法判断实时一致性。")
|
||||
case consistency.OnlineSourceCount < consistency.SourceCount:
|
||||
consistency.Status = "degraded"
|
||||
consistency.Severity = "warning"
|
||||
consistency.Title = "部分来源离线"
|
||||
consistency.Detail = sourceCoverageDetail(consistency.SourceCount, consistency.OnlineSourceCount, "车辆服务可用但需要关注离线来源。")
|
||||
case consistency.MileageDeltaKm > 5:
|
||||
consistency.Status = "mileage_divergent"
|
||||
consistency.Severity = "warning"
|
||||
consistency.Title = "里程差异偏大"
|
||||
consistency.Detail = "多个来源的实时总里程差异超过 5 km,请核对来源解析、单位和上报时间。"
|
||||
case consistency.SourceTimeDeltaSeconds > 300:
|
||||
consistency.Status = "time_divergent"
|
||||
consistency.Severity = "warning"
|
||||
consistency.Title = "来源时间差偏大"
|
||||
consistency.Detail = "多个来源的最新上报时间相差超过 5 分钟,请关注转发延迟或链路断点。"
|
||||
default:
|
||||
consistency.Status = "consistent"
|
||||
consistency.Severity = "ok"
|
||||
consistency.Title = "来源一致"
|
||||
consistency.Detail = sourceCoverageDetail(consistency.SourceCount, consistency.OnlineSourceCount, "多源实时状态一致。")
|
||||
}
|
||||
}
|
||||
|
||||
func parseVehicleServiceTime(value string) (time.Time, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
@@ -477,6 +521,7 @@ func applyVehicleServiceOverviewDerivedFields(overview *VehicleServiceOverview,
|
||||
OnlineSourceCount: overview.OnlineSourceCount,
|
||||
Scope: "all_sources",
|
||||
}
|
||||
applyVehicleSourceConsistencyDiagnosis(overview.SourceConsistency)
|
||||
}
|
||||
|
||||
func statusesForOverview(overview *VehicleServiceOverview) []VehicleSourceStatus {
|
||||
|
||||
@@ -107,6 +107,10 @@ export interface VehicleSourceConsistency {
|
||||
mileageDeltaKm: number;
|
||||
sourceTimeDeltaSeconds: number;
|
||||
scope: string;
|
||||
status: string;
|
||||
severity: string;
|
||||
title: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface VehicleServiceOverview {
|
||||
|
||||
@@ -100,8 +100,8 @@ export function AppShell({
|
||||
</Tag>
|
||||
) : null}
|
||||
{currentVehicleConsistency ? (
|
||||
<Tag color={currentVehicleConsistency.onlineSourceCount === currentVehicleConsistency.sourceCount ? 'green' : 'orange'}>
|
||||
一致性:{currentVehicleConsistency.onlineSourceCount}/{currentVehicleConsistency.sourceCount} 来源
|
||||
<Tag color={currentVehicleConsistency.severity === 'ok' ? 'green' : currentVehicleConsistency.severity === 'error' ? 'red' : 'orange'}>
|
||||
一致性:{currentVehicleConsistency.title || `${currentVehicleConsistency.onlineSourceCount}/${currentVehicleConsistency.sourceCount} 来源`}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Tag color="blue">生产环境</Tag>
|
||||
|
||||
@@ -136,6 +136,8 @@ export function VehicleDetail({
|
||||
const consistencyLocatedSourceCount = consistency?.locatedSourceCount ?? locatedSourceCount;
|
||||
const consistencyMileageDelta = consistency?.mileageDeltaKm ?? mileageDelta;
|
||||
const consistencyTimeDeltaSeconds = consistency?.sourceTimeDeltaSeconds ?? sourceTimeDeltaSeconds;
|
||||
const consistencyTitle = consistency?.title ?? '-';
|
||||
const consistencyDetail = consistency?.detail ?? '-';
|
||||
const activeProtocol = query.protocol?.trim() ?? '';
|
||||
const scopeText = activeProtocol ? `单一来源:${activeProtocol}` : '全部来源聚合';
|
||||
const formKey = `${query.keyword}-${query.protocol ?? ''}`;
|
||||
@@ -328,11 +330,13 @@ export function VehicleDetail({
|
||||
<Descriptions
|
||||
row
|
||||
data={[
|
||||
{ key: '一致性结论', value: consistencyTitle },
|
||||
{ key: '来源覆盖', value: consistencySourceCount > 0 ? `${consistencySourceCount} 个来源` : '-' },
|
||||
{ key: '在线来源', value: consistencySourceCount > 0 ? `${consistencyOnlineSourceCount}/${consistencySourceCount} 在线` : '-' },
|
||||
{ key: '位置覆盖', value: consistencyLocatedSourceCount > 0 ? `${consistencyLocatedSourceCount} 个来源有位置` : '暂无位置' },
|
||||
{ key: '里程差异', value: formatCompactNumber(consistencyMileageDelta, ' km') },
|
||||
{ key: '来源时间差', value: formatSeconds(consistencyTimeDeltaSeconds) },
|
||||
{ key: '诊断说明', value: consistencyDetail },
|
||||
{ key: '车辆服务视角', value: activeProtocol ? `当前只看 ${activeProtocol}` : '三类来源合并为同一车辆服务' }
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -386,7 +386,11 @@ test('shows resolved vehicle service status after topbar search', async () => {
|
||||
locatedSourceCount: 2,
|
||||
mileageDeltaKm: 0,
|
||||
sourceTimeDeltaSeconds: 0,
|
||||
scope: 'all_sources'
|
||||
scope: 'all_sources',
|
||||
status: 'degraded',
|
||||
severity: 'warning',
|
||||
title: '部分来源离线',
|
||||
detail: '1/2 个来源在线,车辆服务可用但需要关注离线来源。'
|
||||
}
|
||||
},
|
||||
traceId: 'trace-test',
|
||||
@@ -432,7 +436,7 @@ test('shows resolved vehicle service status after topbar search', async () => {
|
||||
|
||||
expect(await screen.findByText('当前车辆:后端判定部分离线')).toBeInTheDocument();
|
||||
expect(screen.getByText('粤AG18312 / LB9A32A24R0LS1426')).toBeInTheDocument();
|
||||
expect(screen.getByText('一致性:1/2 来源')).toBeInTheDocument();
|
||||
expect(screen.getByText('一致性:部分来源离线')).toBeInTheDocument();
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312'), undefined);
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/resolve'), undefined);
|
||||
});
|
||||
@@ -1777,7 +1781,11 @@ test('shows cross-source consistency for one vehicle service', async () => {
|
||||
locatedSourceCount: 3,
|
||||
mileageDeltaKm: 0.4,
|
||||
sourceTimeDeltaSeconds: 35,
|
||||
scope: 'all_sources'
|
||||
scope: 'all_sources',
|
||||
status: 'degraded',
|
||||
severity: 'warning',
|
||||
title: '部分来源离线',
|
||||
detail: '2/3 个来源在线,车辆服务可用但需要关注离线来源。'
|
||||
},
|
||||
sources: ['GB32960', 'JT808', 'YUTONG_MQTT'],
|
||||
sourceStatus: [
|
||||
@@ -1815,6 +1823,8 @@ test('shows cross-source consistency for one vehicle service', async () => {
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('跨来源一致性')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('部分来源离线').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('2/3 个来源在线,车辆服务可用但需要关注离线来源。')).toBeInTheDocument();
|
||||
expect(screen.getByText('3 个来源')).toBeInTheDocument();
|
||||
expect(screen.getByText('2/3 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('3 个来源有位置')).toBeInTheDocument();
|
||||
|
||||
@@ -57,6 +57,27 @@ Returns one vehicle service view with identity, realtime summary, source coverag
|
||||
|
||||
`/api/vehicles/detail` remains available as a compatibility alias. New UI and integrations should use `/api/vehicle-service` to make the vehicle-first boundary explicit.
|
||||
|
||||
`sourceConsistency` is the vehicle-level diagnosis across GB32960, JT808, and Yutong MQTT sources. The three protocols are source evidence for one vehicle service, so callers should display the diagnosis result instead of asking users to compare protocol rows manually.
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceConsistency": {
|
||||
"scope": "detail",
|
||||
"sourceCount": 3,
|
||||
"onlineSourceCount": 2,
|
||||
"locatedSourceCount": 2,
|
||||
"mileageDeltaKm": 0.8,
|
||||
"sourceTimeDeltaSeconds": 42,
|
||||
"status": "degraded",
|
||||
"severity": "warning",
|
||||
"title": "部分来源离线",
|
||||
"detail": "2/3 个来源在线,车辆服务可用但需要关注离线来源。"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`status` values are `consistent`, `degraded`, `offline`, `single_source`, `no_source`, `mileage_divergent`, and `time_divergent`. `severity` values are `ok`, `warning`, and `error`. `/api/vehicle-service/overview` and `/api/vehicle-service/overviews` also return `sourceConsistency`; overview responses may only include lightweight counts and diagnosis, while detail responses include location, mileage, and source-time deltas when realtime source rows are available.
|
||||
|
||||
### Realtime Vehicles
|
||||
|
||||
```http
|
||||
|
||||
@@ -22,6 +22,8 @@ Vehicle service coverage is always shown at VIN level. A vehicle may have one or
|
||||
|
||||
Vehicle lists should expose a vehicle-level service status derived from binding and source coverage: identity required, no data source, offline, degraded, or healthy. Protocol tags remain attribution and troubleshooting context, not the primary status.
|
||||
|
||||
Vehicle detail and top search context should expose one source-consistency conclusion, such as `来源一致`, `部分来源离线`, or `里程差异偏大`. This diagnosis comes from `sourceConsistency` and turns GB32960, JT808, and Yutong MQTT into explainable evidence under one vehicle service rather than three competing realtime products.
|
||||
|
||||
## Interaction Rules
|
||||
|
||||
- Tables are the default data surface.
|
||||
|
||||
Reference in New Issue
Block a user