chore: commit mileage updates and accumulated project artifacts
This commit is contained in:
@@ -132,7 +132,7 @@ func NormalizeDailyMileageDelta(deltaKM float64) (float64, bool, string) {
|
||||
|
||||
// DailyMileageFromDayBoundary keeps the business formula explicit: the
|
||||
// current day's latest cumulative odometer minus the nearest earlier
|
||||
// cumulative odometer from the same source.
|
||||
// cumulative reading of the same vehicle odometer or isolated terminal counter.
|
||||
func DailyMileageFromDayBoundary(previousBaselineKM float64, currentDayLatestKM float64) float64 {
|
||||
return currentDayLatestKM - previousBaselineKM
|
||||
}
|
||||
@@ -884,7 +884,11 @@ func lookupPreviousSourceBaseline(ctx context.Context, query Queryer, vin string
|
||||
if query == nil || strings.TrimSpace(vin) == "" || strings.TrimSpace(statDate) == "" || strings.TrimSpace(sourceKey) == "" {
|
||||
return sourceBaseline{}, false, nil
|
||||
}
|
||||
rows, err := query.QueryContext(ctx, previousSourceBaselineSQL, vin, statDate, string(protocol), sourceKey)
|
||||
sqlText := previousSourceBaselineSQL
|
||||
if protocol == envelope.ProtocolGB32960 {
|
||||
sqlText = previousVehicleOdometerBaselineSQL
|
||||
}
|
||||
rows, err := query.QueryContext(ctx, sqlText, vin, statDate, string(protocol), sourceKey)
|
||||
if err != nil {
|
||||
return sourceBaseline{}, false, err
|
||||
}
|
||||
@@ -940,8 +944,9 @@ func lookupPreviousProtocolBaseline(ctx context.Context, query Queryer, vin stri
|
||||
}
|
||||
|
||||
// LookupLatestSourceBaselineBefore returns the nearest durable odometer for the
|
||||
// same VIN, protocol and source before statDate. Missing calendar days are
|
||||
// skipped automatically.
|
||||
// same VIN and protocol before statDate. GB32960 uses the latest selected
|
||||
// vehicle odometer across forwarding sources; other protocols retain terminal
|
||||
// source isolation. Missing calendar days are skipped automatically.
|
||||
func LookupLatestSourceBaselineBefore(ctx context.Context, query Queryer, vin string, statDate string, protocol envelope.Protocol, sourceKey string) (float64, time.Time, bool, error) {
|
||||
baseline, found, err := lookupPreviousSourceBaseline(ctx, query, vin, statDate, protocol, sourceKey)
|
||||
return baseline.LatestTotalKM, baseline.LatestEventTime, found, err
|
||||
@@ -973,3 +978,15 @@ WHERE vin=? AND stat_date<? AND protocol=?
|
||||
ORDER BY stat_date DESC, latest_event_time DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
// GB32960 total mileage belongs to the VIN's vehicle odometer. A forwarding IP
|
||||
// is not a new odometer: prefer the latest selected vehicle baseline over an old
|
||||
// same-source baseline so a returning feed cannot recount already reported miles.
|
||||
// JT808 terminal counters deliberately do not use this cross-source rule.
|
||||
var previousVehicleOdometerBaselineSQL = strings.Replace(
|
||||
strings.Replace(previousSourceBaselineSQL, "AND source_key = ?", `AND (source_key = ? OR is_selected = 1)
|
||||
AND quality_status = 'OK'
|
||||
AND COALESCE(quality_reason,'') <> 'gps_coordinate_accumulation'
|
||||
AND source_ip NOT IN ('legacy-mysql.lingniu-prod','manual-lingniu-prod-day-mileage')`, 1),
|
||||
"ORDER BY stat_date DESC, latest_event_time DESC",
|
||||
"ORDER BY stat_date DESC, is_selected DESC, latest_event_time DESC, source_key ASC", 1)
|
||||
|
||||
@@ -3,6 +3,7 @@ package stats
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -739,3 +740,38 @@ func TestProjectDailyMileagePersistsIndependentDayEndTotal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGB32960ReturningFeedUsesSelectedVehicleBaseline(t *testing.T) {
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
current, baseline, want float64
|
||||
quality string
|
||||
}{
|
||||
{"stale April baseline must not repeat 35042 km", 75429.6, 75429.6, 0, QualityOK},
|
||||
{"source switch only counts new vehicle mileage", 42077, 42006.3, 70.7, QualityOK},
|
||||
{"rollback stays invalid", 10, 75429.6, -75419.6, QualityInvalidDelta},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
stamp := time.Date(2026, 9, 9, 19, 9, 35, 0, loc)
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source.*source_key = \? OR is_selected = 1.*quality_status = 'OK'.*ORDER BY stat_date DESC, is_selected DESC`).
|
||||
WithArgs("LB9A32A26R0LS1055", "2026-09-15", "GB32960", "GB32960:unknown@returning").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"total", "time"}).AddRow(tc.baseline, stamp))
|
||||
candidate := SourceMileageSample{VIN: "LB9A32A26R0LS1055", StatDate: "2026-09-15", Protocol: envelope.ProtocolGB32960, SourceKey: "GB32960:unknown@returning", FirstTotalKM: 40386.7, LatestTotalKM: tc.current, FirstEventTime: time.Date(2026, 4, 24, 12, 14, 51, 0, loc), LatestEventTime: time.Date(2026, 9, 15, 11, 27, 37, 0, loc)}
|
||||
if err := NewWriter(db, loc).applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(candidate.DailyKM-tc.want) > 0.0001 || candidate.QualityStatus != tc.quality || !candidate.FirstEventTime.Equal(stamp) {
|
||||
t.Fatalf("%+v", candidate)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user