功能:完善里程边界与氢耗流式统计
This commit is contained in:
@@ -13,6 +13,7 @@ const (
|
||||
QualityOK = "OK"
|
||||
QualityNoPreviousBaseline = "NO_PREVIOUS_BASELINE"
|
||||
QualityInvalidDelta = "INVALID_DELTA"
|
||||
QualityReasonTotalRollback = "TOTAL_MILEAGE_ROLLBACK"
|
||||
QualityReasonHistorical = "historical_source_baseline"
|
||||
QualityReasonCurrentDayFirst = "current_day_first_baseline"
|
||||
QualityReasonStationaryCarry = "stationary_carry_forward"
|
||||
@@ -136,6 +137,26 @@ func DailyMileageFromDayBoundary(previousBaselineKM float64, currentDayLatestKM
|
||||
return currentDayLatestKM - previousBaselineKM
|
||||
}
|
||||
|
||||
// IsUsableDailyMileageBoundary reports whether a persisted baseline belongs to
|
||||
// the current natural day or its immediately preceding natural day. A baseline
|
||||
// from an older day represents an offline gap; using it would attribute the
|
||||
// whole gap to the day on which the vehicle came back online.
|
||||
func IsUsableDailyMileageBoundary(statDate string, baselineTime time.Time, loc *time.Location) bool {
|
||||
if strings.TrimSpace(statDate) == "" || baselineTime.IsZero() {
|
||||
return false
|
||||
}
|
||||
if loc == nil {
|
||||
loc = baselineTime.Location()
|
||||
}
|
||||
day, err := time.ParseInLocation("2006-01-02", statDate, loc)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
baseline := baselineTime.In(loc)
|
||||
baselineDay := time.Date(baseline.Year(), baseline.Month(), baseline.Day(), 0, 0, 0, 0, loc)
|
||||
return !baselineDay.Before(day.AddDate(0, 0, -1)) && !baselineDay.After(day)
|
||||
}
|
||||
|
||||
func NormalizeDailyMileageDeltaForWindow(deltaKM float64, firstEventTime time.Time, latestEventTime time.Time) (float64, bool, string) {
|
||||
if deltaKM < 0 && deltaKM >= -maxNegativeMileageJitterKM {
|
||||
return 0, true, "negative_jitter_clamped"
|
||||
@@ -444,8 +465,6 @@ const normalizePlatformOutsideDailyRangeSQL = `(` + normalizePlatformDailySQL +
|
||||
const upsertSourcePureHydrogenIncrementSQL = `CASE
|
||||
WHEN latest_event_time IS NOT NULL
|
||||
AND VALUES(latest_event_time) > latest_event_time
|
||||
AND latest_pure_hydrogen_mode_known = 1
|
||||
AND latest_pure_hydrogen_active = 1
|
||||
AND VALUES(latest_pure_hydrogen_mode_known) = 1
|
||||
AND VALUES(latest_pure_hydrogen_active) = 1
|
||||
AND VALUES(latest_total_mileage_km) >= latest_total_mileage_km
|
||||
@@ -731,9 +750,32 @@ const selectedSourceIdentitySampleCountSQL = `(
|
||||
= COALESCE(NULLIF(TRIM(s2.phone), ''), NULLIF(TRIM(s2.device_id), ''), s2.source_key)
|
||||
)`
|
||||
|
||||
// projectDayEndTotalMileageSQL intentionally uses an independent source choice
|
||||
// from the daily-mileage projection. GPS coordinate accumulation can be the
|
||||
// best evidence for distance travelled during the day, but it must never
|
||||
// replace a terminal-reported odometer as the day-end cumulative mileage.
|
||||
const projectDayEndTotalMileageSQL = `COALESCE((
|
||||
SELECT total_candidate.latest_total_mileage_km
|
||||
FROM vehicle_daily_mileage_source total_candidate
|
||||
WHERE total_candidate.vin = s.vin
|
||||
AND total_candidate.stat_date = s.stat_date
|
||||
AND total_candidate.protocol = s.protocol
|
||||
AND total_candidate.quality_status = '` + QualityOK + `'
|
||||
AND total_candidate.latest_total_mileage_km IS NOT NULL
|
||||
AND total_candidate.latest_total_mileage_km >= 0
|
||||
ORDER BY
|
||||
CASE WHEN COALESCE(total_candidate.quality_reason, '') = '` + QualityReasonGPSCoordinate + `' THEN 1 ELSE 0 END,
|
||||
CASE WHEN total_candidate.latest_total_mileage_km > total_candidate.daily_mileage_km THEN 0 ELSE 1 END,
|
||||
total_candidate.is_selected DESC,
|
||||
total_candidate.latest_event_time DESC,
|
||||
total_candidate.sample_count DESC,
|
||||
total_candidate.source_key ASC
|
||||
LIMIT 1
|
||||
), s.latest_total_mileage_km)`
|
||||
|
||||
const projectDailyMileageSQL = `
|
||||
INSERT INTO vehicle_daily_mileage
|
||||
(vin, stat_date, protocol, source_id, daily_mileage_km, pure_hydrogen_mileage_km, latest_total_mileage_km)
|
||||
(vin, stat_date, protocol, source_id, daily_mileage_km, pure_hydrogen_mileage_km, latest_total_mileage_km, day_end_total_mileage_km)
|
||||
SELECT
|
||||
s.vin,
|
||||
s.stat_date,
|
||||
@@ -741,7 +783,8 @@ SELECT
|
||||
ds.id,
|
||||
s.daily_mileage_km,
|
||||
LEAST(s.pure_hydrogen_mileage_km, s.daily_mileage_km),
|
||||
s.latest_total_mileage_km
|
||||
s.latest_total_mileage_km,
|
||||
` + projectDayEndTotalMileageSQL + `
|
||||
FROM vehicle_daily_mileage_source s
|
||||
LEFT JOIN vehicle_data_source ds
|
||||
ON ds.protocol = s.protocol AND ds.source_ip = s.source_ip
|
||||
@@ -774,6 +817,7 @@ ON DUPLICATE KEY UPDATE
|
||||
daily_mileage_km = VALUES(daily_mileage_km),
|
||||
pure_hydrogen_mileage_km = VALUES(pure_hydrogen_mileage_km),
|
||||
latest_total_mileage_km = VALUES(latest_total_mileage_km),
|
||||
day_end_total_mileage_km = VALUES(day_end_total_mileage_km),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`
|
||||
|
||||
@@ -866,6 +910,35 @@ func lookupPreviousSourceBaseline(ctx context.Context, query Queryer, vin string
|
||||
}, latestTotal.Valid, nil
|
||||
}
|
||||
|
||||
// lookupPreviousProtocolBaseline is deliberately source-key independent. A
|
||||
// device replacement must not allow a near-zero odometer to become a normal
|
||||
// continuation of the same vehicle/protocol history.
|
||||
func lookupPreviousProtocolBaseline(ctx context.Context, query Queryer, vin string, statDate string, protocol envelope.Protocol) (sourceBaseline, bool, error) {
|
||||
if query == nil || strings.TrimSpace(vin) == "" || strings.TrimSpace(statDate) == "" {
|
||||
return sourceBaseline{}, false, nil
|
||||
}
|
||||
rows, err := query.QueryContext(ctx, previousProtocolBaselineSQL, vin, statDate, string(protocol))
|
||||
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 total sql.NullFloat64
|
||||
var event sql.NullTime
|
||||
if err := rows.Scan(&total, &event); err != nil {
|
||||
return sourceBaseline{}, false, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return sourceBaseline{}, false, err
|
||||
}
|
||||
return sourceBaseline{LatestTotalKM: total.Float64, LatestEventTime: event.Time, QualityReason: QualityReasonHistorical}, total.Valid, nil
|
||||
}
|
||||
|
||||
// LookupLatestSourceBaselineBefore returns the nearest durable odometer for the
|
||||
// same VIN, protocol and source before statDate. Missing calendar days are
|
||||
// skipped automatically.
|
||||
@@ -889,3 +962,14 @@ WHERE vin = ?
|
||||
ORDER BY stat_date DESC, latest_event_time DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
const previousProtocolBaselineSQL = `
|
||||
SELECT latest_total_mileage_km, latest_event_time
|
||||
FROM vehicle_daily_mileage_source
|
||||
WHERE vin=? AND stat_date<? AND protocol=?
|
||||
AND quality_status='OK'
|
||||
AND latest_total_mileage_km IS NOT NULL AND latest_total_mileage_km>0
|
||||
AND latest_event_time IS NOT NULL
|
||||
ORDER BY stat_date DESC, latest_event_time DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user