fix(master-data): enforce authoritative vehicle totals

This commit is contained in:
lingniu
2026-07-16 19:35:05 +08:00
parent d67f42b4f4
commit c270140006
8 changed files with 227 additions and 24 deletions

View File

@@ -146,8 +146,7 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery {
if len(having) > 0 {
havingSQL = ` HAVING ` + strings.Join(having, " AND ") + ` `
}
vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' ` +
`UNION SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''`
vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''`
groupSQL := `FROM (` + vehicleSetSQL + `) v ` +
`LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin ` +
`LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin ` +
@@ -246,8 +245,7 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery {
if len(having) > 0 {
havingSQL = ` HAVING ` + strings.Join(having, " AND ") + ` `
}
vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> '' ` +
`UNION SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''`
vehicleSetSQL := `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''`
groupSQL := `SELECT v.vin, ` +
`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, ` +

View File

@@ -132,22 +132,26 @@ func (s *ProductionStore) serviceStatusStats(ctx context.Context) ([]ServiceStat
return out, nil
}
func (s *ProductionStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) {
func buildVehicleServiceSummarySQL() string {
canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
rows, err := s.db.QueryContext(ctx, `SELECT
return `SELECT
COUNT(*) AS total_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 THEN 1 ELSE 0 END), 0) AS bound_vehicles,
COALESCE(SUM(CASE WHEN online_source_count > 0 THEN 1 ELSE 0 END), 0) AS online_vehicles,
COALESCE(SUM(CASE WHEN source_count = 1 THEN 1 ELSE 0 END), 0) AS single_source_vehicles,
COALESCE(SUM(CASE WHEN source_count > 1 THEN 1 ELSE 0 END), 0) AS multi_source_vehicles,
COALESCE(SUM(CASE WHEN source_count = 0 THEN 1 ELSE 0 END), 0) AS no_data_vehicles,
COALESCE(SUM(CASE WHEN bound = 0 THEN 1 ELSE 0 END), 0) AS identity_required_vehicles,
(SELECT COUNT(DISTINCT unbound_source.vin)
FROM vehicle_realtime_snapshot unbound_source
LEFT JOIN vehicle_identity_binding bound_identity ON bound_identity.vin = unbound_source.vin
WHERE unbound_source.vin IS NOT NULL AND unbound_source.vin <> ''
AND bound_identity.vin IS NULL) AS identity_required_vehicles,
COALESCE(SUM(CASE WHEN archive_complete = 0 THEN 1 ELSE 0 END), 0) AS archive_incomplete_vehicles,
COALESCE(SUM(CASE WHEN missing_plate = 1 THEN 1 ELSE 0 END), 0) AS missing_plate_vehicles,
COALESCE(SUM(CASE WHEN missing_phone = 1 THEN 1 ELSE 0 END), 0) AS missing_phone_vehicles,
COALESCE(SUM(CASE WHEN missing_oem = 1 THEN 1 ELSE 0 END), 0) AS missing_oem_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count = `+canonicalSourceCount+` AND online_source_count = source_count THEN 1 ELSE 0 END), 0) AS healthy_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count > 0 AND (online_source_count < source_count OR source_count < `+canonicalSourceCount+`) THEN 1 ELSE 0 END), 0) AS degraded_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count = ` + canonicalSourceCount + ` AND online_source_count = source_count THEN 1 ELSE 0 END), 0) AS healthy_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count > 0 AND (online_source_count < source_count OR source_count < ` + canonicalSourceCount + `) THEN 1 ELSE 0 END), 0) AS degraded_vehicles,
COALESCE(SUM(CASE WHEN bound = 1 AND source_count > 0 AND online_source_count = 0 THEN 1 ELSE 0 END), 0) AS offline_vehicles
FROM (
SELECT v.vin,
@@ -164,13 +168,15 @@ CASE WHEN NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL THEN 1 ELSE 0 END AS miss
CASE WHEN NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_oem
FROM (
SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''
UNION
SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''
) v
LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin
LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin
GROUP BY v.vin
) vehicle_service_summary`)
) vehicle_service_summary`
}
func (s *ProductionStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) {
rows, err := s.db.QueryContext(ctx, buildVehicleServiceSummarySQL())
if err != nil {
return VehicleServiceSummary{}, err
}
@@ -223,9 +229,7 @@ GROUP BY v.vin
}
func (s *ProductionStore) missingSourceStats(ctx context.Context) ([]MissingSourceStat, error) {
const vehicleSetSQL = `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''
UNION
SELECT vin FROM vehicle_realtime_snapshot WHERE vin IS NOT NULL AND vin <> ''`
const vehicleSetSQL = `SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''`
out := make([]MissingSourceStat, 0, len(canonicalVehicleProtocols))
for _, protocol := range canonicalVehicleProtocols {
var count int

View File

@@ -90,7 +90,6 @@ func TestBuildVehicleCoverageSQLIncludesNoDataVehicles(t *testing.T) {
built := buildVehicleCoverageSQL(query)
for _, want := range []string{
"vehicle_identity_binding",
"UNION",
"vehicle_realtime_snapshot",
"LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin",
"COUNT(DISTINCT s.protocol) = 0",
@@ -100,6 +99,10 @@ func TestBuildVehicleCoverageSQLIncludesNoDataVehicles(t *testing.T) {
t.Fatalf("no-data coverage SQL missing %q: %s / %s", want, built.Text, built.CountText)
}
}
if strings.Contains(built.Text, "UNION SELECT vin FROM vehicle_realtime_snapshot") ||
strings.Contains(built.CountText, "UNION SELECT vin FROM vehicle_realtime_snapshot") {
t.Fatalf("formal vehicle coverage must not promote unbound realtime identities into the vehicle population: %s / %s", built.Text, built.CountText)
}
}
func TestBuildVehicleCoverageSQLFiltersMissingProtocol(t *testing.T) {
@@ -173,11 +176,32 @@ func TestBuildVehicleCoverageSummarySQL(t *testing.T) {
if strings.Contains(built.Text, "LIMIT") || strings.Contains(built.Text, "OFFSET") {
t.Fatalf("summary SQL should not paginate: %s", built.Text)
}
if strings.Contains(built.Text, "UNION SELECT vin FROM vehicle_realtime_snapshot") {
t.Fatalf("formal vehicle coverage summary must use the authoritative identity population: %s", built.Text)
}
if len(built.Args) != 6 || built.Args[0] != "%粤A%" {
t.Fatalf("args = %#v", built.Args)
}
}
func TestBuildVehicleServiceSummarySQLKeepsFormalAndUnboundPopulationsSeparate(t *testing.T) {
built := buildVehicleServiceSummarySQL()
for _, want := range []string{
"SELECT vin FROM vehicle_identity_binding WHERE vin IS NOT NULL AND vin <> ''",
"SELECT COUNT(DISTINCT unbound_source.vin)",
"LEFT JOIN vehicle_identity_binding bound_identity ON bound_identity.vin = unbound_source.vin",
"bound_identity.vin IS NULL",
} {
if !strings.Contains(built, want) {
t.Fatalf("vehicle service summary SQL missing %q: %s", want, built)
}
}
if strings.Contains(built, "UNION\nSELECT vin FROM vehicle_realtime_snapshot") ||
strings.Contains(built, "UNION SELECT vin FROM vehicle_realtime_snapshot") {
t.Fatalf("vehicle service total must not include unbound realtime-only identities: %s", built)
}
}
func TestBuildRealtimeLocationSQL(t *testing.T) {
query := url.Values{"vin": {"粤A"}, "protocol": {"GB32960"}, "limit": {"10"}, "offset": {"20"}}
built := buildRealtimeLocationSQL(query)