diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 5737b870..7201f1d1 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -276,6 +276,9 @@ func TestHandlerVehicleServiceOverviewEndpoint(t *testing.T) { if body.Data.ServiceStatus == nil || body.Data.ServiceStatus.Status != "degraded" || body.Data.ServiceStatus.Title != "部分来源离线" { t.Fatalf("overview endpoint should expose canonical service status, got %+v", body.Data.ServiceStatus) } + 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 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()) } @@ -330,6 +333,9 @@ func TestHandlerVehicleServiceOverviewsEndpoint(t *testing.T) { if body.Data.Items[0].ServiceStatus == nil || body.Data.Items[1].ServiceStatus == nil { t.Fatalf("batch overview should include canonical status for every vehicle, got %+v", body.Data.Items) } + if body.Data.Items[0].SourceConsistency == nil || body.Data.Items[0].SourceConsistency.SourceCount != 2 { + t.Fatalf("batch overview should include source consistency, got %+v", body.Data.Items[0].SourceConsistency) + } if strings.Contains(rec.Body.String(), `"raw"`) || strings.Contains(rec.Body.String(), `"history"`) { t.Fatalf("batch overview should not return heavy detail pages: %s", rec.Body.String()) } diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index c43c0358..2a51c0a8 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -64,8 +64,8 @@ type VehicleCoverageRow struct { } type VehicleCoverageSummary struct { - TotalVehicles int `json:"totalVehicles"` - OnlineVehicles int `json:"onlineVehicles"` + TotalVehicles int `json:"totalVehicles"` + OnlineVehicles int `json:"onlineVehicles"` SingleSourceVehicles int `json:"singleSourceVehicles"` MultiSourceVehicles int `json:"multiSourceVehicles"` UnboundVehicles int `json:"unboundVehicles"` @@ -113,22 +113,23 @@ type VehicleSourceConsistency struct { } type VehicleServiceOverview struct { - VIN string `json:"vin"` - Plate string `json:"plate"` - Phone string `json:"phone"` - OEM string `json:"oem"` - Protocols []string `json:"protocols"` - PrimaryProtocol string `json:"primaryProtocol"` - CoverageStatus string `json:"coverageStatus"` - SourceCount int `json:"sourceCount"` - OnlineSourceCount int `json:"onlineSourceCount"` - LastSeen string `json:"lastSeen"` - RealtimeCount int `json:"realtimeCount"` - HistoryCount int `json:"historyCount"` - RawCount int `json:"rawCount"` - MileageCount int `json:"mileageCount"` - QualityIssueCount int `json:"qualityIssueCount"` - ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"` + VIN string `json:"vin"` + Plate string `json:"plate"` + Phone string `json:"phone"` + OEM string `json:"oem"` + Protocols []string `json:"protocols"` + PrimaryProtocol string `json:"primaryProtocol"` + CoverageStatus string `json:"coverageStatus"` + SourceCount int `json:"sourceCount"` + OnlineSourceCount int `json:"onlineSourceCount"` + LastSeen string `json:"lastSeen"` + RealtimeCount int `json:"realtimeCount"` + HistoryCount int `json:"historyCount"` + RawCount int `json:"rawCount"` + MileageCount int `json:"mileageCount"` + QualityIssueCount int `json:"qualityIssueCount"` + ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"` + SourceConsistency *VehicleSourceConsistency `json:"sourceConsistency,omitempty"` } type VehicleServiceSummary struct { diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store.go b/vehicle-data-platform/apps/api/internal/platform/production_store.go index ec37d233..0376762d 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go @@ -332,8 +332,7 @@ func (s *ProductionStore) VehicleServiceOverviews(ctx context.Context, query Veh overview.PrimaryProtocol = overview.Protocols[0] } overview.RealtimeCount = overview.SourceCount - overview.CoverageStatus = coverageStatus(overview.SourceCount, overview.OnlineSourceCount) - overview.ServiceStatus = buildVehicleServiceStatus(strings.TrimSpace(overview.VIN) != "", statusesForOverview(&overview)) + applyVehicleServiceOverviewDerivedFields(&overview, strings.TrimSpace(overview.VIN) != "") overviews = append(overviews, overview) current := &overviews[len(overviews)-1] for _, key := range []string{overview.VIN, overview.Plate, overview.Phone} { diff --git a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go index 356ac38e..856d0b42 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store_test.go @@ -40,3 +40,24 @@ func TestVehicleServiceOverviewMatchesPartialKeyword(t *testing.T) { t.Fatalf("overview %+v should not match unrelated keyword", overview) } } + +func TestVehicleServiceOverviewDerivedFieldsIncludeSourceConsistency(t *testing.T) { + overview := &VehicleServiceOverview{ + VIN: "LB9A32A24R0LS1426", + Protocols: []string{"GB32960", "JT808"}, + SourceCount: 2, + OnlineSourceCount: 1, + } + + applyVehicleServiceOverviewDerivedFields(overview, true) + + if overview.SourceConsistency == nil { + t.Fatalf("overview should include source consistency") + } + if overview.SourceConsistency.SourceCount != 2 || overview.SourceConsistency.OnlineSourceCount != 1 { + t.Fatalf("source consistency should mirror overview source coverage, got %+v", overview.SourceConsistency) + } + if overview.ServiceStatus == nil || overview.ServiceStatus.Status != "degraded" { + t.Fatalf("overview should keep canonical service status, got %+v", overview.ServiceStatus) + } +} diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index 153c167a..6e04decd 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -462,10 +462,23 @@ func buildVehicleServiceOverview(vin string, lookupKey string, resolution *Vehic if !resolved && resolution != nil { resolved = resolution.Resolved } - overview.ServiceStatus = buildVehicleServiceStatus(resolved, statusesForOverview(overview)) + applyVehicleServiceOverviewDerivedFields(overview, resolved) return overview } +func applyVehicleServiceOverviewDerivedFields(overview *VehicleServiceOverview, resolved bool) { + if overview == nil { + return + } + overview.CoverageStatus = coverageStatus(overview.SourceCount, overview.OnlineSourceCount) + overview.ServiceStatus = buildVehicleServiceStatus(resolved, statusesForOverview(overview)) + overview.SourceConsistency = &VehicleSourceConsistency{ + SourceCount: overview.SourceCount, + OnlineSourceCount: overview.OnlineSourceCount, + Scope: "all_sources", + } +} + func statusesForOverview(overview *VehicleServiceOverview) []VehicleSourceStatus { statuses := make([]VehicleSourceStatus, 0, overview.SourceCount) for index, protocol := range overview.Protocols { diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index fb353157..3a1fbfa2 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { Toast } from '@douyinfe/semi-ui'; import { api } from './api/client'; -import type { VehicleServiceOverview, VehicleServiceStatus } from './api/types'; +import type { VehicleSourceConsistency, VehicleServiceOverview, VehicleServiceStatus } from './api/types'; import { buildAppHash, parseAppHash } from './domain/appRoute'; import { AppShell, type PageKey } from './layout/AppShell'; import { Dashboard } from './pages/Dashboard'; @@ -23,6 +23,7 @@ export default function App() { const [linkIssueCount, setLinkIssueCount] = useState(null); const [currentVehicleStatus, setCurrentVehicleStatus] = useState(); const [currentVehicleLabel, setCurrentVehicleLabel] = useState(''); + const [currentVehicleConsistency, setCurrentVehicleConsistency] = useState(); const loadVehicleContext = useCallback(async (keyword: string) => { const lookupKey = keyword.trim(); @@ -34,6 +35,7 @@ export default function App() { const resolved = Boolean(overview.vin); setCurrentVehicleStatus(overview.serviceStatus ?? serviceStatusFromOverview(overview)); setCurrentVehicleLabel(resolved ? [overview.plate, overview.vin || nextKey].filter(Boolean).join(' / ') : lookupKey); + setCurrentVehicleConsistency(overview.sourceConsistency); return { resolved, nextKey, overview }; }, []); @@ -190,7 +192,7 @@ export default function App() { }; return ( - + {pages[activePage]} ); diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 18ee8346..66752604 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -126,6 +126,7 @@ export interface VehicleServiceOverview { mileageCount: number; qualityIssueCount: number; serviceStatus?: VehicleServiceStatus; + sourceConsistency?: VehicleSourceConsistency; } export interface VehicleServiceSummary { diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index c5e52a80..ea51dad1 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -11,7 +11,7 @@ import { } from '@douyinfe/semi-icons'; import type { ReactNode } from 'react'; import { useState } from 'react'; -import type { VehicleServiceStatus } from '../api/types'; +import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types'; export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality'; @@ -37,6 +37,7 @@ export function AppShell({ linkIssueCount, currentVehicleStatus, currentVehicleLabel, + currentVehicleConsistency, onChange, onVehicleSearch, children @@ -45,6 +46,7 @@ export function AppShell({ linkIssueCount: number | null; currentVehicleStatus?: VehicleServiceStatus; currentVehicleLabel?: string; + currentVehicleConsistency?: VehicleSourceConsistency; onChange: (page: PageKey) => void; onVehicleSearch: (keyword: string) => void | Promise; children: ReactNode; @@ -97,6 +99,11 @@ export function AppShell({ 当前车辆:{currentVehicleStatus.title} ) : null} + {currentVehicleConsistency ? ( + + 一致性:{currentVehicleConsistency.onlineSourceCount}/{currentVehicleConsistency.sourceCount} 来源 + + ) : null} 生产环境 多源接入 / 一个车辆服务