feat(platform): expose service status on vehicle rows

This commit is contained in:
lingniu
2026-07-04 02:23:57 +08:00
parent 826fa76008
commit cbd02956ff
6 changed files with 81 additions and 11 deletions

View File

@@ -35,6 +35,18 @@ func TestHandlerVehicles(t *testing.T) {
if !strings.Contains(rec.Body.String(), "LB9A32A24R0LS1426") {
t.Fatalf("response missing vehicle: %s", rec.Body.String())
}
var body struct {
Data Page[VehicleRow] `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("vehicle row should include canonical vehicle service status: %s", rec.Body.String())
}
if body.Data.Items[0].ServiceStatus.Status != "degraded" {
t.Fatalf("vehicle row should expose degraded status, got %+v", body.Data.Items[0].ServiceStatus)
}
}
func TestHandlerVehicleCoverage(t *testing.T) {

View File

@@ -61,9 +61,43 @@ func (m *MockStore) DashboardSummary(context.Context) (DashboardSummary, error)
func (m *MockStore) Vehicles(_ context.Context, query url.Values) (Page[VehicleRow], error) {
items := filterVehicles(m.vehicles, query)
for index := range items {
items[index].ServiceStatus = m.vehicleRowServiceStatus(items[index])
}
return page(items, query), nil
}
func (m *MockStore) vehicleRowServiceStatus(row VehicleRow) *VehicleServiceStatus {
status := VehicleCoverageRow{
VIN: row.VIN,
Plate: row.Plate,
Phone: row.Phone,
OEM: row.OEM,
LastSeen: row.LastSeen,
BindingStatus: "bound",
}
if strings.TrimSpace(row.VIN) == "" {
status.BindingStatus = "unbound"
}
for _, vehicle := range m.vehicles {
if vehicle.VIN != row.VIN {
continue
}
if !containsString(status.Protocols, vehicle.Protocol) {
status.Protocols = append(status.Protocols, vehicle.Protocol)
status.SourceCount = len(status.Protocols)
}
if vehicle.Online {
status.Online = true
status.OnlineSourceCount++
}
if vehicle.LastSeen > status.LastSeen {
status.LastSeen = vehicle.LastSeen
}
}
return buildVehicleCoverageServiceStatus(status)
}
func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[VehicleCoverageRow], error) {
vehicles := filterVehicles(m.vehicles, query)
byVIN := map[string]*VehicleCoverageRow{}

View File

@@ -37,15 +37,16 @@ type LinkHealth struct {
}
type VehicleRow struct {
VIN string `json:"vin"`
Plate string `json:"plate"`
Phone string `json:"phone"`
OEM string `json:"oem"`
Protocol string `json:"protocol"`
Online bool `json:"online"`
LastSeen string `json:"lastSeen"`
LocationText string `json:"locationText"`
BindingScore int `json:"bindingScore"`
VIN string `json:"vin"`
Plate string `json:"plate"`
Phone string `json:"phone"`
OEM string `json:"oem"`
Protocol string `json:"protocol"`
Online bool `json:"online"`
LastSeen string `json:"lastSeen"`
LocationText string `json:"locationText"`
BindingScore int `json:"bindingScore"`
ServiceStatus *VehicleServiceStatus `json:"serviceStatus,omitempty"`
}
type VehicleCoverageRow struct {

View File

@@ -32,13 +32,20 @@ func buildVehicleListSQL(query url.Values) SQLQuery {
fromSQL := `FROM vehicle_realtime_snapshot s ` +
`LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` +
`LEFT JOIN vehicle_realtime_location l ON l.vin = s.vin AND l.protocol = s.protocol ` +
`LEFT JOIN (` +
`SELECT vin, COUNT(DISTINCT protocol) AS source_count, ` +
`COUNT(DISTINCT CASE WHEN updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN protocol END) AS online_source_count ` +
`FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> '' GROUP BY vin` +
`) vs ON vs.vin = s.vin ` +
`WHERE ` + strings.Join(where, " AND ")
return SQLQuery{
Text: `SELECT s.vin, COALESCE(NULLIF(s.plate, ''), b.plate, '') AS plate, COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, s.protocol, ` +
`CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN 1 ELSE 0 END AS online, ` +
`COALESCE(DATE_FORMAT(s.updated_at, '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
`COALESCE(CONCAT(l.longitude, ',', l.latitude), '') AS location_text, ` +
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 100 ELSE 60 END AS binding_score ` +
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 100 ELSE 60 END AS binding_score, ` +
`COALESCE(vs.source_count, 0) AS source_count, COALESCE(vs.online_source_count, 0) AS online_source_count, ` +
`CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 'bound' ELSE 'unbound' END AS binding_status ` +
fromSQL + ` ORDER BY s.updated_at DESC, s.vin ASC, s.protocol ASC LIMIT ? OFFSET ?`,
Args: args,
CountText: `SELECT COUNT(*) ` + fromSQL,

View File

@@ -124,10 +124,25 @@ func (s *ProductionStore) Vehicles(ctx context.Context, query url.Values) (Page[
for rows.Next() {
var row VehicleRow
var online int
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &row.Protocol, &online, &row.LastSeen, &row.LocationText, &row.BindingScore); err != nil {
var sourceCount int
var onlineSourceCount int
var bindingStatus string
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &row.Protocol, &online, &row.LastSeen, &row.LocationText, &row.BindingScore, &sourceCount, &onlineSourceCount, &bindingStatus); err != nil {
return Page[VehicleRow]{}, err
}
row.Online = online == 1
row.ServiceStatus = buildVehicleCoverageServiceStatus(VehicleCoverageRow{
VIN: row.VIN,
Plate: row.Plate,
Phone: row.Phone,
OEM: row.OEM,
Protocols: []string{row.Protocol},
SourceCount: sourceCount,
OnlineSourceCount: onlineSourceCount,
Online: onlineSourceCount > 0,
LastSeen: row.LastSeen,
BindingStatus: bindingStatus,
})
items = append(items, row)
}
if err := rows.Err(); err != nil {

View File

@@ -43,6 +43,7 @@ export interface VehicleRow {
lastSeen: string;
locationText: string;
bindingScore: number;
serviceStatus?: VehicleServiceStatus;
}
export interface VehicleCoverageRow {