feat(platform): summarize missing vehicle sources
This commit is contained in:
@@ -185,6 +185,9 @@ func TestHandlerVehicleCoverageSummary(t *testing.T) {
|
|||||||
if body.Data.OnlineVehicles != 1 || body.Data.UnboundVehicles != 0 {
|
if body.Data.OnlineVehicles != 1 || body.Data.UnboundVehicles != 0 {
|
||||||
t.Fatalf("coverage summary should expose filtered online and binding totals, got %+v", body.Data)
|
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) {
|
func TestSplitCSVEmptyReturnsEmptySlice(t *testing.T) {
|
||||||
|
|||||||
@@ -168,6 +168,15 @@ func (m *MockStore) VehicleCoverageSummary(ctx context.Context, query url.Values
|
|||||||
summary.UnboundVehicles++
|
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
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ type VehicleCoverageSummary struct {
|
|||||||
MultiSourceVehicles int `json:"multiSourceVehicles"`
|
MultiSourceVehicles int `json:"multiSourceVehicles"`
|
||||||
NoDataVehicles int `json:"noDataVehicles"`
|
NoDataVehicles int `json:"noDataVehicles"`
|
||||||
UnboundVehicles int `json:"unboundVehicles"`
|
UnboundVehicles int `json:"unboundVehicles"`
|
||||||
|
MissingSources []MissingSourceStat `json:"missingSources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VehicleIdentityResolution struct {
|
type VehicleIdentityResolution struct {
|
||||||
|
|||||||
@@ -297,9 +297,30 @@ func (s *ProductionStore) VehicleCoverageSummary(ctx context.Context, query url.
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return VehicleCoverageSummary{}, err
|
return VehicleCoverageSummary{}, err
|
||||||
}
|
}
|
||||||
|
missingSources, err := s.coverageMissingSourceStats(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return VehicleCoverageSummary{}, err
|
||||||
|
}
|
||||||
|
summary.MissingSources = missingSources
|
||||||
return summary, nil
|
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) {
|
func (s *ProductionStore) VehicleRealtime(ctx context.Context, query url.Values) (Page[VehicleRealtimeRow], error) {
|
||||||
built := buildVehicleRealtimeSQL(query)
|
built := buildVehicleRealtimeSQL(query)
|
||||||
total := 0
|
total := 0
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export interface VehicleCoverageSummary {
|
|||||||
multiSourceVehicles: number;
|
multiSourceVehicles: number;
|
||||||
noDataVehicles: number;
|
noDataVehicles: number;
|
||||||
unboundVehicles: number;
|
unboundVehicles: number;
|
||||||
|
missingSources: MissingSourceStat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VehicleIdentityResolution {
|
export interface VehicleIdentityResolution {
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ export function Vehicles({
|
|||||||
{ label: '暂无来源车辆', value: (summary?.noDataVehicles ?? 0).toLocaleString(), filters: { serviceStatus: 'no_data' } },
|
{ label: '暂无来源车辆', value: (summary?.noDataVehicles ?? 0).toLocaleString(), filters: { serviceStatus: 'no_data' } },
|
||||||
{ label: '待绑定', value: (summary?.unboundVehicles ?? 0).toLocaleString(), filters: { bindingStatus: 'unbound' } }
|
{ 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;
|
return items;
|
||||||
}, [pagination.total, summary]);
|
}, [pagination.total, summary]);
|
||||||
|
|
||||||
|
|||||||
@@ -427,7 +427,8 @@ test('filters vehicle list by missing source evidence', async () => {
|
|||||||
singleSourceVehicles: 0,
|
singleSourceVehicles: 0,
|
||||||
multiSourceVehicles: 1,
|
multiSourceVehicles: 1,
|
||||||
noDataVehicles: 0,
|
noDataVehicles: 0,
|
||||||
unboundVehicles: 0
|
unboundVehicles: 0,
|
||||||
|
missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }]
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
@@ -477,6 +478,8 @@ test('filters vehicle list by missing source evidence', async () => {
|
|||||||
expect(screen.getAllByText('缺失来源').length).toBeGreaterThanOrEqual(2);
|
expect(screen.getAllByText('缺失来源').length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
expect(screen.getAllByText('缺 YUTONG_MQTT').length).toBeGreaterThanOrEqual(1);
|
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' }));
|
fireEvent.click(screen.getByRole('button', { name: '缺 YUTONG_MQTT' }));
|
||||||
expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT');
|
expect(window.location.hash).toBe('#/vehicles?missingProtocol=YUTONG_MQTT');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user