feat(platform): expose coverage service status

This commit is contained in:
lingniu
2026-07-04 02:03:12 +08:00
parent 5d401a8f0b
commit 526377c13f
9 changed files with 94 additions and 13 deletions

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -56,6 +56,7 @@ export interface VehicleCoverageRow {
online: boolean;
lastSeen: string;
bindingStatus: string;
serviceStatus?: VehicleServiceStatus;
}
export interface VehicleIdentityResolution {

View File

@@ -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 };
}

View File

@@ -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,

View File

@@ -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