feat(monitor): retain authorized vehicles without locations
This commit is contained in:
@@ -328,8 +328,8 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
offset := parsePositive(query.Get("offset"), 0)
|
||||
canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
|
||||
args := []any{}
|
||||
where := []string{"l.vin IS NOT NULL", "l.vin <> ''"}
|
||||
where, args = appendVINListFilter(where, args, "l.vin", query.Get("scopeVins"))
|
||||
where := []string{"v.vin IS NOT NULL", "v.vin <> ''"}
|
||||
where, args = appendVINListFilter(where, args, "v.vin", query.Get("scopeVins"))
|
||||
having := []string{}
|
||||
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
|
||||
where = append(where, "l.protocol = ?")
|
||||
@@ -338,7 +338,7 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
||||
if strings.TrimSpace(query.Get("keywords")) != "" {
|
||||
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(keywords)), ",")
|
||||
where = append(where, "(l.vin IN ("+placeholders+") OR l.plate IN ("+placeholders+") OR b.plate IN ("+placeholders+"))")
|
||||
where = append(where, "(v.vin IN ("+placeholders+") OR l.plate IN ("+placeholders+") OR v.plate IN ("+placeholders+"))")
|
||||
for range 3 {
|
||||
for _, keyword := range keywords {
|
||||
args = append(args, keyword)
|
||||
@@ -347,7 +347,7 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
} else {
|
||||
keyword := keywords[0]
|
||||
like := "%" + keyword + "%"
|
||||
where = append(where, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)")
|
||||
where = append(where, "(v.vin LIKE ? OR l.plate LIKE ? OR v.plate LIKE ? OR v.phone LIKE ? OR v.oem LIKE ?)")
|
||||
args = append(args, like, like, like, like, like)
|
||||
}
|
||||
}
|
||||
@@ -355,9 +355,19 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
case "online":
|
||||
having = append(having, "COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0")
|
||||
case "offline":
|
||||
having = append(having, "COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) = 0")
|
||||
having = append(having,
|
||||
"COUNT(DISTINCT l.protocol) > 0",
|
||||
"COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) = 0",
|
||||
)
|
||||
}
|
||||
primarySpeed := "CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.speed_kmh AS CHAR) ORDER BY l.updated_at DESC, l.protocol ASC), ',', 1) AS DECIMAL(18,6))"
|
||||
orderExpr := `CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN 0 ELSE 1 END ASC, ` +
|
||||
`CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN CASE l.protocol ` +
|
||||
`WHEN 'GB32960' THEN 10 WHEN 'YUTONG_MQTT' THEN 20 WHEN 'JT808' THEN 30 ELSE 100 END ELSE 100 END ASC, ` +
|
||||
`l.updated_at DESC, l.protocol ASC`
|
||||
primarySpeed := "CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.speed_kmh AS CHAR) ORDER BY " + orderExpr + "), ',', 1) AS DECIMAL(18,6))"
|
||||
primaryLocationAvailable := `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(` +
|
||||
`CASE WHEN l.vin IS NOT NULL AND l.longitude BETWEEN -180 AND 180 AND l.latitude BETWEEN -90 AND 90 ` +
|
||||
`AND NOT (l.longitude = 0 AND l.latitude = 0) THEN 1 ELSE 0 END ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0)`
|
||||
switch strings.TrimSpace(query.Get("status")) {
|
||||
case "driving":
|
||||
having = append(having,
|
||||
@@ -369,29 +379,31 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
"COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0",
|
||||
primarySpeed+" <= 3",
|
||||
)
|
||||
case "no_location":
|
||||
having = append(having, primaryLocationAvailable+" = 0")
|
||||
}
|
||||
switch strings.TrimSpace(query.Get("serviceStatus")) {
|
||||
case "healthy":
|
||||
having = append(having,
|
||||
"MAX(CASE WHEN b.vin IS NOT NULL THEN 1 ELSE 0 END) = 1",
|
||||
"MAX(CASE WHEN v.binding_status = 'bound' THEN 1 ELSE 0 END) = 1",
|
||||
"COUNT(DISTINCT l.protocol) = "+canonicalSourceCount,
|
||||
"COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) = COUNT(DISTINCT l.protocol)",
|
||||
)
|
||||
case "degraded":
|
||||
having = append(having,
|
||||
"MAX(CASE WHEN b.vin IS NOT NULL THEN 1 ELSE 0 END) = 1",
|
||||
"MAX(CASE WHEN v.binding_status = 'bound' THEN 1 ELSE 0 END) = 1",
|
||||
"COUNT(DISTINCT l.protocol) > 0",
|
||||
"COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0",
|
||||
"(COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) < COUNT(DISTINCT l.protocol) OR COUNT(DISTINCT l.protocol) < "+canonicalSourceCount+")",
|
||||
)
|
||||
case "offline":
|
||||
having = append(having,
|
||||
"MAX(CASE WHEN b.vin IS NOT NULL THEN 1 ELSE 0 END) = 1",
|
||||
"MAX(CASE WHEN v.binding_status = 'bound' THEN 1 ELSE 0 END) = 1",
|
||||
"COUNT(DISTINCT l.protocol) > 0",
|
||||
"COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) = 0",
|
||||
)
|
||||
case "identity_required":
|
||||
having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL THEN 1 ELSE 0 END) = 0")
|
||||
having = append(having, "MAX(CASE WHEN v.binding_status = 'bound' THEN 1 ELSE 0 END) = 0")
|
||||
}
|
||||
countArgs := append([]any(nil), args...)
|
||||
args = append(args, limit, offset)
|
||||
@@ -399,45 +411,62 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
if len(having) > 0 {
|
||||
havingSQL = ` HAVING ` + strings.Join(having, " AND ") + ` `
|
||||
}
|
||||
groupSQL := `FROM vehicle_realtime_location l ` +
|
||||
`LEFT JOIN vehicle_identity_binding b ON b.vin = l.vin ` +
|
||||
`LEFT JOIN vehicle_realtime_snapshot s ON s.vin = l.vin AND s.protocol = l.protocol ` +
|
||||
`WHERE ` + strings.Join(where, " AND ") + ` ` +
|
||||
`GROUP BY l.vin, b.plate, b.phone, b.oem ` +
|
||||
populationSQL := `(` +
|
||||
`SELECT b0.vin, COALESCE(MAX(NULLIF(b0.plate, '')), '') AS plate, ` +
|
||||
`COALESCE(MAX(NULLIF(b0.phone, '')), '') AS phone, COALESCE(MAX(NULLIF(b0.oem, '')), '') AS oem, 'bound' AS binding_status ` +
|
||||
`FROM vehicle_identity_binding b0 WHERE b0.vin IS NOT NULL AND b0.vin <> '' GROUP BY b0.vin ` +
|
||||
`UNION ALL ` +
|
||||
`SELECT l0.vin, COALESCE(MAX(NULLIF(l0.plate, '')), '') AS plate, '' AS phone, '' AS oem, 'unbound' AS binding_status ` +
|
||||
`FROM vehicle_realtime_location l0 LEFT JOIN vehicle_identity_binding b1 ON b1.vin = l0.vin ` +
|
||||
`WHERE l0.vin IS NOT NULL AND l0.vin <> '' AND b1.vin IS NULL GROUP BY l0.vin` +
|
||||
`) v`
|
||||
baseGroupSQL := `FROM ` + populationSQL + ` ` +
|
||||
`LEFT JOIN vehicle_realtime_location l ON l.vin = v.vin ` +
|
||||
`LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin AND s.protocol = l.protocol `
|
||||
groupSuffixSQL := `WHERE ` + strings.Join(where, " AND ") + ` ` +
|
||||
`GROUP BY v.vin, v.plate, v.phone, v.oem, v.binding_status ` +
|
||||
havingSQL
|
||||
groupSQL := baseGroupSQL +
|
||||
`LEFT JOIN (` +
|
||||
`SELECT vin, protocol, MAX(daily_mileage_km) AS daily_mileage_km FROM vehicle_daily_mileage ` +
|
||||
`WHERE stat_date = CURDATE() GROUP BY vin, protocol` +
|
||||
`) m ON m.vin = v.vin AND m.protocol = l.protocol ` +
|
||||
groupSuffixSQL
|
||||
// Keep one authoritative coordinate source while all protocols are healthy.
|
||||
// Ordering only by updated_at makes the selected point flap whenever protocols
|
||||
// report at different cadences. When every source is stale, recency remains the
|
||||
// safest fallback so an old high-priority source cannot mask newer evidence.
|
||||
orderExpr := `CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN 0 ELSE 1 END ASC, ` +
|
||||
`CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN CASE l.protocol ` +
|
||||
`WHEN 'GB32960' THEN 10 WHEN 'YUTONG_MQTT' THEN 20 WHEN 'JT808' THEN 30 ELSE 100 END ELSE 100 END ASC, ` +
|
||||
`l.updated_at DESC, l.protocol ASC`
|
||||
return SQLQuery{
|
||||
Text: `SELECT l.vin, ` +
|
||||
`COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), b.plate, '') AS plate, ` +
|
||||
`COALESCE(b.phone, '') AS phone, COALESCE(b.oem, '') AS oem, ` +
|
||||
Text: `SELECT v.vin, ` +
|
||||
`COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), v.plate, '') AS plate, ` +
|
||||
`COALESCE(v.phone, '') AS phone, COALESCE(v.oem, '') AS oem, ` +
|
||||
`COALESCE(GROUP_CONCAT(DISTINCT l.protocol ORDER BY l.protocol SEPARATOR ','), '') AS protocols, ` +
|
||||
`COALESCE(GROUP_CONCAT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END ORDER BY l.protocol SEPARATOR ','), '') AS online_protocols, ` +
|
||||
`COUNT(DISTINCT l.protocol) AS source_count, ` +
|
||||
`COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) AS online_source_count, ` +
|
||||
`CASE WHEN COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0 THEN 1 ELSE 0 END AS online, ` +
|
||||
`CASE WHEN MAX(CASE WHEN b.vin IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 'bound' ELSE 'unbound' END AS binding_status, ` +
|
||||
`v.binding_status, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(l.protocol ORDER BY ` + orderExpr + `), ',', 1), '') AS primary_protocol, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(l.source_key, ''), CONCAT(l.protocol, ':canonical')) ORDER BY ` + orderExpr + `), ',', 1), '') AS location_source, ` +
|
||||
`COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(l.location_conflict, 0) ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0) AS location_conflict, ` +
|
||||
`SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(CAST(l.location_conflict_distance_m AS CHAR), '-1') ORDER BY ` + orderExpr + `), ',', 1) AS conflict_distance_m, ` +
|
||||
primaryLocationAvailable + ` AS location_available, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.longitude AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS longitude, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.latitude AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS latitude, ` +
|
||||
`COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CASE WHEN l.speed_kmh IS NOT NULL THEN 1 ELSE 0 END ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0) AS speed_available, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.speed_kmh AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS speed_kmh, ` +
|
||||
`COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CASE WHEN l.protocol IN ('GB32960','YUTONG_MQTT') AND l.soc_percent IS NOT NULL THEN 1 ELSE 0 END ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0) AS soc_available, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.soc_percent AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS soc_percent, ` +
|
||||
`COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CASE WHEN l.total_mileage_km IS NOT NULL THEN 1 ELSE 0 END ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0) AS mileage_available, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.total_mileage_km AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS total_mileage_km, ` +
|
||||
`COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CASE WHEN m.daily_mileage_km IS NOT NULL THEN 1 ELSE 0 END ORDER BY ` + orderExpr + `), ',', 1) AS UNSIGNED), 0) AS today_mileage_available, ` +
|
||||
`COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(m.daily_mileage_km AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS today_mileage_km, ` +
|
||||
`COALESCE(DATE_FORMAT(MAX(l.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` +
|
||||
`CAST(SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(s.access_report_interval_ms, -1) ORDER BY ` + orderExpr + `), ',', 1) AS SIGNED) AS report_interval_ms ` +
|
||||
groupSQL +
|
||||
`ORDER BY MAX(l.updated_at) DESC, l.vin ASC LIMIT ? OFFSET ?`,
|
||||
`ORDER BY MAX(l.updated_at) IS NULL ASC, MAX(l.updated_at) DESC, v.vin ASC LIMIT ? OFFSET ?`,
|
||||
Args: args,
|
||||
CountText: `SELECT COUNT(*) FROM (SELECT l.vin ` + groupSQL + `) vehicle_realtime_count`,
|
||||
CountText: `SELECT COUNT(*) FROM (SELECT v.vin ` + baseGroupSQL + groupSuffixSQL + `) vehicle_realtime_count`,
|
||||
CountArgs: countArgs,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user