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(() => {