From 1a86e5d38c3def6ebc673ddff7dddac27641b4b9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 03:00:50 +0800 Subject: [PATCH] feat(platform): return service status in vehicle overview --- .../api/internal/platform/handler_test.go | 3 ++ .../apps/api/internal/platform/model.go | 31 ++++++++++--------- .../apps/api/internal/platform/service.go | 24 ++++++++++++++ vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/api/client.test.ts | 11 ++++++- .../apps/web/src/api/types.ts | 1 + .../apps/web/src/test/App.test.tsx | 12 +++++-- 7 files changed, 65 insertions(+), 19 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 36b37a09..1c11b159 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -224,6 +224,9 @@ func TestHandlerVehicleServiceOverviewEndpoint(t *testing.T) { if body.Data.SourceCount != 2 || body.Data.OnlineSourceCount != 1 || body.Data.CoverageStatus != "partial" { t.Fatalf("overview endpoint should summarize vehicle service coverage, got %+v", body.Data) } + if body.Data.ServiceStatus == nil || body.Data.ServiceStatus.Status != "degraded" || body.Data.ServiceStatus.Title != "部分来源离线" { + t.Fatalf("overview endpoint should expose canonical service status, got %+v", body.Data.ServiceStatus) + } if strings.Contains(rec.Body.String(), `"raw"`) || strings.Contains(rec.Body.String(), `"history"`) { t.Fatalf("overview endpoint should not return heavy detail pages: %s", rec.Body.String()) } diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index 81efec57..35b78fd4 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -95,21 +95,22 @@ type VehicleDetail struct { } type VehicleServiceOverview struct { - VIN string `json:"vin"` - Plate string `json:"plate"` - Phone string `json:"phone"` - OEM string `json:"oem"` - Protocols []string `json:"protocols"` - PrimaryProtocol string `json:"primaryProtocol"` - CoverageStatus string `json:"coverageStatus"` - SourceCount int `json:"sourceCount"` - OnlineSourceCount int `json:"onlineSourceCount"` - LastSeen string `json:"lastSeen"` - RealtimeCount int `json:"realtimeCount"` - HistoryCount int `json:"historyCount"` - RawCount int `json:"rawCount"` - MileageCount int `json:"mileageCount"` - QualityIssueCount int `json:"qualityIssueCount"` + VIN string `json:"vin"` + Plate string `json:"plate"` + Phone string `json:"phone"` + OEM string `json:"oem"` + Protocols []string `json:"protocols"` + PrimaryProtocol string `json:"primaryProtocol"` + CoverageStatus string `json:"coverageStatus"` + SourceCount int `json:"sourceCount"` + OnlineSourceCount int `json:"onlineSourceCount"` + LastSeen string `json:"lastSeen"` + RealtimeCount int `json:"realtimeCount"` + HistoryCount int `json:"historyCount"` + RawCount int `json:"rawCount"` + MileageCount int `json:"mileageCount"` + QualityIssueCount int `json:"qualityIssueCount"` + ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"` } type VehicleServiceStatus struct { diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index 62c7daf9..7a3e19c7 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -296,9 +296,33 @@ func buildVehicleServiceOverview(vin string, lookupKey string, resolution *Vehic overview.PrimaryProtocol = overview.Protocols[0] } overview.CoverageStatus = coverageStatus(overview.SourceCount, overview.OnlineSourceCount) + resolved := identity != nil && strings.TrimSpace(identity.VIN) != "" + if !resolved && resolution != nil { + resolved = resolution.Resolved + } + overview.ServiceStatus = buildVehicleServiceStatus(resolved, statusesForOverview(overview)) return overview } +func statusesForOverview(overview *VehicleServiceOverview) []VehicleSourceStatus { + statuses := make([]VehicleSourceStatus, 0, overview.SourceCount) + for index, protocol := range overview.Protocols { + statuses = append(statuses, VehicleSourceStatus{ + Protocol: protocol, + Online: index < overview.OnlineSourceCount, + LastSeen: overview.LastSeen, + }) + } + for len(statuses) < overview.SourceCount { + statuses = append(statuses, VehicleSourceStatus{ + Protocol: "UNKNOWN", + Online: len(statuses) < overview.OnlineSourceCount, + LastSeen: overview.LastSeen, + }) + } + return statuses +} + func pageCount(total int, itemCount int) int { if total > 0 { return total diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index e462a006..4cd0ac2b 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -31,7 +31,7 @@ export default function App() { const overview = await api.vehicleServiceOverview(new URLSearchParams({ keyword: lookupKey })); const nextKey = overview.vin || lookupKey; const resolved = Boolean(overview.vin); - setCurrentVehicleStatus(serviceStatusFromOverview(overview)); + setCurrentVehicleStatus(overview.serviceStatus ?? serviceStatusFromOverview(overview)); setCurrentVehicleLabel(resolved ? [overview.plate, overview.vin || nextKey].filter(Boolean).join(' / ') : lookupKey); return { resolved, nextKey, overview }; }, []); diff --git a/vehicle-data-platform/apps/web/src/api/client.test.ts b/vehicle-data-platform/apps/web/src/api/client.test.ts index 70e190de..a3112bf9 100644 --- a/vehicle-data-platform/apps/web/src/api/client.test.ts +++ b/vehicle-data-platform/apps/web/src/api/client.test.ts @@ -83,7 +83,15 @@ test('vehicleServiceOverview sends keyword to the lightweight overview endpoint' historyCount: 0, rawCount: 0, mileageCount: 0, - qualityIssueCount: 0 + qualityIssueCount: 0, + serviceStatus: { + status: 'degraded', + severity: 'warning', + title: '部分来源离线', + detail: '1/2 个来源在线', + sourceCount: 2, + onlineSourceCount: 1 + } }, traceId: 'trace-test', timestamp: 1783094400000 @@ -94,6 +102,7 @@ test('vehicleServiceOverview sends keyword to the lightweight overview endpoint' expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312', undefined); expect(result.coverageStatus).toBe('partial'); + expect(result.serviceStatus?.status).toBe('degraded'); }); test('api errors include backend message, detail, and trace id', async () => { diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index b9317f42..d9ade0a1 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -107,6 +107,7 @@ export interface VehicleServiceOverview { rawCount: number; mileageCount: number; qualityIssueCount: number; + serviceStatus?: VehicleServiceStatus; } export interface VehicleServiceStatus { 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 79b42555..63666e1b 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -100,7 +100,15 @@ test('shows resolved vehicle service status after topbar search', async () => { historyCount: 0, rawCount: 0, mileageCount: 0, - qualityIssueCount: 0 + qualityIssueCount: 0, + serviceStatus: { + status: 'degraded', + severity: 'warning', + title: '后端判定部分离线', + detail: '后端统一车辆服务状态', + sourceCount: 2, + onlineSourceCount: 1 + } }, traceId: 'trace-test', timestamp: 1783094400000 @@ -143,7 +151,7 @@ test('shows resolved vehicle service status after topbar search', async () => { fireEvent.change(screen.getByPlaceholderText('搜索 VIN / 车牌 / 手机号'), { target: { value: '粤AG18312' } }); fireEvent.click(screen.getByRole('button', { name: '查询车辆' })); - expect(await screen.findByText('当前车辆:部分来源离线')).toBeInTheDocument(); + expect(await screen.findByText('当前车辆:后端判定部分离线')).toBeInTheDocument(); expect(screen.getByText('粤AG18312 / LB9A32A24R0LS1426')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service/overview?keyword=%E7%B2%A4AG18312'), undefined); expect(fetchMock).not.toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/resolve'), undefined);