feat(platform): summarize missing vehicle sources

This commit is contained in:
lingniu
2026-07-04 06:13:38 +08:00
parent 1c210897bb
commit 8cd90f629b
7 changed files with 53 additions and 7 deletions

View File

@@ -185,6 +185,9 @@ func TestHandlerVehicleCoverageSummary(t *testing.T) {
if body.Data.OnlineVehicles != 1 || body.Data.UnboundVehicles != 0 {
t.Fatalf("coverage summary should expose filtered online and binding totals, got %+v", body.Data)
}
if len(body.Data.MissingSources) == 0 {
t.Fatalf("coverage summary should expose missing source counts, got %+v", body.Data)
}
}
func TestSplitCSVEmptyReturnsEmptySlice(t *testing.T) {

View File

@@ -168,6 +168,15 @@ func (m *MockStore) VehicleCoverageSummary(ctx context.Context, query url.Values
summary.UnboundVehicles++
}
}
for _, protocol := range canonicalVehicleProtocols {
missing := 0
for _, row := range coverage.Items {
if !containsString(row.Protocols, protocol) {
missing++
}
}
summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing})
}
return summary, nil
}

View File

@@ -65,12 +65,13 @@ type VehicleCoverageRow struct {
}
type VehicleCoverageSummary struct {
TotalVehicles int `json:"totalVehicles"`
OnlineVehicles int `json:"onlineVehicles"`
SingleSourceVehicles int `json:"singleSourceVehicles"`
MultiSourceVehicles int `json:"multiSourceVehicles"`
NoDataVehicles int `json:"noDataVehicles"`
UnboundVehicles int `json:"unboundVehicles"`
TotalVehicles int `json:"totalVehicles"`
OnlineVehicles int `json:"onlineVehicles"`
SingleSourceVehicles int `json:"singleSourceVehicles"`
MultiSourceVehicles int `json:"multiSourceVehicles"`
NoDataVehicles int `json:"noDataVehicles"`
UnboundVehicles int `json:"unboundVehicles"`
MissingSources []MissingSourceStat `json:"missingSources"`
}
type VehicleIdentityResolution struct {

View File

@@ -297,9 +297,30 @@ func (s *ProductionStore) VehicleCoverageSummary(ctx context.Context, query url.
); err != nil {
return VehicleCoverageSummary{}, err
}
missingSources, err := s.coverageMissingSourceStats(ctx, query)
if err != nil {
return VehicleCoverageSummary{}, err
}
summary.MissingSources = missingSources
return summary, nil
}
func (s *ProductionStore) coverageMissingSourceStats(ctx context.Context, query url.Values) ([]MissingSourceStat, error) {
out := make([]MissingSourceStat, 0, len(canonicalVehicleProtocols))
for _, protocol := range canonicalVehicleProtocols {
next := copyValues(query)
next.Set("missingProtocol", protocol)
next.Set("limit", "1")
next.Set("offset", "0")
page, err := s.VehicleCoverage(ctx, next)
if err != nil {
return nil, err
}
out = append(out, MissingSourceStat{Protocol: protocol, Count: page.Total})
}
return out, nil
}
func (s *ProductionStore) VehicleRealtime(ctx context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
built := buildVehicleRealtimeSQL(query)
total := 0

View File

@@ -68,6 +68,7 @@ export interface VehicleCoverageSummary {
multiSourceVehicles: number;
noDataVehicles: number;
unboundVehicles: number;
missingSources: MissingSourceStat[];
}
export interface VehicleIdentityResolution {

View File

@@ -65,6 +65,14 @@ export function Vehicles({
{ label: '暂无来源车辆', value: (summary?.noDataVehicles ?? 0).toLocaleString(), filters: { serviceStatus: 'no_data' } },
{ label: '待绑定', value: (summary?.unboundVehicles ?? 0).toLocaleString(), filters: { bindingStatus: 'unbound' } }
];
for (const source of summary?.missingSources ?? []) {
if (source.count <= 0) continue;
items.push({
label: `${source.protocol}`,
value: source.count.toLocaleString(),
filters: { missingProtocol: source.protocol }
});
}
return items;
}, [pagination.total, summary]);

View File

@@ -427,7 +427,8 @@ test('filters vehicle list by missing source evidence', async () => {
singleSourceVehicles: 0,
multiSourceVehicles: 1,
noDataVehicles: 0,
unboundVehicles: 0
unboundVehicles: 0,
missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }]
},
traceId: 'trace-test',
timestamp: 1783094400000
@@ -477,6 +478,8 @@ test('filters vehicle list by missing source evidence', async () => {
expect(screen.getAllByText('缺失来源').length).toBeGreaterThanOrEqual(2);
});
expect(screen.getAllByText('缺 YUTONG_MQTT').length).toBeGreaterThanOrEqual(1);
fireEvent.click(screen.getByRole('button', { name: '缺 YUTONG_MQTT 1' }));
expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT');
fireEvent.click(screen.getByRole('button', { name: '缺 YUTONG_MQTT' }));
expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT');
await waitFor(() => {