From 1d691306db12d2fc037c0c51e9d0b6eda47a2edb Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 07:54:38 +0800 Subject: [PATCH] feat(platform): keep canonical source summary slots --- .../apps/api/internal/platform/mock_store.go | 2 +- .../api/internal/platform/production_store.go | 5 ++- .../apps/api/internal/platform/service.go | 31 +++++++++++++++++++ .../api/internal/platform/service_test.go | 19 ++++++++++++ .../apps/web/src/pages/Dashboard.tsx | 9 +++++- .../apps/web/src/test/App.test.tsx | 11 +++++-- 6 files changed, 72 insertions(+), 5 deletions(-) diff --git a/vehicle-data-platform/apps/api/internal/platform/mock_store.go b/vehicle-data-platform/apps/api/internal/platform/mock_store.go index ccd1f140..58e8db3b 100644 --- a/vehicle-data-platform/apps/api/internal/platform/mock_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/mock_store.go @@ -261,6 +261,7 @@ func (m *MockStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSu for _, protocol := range protocolCounts { summary.Protocols = append(summary.Protocols, *protocol) } + summary.Protocols = completeProtocolStats(summary.Protocols) for _, protocol := range canonicalVehicleProtocols { missing := 0 for _, row := range coverage.Items { @@ -271,7 +272,6 @@ func (m *MockStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSu summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing}) } sort.Slice(summary.ServiceStatuses, func(i, j int) bool { return summary.ServiceStatuses[i].Status < summary.ServiceStatuses[j].Status }) - sort.Slice(summary.Protocols, func(i, j int) bool { return summary.Protocols[i].Protocol < summary.Protocols[j].Protocol }) return summary, nil } 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 0abc8ed3..0600203c 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go @@ -923,7 +923,10 @@ func (s *ProductionStore) protocolStats(ctx context.Context) ([]ProtocolStat, er } out = append(out, row) } - return out, rows.Err() + if err := rows.Err(); err != nil { + return nil, err + } + return completeProtocolStats(out), nil } func parsedFieldsFromString(value string) map[string]any { diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index 9838657e..dfc872d1 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -58,6 +58,37 @@ type Service struct { var canonicalVehicleProtocols = []string{"GB32960", "JT808", "YUTONG_MQTT"} +func completeProtocolStats(stats []ProtocolStat) []ProtocolStat { + byProtocol := make(map[string]ProtocolStat, len(stats)+len(canonicalVehicleProtocols)) + for _, stat := range stats { + protocol := strings.TrimSpace(stat.Protocol) + if protocol == "" { + continue + } + stat.Protocol = protocol + byProtocol[protocol] = stat + } + out := make([]ProtocolStat, 0, len(byProtocol)+len(canonicalVehicleProtocols)) + seen := map[string]struct{}{} + for _, protocol := range canonicalVehicleProtocols { + stat := byProtocol[protocol] + stat.Protocol = protocol + out = append(out, stat) + seen[protocol] = struct{}{} + } + extra := make([]string, 0) + for protocol := range byProtocol { + if _, ok := seen[protocol]; !ok { + extra = append(extra, protocol) + } + } + sort.Strings(extra) + for _, protocol := range extra { + out = append(out, byProtocol[protocol]) + } + return out +} + func missingCanonicalProtocols(protocols []string) []string { missing := make([]string, 0, len(canonicalVehicleProtocols)) for _, protocol := range canonicalVehicleProtocols { diff --git a/vehicle-data-platform/apps/api/internal/platform/service_test.go b/vehicle-data-platform/apps/api/internal/platform/service_test.go index c0b18ec8..ac7bcf88 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/service_test.go @@ -83,3 +83,22 @@ func TestVehicleServiceSummaryCountsProtocolOnlineByProtocolSlot(t *testing.T) { t.Fatalf("GB32960 online count must use GB32960 source status only, got %+v", gb32960) } } + +func TestCompleteProtocolStatsIncludesCanonicalSlots(t *testing.T) { + stats := completeProtocolStats([]ProtocolStat{{Protocol: "JT808", Online: 2, Total: 5}}) + byProtocol := map[string]ProtocolStat{} + for _, stat := range stats { + byProtocol[stat.Protocol] = stat + } + for _, protocol := range canonicalVehicleProtocols { + if _, ok := byProtocol[protocol]; !ok { + t.Fatalf("canonical protocol %s should be present, got %+v", protocol, stats) + } + } + if byProtocol["JT808"].Online != 2 || byProtocol["JT808"].Total != 5 { + t.Fatalf("existing protocol stats should be preserved, got %+v", byProtocol["JT808"]) + } + if byProtocol["GB32960"].Online != 0 || byProtocol["GB32960"].Total != 0 { + t.Fatalf("missing canonical protocol should be exposed as zero slot, got %+v", byProtocol["GB32960"]) + } +} diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index fb184e92..c25402c5 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -44,6 +44,13 @@ function formatCount(value?: number | null) { return value == null ? '0' : value.toLocaleString(); } +function formatProtocolRate(row: ProtocolStat) { + if (!Number.isFinite(row.total) || row.total <= 0) { + return '0%'; + } + return `${Math.round((row.online / row.total) * 100)}%`; +} + function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) { if (row.serviceStatus) { return { @@ -201,7 +208,7 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { on { title: '总数', dataIndex: 'total' }, { title: '在线率', - render: (_: unknown, row: ProtocolStat) => `${Math.round((row.online / row.total) * 100)}%` + render: (_: unknown, row: ProtocolStat) => formatProtocolRate(row) }, { title: '缺失车辆', diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index b67ca9e6..885c0472 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -96,7 +96,11 @@ test('dashboard renders vehicle service summary metrics', async () => { { status: 'healthy', title: '服务正常', count: 161 }, { status: 'degraded', title: '部分来源离线', count: 41 } ], - protocols: [{ protocol: 'JT808', online: 157, total: 388 }] + protocols: [ + { protocol: 'JT808', online: 157, total: 388 }, + { protocol: 'YUTONG_MQTT', online: 0, total: 0 } + ], + missingSources: [{ protocol: 'YUTONG_MQTT', count: 1033 }] }, traceId: 'trace-test', timestamp: 1783094400000 @@ -116,7 +120,7 @@ test('dashboard renders vehicle service summary metrics', async () => { render(); expect(await screen.findByText('总车辆')).toBeInTheDocument(); - expect(screen.getByText('1,033')).toBeInTheDocument(); + expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('单源车辆')).toBeInTheDocument(); expect(screen.getByText('391')).toBeInTheDocument(); expect(screen.getByText('多源车辆')).toBeInTheDocument(); @@ -125,6 +129,9 @@ test('dashboard renders vehicle service summary metrics', async () => { expect(screen.getByText('461')).toBeInTheDocument(); expect(screen.getByText('身份未绑定')).toBeInTheDocument(); expect(screen.getByText('来源证据在线分布')).toBeInTheDocument(); + expect(screen.getByText('YUTONG_MQTT')).toBeInTheDocument(); + expect(screen.getByText('0%')).toBeInTheDocument(); + expect(screen.queryByText('NaN%')).not.toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', undefined); });