package stats import ( "context" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) const ( QualityOK = "OK" QualityNoPreviousBaseline = "NO_PREVIOUS_BASELINE" QualityInvalidDelta = "INVALID_DELTA" maxSelectedDailyMileageKM = 1000 ) type SourceMileageSample struct { VIN string StatDate string Protocol envelope.Protocol SourceKey string SourceIP string SourceEndpoint string Phone string DeviceID string PlatformName string FirstTotalKM float64 LatestTotalKM float64 DailyKM float64 SampleCount int64 FirstEventTime time.Time LatestEventTime time.Time QualityStatus string QualityReason string } func SourceKey(protocol envelope.Protocol, phone string, deviceID string, sourceIP string) string { identity := strings.TrimSpace(phone) if identity == "" { identity = strings.TrimSpace(deviceID) } if identity == "" { identity = "unknown" } return string(protocol) + ":" + identity + "@" + strings.TrimSpace(sourceIP) } func SourceMileageSampleFromMetric(sample MetricSample, identity SourceIdentity) SourceMileageSample { eventTime := sample.EventTime return SourceMileageSample{ VIN: sample.VIN, StatDate: sample.StatDate, Protocol: sample.Protocol, SourceKey: SourceKey(sample.Protocol, sample.Phone, sample.DeviceID, identity.SourceIP), SourceIP: identity.SourceIP, SourceEndpoint: identity.SourceEndpoint, Phone: sample.Phone, DeviceID: sample.DeviceID, FirstTotalKM: sample.TotalMileageKM, LatestTotalKM: sample.TotalMileageKM, DailyKM: 0, SampleCount: 1, FirstEventTime: eventTime, LatestEventTime: eventTime, QualityStatus: QualityOK, QualityReason: "realtime_sample", } } func UpsertSourceMileage(ctx context.Context, exec Execer, sample SourceMileageSample) error { if exec == nil { panic("stats execer must not be nil") } if sample.VIN == "" || sample.SourceKey == "" || strings.TrimSpace(sample.SourceIP) == "" { return nil } if sample.QualityStatus == "" { sample.QualityStatus = QualityOK } _, err := exec.ExecContext(ctx, upsertSourceMileageSQL, sample.VIN, sample.StatDate, string(sample.Protocol), sample.SourceKey, sample.SourceIP, sample.SourceEndpoint, sample.Phone, sample.DeviceID, sample.PlatformName, sample.FirstTotalKM, sample.LatestTotalKM, sample.DailyKM, sample.SampleCount, sample.FirstEventTime, sample.LatestEventTime, sample.QualityStatus, sample.QualityReason, ) return err } func ProjectDailyMileage(ctx context.Context, exec Execer, vin string, statDate string, protocol envelope.Protocol) error { if exec == nil { panic("stats execer must not be nil") } if strings.TrimSpace(vin) == "" || strings.TrimSpace(statDate) == "" { return nil } if _, err := exec.ExecContext(ctx, clearSelectedSourceSQL, vin, statDate, string(protocol)); err != nil { return err } if _, err := exec.ExecContext(ctx, projectDailyMileageSQL, vin, statDate, string(protocol), maxSelectedDailyMileageKM, ); err != nil { return err } _, err := exec.ExecContext(ctx, markSelectedSourceSQL, vin, statDate, string(protocol)) return err } const upsertSourceMileageSQL = ` INSERT INTO vehicle_daily_mileage_source (vin, stat_date, protocol, source_key, source_ip, source_endpoint, phone, device_id, platform_name, first_total_mileage_km, latest_total_mileage_km, daily_mileage_km, sample_count, first_event_time, latest_event_time, quality_status, quality_reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE source_ip = VALUES(source_ip), source_endpoint = VALUES(source_endpoint), phone = VALUES(phone), device_id = VALUES(device_id), platform_name = COALESCE(NULLIF(TRIM(VALUES(platform_name)), ''), platform_name), first_total_mileage_km = CASE WHEN first_total_mileage_km IS NULL OR first_total_mileage_km <= 0 THEN VALUES(first_total_mileage_km) ELSE LEAST(first_total_mileage_km, VALUES(first_total_mileage_km)) END, latest_total_mileage_km = CASE WHEN latest_total_mileage_km IS NULL OR latest_total_mileage_km <= 0 THEN VALUES(latest_total_mileage_km) ELSE GREATEST(latest_total_mileage_km, VALUES(latest_total_mileage_km)) END, daily_mileage_km = GREATEST( CASE WHEN latest_total_mileage_km IS NULL OR latest_total_mileage_km <= 0 THEN VALUES(latest_total_mileage_km) ELSE latest_total_mileage_km END, VALUES(latest_total_mileage_km) ) - CASE WHEN first_total_mileage_km IS NULL OR first_total_mileage_km <= 0 THEN VALUES(first_total_mileage_km) ELSE LEAST(first_total_mileage_km, VALUES(first_total_mileage_km)) END, sample_count = sample_count + VALUES(sample_count), first_event_time = CASE WHEN first_event_time IS NULL OR VALUES(first_event_time) < first_event_time THEN VALUES(first_event_time) ELSE first_event_time END, latest_event_time = CASE WHEN latest_event_time IS NULL OR VALUES(latest_event_time) > latest_event_time THEN VALUES(latest_event_time) ELSE latest_event_time END, quality_status = VALUES(quality_status), quality_reason = VALUES(quality_reason), updated_at = CURRENT_TIMESTAMP ` const clearSelectedSourceSQL = ` UPDATE vehicle_daily_mileage_source SET is_selected = 0 WHERE vin = ? AND stat_date = ? AND protocol = ? ` const projectDailyMileageSQL = ` INSERT INTO vehicle_daily_mileage (vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count) SELECT s.vin, s.stat_date, s.protocol, s.daily_mileage_km, s.first_total_mileage_km, s.latest_total_mileage_km, s.source_key, s.phone, ds.latest_source_endpoint, s.sample_count FROM vehicle_daily_mileage_source s LEFT JOIN vehicle_data_source ds ON ds.protocol = s.protocol AND ds.source_ip = s.source_ip WHERE s.vin = ? AND s.stat_date = ? AND s.protocol = ? AND s.quality_status = '` + QualityOK + `' AND s.daily_mileage_km BETWEEN 0 AND ? AND COALESCE(ds.enabled, 1) = 1 ORDER BY COALESCE(ds.trust_priority, 100), s.sample_count DESC, s.latest_event_time DESC, s.source_key ASC LIMIT 1 ON DUPLICATE KEY UPDATE daily_mileage_km = VALUES(daily_mileage_km), first_total_mileage_km = VALUES(first_total_mileage_km), latest_total_mileage_km = VALUES(latest_total_mileage_km), trusted_source_key = VALUES(trusted_source_key), trusted_phone = VALUES(trusted_phone), trusted_source_endpoint = VALUES(trusted_source_endpoint), sample_count = VALUES(sample_count), updated_at = CURRENT_TIMESTAMP ` const markSelectedSourceSQL = ` UPDATE vehicle_daily_mileage_source s JOIN vehicle_daily_mileage m ON m.vin = s.vin AND m.stat_date = s.stat_date AND m.protocol = s.protocol AND m.trusted_source_key = s.source_key SET s.is_selected = 1 WHERE s.vin = ? AND s.stat_date = ? AND s.protocol = ? `