feat(platform): expose coverage service status
This commit is contained in:
@@ -50,6 +50,18 @@ func TestHandlerVehicleCoverage(t *testing.T) {
|
||||
t.Fatalf("response missing %q: %s", want, rec.Body.String())
|
||||
}
|
||||
}
|
||||
var body struct {
|
||||
Data Page[VehicleCoverageRow] `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
|
||||
}
|
||||
if len(body.Data.Items) == 0 || body.Data.Items[0].ServiceStatus == nil {
|
||||
t.Fatalf("coverage row should include canonical vehicle service status: %s", rec.Body.String())
|
||||
}
|
||||
if body.Data.Items[0].ServiceStatus.Status != "degraded" {
|
||||
t.Fatalf("coverage row should expose degraded status, got %+v", body.Data.Items[0].ServiceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleCoverageFiltersServiceStatus(t *testing.T) {
|
||||
|
||||
@@ -94,6 +94,7 @@ func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[V
|
||||
}
|
||||
items := make([]VehicleCoverageRow, 0, len(byVIN))
|
||||
for _, row := range byVIN {
|
||||
row.ServiceStatus = buildVehicleCoverageServiceStatus(*row)
|
||||
if keepCoverageRow(*row, query) {
|
||||
items = append(items, *row)
|
||||
}
|
||||
|
||||
@@ -49,16 +49,17 @@ type VehicleRow struct {
|
||||
}
|
||||
|
||||
type VehicleCoverageRow struct {
|
||||
VIN string `json:"vin"`
|
||||
Plate string `json:"plate"`
|
||||
Phone string `json:"phone"`
|
||||
OEM string `json:"oem"`
|
||||
Protocols []string `json:"protocols"`
|
||||
SourceCount int `json:"sourceCount"`
|
||||
OnlineSourceCount int `json:"onlineSourceCount"`
|
||||
Online bool `json:"online"`
|
||||
LastSeen string `json:"lastSeen"`
|
||||
BindingStatus string `json:"bindingStatus"`
|
||||
VIN string `json:"vin"`
|
||||
Plate string `json:"plate"`
|
||||
Phone string `json:"phone"`
|
||||
OEM string `json:"oem"`
|
||||
Protocols []string `json:"protocols"`
|
||||
SourceCount int `json:"sourceCount"`
|
||||
OnlineSourceCount int `json:"onlineSourceCount"`
|
||||
Online bool `json:"online"`
|
||||
LastSeen string `json:"lastSeen"`
|
||||
BindingStatus string `json:"bindingStatus"`
|
||||
ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"`
|
||||
}
|
||||
|
||||
type VehicleIdentityResolution struct {
|
||||
|
||||
@@ -163,6 +163,7 @@ func (s *ProductionStore) VehicleCoverage(ctx context.Context, query url.Values)
|
||||
}
|
||||
row.Protocols = splitCSV(protocols)
|
||||
row.Online = online == 1
|
||||
row.ServiceStatus = buildVehicleCoverageServiceStatus(row)
|
||||
items = append(items, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
|
||||
@@ -478,6 +478,57 @@ func buildVehicleServiceStatus(resolved bool, statuses []VehicleSourceStatus) *V
|
||||
}
|
||||
}
|
||||
|
||||
func buildVehicleCoverageServiceStatus(row VehicleCoverageRow) *VehicleServiceStatus {
|
||||
if row.BindingStatus != "bound" {
|
||||
return &VehicleServiceStatus{
|
||||
Status: "identity_required",
|
||||
Severity: "warning",
|
||||
Title: "身份未绑定",
|
||||
Detail: "车辆身份绑定不完整,需先维护 VIN、车牌或手机号映射。",
|
||||
SourceCount: row.SourceCount,
|
||||
OnlineSourceCount: row.OnlineSourceCount,
|
||||
}
|
||||
}
|
||||
if row.SourceCount == 0 {
|
||||
return &VehicleServiceStatus{
|
||||
Status: "no_data",
|
||||
Severity: "warning",
|
||||
Title: "暂无数据来源",
|
||||
Detail: "车辆已绑定,但暂未查询到 32960、808 或 MQTT 数据来源。",
|
||||
SourceCount: row.SourceCount,
|
||||
OnlineSourceCount: row.OnlineSourceCount,
|
||||
}
|
||||
}
|
||||
if row.OnlineSourceCount == 0 {
|
||||
return &VehicleServiceStatus{
|
||||
Status: "offline",
|
||||
Severity: "error",
|
||||
Title: "车辆离线",
|
||||
Detail: "所有已知数据来源均未在线,需要检查平台转发、终端上报或链路状态。",
|
||||
SourceCount: row.SourceCount,
|
||||
OnlineSourceCount: row.OnlineSourceCount,
|
||||
}
|
||||
}
|
||||
if row.OnlineSourceCount < row.SourceCount {
|
||||
return &VehicleServiceStatus{
|
||||
Status: "degraded",
|
||||
Severity: "warning",
|
||||
Title: "部分来源离线",
|
||||
Detail: sourceCoverageDetail(row.SourceCount, row.OnlineSourceCount, "车辆服务可用但需要关注离线来源。"),
|
||||
SourceCount: row.SourceCount,
|
||||
OnlineSourceCount: row.OnlineSourceCount,
|
||||
}
|
||||
}
|
||||
return &VehicleServiceStatus{
|
||||
Status: "healthy",
|
||||
Severity: "ok",
|
||||
Title: "服务正常",
|
||||
Detail: sourceCoverageDetail(row.SourceCount, row.OnlineSourceCount, "全部已知来源在线。"),
|
||||
SourceCount: row.SourceCount,
|
||||
OnlineSourceCount: row.OnlineSourceCount,
|
||||
}
|
||||
}
|
||||
|
||||
func sourceCoverageDetail(sourceCount int, onlineSourceCount int, suffix string) string {
|
||||
return strconv.Itoa(onlineSourceCount) + "/" + strconv.Itoa(sourceCount) + " 个来源在线," + suffix
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface VehicleCoverageRow {
|
||||
online: boolean;
|
||||
lastSeen: string;
|
||||
bindingStatus: string;
|
||||
serviceStatus?: VehicleServiceStatus;
|
||||
}
|
||||
|
||||
export interface VehicleIdentityResolution {
|
||||
|
||||
@@ -7,6 +7,12 @@ import { PageHeader } from '../components/PageHeader';
|
||||
import { StatusTag } from '../components/StatusTag';
|
||||
|
||||
function vehicleServiceStatus(row: VehicleCoverageRow) {
|
||||
if (row.serviceStatus) {
|
||||
return {
|
||||
label: row.serviceStatus.title,
|
||||
color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const
|
||||
};
|
||||
}
|
||||
if (row.bindingStatus !== 'bound') {
|
||||
return { label: '身份未绑定', color: 'orange' as const };
|
||||
}
|
||||
|
||||
@@ -303,11 +303,19 @@ test('shows vehicle service status in vehicle list', async () => {
|
||||
phone: '13307795425',
|
||||
oem: 'G7s',
|
||||
protocols: ['GB32960', 'JT808'],
|
||||
sourceCount: 2,
|
||||
sourceCount: 1,
|
||||
onlineSourceCount: 1,
|
||||
online: true,
|
||||
lastSeen: '2026-07-03 20:12:10',
|
||||
bindingStatus: 'bound'
|
||||
bindingStatus: 'bound',
|
||||
serviceStatus: {
|
||||
status: 'degraded',
|
||||
severity: 'warning',
|
||||
title: '部分来源离线',
|
||||
detail: '由车辆服务 API 统一判定',
|
||||
sourceCount: 2,
|
||||
onlineSourceCount: 1
|
||||
}
|
||||
}],
|
||||
total: 1,
|
||||
limit: 20,
|
||||
|
||||
@@ -71,7 +71,7 @@ Returns VIN-level realtime rows. Protocol is a source filter, not a product boun
|
||||
GET /api/vehicles/coverage?keyword=粤AG18312&serviceStatus=degraded&limit=20&offset=0
|
||||
```
|
||||
|
||||
Returns VIN-level source coverage rows for the vehicle service list. `serviceStatus` accepts `healthy`, `degraded`, `offline`, and `identity_required`.
|
||||
Returns VIN-level source coverage rows for the vehicle service list. Each row includes the canonical vehicle-level `serviceStatus` so frontend, exports, and external integrations share the same health definition. `serviceStatus` accepts `healthy`, `degraded`, `offline`, and `identity_required`.
|
||||
|
||||
### History Locations
|
||||
|
||||
|
||||
Reference in New Issue
Block a user