package platform import ( "crypto/sha1" "encoding/hex" "strconv" "strings" "time" ) func buildRawFrameSQL(database string, query RawFrameQuery) SQLQuery { table := rawFrameTable(database, query) where := rawFrameWhere(query) parsedFieldsSelect := "'' AS parsed_fields" if query.IncludeFields || len(query.Fields) > 0 { parsedFieldsSelect = "parsed_json AS parsed_fields" } args := []any{} limit := query.Limit if limit <= 0 { limit = 100 } offset := query.Offset if offset < 0 { offset = 0 } text := `SELECT ts, frame_id, event_time, received_at, raw_size_bytes, ` + parsedFieldsSelect + `, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone FROM ` + table if len(where) > 0 { text += ` WHERE ` + strings.Join(where, " AND ") } countText := `SELECT COUNT(*) FROM ` + table if len(where) > 0 { countText += ` WHERE ` + strings.Join(where, " AND ") } if query.SkipCount { countText = "" } text += ` ORDER BY ts DESC, frame_id ASC LIMIT ` + strconv.Itoa(limit) + ` OFFSET ` + strconv.Itoa(offset) return SQLQuery{Text: text, Args: args, CountText: countText} } func rawFrameWhere(query RawFrameQuery) []string { where := make([]string, 0, 4) if query.Protocol != "" { where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(strings.TrimSpace(query.Protocol)))+"'") } if query.VIN != "" { where = append(where, "vin = '"+quoteTDengine(strings.TrimSpace(query.VIN))+"'") } if query.DateFrom != "" { where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineTime(query.DateFrom))+"'") } if query.DateTo != "" { where = append(where, "ts <= '"+quoteTDengine(normalizeTDengineTime(query.DateTo))+"'") } return where } func buildHistoryLocationSQL(database string, query map[string]string) SQLQuery { table := locationTable(database, query) where := make([]string, 0, 4) args := []any{} if protocol := strings.TrimSpace(query["protocol"]); protocol != "" { where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(protocol))+"'") } if vin := strings.TrimSpace(query["vin"]); vin != "" { where = append(where, "vin = '"+quoteTDengine(vin)+"'") } if dateFrom := strings.TrimSpace(query["dateFrom"]); dateFrom != "" { where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineTime(dateFrom))+"'") } if dateTo := strings.TrimSpace(query["dateTo"]); dateTo != "" { where = append(where, "ts <= '"+quoteTDengine(normalizeTDengineTime(dateTo))+"'") } limit, offset := parseLimitOffset(query["limit"], query["offset"]) text := `SELECT CAST(ts AS BIGINT), vin, protocol, longitude, latitude, speed_kmh, soc_percent, direction_deg, alarm_flag, total_mileage_km, CAST(received_at AS BIGINT) FROM ` + table if len(where) > 0 { text += ` WHERE ` + strings.Join(where, " AND ") } countText := `SELECT COUNT(*) FROM ` + table if len(where) > 0 { countText += ` WHERE ` + strings.Join(where, " AND ") } if strings.TrimSpace(query["skipCount"]) == "1" { countText = "" } text += ` ORDER BY ts DESC, vin ASC, protocol ASC LIMIT ` + strconv.Itoa(limit) + ` OFFSET ` + strconv.Itoa(offset) return SQLQuery{Text: text, Args: args, CountText: countText} } func buildHistoryLocationSeriesSQL(database string, query HistoryLocationSeriesQuery) SQLQuery { tableQuery := map[string]string{"vin": query.VIN, "protocol": query.Protocol} where := []string{"vin = '" + quoteTDengine(strings.TrimSpace(query.VIN)) + "'"} if protocol := strings.TrimSpace(query.Protocol); protocol != "" { where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(protocol))+"'") } where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineSeriesTime(query.DateFrom))+"'", "ts <= '"+quoteTDengine(normalizeTDengineSeriesTime(query.DateTo))+"'", ) grain := query.GrainSeconds if grain <= 0 { grain = 60 } text := `SELECT _wstart, protocol, COUNT(*), ` + `AVG(speed_kmh), MIN(speed_kmh), MAX(speed_kmh), LAST(speed_kmh), ` + `AVG(total_mileage_km), MIN(total_mileage_km), MAX(total_mileage_km), LAST(total_mileage_km) ` + `FROM ` + locationTable(database, tableQuery) + ` WHERE ` + strings.Join(where, " AND ") + ` PARTITION BY protocol INTERVAL(` + strconv.Itoa(grain) + `s) ORDER BY _wstart ASC, protocol ASC` return SQLQuery{Text: text} } func buildHistoryExportBatchSQL(database string, query HistoryExportStoreQuery, cursor HistoryExportCursor, limit int) SQLQuery { if limit <= 0 || limit > 5000 { limit = 5000 } switch query.Category { case "raw": return buildRawExportBatchSQL(database, query, cursor, limit) default: return buildLocationExportBatchSQL(database, query, cursor, limit) } } func buildLocationExportBatchSQL(database string, query HistoryExportStoreQuery, cursor HistoryExportCursor, limit int) SQLQuery { tableQuery := map[string]string{"vin": query.VIN, "protocol": query.Protocol} where := historyExportTDengineWhere(query) if cursor.Time != "" { cursorTime := quoteTDengine(cursor.Time) cursorProtocol := quoteTDengine(cursor.Protocol) where = append(where, "(ts > '"+cursorTime+"' OR (ts = '"+cursorTime+"' AND protocol > '"+cursorProtocol+"'))") } table := locationTable(database, tableQuery) return SQLQuery{ Text: `SELECT ts, vin, protocol, longitude, latitude, speed_kmh, total_mileage_km, received_at FROM ` + table + ` WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ts ASC, protocol ASC LIMIT ` + strconv.Itoa(limit), CountText: `SELECT COUNT(*) FROM ` + table + ` WHERE ` + strings.Join(historyExportTDengineWhere(query), " AND "), } } func buildRawExportBatchSQL(database string, query HistoryExportStoreQuery, cursor HistoryExportCursor, limit int) SQLQuery { rawQuery := RawFrameQuery{VIN: query.VIN, Protocol: query.Protocol} where := historyExportTDengineWhere(query) if cursor.Time != "" { timeValue := quoteTDengine(cursor.Time) protocol := quoteTDengine(cursor.Protocol) identifier := quoteTDengine(cursor.ID) where = append(where, "(ts > '"+timeValue+"' OR (ts = '"+timeValue+"' AND (protocol > '"+protocol+"' OR (protocol = '"+protocol+"' AND frame_id > '"+identifier+"'))))") } table := rawFrameTable(database, rawQuery) selectText := `SELECT ts, frame_id, event_time, received_at, raw_size_bytes, parsed_json, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone FROM ` + table + ` WHERE ` + strings.Join(where, " AND ") return SQLQuery{Text: selectText + ` ORDER BY ts ASC, protocol ASC, frame_id ASC LIMIT ` + strconv.Itoa(limit), CountText: `SELECT COUNT(*) FROM ` + table + ` WHERE ` + strings.Join(historyExportTDengineWhere(query), " AND ")} } func historyExportTDengineWhere(query HistoryExportStoreQuery) []string { where := []string{"vin = '" + quoteTDengine(strings.TrimSpace(query.VIN)) + "'"} if protocol := strings.TrimSpace(query.Protocol); protocol != "" { where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(protocol))+"'") } if query.DateFrom != "" { where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineSeriesTime(query.DateFrom))+"'") } if query.DateTo != "" { where = append(where, "ts <= '"+quoteTDengine(normalizeTDengineSeriesTime(query.DateTo))+"'") } return where } func normalizeTDengineSeriesTime(value string) string { if parsed, ok := parseTrackRequestTime(value); ok { return parsed.Format(time.RFC3339) } return strings.TrimSpace(value) } func buildTodayRawFrameCountSQL(database string, now time.Time) SQLQuery { start := shanghaiDayStartUTC(now) return SQLQuery{ Text: `SELECT COUNT(*) FROM ` + qualifyTDengine(database, "raw_frames") + ` WHERE ts >= '` + quoteTDengine(start) + `'`, } } func qualifyTDengine(database, table string) string { if strings.TrimSpace(database) == "" { return table } return database + "." + table } func rawFrameTable(database string, query RawFrameQuery) string { protocol := strings.ToUpper(strings.TrimSpace(query.Protocol)) vin := strings.TrimSpace(query.VIN) if protocol == "" || vin == "" { return qualifyTDengine(database, "raw_frames") } if protocol == "JT808" { return qualifyTDengine(database, "raw_frames") } return qualifyTDengine(database, "raw_"+strings.ToLower(protocol)+"_"+hash16(vin)) } func locationTable(database string, query map[string]string) string { protocol := strings.ToUpper(strings.TrimSpace(query["protocol"])) vin := strings.TrimSpace(query["vin"]) if protocol == "" || vin == "" { return qualifyTDengine(database, "vehicle_locations") } return qualifyTDengine(database, "loc_"+strings.ToLower(protocol)+"_"+hash16(vin)) } func hash16(value string) string { sum := sha1.Sum([]byte(value)) return hex.EncodeToString(sum[:8]) } func quoteTDengine(value string) string { return strings.ReplaceAll(value, "'", "''") } func normalizeTDengineTime(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } shanghai := time.FixedZone("Asia/Shanghai", 8*3600) for _, layout := range []string{"2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02 15:04:05", "2006-01-02 15:04", "2006-01-02"} { if parsed, err := time.ParseInLocation(layout, value, shanghai); err == nil { return parsed.Format(time.RFC3339) } } if parsed, err := time.Parse(time.RFC3339, value); err == nil { return parsed.Format(time.RFC3339) } return value } func shanghaiDayStartUTC(now time.Time) string { shanghai := time.FixedZone("Asia/Shanghai", 8*3600) local := now.In(shanghai) start := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghai) return start.Format(time.RFC3339) }