package realtime import ( "context" "database/sql" "encoding/json" "errors" "math" "net" "strings" "sync" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/telemetry" ) type SnapshotExecer interface { ExecContext(context.Context, string, ...any) (sql.Result, error) } type PlateResolver interface { PlateByVIN(context.Context, string) (string, error) } type CachedPlateResolver struct { delegate PlateResolver ttl time.Duration maxEntries int now func() time.Time observe func(PlateCacheStats) mu sync.Mutex entries map[string]cachedPlateEntry evictions int } type PlateCacheStats struct { Entries int MaxEntries int Evictions int } type cachedPlateEntry struct { plate string notFound bool expiresAt time.Time } func NewCachedPlateResolver(delegate PlateResolver, ttl time.Duration) *CachedPlateResolver { return NewCachedPlateResolverWithMaxEntries(delegate, ttl, 200000) } func NewCachedPlateResolverWithMaxEntries(delegate PlateResolver, ttl time.Duration, maxEntries int) *CachedPlateResolver { if delegate == nil { panic("cached plate resolver delegate must not be nil") } if ttl <= 0 { ttl = 10 * time.Minute } if maxEntries < 0 { maxEntries = 0 } return &CachedPlateResolver{ delegate: delegate, ttl: ttl, maxEntries: maxEntries, now: time.Now, entries: map[string]cachedPlateEntry{}, } } func (r *CachedPlateResolver) SetStatsObserver(observe func(PlateCacheStats)) { r.mu.Lock() r.observe = observe r.mu.Unlock() } func (r *CachedPlateResolver) CacheStats() PlateCacheStats { r.mu.Lock() defer r.mu.Unlock() return PlateCacheStats{Entries: len(r.entries), MaxEntries: r.maxEntries, Evictions: r.evictions} } func (r *CachedPlateResolver) PlateByVIN(ctx context.Context, vin string) (string, error) { vin = strings.TrimSpace(vin) if vin == "" { return "", sql.ErrNoRows } now := r.now() r.mu.Lock() entry, ok := r.entries[vin] if ok && now.Before(entry.expiresAt) { r.mu.Unlock() if entry.notFound { return "", sql.ErrNoRows } return entry.plate, nil } r.mu.Unlock() plate, err := r.delegate.PlateByVIN(ctx, vin) if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", err } r.mu.Lock() r.entries[vin] = cachedPlateEntry{ plate: strings.TrimSpace(plate), notFound: errors.Is(err, sql.ErrNoRows), expiresAt: now.Add(r.ttl), } r.enforceLimitLocked(now) stats := PlateCacheStats{Entries: len(r.entries), MaxEntries: r.maxEntries, Evictions: r.evictions} observe := r.observe r.mu.Unlock() if observe != nil { observe(stats) } if err != nil { return "", err } return strings.TrimSpace(plate), nil } func (r *CachedPlateResolver) enforceLimitLocked(now time.Time) { if r.maxEntries <= 0 || len(r.entries) <= r.maxEntries { return } for vin, entry := range r.entries { if now.After(entry.expiresAt) || now.Equal(entry.expiresAt) { delete(r.entries, vin) r.evictions++ } } for len(r.entries) > r.maxEntries { victim := "" var victimExpiresAt time.Time for vin, entry := range r.entries { if victim == "" || entry.expiresAt.Before(victimExpiresAt) { victim = vin victimExpiresAt = entry.expiresAt } } if victim == "" { return } delete(r.entries, victim) r.evictions++ } } type SnapshotWriter struct { exec SnapshotExecer plateResolver PlateResolver } func NewSnapshotWriter(exec SnapshotExecer) *SnapshotWriter { return NewSnapshotWriterWithPlateResolver(exec, nil) } func NewSnapshotWriterWithPlateResolver(exec SnapshotExecer, plateResolver PlateResolver) *SnapshotWriter { if exec == nil { panic("snapshot execer must not be nil") } return &SnapshotWriter{exec: exec, plateResolver: plateResolver} } func (w *SnapshotWriter) EnsureSchema(ctx context.Context) error { if _, err := w.exec.ExecContext(ctx, realtimeSnapshotTableSQL); err != nil { return err } for _, statement := range realtimeSnapshotCompatibilitySQL { if _, err := w.exec.ExecContext(ctx, statement); err != nil && !isDuplicateColumnError(err) { return err } } if _, err := w.exec.ExecContext(ctx, realtimeLocationTableSQL); err != nil { return err } for _, statement := range realtimeLocationCompatibilitySQL { if _, err := w.exec.ExecContext(ctx, statement); err != nil && !isDuplicateColumnError(err) { return err } } if _, err := w.exec.ExecContext(ctx, jt808RealtimeLocationSourceTableSQL); err != nil { return err } if _, err := w.exec.ExecContext(ctx, vehicleLocationSourcePolicyTableSQL); err != nil { return err } for _, statement := range cleanupInvalidRealtimeTotalMileageSQL { if _, err := w.exec.ExecContext(ctx, statement); err != nil { return err } } if _, err := w.exec.ExecContext(ctx, backfillRealtimeAccessProjectionSQL); err != nil { return err } return nil } func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope) error { if !envelope.IsRealtimeTelemetryFrame(env) { return nil } vin := strings.TrimSpace(env.VIN) if vin == "" { return nil } if !isRealtimeSnapshotEvent(env) { return nil } plate, err := w.plateForEnvelope(ctx, env) if err != nil { return err } eventTimeMS, _ := envelope.NormalizedEventTimeMS(env) eventTime := nullableTime(eventTimeMS) receivedAt := nullableTime(env.ReceivedAtMS) platformName := platformNameFromEnvelope(env) parsed := snapshotFieldsForEnvelope(env) if _, err = w.exec.ExecContext(ctx, upsertRealtimeSnapshotSQL, string(env.Protocol), vin, plate, platformName, env.SourceEndpoint, marshalParsedJSON(parsed), eventTime, receivedAt, env.StableEventID(), receivedAt, receivedAt, env.StableEventID(), ); err != nil { return err } location, ok := realtimeLocationFromEnvelope(env, vin, plate) if !ok { return nil } locationArgs := []any{ location.Protocol, location.VIN, location.Plate, location.EventTime, location.Latitude, location.Longitude, location.SpeedKMH, location.TotalMileageKM, location.TotalMileageAt, location.SOCPercent, location.AltitudeM, location.DirectionDeg, location.AlarmFlag, location.StatusFlag, location.ReceivedAt, location.EventID, } if env.Protocol != envelope.ProtocolJT808 { _, err = w.exec.ExecContext(ctx, upsertRealtimeLocationSQL, locationArgs...) return err } sourceKey := realtimeLocationSourceKey(env, vin) sourceArgs := append([]any{sourceKey, strings.TrimSpace(env.Phone), strings.TrimSpace(env.DeviceID), strings.TrimSpace(env.SourceCode), normalizedSourceKind(env.SourceKind), strings.TrimSpace(env.SourceEndpoint)}, locationArgs...) sourceArgs = append(sourceArgs, location.ReceivedAt) if _, err = w.exec.ExecContext(ctx, upsertJT808RealtimeLocationSourceSQL, sourceArgs...); err != nil { return err } _, err = w.exec.ExecContext(ctx, electJT808RealtimeLocationSQL, vin) return err } func realtimeLocationSourceKey(env envelope.FrameEnvelope, vin string) string { identity := strings.TrimSpace(env.Phone) if identity == "" { identity = strings.TrimSpace(env.DeviceID) } if identity == "" { identity = strings.TrimSpace(vin) } scope := strings.TrimSpace(env.SourceCode) if scope == "" { scope = normalizedSourceKind(env.SourceKind) } if scope == "" || scope == "UNKNOWN" { scope = sourceHost(env.SourceEndpoint) } if scope == "" { scope = "unknown" } return string(env.Protocol) + ":" + identity + "@" + scope } func normalizedSourceKind(value string) string { value = strings.ToUpper(strings.TrimSpace(value)) switch value { case "DIRECT", "PLATFORM": return value default: return "UNKNOWN" } } func sourceHost(endpoint string) string { endpoint = strings.TrimSpace(endpoint) if endpoint == "" { return "" } if host, _, err := net.SplitHostPort(endpoint); err == nil { return strings.TrimSpace(host) } if index := strings.LastIndex(endpoint, ":"); index > 0 && strings.Count(endpoint, ":") == 1 { return endpoint[:index] } return endpoint } func snapshotFieldsForEnvelope(env envelope.FrameEnvelope) map[string]any { if len(env.ParsedFields) == 0 { return nil } return realtimeSnapshotFlatFields(env) } func realtimeSnapshotFlatFields(env envelope.FrameEnvelope) map[string]any { fields := cloneMap(env.ParsedFields) filterInvalidRealtimeMeasurementFields(fields) if len(fields) == 0 { return nil } return fields } func platformNameFromEnvelope(env envelope.FrameEnvelope) string { if value := strings.TrimSpace(env.PlatformName); value != "" { return value } if env.Parsed != nil { if value := strings.TrimSpace(stringValue(env.Parsed["platform_name"])); value != "" { return value } if login, ok := env.Parsed["platform_login"].(map[string]any); ok { return strings.TrimSpace(stringValue(login["username"])) } } return "" } func stringValue(value any) string { switch typed := value.(type) { case string: return typed default: return "" } } func marshalParsedJSON(parsed map[string]any) any { if len(parsed) == 0 { return nil } data, err := json.Marshal(parsed) if err != nil { return nil } return string(data) } func (w *SnapshotWriter) plateForEnvelope(ctx context.Context, env envelope.FrameEnvelope) (string, error) { if plate := strings.TrimSpace(env.Plate); plate != "" { return plate, nil } if w.plateResolver == nil { return "", nil } vin := strings.TrimSpace(env.VIN) if vin == "" { return "", nil } plate, err := w.plateResolver.PlateByVIN(ctx, vin) if err != nil { if errors.Is(err, sql.ErrNoRows) { return "", nil } return "", err } return strings.TrimSpace(plate), nil } type realtimeLocationRow struct { Protocol string VIN string Plate string EventTime any Latitude float64 Longitude float64 SpeedKMH any TotalMileageKM any TotalMileageAt any SOCPercent any AltitudeM any DirectionDeg any AlarmFlag any StatusFlag any ReceivedAt any EventID string } func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vin string, plate string) (realtimeLocationRow, bool) { location, ok := telemetry.LocationProjectionForProtocol(env.Protocol, env.ParsedFields) if !ok { return realtimeLocationRow{}, false } if math.IsNaN(location.Latitude) || math.IsNaN(location.Longitude) || math.IsInf(location.Latitude, 0) || math.IsInf(location.Longitude, 0) || location.Latitude < -90 || location.Latitude > 90 || location.Longitude < -180 || location.Longitude > 180 || (math.Abs(location.Latitude) < 0.000001 && math.Abs(location.Longitude) < 0.000001) { return realtimeLocationRow{}, false } eventTimeMS, _ := envelope.NormalizedEventTimeMS(env) totalMileageKM, hasTotalMileage := telemetry.TotalMileageKM(env.Protocol, env.ParsedFields) if totalMileageKM <= 0 { hasTotalMileage = false } var totalMileageValue any var totalMileageAt any if hasTotalMileage { totalMileageValue = totalMileageKM totalMileageAt = nullableTime(eventTimeMS) } return realtimeLocationRow{ Protocol: string(env.Protocol), VIN: vin, Plate: plate, EventTime: nullableTime(eventTimeMS), Latitude: location.Latitude, Longitude: location.Longitude, SpeedKMH: optionalFloatValue(location.SpeedKMH), TotalMileageKM: totalMileageValue, TotalMileageAt: totalMileageAt, SOCPercent: optionalFloatValue(location.SOCPercent), AltitudeM: optionalFloatValue(location.AltitudeM), DirectionDeg: optionalFloatValue(location.DirectionDeg), AlarmFlag: optionalInt64Value(location.AlarmFlag), StatusFlag: optionalInt64Value(location.StatusFlag), ReceivedAt: nullableTime(env.ReceivedAtMS), EventID: env.StableEventID(), }, true } func optionalFloatValue(value *float64) any { if value == nil { return nil } return *value } func optionalInt64Value(value *int64) any { if value == nil { return nil } return *value } type BindingPlateResolver struct { queryer Queryer table string } type Queryer interface { QueryRowContext(context.Context, string, ...any) *sql.Row } func NewBindingPlateResolver(queryer Queryer, table string) *BindingPlateResolver { if queryer == nil { panic("plate binding queryer must not be nil") } table = strings.TrimSpace(table) if table == "" || !safeIdentifier(table) { table = "vehicle_identity_binding" } return &BindingPlateResolver{queryer: queryer, table: table} } func (r *BindingPlateResolver) PlateByVIN(ctx context.Context, vin string) (string, error) { vin = strings.TrimSpace(vin) if vin == "" { return "", sql.ErrNoRows } query := "SELECT plate FROM " + r.table + " WHERE vin = ? AND plate IS NOT NULL AND plate <> ''" var plate string err := r.queryer.QueryRowContext(ctx, query, vin).Scan(&plate) if err != nil { return "", err } return strings.TrimSpace(plate), nil } func safeIdentifier(value string) bool { if value == "" { return false } for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { continue } return false } return true } func nullableTime(ms int64) any { if ms <= 0 { return nil } return time.UnixMilli(ms) } func isRealtimeSnapshotEvent(env envelope.FrameEnvelope) bool { switch env.Protocol { case envelope.ProtocolGB32960: return (env.MessageID == "0x02" || env.MessageID == "0x03") && telemetry.HasRealtimeFields(env.Protocol, env.ParsedFields) case envelope.ProtocolJT808: return env.MessageID == "0x0200" && hasLocationFields(env) case envelope.ProtocolYutongMQTT: return telemetry.HasRealtimeFields(env.Protocol, env.ParsedFields) default: return false } } func hasLocationFields(env envelope.FrameEnvelope) bool { _, ok := telemetry.LocationProjectionForProtocol(env.Protocol, env.ParsedFields) return ok } const realtimeSnapshotTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_snapshot ( protocol VARCHAR(32) NOT NULL, vin VARCHAR(32) NOT NULL DEFAULT '', plate VARCHAR(32) NOT NULL DEFAULT '', platform_name VARCHAR(64) NOT NULL DEFAULT '', peer VARCHAR(128) NOT NULL DEFAULT '', parsed_json LONGTEXT NULL, event_time DATETIME(3) NULL, received_at DATETIME(3) NULL, event_id VARCHAR(64) NOT NULL DEFAULT '', access_first_seen_at DATETIME(3) NULL, access_previous_received_at DATETIME(3) NULL, access_latest_received_at DATETIME(3) NULL, access_report_interval_ms BIGINT UNSIGNED NULL, access_sample_count BIGINT UNSIGNED NOT NULL DEFAULT 0, access_latest_event_id VARCHAR(64) NOT NULL DEFAULT '', access_first_seen_source VARCHAR(32) NOT NULL DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (protocol, vin), KEY idx_vin (vin), KEY idx_protocol_updated (protocol, updated_at), KEY idx_access_latest (access_latest_received_at, vin) )` var realtimeSnapshotCompatibilitySQL = []string{ "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN platform_name VARCHAR(64) NOT NULL DEFAULT '' AFTER plate", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN peer VARCHAR(128) NOT NULL DEFAULT '' AFTER platform_name", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN parsed_json LONGTEXT NULL AFTER peer", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_first_seen_at DATETIME(3) NULL AFTER event_id", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_previous_received_at DATETIME(3) NULL AFTER access_first_seen_at", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_latest_received_at DATETIME(3) NULL AFTER access_previous_received_at", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_report_interval_ms BIGINT UNSIGNED NULL AFTER access_latest_received_at", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_sample_count BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER access_report_interval_ms", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_latest_event_id VARCHAR(64) NOT NULL DEFAULT '' AFTER access_sample_count", "ALTER TABLE vehicle_realtime_snapshot ADD COLUMN access_first_seen_source VARCHAR(32) NOT NULL DEFAULT '' AFTER access_latest_event_id", } func isDuplicateColumnError(err error) bool { text := strings.ToLower(err.Error()) return strings.Contains(text, "duplicate column") || strings.Contains(text, "1060") } const upsertRealtimeSnapshotSQL = ` INSERT INTO vehicle_realtime_snapshot (protocol, vin, plate, platform_name, peer, parsed_json, event_time, received_at, event_id, access_first_seen_at, access_latest_received_at, access_sample_count, access_latest_event_id, access_first_seen_source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 'live_writer') ON DUPLICATE KEY UPDATE plate = IF(VALUES(plate) <> '', VALUES(plate), plate), platform_name = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(platform_name), platform_name), peer = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(peer), peer), parsed_json = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), IF(VALUES(parsed_json) IS NULL OR VALUES(parsed_json) = '', parsed_json, JSON_MERGE_PATCH(COALESCE(NULLIF(vehicle_realtime_snapshot.parsed_json, ''), JSON_OBJECT()), VALUES(parsed_json))), parsed_json), event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(event_time), event_time), received_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(received_at), received_at), event_id = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), VALUES(event_id), event_id), updated_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_snapshot.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_snapshot.event_time), CURRENT_TIMESTAMP, updated_at), access_previous_received_at = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, vehicle_realtime_snapshot.access_latest_received_at, access_previous_received_at), access_report_interval_ms = IF(VALUES(access_latest_received_at) IS NOT NULL AND vehicle_realtime_snapshot.access_latest_received_at IS NOT NULL AND VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, TIMESTAMPDIFF(MICROSECOND, vehicle_realtime_snapshot.access_latest_received_at, VALUES(access_latest_received_at)) DIV 1000, access_report_interval_ms), access_sample_count = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, access_sample_count + 1, access_sample_count), access_latest_received_at = IF(VALUES(access_latest_received_at) IS NOT NULL AND (vehicle_realtime_snapshot.access_latest_received_at IS NULL OR VALUES(access_latest_received_at) > vehicle_realtime_snapshot.access_latest_received_at) AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, VALUES(access_latest_received_at), access_latest_received_at), access_latest_event_id = IF(VALUES(access_latest_received_at) IS NOT NULL AND VALUES(access_latest_received_at) = vehicle_realtime_snapshot.access_latest_received_at AND VALUES(access_latest_event_id) <> vehicle_realtime_snapshot.access_latest_event_id, VALUES(access_latest_event_id), access_latest_event_id) ` const realtimeLocationTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_location ( protocol VARCHAR(32) NOT NULL, vin VARCHAR(32) NOT NULL DEFAULT '', plate VARCHAR(32) NOT NULL DEFAULT '', event_time DATETIME(3) NULL, latitude DECIMAL(12,6) NOT NULL, longitude DECIMAL(12,6) NOT NULL, speed_kmh DECIMAL(10,3) NULL, total_mileage_km DECIMAL(18,3) NULL, total_mileage_event_time DATETIME(3) NULL, soc_percent DECIMAL(6,2) NULL, altitude_m DECIMAL(10,3) NULL, direction_deg DECIMAL(10,3) NULL, alarm_flag BIGINT NULL, status_flag BIGINT NULL, received_at DATETIME(3) NULL, event_id VARCHAR(64) NOT NULL DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (protocol, vin), KEY idx_vin (vin), KEY idx_protocol_updated (protocol, updated_at) )` var realtimeLocationCompatibilitySQL = []string{ "ALTER TABLE vehicle_realtime_location ADD COLUMN total_mileage_event_time DATETIME(3) NULL AFTER total_mileage_km", "ALTER TABLE vehicle_realtime_location ADD COLUMN source_key VARCHAR(256) NOT NULL DEFAULT '' AFTER plate", "ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict TINYINT(1) NOT NULL DEFAULT 0 AFTER status_flag", "ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict_distance_m DECIMAL(12,1) NULL AFTER location_conflict", } const jt808RealtimeLocationSourceTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_location_source ( protocol VARCHAR(32) NOT NULL, vin VARCHAR(32) NOT NULL, source_key VARCHAR(256) NOT NULL, phone VARCHAR(32) NOT NULL DEFAULT '', device_id VARCHAR(64) NOT NULL DEFAULT '', source_code VARCHAR(64) NOT NULL DEFAULT '', source_kind VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN', source_endpoint VARCHAR(128) NOT NULL DEFAULT '', plate VARCHAR(32) NOT NULL DEFAULT '', event_time DATETIME(3) NULL, latitude DECIMAL(12,6) NOT NULL, longitude DECIMAL(12,6) NOT NULL, speed_kmh DECIMAL(10,3) NULL, total_mileage_km DECIMAL(18,3) NULL, total_mileage_event_time DATETIME(3) NULL, soc_percent DECIMAL(6,2) NULL, altitude_m DECIMAL(10,3) NULL, direction_deg DECIMAL(10,3) NULL, alarm_flag BIGINT NULL, status_flag BIGINT NULL, received_at DATETIME(3) NULL, event_id VARCHAR(64) NOT NULL DEFAULT '', quality_status VARCHAR(32) NOT NULL DEFAULT 'OK', quality_reason VARCHAR(64) NOT NULL DEFAULT '', consecutive_good_samples INT NOT NULL DEFAULT 1, latest_sample_received_at DATETIME(3) NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (protocol, vin, source_key), KEY idx_location_source_vehicle_fresh (vin, protocol, received_at), KEY idx_location_source_quality (quality_status, updated_at) )` const vehicleLocationSourcePolicyTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_location_source_policy ( vin VARCHAR(32) NOT NULL, protocol VARCHAR(32) NOT NULL, source_key VARCHAR(256) NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, priority INT NOT NULL DEFAULT 100, remark VARCHAR(255) NOT NULL DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (vin, protocol, source_key), KEY idx_location_policy_enabled (vin, protocol, enabled, priority) )` const jt808AcceptedLocationCondition = `VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location_source.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location_source.event_time) AND (vehicle_realtime_location_source.event_time IS NULL OR ST_Distance_Sphere( POINT(vehicle_realtime_location_source.longitude, vehicle_realtime_location_source.latitude), POINT(VALUES(longitude), VALUES(latitude)) ) <= GREATEST(500, GREATEST(1, TIMESTAMPDIFF(SECOND, vehicle_realtime_location_source.event_time, VALUES(event_time))) * 70))` var upsertJT808RealtimeLocationSourceSQL = ` INSERT INTO vehicle_realtime_location_source (source_key, phone, device_id, source_code, source_kind, source_endpoint, protocol, vin, plate, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time, soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id, latest_sample_received_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quality_status = IF(` + jt808AcceptedLocationCondition + `, 'OK', 'REJECTED'), quality_reason = IF(quality_status = 'OK', '', 'impossible_jump'), consecutive_good_samples = IF(quality_status = 'OK', IF(VALUES(event_id) <> event_id, consecutive_good_samples + 1, consecutive_good_samples), 0), latest_sample_received_at = VALUES(latest_sample_received_at), phone = IF(VALUES(phone) <> '', VALUES(phone), phone), device_id = IF(VALUES(device_id) <> '', VALUES(device_id), device_id), source_code = IF(VALUES(source_code) <> '', VALUES(source_code), source_code), source_kind = IF(VALUES(source_kind) <> 'UNKNOWN', VALUES(source_kind), source_kind), source_endpoint = IF(VALUES(source_endpoint) <> '', VALUES(source_endpoint), source_endpoint), plate = IF(VALUES(plate) <> '', VALUES(plate), plate), event_time = IF(quality_status = 'OK', VALUES(event_time), event_time), latitude = IF(quality_status = 'OK', VALUES(latitude), latitude), longitude = IF(quality_status = 'OK', VALUES(longitude), longitude), speed_kmh = IF(quality_status = 'OK', COALESCE(VALUES(speed_kmh), speed_kmh), speed_kmh), total_mileage_km = IF(quality_status = 'OK', COALESCE(VALUES(total_mileage_km), total_mileage_km), total_mileage_km), total_mileage_event_time = IF(quality_status = 'OK' AND VALUES(total_mileage_km) IS NOT NULL, COALESCE(VALUES(total_mileage_event_time), total_mileage_event_time), total_mileage_event_time), soc_percent = IF(quality_status = 'OK', COALESCE(VALUES(soc_percent), soc_percent), soc_percent), altitude_m = IF(quality_status = 'OK', COALESCE(VALUES(altitude_m), altitude_m), altitude_m), direction_deg = IF(quality_status = 'OK', COALESCE(VALUES(direction_deg), direction_deg), direction_deg), alarm_flag = IF(quality_status = 'OK', COALESCE(VALUES(alarm_flag), alarm_flag), alarm_flag), status_flag = IF(quality_status = 'OK', COALESCE(VALUES(status_flag), status_flag), status_flag), received_at = IF(quality_status = 'OK', VALUES(received_at), received_at), event_id = IF(quality_status = 'OK', VALUES(event_id), event_id) ` const electJT808RealtimeLocationSQL = ` INSERT INTO vehicle_realtime_location (protocol, vin, plate, source_key, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time, soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, location_conflict, location_conflict_distance_m, received_at, event_id) SELECT s.protocol, s.vin, s.plate, s.source_key, s.event_time, s.latitude, s.longitude, s.speed_kmh, s.total_mileage_km, s.total_mileage_event_time, s.soc_percent, s.altitude_m, s.direction_deg, s.alarm_flag, s.status_flag, EXISTS(SELECT 1 FROM vehicle_realtime_location_source other WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) AND ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude)) > 200), (SELECT MAX(ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude))) FROM vehicle_realtime_location_source other WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)), s.received_at, s.event_id FROM vehicle_realtime_location_source s LEFT JOIN vehicle_location_source_policy policy ON policy.vin = s.vin AND policy.protocol = s.protocol AND policy.source_key = s.source_key LEFT JOIN vehicle_realtime_location cur ON cur.vin = s.vin AND cur.protocol = s.protocol WHERE s.vin = ? AND s.protocol = 'JT808' AND s.quality_status = 'OK' AND COALESCE(policy.enabled, 1) = 1 ORDER BY CASE WHEN s.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN 0 ELSE 1 END ASC, (COALESCE(policy.priority, CASE s.source_kind WHEN 'DIRECT' THEN 20 WHEN 'PLATFORM' THEN 30 ELSE 40 END) + CASE WHEN cur.source_key = s.source_key AND cur.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN -5 ELSE 0 END + CASE WHEN cur.source_key <> '' AND cur.source_key <> s.source_key AND s.consecutive_good_samples < 3 THEN 1000 ELSE 0 END) ASC, s.received_at DESC, s.source_key ASC LIMIT 1 ON DUPLICATE KEY UPDATE plate = IF(VALUES(plate) <> '', VALUES(plate), vehicle_realtime_location.plate), source_key = VALUES(source_key), event_time = VALUES(event_time), latitude = VALUES(latitude), longitude = VALUES(longitude), speed_kmh = VALUES(speed_kmh), total_mileage_km = VALUES(total_mileage_km), total_mileage_event_time = VALUES(total_mileage_event_time), soc_percent = VALUES(soc_percent), altitude_m = VALUES(altitude_m), direction_deg = VALUES(direction_deg), alarm_flag = VALUES(alarm_flag), status_flag = VALUES(status_flag), location_conflict = VALUES(location_conflict), location_conflict_distance_m = VALUES(location_conflict_distance_m), received_at = VALUES(received_at), event_id = VALUES(event_id), updated_at = CURRENT_TIMESTAMP ` var cleanupInvalidRealtimeTotalMileageSQL = []string{ `UPDATE vehicle_realtime_location SET total_mileage_km = NULL, total_mileage_event_time = NULL WHERE total_mileage_km IS NOT NULL AND total_mileage_km <= 0`, `UPDATE vehicle_realtime_snapshot SET parsed_json = JSON_REMOVE(parsed_json, '$."jt808.location.total_mileage_km"') WHERE protocol = 'JT808' AND parsed_json IS NOT NULL AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."jt808.location.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`, `UPDATE vehicle_realtime_snapshot SET parsed_json = JSON_REMOVE(parsed_json, '$."gb32960.vehicle.total_mileage_km"') WHERE protocol = 'GB32960' AND parsed_json IS NOT NULL AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."gb32960.vehicle.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`, `UPDATE vehicle_realtime_snapshot SET parsed_json = JSON_REMOVE(parsed_json, '$."yutong_mqtt.data.total_mileage_km"') WHERE protocol = 'YUTONG_MQTT' AND parsed_json IS NOT NULL AND CAST(JSON_UNQUOTE(JSON_EXTRACT(parsed_json, '$."yutong_mqtt.data.total_mileage_km"')) AS DECIMAL(18,3)) <= 0`, } const backfillRealtimeAccessProjectionSQL = `UPDATE vehicle_realtime_snapshot SET access_first_seen_at = COALESCE(access_first_seen_at, received_at, updated_at), access_latest_received_at = COALESCE(access_latest_received_at, received_at, updated_at), access_sample_count = IF(access_sample_count = 0, 1, access_sample_count), access_latest_event_id = IF(access_latest_event_id = '', event_id, access_latest_event_id), access_first_seen_source = IF(access_first_seen_source = '', 'snapshot_backfill', access_first_seen_source) WHERE access_first_seen_at IS NULL OR access_latest_received_at IS NULL OR access_sample_count = 0 OR access_latest_event_id = '' OR access_first_seen_source = ''` const upsertRealtimeLocationSQL = ` INSERT INTO vehicle_realtime_location (protocol, vin, plate, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time, soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE plate = IF(VALUES(plate) <> '', VALUES(plate), plate), event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(event_time), event_time), latitude = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(latitude), latitude), longitude = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(longitude), longitude), speed_kmh = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(speed_kmh), speed_kmh), speed_kmh), total_mileage_km = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(total_mileage_km), total_mileage_km), total_mileage_km), total_mileage_event_time = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), IF(VALUES(total_mileage_km) IS NOT NULL, COALESCE(VALUES(total_mileage_event_time), total_mileage_event_time), total_mileage_event_time), total_mileage_event_time), soc_percent = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(soc_percent), soc_percent), soc_percent), altitude_m = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(altitude_m), altitude_m), altitude_m), direction_deg = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(direction_deg), direction_deg), direction_deg), alarm_flag = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(alarm_flag), alarm_flag), alarm_flag), status_flag = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), COALESCE(VALUES(status_flag), status_flag), status_flag), received_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(received_at), received_at), event_id = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), VALUES(event_id), event_id), updated_at = IF(VALUES(event_time) IS NOT NULL AND (vehicle_realtime_location.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location.event_time), CURRENT_TIMESTAMP, updated_at) `