feat(platform): summarize vehicle service status

This commit is contained in:
lingniu
2026-07-04 01:47:24 +08:00
parent 8e800f795c
commit aa3b39f5e3
7 changed files with 156 additions and 0 deletions

View File

@@ -88,6 +88,31 @@ func TestHandlerVehicleServiceCanonicalEndpoint(t *testing.T) {
}
}
func TestHandlerVehicleServiceIncludesVehicleLevelStatus(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/vehicle-service?keyword=粤AG18312", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var body struct {
Data VehicleDetail `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 body.Data.ServiceStatus == nil {
t.Fatalf("vehicle service should include serviceStatus: %s", rec.Body.String())
}
if body.Data.ServiceStatus.Status != "degraded" || body.Data.ServiceStatus.Title != "部分来源离线" {
t.Fatalf("vehicle service should summarize partial source health, got %+v body=%s", body.Data.ServiceStatus, rec.Body.String())
}
if body.Data.ServiceStatus.OnlineSourceCount != 1 || body.Data.ServiceStatus.SourceCount != 2 {
t.Fatalf("vehicle service should expose source counts, got %+v body=%s", body.Data.ServiceStatus, rec.Body.String())
}
}
func TestHandlerVehicleDetailResolvesPlateToVIN(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()

View File

@@ -73,6 +73,7 @@ type VehicleDetail struct {
Resolution *VehicleIdentityResolution `json:"resolution,omitempty"`
Identity *VehicleRow `json:"identity,omitempty"`
RealtimeSummary *VehicleRealtimeRow `json:"realtimeSummary,omitempty"`
ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"`
Sources []string `json:"sources"`
SourceStatus []VehicleSourceStatus `json:"sourceStatus"`
Realtime []RealtimeLocationRow `json:"realtime"`
@@ -82,6 +83,15 @@ type VehicleDetail struct {
Quality Page[QualityIssueRow] `json:"quality"`
}
type VehicleServiceStatus struct {
Status string `json:"status"`
Severity string `json:"severity"`
Title string `json:"title"`
Detail string `json:"detail"`
SourceCount int `json:"sourceCount"`
OnlineSourceCount int `json:"onlineSourceCount"`
}
type VehicleSourceStatus struct {
Protocol string `json:"protocol"`
Online bool `json:"online"`

View File

@@ -4,6 +4,7 @@ import (
"context"
"net/url"
"sort"
"strconv"
"strings"
)
@@ -118,6 +119,7 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
LookupKey: keyword,
LookupResolved: false,
Resolution: &resolution,
ServiceStatus: buildVehicleServiceStatus(false, nil),
Sources: []string{},
SourceStatus: []VehicleSourceStatus{},
Realtime: []RealtimeLocationRow{},
@@ -176,6 +178,7 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
Resolution: &resolution,
Identity: identity,
RealtimeSummary: summary,
ServiceStatus: buildVehicleServiceStatus(true, sourceStatus),
Sources: sourceNames(sourceStatus),
SourceStatus: sourceStatus,
Realtime: realtime.Items,
@@ -419,6 +422,66 @@ func vehicleSourceStatus(vehicles []VehicleRow, realtime []RealtimeLocationRow,
return out
}
func buildVehicleServiceStatus(resolved bool, statuses []VehicleSourceStatus) *VehicleServiceStatus {
if !resolved {
return &VehicleServiceStatus{
Status: "identity_required",
Severity: "warning",
Title: "身份未绑定",
Detail: "车辆关键词暂未解析到 VIN需先维护身份绑定后才能形成完整车辆服务。",
}
}
sourceCount := len(statuses)
onlineSourceCount := 0
for _, status := range statuses {
if status.Online {
onlineSourceCount++
}
}
if sourceCount == 0 {
return &VehicleServiceStatus{
Status: "no_data",
Severity: "warning",
Title: "暂无数据来源",
Detail: "车辆已解析,但暂未查询到 32960、808 或 MQTT 数据来源。",
SourceCount: sourceCount,
OnlineSourceCount: onlineSourceCount,
}
}
if onlineSourceCount == 0 {
return &VehicleServiceStatus{
Status: "offline",
Severity: "error",
Title: "车辆离线",
Detail: "所有已知数据来源均未在线,需要检查平台转发、终端上报或链路状态。",
SourceCount: sourceCount,
OnlineSourceCount: onlineSourceCount,
}
}
if onlineSourceCount < sourceCount {
return &VehicleServiceStatus{
Status: "degraded",
Severity: "warning",
Title: "部分来源离线",
Detail: sourceCoverageDetail(sourceCount, onlineSourceCount, "车辆服务可用但需要关注离线来源。"),
SourceCount: sourceCount,
OnlineSourceCount: onlineSourceCount,
}
}
return &VehicleServiceStatus{
Status: "healthy",
Severity: "ok",
Title: "服务正常",
Detail: sourceCoverageDetail(sourceCount, onlineSourceCount, "全部已知来源在线。"),
SourceCount: sourceCount,
OnlineSourceCount: onlineSourceCount,
}
}
func sourceCoverageDetail(sourceCount int, onlineSourceCount int, suffix string) string {
return strconv.Itoa(onlineSourceCount) + "/" + strconv.Itoa(sourceCount) + " 个来源在线," + suffix
}
func latestString(left string, right string) string {
left = strings.TrimSpace(left)
right = strings.TrimSpace(right)