package platform import ( "net/url" "strconv" "strings" ) type SQLQuery struct { Text string Args []any CountText string CountArgs []any } func buildVehicleListSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) args := []any{} where := []string{"1 = 1"} if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" { where = append(where, "(b.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + keyword + "%" args = append(args, like, like, like, like) } if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "s.protocol = ?") args = append(args, protocol) } switch strings.TrimSpace(query.Get("serviceStatus")) { case "identity_required": where = append(where, "(b.vin IS NULL OR b.vin = '')") case "offline": where = append(where, "b.vin IS NOT NULL", "b.vin <> ''", "COALESCE(vs.source_count, 0) > 0", "COALESCE(vs.online_source_count, 0) = 0") case "degraded": where = append(where, "b.vin IS NOT NULL", "b.vin <> ''", "vs.source_count > 0", "vs.online_source_count > 0", "(vs.online_source_count < vs.source_count OR vs.source_count < "+canonicalSourceCount+")") case "healthy": where = append(where, "b.vin IS NOT NULL", "b.vin <> ''", "vs.source_count = "+canonicalSourceCount, "vs.online_source_count = vs.source_count") } countArgs := append([]any(nil), args...) args = append(args, limit, offset) 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, ` + `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, CountArgs: countArgs, } } func buildVehicleCoverageSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) args := []any{} where := []string{"v.vin IS NOT NULL", "v.vin <> ''"} having := []string{} if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" { where = append(where, "(v.vin LIKE ? OR s.plate LIKE ? OR b.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + keyword + "%" args = append(args, like, like, like, like, like, like) } if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "s.protocol = ?") args = append(args, protocol) } switch strings.TrimSpace(query.Get("coverage")) { case "single": having = append(having, "COUNT(DISTINCT s.protocol) = 1") case "multi": having = append(having, "COUNT(DISTINCT s.protocol) > 1") } if missingProtocol := strings.TrimSpace(query.Get("missingProtocol")); missingProtocol != "" { having = append(having, "COUNT(DISTINCT CASE WHEN s.protocol = ? THEN s.protocol END) = 0") args = append(args, missingProtocol) } switch strings.TrimSpace(query.Get("online")) { case "online": having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) > 0") case "offline": having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = 0") } switch strings.TrimSpace(query.Get("bindingStatus")) { case "bound": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") case "unbound": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0") } switch strings.TrimSpace(query.Get("archiveStatus")) { case "complete": having = append(having, "v.vin IS NOT NULL AND v.vin <> ''") having = append(having, "COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL") having = append(having, "NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL") having = append(having, "NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL") case "incomplete": having = append(having, "(v.vin IS NULL OR v.vin = '' OR COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL OR NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL OR NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL)") } if predicate := archiveMissingFieldPredicate(query.Get("archiveMissing")); predicate != "" { having = append(having, predicate) } switch strings.TrimSpace(query.Get("serviceStatus")) { case "identity_required": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0") case "no_data": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) = 0") case "offline": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) > 0") having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = 0") case "degraded": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) > 0") having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) > 0") having = append(having, "(COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) < COUNT(DISTINCT s.protocol) OR COUNT(DISTINCT s.protocol) < "+canonicalSourceCount+")") case "healthy": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) = "+canonicalSourceCount) having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = COUNT(DISTINCT s.protocol)") } countArgs := append([]any(nil), args...) args = append(args, limit, offset) havingSQL := "" 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 <> ''` 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 ` + `WHERE ` + strings.Join(where, " AND ") + ` ` + `GROUP BY v.vin, b.plate, b.phone, b.oem, b.vin ` + havingSQL return SQLQuery{ Text: `SELECT v.vin, ` + `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, ` + `COALESCE(DATE_FORMAT(MAX(s.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen, ` + `CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 'bound' ELSE 'unbound' END AS binding_status ` + groupSQL + `ORDER BY MAX(s.updated_at) DESC, v.vin ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) FROM (SELECT v.vin ` + groupSQL + `) vehicle_coverage_count`, CountArgs: countArgs, } } func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery { canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) args := []any{} where := []string{"v.vin IS NOT NULL", "v.vin <> ''"} having := []string{} if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" { where = append(where, "(v.vin LIKE ? OR s.plate LIKE ? OR b.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + keyword + "%" args = append(args, like, like, like, like, like, like) } if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "s.protocol = ?") args = append(args, protocol) } switch strings.TrimSpace(query.Get("coverage")) { case "single": having = append(having, "COUNT(DISTINCT s.protocol) = 1") case "multi": having = append(having, "COUNT(DISTINCT s.protocol) > 1") } if missingProtocol := strings.TrimSpace(query.Get("missingProtocol")); missingProtocol != "" { having = append(having, "COUNT(DISTINCT CASE WHEN s.protocol = ? THEN s.protocol END) = 0") args = append(args, missingProtocol) } switch strings.TrimSpace(query.Get("online")) { case "online": having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) > 0") case "offline": having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = 0") } switch strings.TrimSpace(query.Get("bindingStatus")) { case "bound": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") case "unbound": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0") } switch strings.TrimSpace(query.Get("archiveStatus")) { case "complete": having = append(having, "v.vin IS NOT NULL AND v.vin <> ''") having = append(having, "COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL") having = append(having, "NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL") having = append(having, "NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL") case "incomplete": having = append(having, "(v.vin IS NULL OR v.vin = '' OR COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL OR NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL OR NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL)") } if predicate := archiveMissingFieldPredicate(query.Get("archiveMissing")); predicate != "" { having = append(having, predicate) } switch strings.TrimSpace(query.Get("serviceStatus")) { case "identity_required": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 0") case "no_data": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) = 0") case "offline": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) > 0") having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = 0") case "degraded": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) > 0") having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) > 0") having = append(having, "(COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) < COUNT(DISTINCT s.protocol) OR COUNT(DISTINCT s.protocol) < "+canonicalSourceCount+")") case "healthy": having = append(having, "MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) = 1") having = append(having, "COUNT(DISTINCT s.protocol) = "+canonicalSourceCount) having = append(having, "COUNT(DISTINCT CASE WHEN s.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN s.protocol END) = COUNT(DISTINCT s.protocol)") } havingSQL := "" 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 <> ''` 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, ` + `MAX(CASE WHEN b.vin IS NOT NULL AND b.vin <> '' THEN 1 ELSE 0 END) AS bound, ` + `CASE WHEN v.vin IS NOT NULL AND v.vin <> '' ` + `AND COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NOT NULL ` + `AND NULLIF(MAX(NULLIF(b.phone, '')), '') IS NOT NULL ` + `AND NULLIF(MAX(NULLIF(b.oem, '')), '') IS NOT NULL ` + `THEN 1 ELSE 0 END AS archive_complete, ` + `CASE WHEN COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL THEN 1 ELSE 0 END AS missing_plate, ` + `CASE WHEN NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_phone, ` + `CASE WHEN NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL THEN 1 ELSE 0 END AS missing_oem ` + `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 ` + `WHERE ` + strings.Join(where, " AND ") + ` ` + `GROUP BY v.vin ` + havingSQL return SQLQuery{ Text: `SELECT COUNT(*) AS total_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 unbound_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 ` + `FROM (` + groupSQL + `) vehicle_coverage_summary`, Args: args, } } func archiveMissingFieldPredicate(field string) string { switch strings.TrimSpace(field) { case "plate": return "COALESCE(NULLIF(MAX(NULLIF(s.plate, '')), ''), NULLIF(MAX(NULLIF(b.plate, '')), '')) IS NULL" case "phone": return "NULLIF(MAX(NULLIF(b.phone, '')), '') IS NULL" case "oem": return "NULLIF(MAX(NULLIF(b.oem, '')), '') IS NULL" default: return "" } } func buildRealtimeLocationSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) args := []any{} where := []string{"1 = 1"} if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "l.protocol = ?") args = append(args, protocol) } if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ?)") like := "%" + vin + "%" args = append(args, like, like, like) } countArgs := append([]any(nil), args...) args = append(args, limit, offset) fromSQL := `FROM vehicle_realtime_location l LEFT JOIN vehicle_identity_binding b ON b.vin = l.vin WHERE ` + strings.Join(where, " AND ") return SQLQuery{ Text: `SELECT l.vin, COALESCE(NULLIF(l.plate, ''), b.plate, '') AS plate, l.protocol, l.longitude, l.latitude, ` + `COALESCE(l.speed_kmh, 0), COALESCE(l.soc_percent, 0), COALESCE(l.total_mileage_km, 0), ` + `COALESCE(DATE_FORMAT(l.updated_at, '%Y-%m-%d %H:%i:%s'), '') ` + fromSQL + ` ORDER BY l.updated_at DESC, l.vin ASC, l.protocol ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) ` + fromSQL, CountArgs: countArgs, } } func buildVehicleRealtimeSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) args := []any{} where := []string{"l.vin IS NOT NULL", "l.vin <> ''"} having := []string{} if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "l.protocol = ?") args = append(args, protocol) } if keywords := vehicleSearchKeywords(query); len(keywords) > 0 { matches := make([]string, 0, len(keywords)) for _, keyword := range keywords { matches = append(matches, "(l.vin LIKE ? OR l.plate LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + keyword + "%" args = append(args, like, like, like, like, like) } where = append(where, "("+strings.Join(matches, " OR ")+")") } switch strings.TrimSpace(query.Get("online")) { 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") } 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))" switch strings.TrimSpace(query.Get("status")) { case "driving": having = append(having, "COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0", primarySpeed+" > 3", ) case "idle": having = append(having, "COUNT(DISTINCT CASE WHEN l.updated_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE) THEN l.protocol END) > 0", primarySpeed+" <= 3", ) } 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", "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", "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", "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") } countArgs := append([]any(nil), args...) args = append(args, limit, offset) havingSQL := "" 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 ` + `WHERE ` + strings.Join(where, " AND ") + ` ` + `GROUP BY l.vin, b.plate, b.phone, b.oem ` + havingSQL orderExpr := `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, ` + `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, ` + `COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(l.protocol ORDER BY ` + orderExpr + `), ',', 1), '') AS primary_protocol, ` + `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(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.speed_kmh AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS speed_kmh, ` + `COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.soc_percent AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS soc_percent, ` + `COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.total_mileage_km AS CHAR) ORDER BY ` + orderExpr + `), ',', 1), '') AS total_mileage_km, ` + `COALESCE(DATE_FORMAT(MAX(l.updated_at), '%Y-%m-%d %H:%i:%s'), '') AS last_seen ` + groupSQL + `ORDER BY MAX(l.updated_at) DESC, l.vin ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) FROM (SELECT l.vin ` + groupSQL + `) vehicle_realtime_count`, CountArgs: countArgs, } } func buildDailyMileageSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) args := []any{} where := []string{"1 = 1"} if strings.EqualFold(strings.TrimSpace(query.Get("vehicleScope")), "bound") { where = append(where, "b.vin IS NOT NULL", "b.vin <> ''") } if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(m.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + vin + "%" args = append(args, like, like, like, like) } where, args = appendVINListFilter(where, args, "m.vin", query.Get("vins")) protocols := parseMileageProtocols(query.Get("protocols")) if len(protocols) > 0 { where, args = appendMileageProtocolFilter(where, args, "m.protocol", protocols) } else if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "m.protocol = ?") args = append(args, protocol) } if dateFrom := strings.TrimSpace(query.Get("dateFrom")); dateFrom != "" { where = append(where, "m.stat_date >= ?") args = append(args, dateFrom) } if dateTo := strings.TrimSpace(query.Get("dateTo")); dateTo != "" { where = append(where, "m.stat_date <= ?") args = append(args, dateTo) } countArgs := append([]any(nil), args...) args = append(args, limit, offset) fromSQL := `FROM vehicle_daily_mileage m LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin WHERE ` + strings.Join(where, " AND ") if query.Get("deduplicate") == "1" || strings.EqualFold(query.Get("deduplicate"), "true") { selectionOrder := `m.daily_mileage_km DESC, m.protocol ASC` dailyMileageExpression := `MAX(COALESCE(m.daily_mileage_km, 0))` if len(protocols) > 0 { selectionOrder = mileageProtocolOrder("m.protocol", protocols) dailyMileageExpression = `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(COALESCE(m.daily_mileage_km, 0) AS CHAR) ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)), 0)` } groupSQL := fromSQL + ` GROUP BY m.vin, m.stat_date` return SQLQuery{ Text: `SELECT m.vin, COALESCE(MAX(NULLIF(b.plate, '')), '') AS plate, DATE_FORMAT(m.stat_date, '%Y-%m-%d') AS stat_date, ` + `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(m.latest_total_mileage_km - m.daily_mileage_km AS CHAR) ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)), 0) AS start_mileage_km, ` + `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(m.latest_total_mileage_km AS CHAR) ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)), 0) AS end_mileage_km, ` + dailyMileageExpression + ` AS daily_mileage_km, ` + `COALESCE(SUBSTRING_INDEX(GROUP_CONCAT(m.protocol ORDER BY ` + selectionOrder + `), ',', 1), '') AS protocol ` + groupSQL + ` ORDER BY m.stat_date DESC, m.vin ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) FROM (SELECT m.vin ` + groupSQL + `) vehicle_daily_mileage_count`, CountArgs: countArgs, } } return SQLQuery{ Text: `SELECT m.vin, COALESCE(b.plate, '') AS plate, DATE_FORMAT(m.stat_date, '%Y-%m-%d') AS stat_date, ` + `COALESCE(m.latest_total_mileage_km - m.daily_mileage_km, 0) AS start_mileage_km, ` + `COALESCE(m.latest_total_mileage_km, 0) AS end_mileage_km, m.daily_mileage_km, m.protocol ` + fromSQL + ` ORDER BY m.stat_date DESC, m.vin ASC, m.protocol ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) ` + fromSQL, CountArgs: countArgs, } } func buildMileageSummarySQL(query url.Values) SQLQuery { args := []any{} where := []string{"1 = 1"} if strings.EqualFold(strings.TrimSpace(query.Get("vehicleScope")), "bound") { where = append(where, "b.vin IS NOT NULL", "b.vin <> ''") } if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(m.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + vin + "%" args = append(args, like, like, like, like) } where, args = appendVINListFilter(where, args, "m.vin", query.Get("vins")) if protocols := parseMileageProtocols(query.Get("protocols")); len(protocols) > 0 { where, args = appendMileageProtocolFilter(where, args, "m.protocol", protocols) } else if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "m.protocol = ?") args = append(args, protocol) } if dateFrom := strings.TrimSpace(query.Get("dateFrom")); dateFrom != "" { where = append(where, "m.stat_date >= ?") args = append(args, dateFrom) } if dateTo := strings.TrimSpace(query.Get("dateTo")); dateTo != "" { where = append(where, "m.stat_date <= ?") args = append(args, dateTo) } fromSQL := `FROM vehicle_daily_mileage m LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin WHERE ` + strings.Join(where, " AND ") return SQLQuery{ Text: `SELECT COUNT(DISTINCT m.vin) AS vehicle_count, COUNT(*) AS record_count, ` + `COUNT(DISTINCT m.protocol) AS source_count, COALESCE(SUM(m.daily_mileage_km), 0) AS total_mileage_km ` + fromSQL, Args: args, } } func buildMileageStatisticsWhere(query url.Values) (string, []any) { where := []string{"1 = 1"} args := []any{} if strings.EqualFold(strings.TrimSpace(query.Get("vehicleScope")), "bound") { where = append(where, "b.vin IS NOT NULL", "b.vin <> ''") } if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(m.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + vin + "%" args = append(args, like, like, like, like) } where, args = appendVINListFilter(where, args, "m.vin", query.Get("vins")) if protocols := parseMileageProtocols(query.Get("protocols")); len(protocols) > 0 { where, args = appendMileageProtocolFilter(where, args, "m.protocol", protocols) } else if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "m.protocol = ?") args = append(args, protocol) } if dateFrom := strings.TrimSpace(query.Get("dateFrom")); dateFrom != "" { where = append(where, "m.stat_date >= ?") args = append(args, dateFrom) } if dateTo := strings.TrimSpace(query.Get("dateTo")); dateTo != "" { where = append(where, "m.stat_date <= ?") args = append(args, dateTo) } return strings.Join(where, " AND "), args } func buildMileageStatisticsBaseSQL(query url.Values) (string, []any) { where, args := buildMileageStatisticsWhere(query) protocols := parseMileageProtocols(query.Get("protocols")) dailyMileageExpression := `MAX(COALESCE(m.daily_mileage_km, 0))` latestMileageExpression := `MAX(COALESCE(m.latest_total_mileage_km, 0))` if len(protocols) > 0 { selectionOrder := mileageProtocolOrder("m.protocol", protocols) dailyMileageExpression = `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(COALESCE(m.daily_mileage_km, 0) AS CHAR) ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)), 0)` latestMileageExpression = `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(COALESCE(m.latest_total_mileage_km, 0) AS CHAR) ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)), 0)` } return `SELECT m.vin, COALESCE(MAX(NULLIF(b.plate, '')), '') AS plate, m.stat_date, ` + dailyMileageExpression + ` AS daily_mileage_km, ` + latestMileageExpression + ` AS latest_mileage_km ` + `FROM vehicle_daily_mileage m LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin ` + `WHERE ` + where + ` GROUP BY m.vin, m.stat_date`, args } func buildMileageStatisticsSummarySQL(query url.Values) SQLQuery { base, args := buildMileageStatisticsBaseSQL(query) return SQLQuery{Text: `SELECT COUNT(DISTINCT d.vin), COUNT(*), COALESCE(SUM(d.daily_mileage_km), 0), ` + `COALESCE(AVG(d.daily_mileage_km), 0) FROM (` + base + `) d`, Args: args} } func buildMileageStatisticsTrendSQL(query url.Values) SQLQuery { base, args := buildMileageStatisticsBaseSQL(query) return SQLQuery{Text: `SELECT DATE_FORMAT(d.stat_date, '%Y-%m-%d'), COALESCE(SUM(d.daily_mileage_km), 0), ` + `COUNT(DISTINCT d.vin) FROM (` + base + `) d GROUP BY d.stat_date ORDER BY d.stat_date ASC`, Args: args} } func buildMileageStatisticsRankingSQL(query url.Values) SQLQuery { base, args := buildMileageStatisticsBaseSQL(query) return SQLQuery{Text: `SELECT d.vin, COALESCE(MAX(d.plate), ''), COALESCE(SUM(d.daily_mileage_km), 0), ` + `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(d.latest_mileage_km AS CHAR) ORDER BY d.stat_date DESC), ',', 1) AS DECIMAL(18,3)), 0), ` + `COUNT(DISTINCT d.stat_date) FROM (` + base + `) d ` + `GROUP BY d.vin ORDER BY SUM(d.daily_mileage_km) DESC, d.vin ASC LIMIT 20`, Args: args} } func buildMileageStatisticsSourceCountSQL(query url.Values) SQLQuery { where, args := buildMileageStatisticsWhere(query) return SQLQuery{Text: `SELECT COUNT(DISTINCT m.protocol) FROM vehicle_daily_mileage m ` + `LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin WHERE ` + where, Args: args} } func buildFleetLatestMileageSQL(query url.Values) SQLQuery { where := []string{"l.total_mileage_km IS NOT NULL", "l.total_mileage_km >= 0"} args := []any{} if strings.EqualFold(strings.TrimSpace(query.Get("vehicleScope")), "bound") { where = append(where, "b.vin IS NOT NULL", "b.vin <> ''") } if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(l.vin LIKE ? OR b.plate LIKE ? OR b.phone LIKE ? OR b.oem LIKE ?)") like := "%" + vin + "%" args = append(args, like, like, like, like) } where, args = appendVINListFilter(where, args, "l.vin", query.Get("vins")) protocols := parseMileageProtocols(query.Get("protocols")) if len(protocols) > 0 { where, args = appendMileageProtocolFilter(where, args, "l.protocol", protocols) } else if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" { where = append(where, "l.protocol = ?") args = append(args, protocol) } selectionOrder := `l.updated_at DESC, l.protocol ASC` if len(protocols) > 0 { selectionOrder = mileageProtocolOrder("l.protocol", protocols) + `, l.updated_at DESC` } inner := `SELECT l.vin, CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(l.total_mileage_km AS CHAR) ` + `ORDER BY ` + selectionOrder + `), ',', 1) AS DECIMAL(18,3)) AS latest_mileage_km ` + `FROM vehicle_realtime_location l LEFT JOIN vehicle_identity_binding b ON b.vin = l.vin WHERE ` + strings.Join(where, " AND ") + ` GROUP BY l.vin` return SQLQuery{Text: `SELECT COALESCE(SUM(x.latest_mileage_km), 0) FROM (` + inner + `) x`, Args: args} } func parseMileageProtocols(raw string) []string { seen := map[string]bool{} values := make([]string, 0, 3) for _, item := range strings.Split(raw, ",") { protocol := strings.ToUpper(strings.TrimSpace(item)) switch protocol { case "GB32960", "JT808", "YUTONG_MQTT": if !seen[protocol] { seen[protocol] = true values = append(values, protocol) } } } return values } func appendMileageProtocolFilter(where []string, args []any, column string, protocols []string) ([]string, []any) { placeholders := make([]string, len(protocols)) for index, protocol := range protocols { placeholders[index] = "?" args = append(args, protocol) } return append(where, column+" IN ("+strings.Join(placeholders, ",")+")"), args } func mileageProtocolOrder(column string, protocols []string) string { parts := make([]string, 0, len(protocols)+1) parts = append(parts, "CASE "+column) for index, protocol := range protocols { parts = append(parts, "WHEN '"+protocol+"' THEN "+strconv.Itoa(index+1)) } parts = append(parts, "ELSE 999 END") return strings.Join(parts, " ") + ", " + column + " ASC" } func appendVINListFilter(where []string, args []any, column string, raw string) ([]string, []any) { seen := map[string]bool{} values := make([]string, 0) for _, item := range strings.Split(raw, ",") { vin := strings.TrimSpace(item) if vin == "" || seen[vin] { continue } seen[vin] = true values = append(values, vin) } if len(values) == 0 { return where, args } placeholders := make([]string, len(values)) for index, vin := range values { placeholders[index] = "?" args = append(args, vin) } return append(where, column+" IN ("+strings.Join(placeholders, ",")+")"), args } func buildLimitOffset(query url.Values) (int, int) { return parsePositive(query.Get("limit"), 20), parsePositive(query.Get("offset"), 0) } func parseLimitOffset(rawLimit, rawOffset string) (int, int) { return parsePositive(rawLimit, 20), parsePositive(rawOffset, 0) } func mustInt(value string) int { n, _ := strconv.Atoi(value) return n }