feat: expand vehicle data platform capabilities
This commit is contained in:
@@ -76,10 +76,22 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery {
|
||||
vehicleSetSQL, args := buildVehicleCoverageSetSQL(query.Get("scopeVins"))
|
||||
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)
|
||||
where, args = appendBusinessCoverageFilters(where, args, query)
|
||||
if keywords := vehicleSearchKeywords(query); len(keywords) > 0 {
|
||||
if strings.TrimSpace(query.Get("keywords")) != "" {
|
||||
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(keywords)), ",")
|
||||
where = append(where, "(v.vin IN ("+placeholders+") OR s.plate IN ("+placeholders+") OR b.vin IN ("+placeholders+") OR b.plate IN ("+placeholders+") OR b.phone IN ("+placeholders+"))")
|
||||
for range 5 {
|
||||
for _, keyword := range keywords {
|
||||
args = append(args, keyword)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keyword := keywords[0]
|
||||
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 = ?")
|
||||
@@ -148,6 +160,8 @@ func buildVehicleCoverageSQL(query url.Values) SQLQuery {
|
||||
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 ` +
|
||||
`LEFT JOIN business_scope_state bst ON bst.id = 1 ` +
|
||||
`LEFT JOIN business_customer_vehicle_scope bs ON BINARY bs.source_version = BINARY bst.active_version AND BINARY bs.vin = BINARY v.vin ` +
|
||||
`WHERE ` + strings.Join(where, " AND ") + ` ` +
|
||||
`GROUP BY v.vin, b.plate, b.phone, b.oem, b.vin ` +
|
||||
havingSQL
|
||||
@@ -175,6 +189,7 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery {
|
||||
vehicleSetSQL, args := buildVehicleCoverageSetSQL(query.Get("scopeVins"))
|
||||
where := []string{"v.vin IS NOT NULL", "v.vin <> ''"}
|
||||
having := []string{}
|
||||
where, args = appendBusinessCoverageFilters(where, args, query)
|
||||
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 + "%"
|
||||
@@ -257,6 +272,8 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery {
|
||||
`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 ` +
|
||||
`LEFT JOIN business_scope_state bst ON bst.id = 1 ` +
|
||||
`LEFT JOIN business_customer_vehicle_scope bs ON BINARY bs.source_version = BINARY bst.active_version AND BINARY bs.vin = BINARY v.vin ` +
|
||||
`WHERE ` + strings.Join(where, " AND ") + ` ` +
|
||||
`GROUP BY v.vin ` + havingSQL
|
||||
return SQLQuery{
|
||||
@@ -275,6 +292,14 @@ func buildVehicleCoverageSummarySQL(query url.Values) SQLQuery {
|
||||
}
|
||||
}
|
||||
|
||||
func appendBusinessCoverageFilters(where []string, args []any, query url.Values) ([]string, []any) {
|
||||
where, args = appendCSVListFilter(where, args, "bs.department_id", query.Get("departmentIds"))
|
||||
where, args = appendCSVListFilter(where, args, "bs.responsible_user_id", query.Get("responsibleUserIds"))
|
||||
where, args = appendCSVListFilter(where, args, "CAST(bs.customer_id AS CHAR)", query.Get("customerIds"))
|
||||
where, args = appendCSVListFilter(where, args, "bs.operation_status", query.Get("operationStatuses"))
|
||||
return where, args
|
||||
}
|
||||
|
||||
func buildVehicleCoverageSetSQL(scope string) (string, []any) {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
@@ -454,6 +479,10 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
// 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.
|
||||
vehicleOrderSQL := `MAX(l.updated_at) IS NULL ASC, MAX(l.updated_at) DESC, v.vin ASC`
|
||||
if strings.EqualFold(strings.TrimSpace(query.Get("sort")), "identity") {
|
||||
vehicleOrderSQL = `COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), v.plate, '') ASC, v.vin ASC`
|
||||
}
|
||||
return SQLQuery{
|
||||
Text: `SELECT v.vin, ` +
|
||||
`COALESCE(NULLIF(MAX(NULLIF(l.plate, '')), ''), v.plate, '') AS plate, ` +
|
||||
@@ -482,7 +511,7 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
|
||||
`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) IS NULL ASC, MAX(l.updated_at) DESC, v.vin ASC LIMIT ? OFFSET ?`,
|
||||
`ORDER BY ` + vehicleOrderSQL + ` LIMIT ? OFFSET ?`,
|
||||
Args: args,
|
||||
CountText: `SELECT COUNT(*) FROM (SELECT v.vin ` + baseGroupSQL + groupSuffixSQL + `) vehicle_realtime_count`,
|
||||
CountArgs: countArgs,
|
||||
@@ -522,36 +551,57 @@ func buildDailyMileageSQL(query url.Values) SQLQuery {
|
||||
}
|
||||
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 ")
|
||||
fromSQL := `FROM vehicle_daily_mileage m
|
||||
LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin
|
||||
LEFT JOIN vehicle_open_daily_energy h
|
||||
ON h.vin COLLATE utf8mb4_unicode_ci = m.vin COLLATE utf8mb4_unicode_ci
|
||||
AND h.stat_date = m.stat_date
|
||||
AND h.energy_type = 'HYDROGEN' AND h.quality_status = 'OK'
|
||||
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))`
|
||||
pureHydrogenMileageExpression := `MAX(COALESCE(m.pure_hydrogen_mileage_km, 0))`
|
||||
if len(protocols) > 0 {
|
||||
selectionOrder = mileageDailySelectionOrder("m.daily_mileage_km", "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)`
|
||||
pureHydrogenMileageExpression = `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(COALESCE(m.pure_hydrogen_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{
|
||||
built := 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, ` +
|
||||
pureHydrogenMileageExpression + ` AS pure_hydrogen_mileage_km, ` +
|
||||
`MAX(h.consumption_kg) AS hydrogen_consumption_kg, ` +
|
||||
`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,
|
||||
}
|
||||
if query.Get("skipCount") == "1" || strings.EqualFold(query.Get("skipCount"), "true") {
|
||||
built.CountText = ""
|
||||
built.CountArgs = nil
|
||||
}
|
||||
return built
|
||||
}
|
||||
return SQLQuery{
|
||||
built := 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 ` +
|
||||
`COALESCE(m.latest_total_mileage_km, 0) AS end_mileage_km, m.daily_mileage_km, ` +
|
||||
`COALESCE(m.pure_hydrogen_mileage_km, 0), h.consumption_kg, 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,
|
||||
}
|
||||
if query.Get("skipCount") == "1" || strings.EqualFold(query.Get("skipCount"), "true") {
|
||||
built.CountText = ""
|
||||
built.CountArgs = nil
|
||||
}
|
||||
return built
|
||||
}
|
||||
|
||||
func buildMileageSummarySQL(query url.Values) SQLQuery {
|
||||
@@ -585,7 +635,8 @@ func buildMileageSummarySQL(query url.Values) SQLQuery {
|
||||
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,
|
||||
`COUNT(DISTINCT m.protocol) AS source_count, COALESCE(SUM(m.daily_mileage_km), 0) AS total_mileage_km, ` +
|
||||
`COALESCE(SUM(m.pure_hydrogen_mileage_km), 0) AS total_pure_hydrogen_mileage_km ` + fromSQL,
|
||||
Args: args,
|
||||
}
|
||||
}
|
||||
@@ -625,28 +676,43 @@ func buildMileageStatisticsBaseSQL(query url.Values) (string, []any) {
|
||||
where, args := buildMileageStatisticsWhere(query)
|
||||
protocols := parseMileageProtocols(query.Get("protocols"))
|
||||
dailyMileageExpression := `MAX(COALESCE(m.daily_mileage_km, 0))`
|
||||
pureHydrogenMileageExpression := `MAX(COALESCE(m.pure_hydrogen_mileage_km, 0))`
|
||||
latestMileageExpression := `MAX(COALESCE(m.latest_total_mileage_km, 0))`
|
||||
if len(protocols) > 0 {
|
||||
selectionOrder := mileageDailySelectionOrder("m.daily_mileage_km", "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)`
|
||||
pureHydrogenMileageExpression = `COALESCE(CAST(SUBSTRING_INDEX(GROUP_CONCAT(CAST(COALESCE(m.pure_hydrogen_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, ` +
|
||||
pureHydrogenMileageExpression + ` AS pure_hydrogen_mileage_km, ` +
|
||||
`MAX(h.consumption_kg) AS hydrogen_consumption_kg, ` +
|
||||
latestMileageExpression + ` AS latest_mileage_km ` +
|
||||
`FROM vehicle_daily_mileage m LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin ` +
|
||||
`FROM vehicle_daily_mileage m
|
||||
LEFT JOIN vehicle_identity_binding b ON b.vin = m.vin
|
||||
LEFT JOIN vehicle_open_daily_energy h
|
||||
ON h.vin COLLATE utf8mb4_unicode_ci = m.vin COLLATE utf8mb4_unicode_ci
|
||||
AND h.stat_date = m.stat_date
|
||||
AND h.energy_type = 'HYDROGEN' AND h.quality_status = 'OK' ` +
|
||||
`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(SUM(d.pure_hydrogen_mileage_km), 0), ` +
|
||||
`COUNT(d.hydrogen_consumption_kg), COALESCE(SUM(d.hydrogen_consumption_kg), 0), ` +
|
||||
`COALESCE(SUM(CASE WHEN d.hydrogen_consumption_kg IS NOT NULL THEN d.daily_mileage_km ELSE 0 END), 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), ` +
|
||||
`COALESCE(SUM(d.pure_hydrogen_mileage_km), 0), ` +
|
||||
`COUNT(d.hydrogen_consumption_kg), COALESCE(SUM(d.hydrogen_consumption_kg), 0), ` +
|
||||
`COALESCE(SUM(CASE WHEN d.hydrogen_consumption_kg IS NOT NULL THEN d.daily_mileage_km ELSE 0 END), 0), ` +
|
||||
`COUNT(DISTINCT d.vin) FROM (` + base + `) d GROUP BY d.stat_date ORDER BY d.stat_date ASC`, Args: args}
|
||||
}
|
||||
|
||||
@@ -740,23 +806,31 @@ func mileageDailySelectionOrder(mileageColumn, protocolColumn string, protocols
|
||||
}
|
||||
|
||||
func appendVINListFilter(where []string, args []any, column string, raw string) ([]string, []any) {
|
||||
return appendListFilter(where, args, column, raw, 0)
|
||||
}
|
||||
|
||||
func appendCSVListFilter(where []string, args []any, column string, raw string) ([]string, []any) {
|
||||
return appendListFilter(where, args, column, raw, 200)
|
||||
}
|
||||
|
||||
func appendListFilter(where []string, args []any, column string, raw string, maxValues int) ([]string, []any) {
|
||||
seen := map[string]bool{}
|
||||
values := make([]string, 0)
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
vin := strings.TrimSpace(item)
|
||||
if vin == "" || seen[vin] {
|
||||
value := strings.TrimSpace(item)
|
||||
if value == "" || seen[value] || (maxValues > 0 && len(values) >= maxValues) {
|
||||
continue
|
||||
}
|
||||
seen[vin] = true
|
||||
values = append(values, vin)
|
||||
seen[value] = true
|
||||
values = append(values, value)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return where, args
|
||||
}
|
||||
placeholders := make([]string, len(values))
|
||||
for index, vin := range values {
|
||||
for index, value := range values {
|
||||
placeholders[index] = "?"
|
||||
args = append(args, vin)
|
||||
args = append(args, value)
|
||||
}
|
||||
return append(where, column+" IN ("+strings.Join(placeholders, ",")+")"), args
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user