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) 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) } args = append(args, limit, offset) 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 ` + `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 ` + `WHERE ` + strings.Join(where, " AND ") + ` ORDER BY s.updated_at DESC LIMIT ? OFFSET ?`, Args: args, } } func buildVehicleCoverageSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) args := []any{} where := []string{"s.vin IS NOT NULL", "s.vin <> ''"} having := []string{} if keyword := strings.TrimSpace(query.Get("keyword")); keyword != "" { where = append(where, "(s.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") } 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") } 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_snapshot s ` + `LEFT JOIN vehicle_identity_binding b ON b.vin = s.vin ` + `WHERE ` + strings.Join(where, " AND ") + ` ` + `GROUP BY s.vin, b.plate, b.phone, b.oem, b.vin ` + havingSQL return SQLQuery{ Text: `SELECT s.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, ` + `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, s.vin ASC LIMIT ? OFFSET ?`, Args: args, CountText: `SELECT COUNT(*) FROM (SELECT s.vin ` + groupSQL + `) vehicle_coverage_count`, CountArgs: countArgs, } } 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 buildDailyMileageSQL(query url.Values) SQLQuery { limit := parsePositive(query.Get("limit"), 20) offset := parsePositive(query.Get("offset"), 0) args := []any{} where := []string{"1 = 1"} if vin := strings.TrimSpace(query.Get("vin")); vin != "" { where = append(where, "(m.vin LIKE ? OR b.plate LIKE ?)") like := "%" + vin + "%" args = append(args, like, like) } 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 ") return SQLQuery{ Text: `SELECT m.vin, COALESCE(b.plate, '') AS plate, DATE_FORMAT(m.stat_date, '%Y-%m-%d') AS stat_date, ` + `m.first_total_mileage_km, m.latest_total_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 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 }