feat(platform): consolidate production vehicle data workflows

This commit is contained in:
lingniu
2026-07-15 23:26:29 +08:00
parent 6cddc0a43d
commit 3fabcf181a
59 changed files with 6849 additions and 3532 deletions

View File

@@ -326,10 +326,14 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
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)
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":
@@ -415,12 +419,19 @@ func buildDailyMileageSQL(query url.Values) SQLQuery {
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)
}
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
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)
}
@@ -435,6 +446,26 @@ 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 ")
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, ` +
@@ -449,12 +480,18 @@ func buildDailyMileageSQL(query url.Values) SQLQuery {
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)
}
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
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)
}
@@ -477,12 +514,18 @@ func buildMileageSummarySQL(query url.Values) SQLQuery {
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)
}
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
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)
}
@@ -499,9 +542,17 @@ func buildMileageStatisticsWhere(query url.Values) (string, []any) {
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, ` +
`MAX(COALESCE(m.daily_mileage_km, 0)) AS daily_mileage_km, ` +
`MAX(COALESCE(m.latest_total_mileage_km, 0)) AS latest_mileage_km ` +
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
}
@@ -535,22 +586,90 @@ func buildMileageStatisticsSourceCountSQL(query url.Values) SQLQuery {
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)
}
if protocol := strings.TrimSpace(query.Get("protocol")); protocol != "" {
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 l.updated_at DESC, l.protocol ASC), ',', 1) AS DECIMAL(18,3)) AS latest_mileage_km ` +
`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)
}