package stats import ( "context" "strings" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) const ( QualityOK = "OK" QualityNoPreviousBaseline = "NO_PREVIOUS_BASELINE" QualityInvalidDelta = "INVALID_DELTA" ) 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 == "" { 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 } 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(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 = VALUES(daily_mileage_km) */ 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 `