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();
|
||||
|
||||
Reference in New Issue
Block a user