339 lines
9.8 KiB
Go
339 lines
9.8 KiB
Go
package stats
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"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),
|
|
maxSelectedDailyMileageKM,
|
|
vin,
|
|
statDate,
|
|
string(protocol),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = exec.ExecContext(ctx, cleanupProjectedDailyMileageSQL,
|
|
vin,
|
|
statDate,
|
|
string(protocol),
|
|
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 (
|
|
SELECT
|
|
s2.source_key,
|
|
s2.vin,
|
|
s2.stat_date,
|
|
s2.protocol
|
|
FROM vehicle_daily_mileage_source s2
|
|
LEFT JOIN vehicle_data_source ds
|
|
ON ds.protocol = s2.protocol AND ds.source_ip = s2.source_ip
|
|
WHERE s2.vin = ?
|
|
AND s2.stat_date = ?
|
|
AND s2.protocol = ?
|
|
AND s2.quality_status = '` + QualityOK + `'
|
|
AND s2.daily_mileage_km BETWEEN 0 AND ?
|
|
AND COALESCE(ds.enabled, 1) = 1
|
|
ORDER BY COALESCE(ds.trust_priority, 100),
|
|
s2.sample_count DESC,
|
|
s2.latest_event_time DESC,
|
|
s2.source_key ASC
|
|
LIMIT 1
|
|
) selected_source
|
|
ON selected_source.source_key = s.source_key
|
|
AND selected_source.vin = s.vin
|
|
AND selected_source.stat_date = s.stat_date
|
|
AND selected_source.protocol = s.protocol
|
|
SET s.is_selected = 1
|
|
WHERE s.vin = ? AND s.stat_date = ? AND s.protocol = ?
|
|
`
|
|
|
|
const cleanupProjectedDailyMileageSQL = `
|
|
DELETE FROM vehicle_daily_mileage
|
|
WHERE vin = ? AND stat_date = ? AND protocol = ?
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM vehicle_daily_mileage_source
|
|
WHERE vin = ? AND stat_date = ? AND protocol = ? AND is_selected = 1
|
|
)
|
|
`
|
|
|
|
type sourceBaseline struct {
|
|
LatestTotalKM float64
|
|
LatestEventTime time.Time
|
|
}
|
|
|
|
func lookupPreviousSourceBaseline(ctx context.Context, query Queryer, vin string, statDate string, protocol envelope.Protocol, sourceKey string) (sourceBaseline, bool, error) {
|
|
if query == nil || strings.TrimSpace(vin) == "" || strings.TrimSpace(statDate) == "" || strings.TrimSpace(sourceKey) == "" {
|
|
return sourceBaseline{}, false, nil
|
|
}
|
|
previousDate, ok := previousStatDate(statDate)
|
|
if !ok {
|
|
return sourceBaseline{}, false, nil
|
|
}
|
|
rows, err := query.QueryContext(ctx, previousSourceBaselineSQL, vin, previousDate, string(protocol), sourceKey)
|
|
if err != nil {
|
|
return sourceBaseline{}, false, err
|
|
}
|
|
defer rows.Close()
|
|
if !rows.Next() {
|
|
if err := rows.Err(); err != nil {
|
|
return sourceBaseline{}, false, err
|
|
}
|
|
return sourceBaseline{}, false, nil
|
|
}
|
|
var latestTotal sql.NullFloat64
|
|
var latestEvent sql.NullTime
|
|
if err := rows.Scan(&latestTotal, &latestEvent); err != nil {
|
|
return sourceBaseline{}, false, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return sourceBaseline{}, false, err
|
|
}
|
|
return sourceBaseline{
|
|
LatestTotalKM: latestTotal.Float64,
|
|
LatestEventTime: latestEvent.Time,
|
|
}, latestTotal.Valid, nil
|
|
}
|
|
|
|
func previousStatDate(statDate string) (string, bool) {
|
|
day, err := time.Parse("2006-01-02", strings.TrimSpace(statDate))
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return day.AddDate(0, 0, -1).Format("2006-01-02"), true
|
|
}
|
|
|
|
const previousSourceBaselineSQL = `
|
|
SELECT latest_total_mileage_km, latest_event_time
|
|
FROM vehicle_daily_mileage_source
|
|
WHERE vin = ?
|
|
AND stat_date = ?
|
|
AND protocol = ?
|
|
AND source_key = ?
|
|
ORDER BY latest_event_time DESC
|
|
LIMIT 1
|
|
`
|