fix(stats): recover stale odometer mileage

This commit is contained in:
lingniu
2026-07-20 01:12:09 +08:00
parent 58ab1ff7bb
commit 2ef027c7c2
4 changed files with 123 additions and 1 deletions

View File

@@ -2109,6 +2109,78 @@ func TestWriterAppendWithResultReportsDuplicateMileage(t *testing.T) {
}
}
func TestWriterDuplicateYutongOdometerStillEntersGPSFallback(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
}
defer db.Close()
loc := time.FixedZone("Asia/Shanghai", 8*3600)
writer := NewWriter(db, loc)
eventTime := time.Date(2026, 7, 19, 8, 0, 0, 0, loc)
event := managedTestSource(envelope.FrameEnvelope{
EventID: "evt-yutong-stale-odometer",
Protocol: envelope.ProtocolYutongMQTT,
VIN: "LMRKH9AC9R1004099",
DeviceID: "LMRKH9AC9R1004099",
SourceEndpoint: "mqtt://yutong/ytforward/shln/2",
EventTimeMS: eventTime.UnixMilli(),
Fields: map[string]any{
"yutong_mqtt.data.total_mileage": 24245000,
"yutong_mqtt.data.longitude": 114.435672,
"yutong_mqtt.data.latitude": 30.645438,
"yutong_mqtt.data.meter_speed": 18,
},
})
samples, _, err := samplesFromEnvelopeWithResult(event, loc)
if err != nil || len(samples) != 1 {
t.Fatalf("samplesFromEnvelopeWithResult() samples=%+v err=%v", samples, err)
}
identity, ok := NewSourceIdentityFromEnvelope(event)
if !ok {
t.Fatal("expected managed Yutong source identity")
}
writer.markSourceTouched(identity, eventTime)
writer.markMileageWritten(samples[0])
mock.ExpectBegin()
mock.ExpectQuery(`SELECT first_event_time, latest_event_time`).
WillReturnRows(sqlmock.NewRows([]string{
"first_event_time", "latest_event_time", "latest_event_id",
"latest_longitude", "latest_latitude", "daily_mileage_km",
"point_count", "usable_segment_count", "bad_jump_count",
"long_gap_count", "out_of_order_count",
}).AddRow(eventTime.Add(-20*time.Second), eventTime.Add(-20*time.Second), "evt-before", 114.434672, 30.645438, 0, 1, 0, 0, 0, 0))
mock.ExpectExec(`UPDATE vehicle_daily_gps_mileage_state`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_daily_mileage`).
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectCommit()
result, err := writer.AppendWithResult(context.Background(), event)
if err != nil {
t.Fatalf("AppendWithResult() error = %v", err)
}
if result.SamplesFound != 1 || result.SamplesSkippedSameMileage != 1 ||
result.SamplesRecoveredGPSCoordinate != 1 || result.SamplesWritten != 1 ||
result.ProjectionsAttempted != 1 || result.ProjectionsWritten != 1 {
t.Fatalf("duplicate odometer evidence = %+v", result)
}
if got := writer.CacheStats().GPSAccumulationEntries; got != 1 {
t.Fatalf("GPS accumulation entries = %d, want stale moving odometer to enter fallback", got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet SQL expectations: %v", err)
}
}
func TestWriterDoesNotCacheMileageWhenUpsertFails(t *testing.T) {
exec := &recordingExec{errs: []error{errors.New("mysql unavailable"), nil}}
writer := NewWriter(exec, time.FixedZone("Asia/Shanghai", 8*3600))