From 5e7c4b56ef9305057f744338da56de008ef96f3a Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 05:42:39 +0800 Subject: [PATCH] feat(platform): expose missing sources per vehicle --- .../apps/api/internal/platform/handler_test.go | 12 ++++++++++++ .../apps/api/internal/platform/mock_store.go | 1 + .../apps/api/internal/platform/model.go | 1 + .../apps/api/internal/platform/production_store.go | 1 + .../apps/api/internal/platform/service.go | 10 ++++++++++ vehicle-data-platform/apps/web/src/api/types.ts | 1 + .../apps/web/src/pages/Vehicles.tsx | 11 +++++++++++ vehicle-data-platform/apps/web/src/test/App.test.tsx | 9 +++++++-- 8 files changed, 44 insertions(+), 2 deletions(-) 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 b36c42ee..f05874ad 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -92,6 +92,18 @@ func TestHandlerVehicleCoverage(t *testing.T) { if body.Data.Items[0].ServiceStatus.Status != "degraded" { t.Fatalf("coverage row should expose degraded status, got %+v", body.Data.Items[0].ServiceStatus) } + var rawBody struct { + Data Page[struct { + VIN string `json:"vin"` + MissingProtocols []string `json:"missingProtocols"` + }] `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &rawBody); err != nil { + t.Fatalf("response JSON should decode missing source evidence: %v body=%s", err, rec.Body.String()) + } + if len(rawBody.Data.Items) == 0 || !containsString(rawBody.Data.Items[0].MissingProtocols, "YUTONG_MQTT") { + t.Fatalf("coverage row should expose missing canonical protocols, got %+v body=%s", rawBody.Data.Items, rec.Body.String()) + } } func TestHandlerVehicleCoverageFiltersServiceStatus(t *testing.T) { 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 901091f5..26374e4f 100644 --- a/vehicle-data-platform/apps/api/internal/platform/mock_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/mock_store.go @@ -132,6 +132,7 @@ func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[V } items := make([]VehicleCoverageRow, 0, len(byVIN)) for _, row := range byVIN { + row.MissingProtocols = missingCanonicalProtocols(row.Protocols) row.ServiceStatus = buildVehicleCoverageServiceStatus(*row) if keepCoverageRow(*row, query) { items = append(items, *row) diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index deecb28d..07831c67 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -55,6 +55,7 @@ type VehicleCoverageRow struct { Phone string `json:"phone"` OEM string `json:"oem"` Protocols []string `json:"protocols"` + MissingProtocols []string `json:"missingProtocols"` SourceCount int `json:"sourceCount"` OnlineSourceCount int `json:"onlineSourceCount"` Online bool `json:"online"` 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 94f69ca5..7227bbce 100644 --- a/vehicle-data-platform/apps/api/internal/platform/production_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/production_store.go @@ -268,6 +268,7 @@ func (s *ProductionStore) VehicleCoverage(ctx context.Context, query url.Values) return Page[VehicleCoverageRow]{}, err } row.Protocols = splitCSV(protocols) + row.MissingProtocols = missingCanonicalProtocols(row.Protocols) row.Online = online == 1 row.ServiceStatus = buildVehicleCoverageServiceStatus(row) items = append(items, row) diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index fcc20d5b..81af4fbe 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -58,6 +58,16 @@ type Service struct { var canonicalVehicleProtocols = []string{"GB32960", "JT808", "YUTONG_MQTT"} +func missingCanonicalProtocols(protocols []string) []string { + missing := make([]string, 0, len(canonicalVehicleProtocols)) + for _, protocol := range canonicalVehicleProtocols { + if !containsString(protocols, protocol) { + missing = append(missing, protocol) + } + } + return missing +} + func NewService(store Store) *Service { return &Service{store: store} } diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 81aafd88..c838a78c 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -52,6 +52,7 @@ export interface VehicleCoverageRow { phone: string; oem: string; protocols: string[]; + missingProtocols: string[]; sourceCount: number; onlineSourceCount: number; online: boolean; diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index 50d92882..ca774597 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -125,6 +125,17 @@ export function Vehicles({ ) }, + { + title: '缺失来源', + width: 220, + render: (_: unknown, row: VehicleCoverageRow) => ( + + {(row.missingProtocols ?? []).length > 0 + ? row.missingProtocols.map((protocol) => 缺 {protocol}) + : 完整} + + ) + }, { title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleCoverageRow) => sourceEvidenceText(row) }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '最后在线', dataIndex: 'lastSeen', width: 170 }, 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 fae83056..d2216a99 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -445,6 +445,7 @@ test('filters vehicle list by missing source evidence', async () => { phone: '13307795425', oem: 'G7s', protocols: ['GB32960', 'JT808'], + missingProtocols: ['YUTONG_MQTT'], sourceCount: 2, onlineSourceCount: 0, online: false, @@ -472,9 +473,13 @@ test('filters vehicle list by missing source evidence', async () => { render(); - expect(await screen.findByText('缺失来源')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getAllByText('缺失来源').length).toBeGreaterThanOrEqual(2); + }); + expect(screen.getAllByText('缺 YUTONG_MQTT').length).toBeGreaterThanOrEqual(1); fireEvent.click(screen.getByTestId('missing-protocol-filter')); - fireEvent.click(await screen.findByText('缺 YUTONG_MQTT')); + const mqttOptions = await screen.findAllByText('缺 YUTONG_MQTT'); + fireEvent.click(mqttOptions[mqttOptions.length - 1]); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => {