426 lines
22 KiB
Go
426 lines
22 KiB
Go
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("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("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 ` +
|
|
`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 ` +
|
|
`FROM (` + groupSQL + `) vehicle_coverage_summary`,
|
|
Args: args,
|
|
}
|
|
}
|
|
|
|
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 keyword := strings.TrimSpace(query.Get("vin")); keyword != "" {
|
|
where = append(where, "(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)
|
|
}
|
|
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")
|
|
}
|
|
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 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)
|
|
}
|
|
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 buildMileageSummarySQL(query url.Values) SQLQuery {
|
|
args := []any{}
|
|
where := []string{"1 = 1"}
|
|
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)
|
|
}
|
|
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 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
|
|
}
|