feat(platform): expose source slots in vehicle service

This commit is contained in:
lingniu
2026-07-04 07:30:56 +08:00
parent 2cfb9e551f
commit c528376ef9
9 changed files with 67 additions and 3 deletions

View File

@@ -96,6 +96,7 @@ func TestHandlerVehicleCoverage(t *testing.T) {
Data Page[struct {
VIN string `json:"vin"`
MissingProtocols []string `json:"missingProtocols"`
SourceStatus []VehicleSourceStatus `json:"sourceStatus"`
SourceConsistency *VehicleSourceConsistency `json:"sourceConsistency"`
}] `json:"data"`
}
@@ -111,6 +112,18 @@ func TestHandlerVehicleCoverage(t *testing.T) {
if rawBody.Data.Items[0].SourceConsistency.Status != "degraded" || !containsString(rawBody.Data.Items[0].SourceConsistency.MissingProtocols, "YUTONG_MQTT") {
t.Fatalf("coverage source consistency should diagnose missing source evidence, got %+v body=%s", rawBody.Data.Items[0].SourceConsistency, rec.Body.String())
}
byProtocol := map[string]VehicleSourceStatus{}
for _, source := range rawBody.Data.Items[0].SourceStatus {
byProtocol[source.Protocol] = source
}
for _, protocol := range []string{"GB32960", "JT808", "YUTONG_MQTT"} {
if _, ok := byProtocol[protocol]; !ok {
t.Fatalf("coverage row should expose canonical source slot %s, got %+v body=%s", protocol, rawBody.Data.Items[0].SourceStatus, rec.Body.String())
}
}
if !byProtocol["JT808"].Online || byProtocol["YUTONG_MQTT"].Online || byProtocol["YUTONG_MQTT"].HasRealtime {
t.Fatalf("coverage source slots should expose online and missing evidence, got %+v", byProtocol)
}
}
func TestHandlerVehicleCoverageFiltersServiceStatus(t *testing.T) {

View File

@@ -133,6 +133,13 @@ func (m *MockStore) VehicleCoverage(_ context.Context, query url.Values) (Page[V
items := make([]VehicleCoverageRow, 0, len(byVIN))
for _, row := range byVIN {
row.MissingProtocols = missingCanonicalProtocols(row.Protocols)
onlineProtocols := make([]string, 0, row.OnlineSourceCount)
for _, vehicle := range vehicles {
if vehicle.VIN == row.VIN && vehicle.Online {
onlineProtocols = append(onlineProtocols, vehicle.Protocol)
}
}
row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, onlineProtocols, row.LastSeen)
row.ServiceStatus = buildVehicleCoverageServiceStatus(*row)
row.SourceConsistency = buildVehicleCoverageSourceConsistency(*row)
if keepCoverageRow(*row, query) {

View File

@@ -56,6 +56,7 @@ type VehicleCoverageRow struct {
OEM string `json:"oem"`
Protocols []string `json:"protocols"`
MissingProtocols []string `json:"missingProtocols"`
SourceStatus []VehicleSourceStatus `json:"sourceStatus"`
SourceCount int `json:"sourceCount"`
OnlineSourceCount int `json:"onlineSourceCount"`
Online bool `json:"online"`

View File

@@ -141,6 +141,7 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery {
`COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), b.plate, '') AS plate, ` +
`COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, ` +
`COALESCE(GROUP_CONCAT(DISTINCT s.protocol ORDER BY s.protocol SEPARATOR ','), '') AS protocols, ` +
`COALESCE(GROUP_CONCAT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END ORDER BY s.protocol SEPARATOR ','), '') AS online_protocols, ` +
`COUNT(DISTINCT s.protocol) AS source_count, ` +
`COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) AS online_source_count, ` +
`CASE WHEN COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) > 0 THEN 1 ELSE 0 END AS online, ` +

View File

@@ -264,12 +264,14 @@ func (s *ProductionStore) VehicleCoverage(ctx context.Context, query url.Values)
for rows.Next() {
var row VehicleCoverageRow
var protocols string
var onlineProtocols string
var online int
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.LastSeen, &row.BindingStatus); err != nil {
if err := rows.Scan(&row.VIN, &row.Plate, &row.Phone, &row.OEM, &protocols, &onlineProtocols, &row.SourceCount, &row.OnlineSourceCount, &online, &row.LastSeen, &row.BindingStatus); err != nil {
return Page[VehicleCoverageRow]{}, err
}
row.Protocols = splitCSV(protocols)
row.MissingProtocols = missingCanonicalProtocols(row.Protocols)
row.SourceStatus = buildVehicleCoverageSourceStatus(row.Protocols, splitCSV(onlineProtocols), row.LastSeen)
row.Online = online == 1
row.ServiceStatus = buildVehicleCoverageServiceStatus(row)
row.SourceConsistency = buildVehicleCoverageSourceConsistency(row)

View File

@@ -68,6 +68,23 @@ func missingCanonicalProtocols(protocols []string) []string {
return missing
}
func buildVehicleCoverageSourceStatus(protocols []string, onlineProtocols []string, lastSeen string) []VehicleSourceStatus {
statuses := make([]VehicleSourceStatus, 0, len(protocols))
for _, protocol := range protocols {
protocol = strings.TrimSpace(protocol)
if protocol == "" {
continue
}
statuses = append(statuses, VehicleSourceStatus{
Protocol: protocol,
Online: containsString(onlineProtocols, protocol),
LastSeen: lastSeen,
HasRealtime: true,
})
}
return completeVehicleSourceStatus(statuses, "")
}
func NewService(store Store) *Service {
return &Service{store: store}
}