feat(platform): consolidate production vehicle data workflows

This commit is contained in:
lingniu
2026-07-15 23:26:29 +08:00
parent 6cddc0a43d
commit 3fabcf181a
59 changed files with 6849 additions and 3532 deletions

View File

@@ -23,6 +23,9 @@ func TestAccessSummaryUsesDynamicFreshnessAndDelay(t *testing.T) {
if summary.DelayAbnormal != 1 || summary.ThresholdVersion != 1 {
t.Fatalf("summary must expose dynamic delay and threshold version: %+v", summary)
}
if summary.HealthyVehicles != 0 || summary.IncompleteVehicles != 3 || summary.DegradedVehicles != 0 {
t.Fatalf("connection-state counters must be mutually exclusive: %+v", summary)
}
}
func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) {
@@ -42,6 +45,11 @@ func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) {
if err != nil || delayed.Total != 1 || delayed.Items[0].DataDelaySec == nil || *delayed.Items[0].DataDelaySec != 45 {
t.Fatalf("delay filter should use event/receive difference: page=%+v err=%v", delayed, err)
}
attention, err := service.AccessVehicles(context.Background(), AccessQuery{ConnectionState: "attention", Limit: 20})
if err != nil || attention.Total != 6 {
t.Fatalf("attention filter should return every vehicle with an access difference: page=%+v err=%v", attention, err)
}
}
func TestAccessVehiclesSupportsModelProviderAndTimeFilters(t *testing.T) {
@@ -96,6 +104,42 @@ func TestAccessVehicleCarriesDurableProjectionEvidenceAndMasterData(t *testing.T
}
}
func TestAccessVehiclesKeywordMatchesFleetAndModel(t *testing.T) {
service := NewService(NewMockStore())
fleet, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "岭牛示范车队", Limit: 20})
if err != nil || fleet.Total != 1 || fleet.Items[0].VIN != "LB9A32A24R0LS1426" {
t.Fatalf("fleet keyword should find its vehicles: page=%+v err=%v", fleet, err)
}
model, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "氢燃料车型", Limit: 20})
if err != nil || model.Total != 1 || model.Items[0].VIN != "LNXNEGRR7SR318212" {
t.Fatalf("model keyword should find its vehicles: page=%+v err=%v", model, err)
}
}
func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) {
store := NewMockStore()
service := NewService(store)
page, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "LB9A32A24R0LS1426", Limit: 10})
if err != nil || len(page.Items) != 1 {
t.Fatalf("access vehicle query failed: page=%+v err=%v", page, err)
}
row := page.Items[0]
if len(row.ExpectedProtocols) != 3 || len(row.ProtocolStatuses) != 3 {
t.Fatalf("vehicle must expose three canonical protocol slots: %+v", row)
}
if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 2 {
t.Fatalf("actual and missing protocols are incorrect: %+v", row)
}
if row.ConnectionState != "incomplete" || row.OnlineState != "online" {
t.Fatalf("single online source should be an online but incomplete vehicle: %+v", row)
}
for _, status := range row.ProtocolStatuses {
if status.Protocol == "JT808" && (!status.Connected || status.LatestReceivedAt == "") {
t.Fatalf("connected protocol lost evidence: %+v", status)
}
}
}
func TestProductionAccessEvidenceReadsDurableWriterProjection(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
@@ -103,12 +147,12 @@ func TestProductionAccessEvidenceReadsDurableWriterProjection(t *testing.T) {
}
defer db.Close()
store := NewProductionStore(db, nil, "")
mock.ExpectQuery("(?s)SELECT.*access_first_seen_at.*access_first_seen_source.*access_report_interval_ms.*vehicle_profile.*access_latest_received_at").
mock.ExpectQuery("(?s)SELECT.*access_first_seen_at.*access_latest_received_at.*FROM.*SELECT DISTINCT vin FROM vehicle_identity_binding.*vehicle_profile.*vehicle_realtime_snapshot").
WithArgs(accessEvidenceLimit + 1).
WillReturnRows(sqlmock.NewRows([]string{
"vin", "plate", "oem", "model_name", "company_name", "protocol", "provider", "first_seen_at", "first_seen_source",
"event_time", "received_at", "updated_at", "report_interval_ms", "report_sample_count", "event_id",
}).AddRow("VIN001", "粤A1", "示范厂家", "纯电客车", "示范公交", "GB32960", "车厂平台", "2026-07-14 05:00:00.000", "snapshot_backfill", "2026-07-14 05:01:00.000", "2026-07-14 05:01:30.500", "2026-07-14 05:01:30.500", int64(30500), int64(3), "evt-3"))
"event_time", "received_at", "updated_at", "report_interval_ms", "report_sample_count", "event_id", "longitude", "latitude", "speed_kmh", "soc_percent", "total_mileage_km", "daily_mileage_km",
}).AddRow("VIN001", "粤A1", "示范厂家", "纯电客车", "示范公交", "GB32960", "车厂平台", "2026-07-14 05:00:00.000", "snapshot_backfill", "2026-07-14 05:01:00.000", "2026-07-14 05:01:30.500", "2026-07-14 05:01:30.500", int64(30500), int64(3), "evt-3", 113.1, 23.1, 32.5, 76.0, 12000.5, 38.6))
rows, err := store.AccessEvidence(context.Background())
if err != nil || len(rows) != 1 {
t.Fatalf("AccessEvidence rows=%+v err=%v", rows, err)
@@ -117,6 +161,9 @@ func TestProductionAccessEvidenceReadsDurableWriterProjection(t *testing.T) {
if row.ReportIntervalSec == nil || *row.ReportIntervalSec != 31 || row.ReportSampleCount != 3 || row.Model != "纯电客车" || row.Company != "示范公交" {
t.Fatalf("unexpected durable projection row: %+v", row)
}
if row.SpeedKmh != 32.5 || row.TotalMileageKm != 12000.5 || row.DailyMileageKm != 38.6 {
t.Fatalf("realtime source metrics missing: %+v", row)
}
if row.FirstSeenEvidence != "上线基线:由实时快照回填(非历史首次接入)" || row.ReportIntervalProof == "" {
t.Fatalf("projection evidence boundary missing: %+v", row)
}