功能:完善里程边界与氢耗流式统计
This commit is contained in:
@@ -1054,6 +1054,9 @@ func TestWriterAppendUsesCurrentSampleWhenNoHistoricalBaseline(t *testing.T) {
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
|
||||
WithArgs(
|
||||
@@ -1138,6 +1141,9 @@ func TestWriterCachesMissingBaselineAfterSuccessfulFirstSample(t *testing.T) {
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
|
||||
WithArgs(
|
||||
@@ -1447,7 +1453,7 @@ func TestWriterAppendUsesPreviousSourceBaselineForRealtimeCandidate(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterAppendUsesOlderHistoricalSourceBaseline(t *testing.T) {
|
||||
func TestWriterAppendStartsCurrentDayBoundaryAfterOfflineGap(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
@@ -1488,18 +1494,18 @@ func TestWriterAppendUsesOlderHistoricalSourceBaseline(t *testing.T) {
|
||||
"",
|
||||
"LMRKH9AC2R1004087",
|
||||
"",
|
||||
float64(120672.0),
|
||||
float64(120788.0),
|
||||
approxFloat64{want: 116.0, tolerance: 0.000001},
|
||||
float64(120788.0),
|
||||
float64(0),
|
||||
float64(0),
|
||||
int64(1),
|
||||
int64(0),
|
||||
historicalTime,
|
||||
currentTime,
|
||||
currentTime,
|
||||
false,
|
||||
false,
|
||||
QualityOK,
|
||||
QualityReasonHistorical,
|
||||
QualityReasonCurrentDayFirst,
|
||||
).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage`).
|
||||
@@ -1608,7 +1614,7 @@ func TestWriterApplyRealtimeBaselineKeepsNegativeHistoricalDeltaInvalid(t *testi
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != "outside_daily_range" {
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != QualityReasonTotalRollback {
|
||||
t.Fatalf("quality = %s/%s", candidate.QualityStatus, candidate.QualityReason)
|
||||
}
|
||||
if candidate.DailyKM > -55 || candidate.DailyKM < -56 {
|
||||
@@ -1651,7 +1657,7 @@ func TestWriterApplyRealtimeBaselineKeepsInvalidWhenCurrentDayBaselineIsStillHig
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != "outside_daily_range" {
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != QualityReasonTotalRollback {
|
||||
t.Fatalf("quality = %s/%s", candidate.QualityStatus, candidate.QualityReason)
|
||||
}
|
||||
if candidate.DailyKM > -55 || candidate.DailyKM < -56 {
|
||||
@@ -1691,7 +1697,7 @@ func TestWriterApplyRealtimeBaselineRejectsNegativeDeltaEvenWithLowerCurrentDayS
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != "outside_daily_range" {
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != QualityReasonTotalRollback {
|
||||
t.Fatalf("quality = %s/%s", candidate.QualityStatus, candidate.QualityReason)
|
||||
}
|
||||
if candidate.DailyKM > -55 || candidate.DailyKM < -56 {
|
||||
@@ -1705,7 +1711,7 @@ func TestWriterApplyRealtimeBaselineRejectsNegativeDeltaEvenWithLowerCurrentDayS
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterApplyRealtimeBaselineAcceptsPlausibleMultiDayFallbackDelta(t *testing.T) {
|
||||
func TestWriterApplyRealtimeBaselineIgnoresPlausibleMultiDayFallbackDelta(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
@@ -1734,18 +1740,65 @@ func TestWriterApplyRealtimeBaselineAcceptsPlausibleMultiDayFallbackDelta(t *tes
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityOK || candidate.QualityReason != QualityReasonHistorical {
|
||||
if candidate.QualityStatus != QualityOK || candidate.QualityReason != QualityReasonCurrentDayFirst {
|
||||
t.Fatalf("quality = %s/%s", candidate.QualityStatus, candidate.QualityReason)
|
||||
}
|
||||
if candidate.DailyKM < 7833.4 || candidate.DailyKM > 7833.6 {
|
||||
t.Fatalf("daily km = %v, want multi-day gap delta", candidate.DailyKM)
|
||||
if candidate.DailyKM != 0 {
|
||||
t.Fatalf("daily km = %v, want current-day first baseline after offline gap", candidate.DailyKM)
|
||||
}
|
||||
if candidate.FirstTotalKM != candidate.LatestTotalKM || !candidate.FirstEventTime.Equal(currentTime) {
|
||||
t.Fatalf("current-day boundary not retained: %#v", candidate)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterApplyRealtimeBaselineRejectsHistoricalBaselineJump(t *testing.T) {
|
||||
func TestWriterApplyRealtimeBaselineRejectsProtocolOdometerRollbackAfterSourceChange(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)
|
||||
previousTime := time.Date(2026, 8, 5, 22, 41, 0, 0, loc)
|
||||
currentTime := time.Date(2026, 8, 6, 8, 12, 0, 0, loc)
|
||||
candidate := SourceMileageSample{
|
||||
VIN: "LNXNEGRR1SR321395",
|
||||
StatDate: "2026-08-06",
|
||||
Protocol: envelope.ProtocolGB32960,
|
||||
SourceKey: "GB32960:new-device@8.134.95.166",
|
||||
LatestTotalKM: 0.1,
|
||||
LatestEventTime: currentTime,
|
||||
}
|
||||
|
||||
// The new device has no source-key baseline, but this VIN already has a
|
||||
// durable GB32960 odometer. The reset must not become a NORMAL day start.
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LNXNEGRR1SR321395", "2026-08-06", "GB32960", "GB32960:new-device@8.134.95.166").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LNXNEGRR1SR321395", "2026-08-06", "GB32960").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}).
|
||||
AddRow(48233.3, previousTime))
|
||||
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != QualityReasonTotalRollback {
|
||||
t.Fatalf("quality = %s/%s, want %s/%s", candidate.QualityStatus, candidate.QualityReason, QualityInvalidDelta, QualityReasonTotalRollback)
|
||||
}
|
||||
if candidate.DailyKM != 0 {
|
||||
t.Fatalf("daily km = %v, want 0", candidate.DailyKM)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterApplyRealtimeBaselineIgnoresHistoricalBaselineJumpAcrossOfflineGap(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
@@ -1774,14 +1827,14 @@ func TestWriterApplyRealtimeBaselineRejectsHistoricalBaselineJump(t *testing.T)
|
||||
if err := writer.applyRealtimeBaseline(context.Background(), &candidate); err != nil {
|
||||
t.Fatalf("applyRealtimeBaseline() error = %v", err)
|
||||
}
|
||||
if candidate.QualityStatus != QualityInvalidDelta || candidate.QualityReason != "outside_daily_range" {
|
||||
if candidate.QualityStatus != QualityOK || candidate.QualityReason != QualityReasonCurrentDayFirst {
|
||||
t.Fatalf("quality = %s/%s", candidate.QualityStatus, candidate.QualityReason)
|
||||
}
|
||||
if candidate.DailyKM < 29999.9 || candidate.DailyKM > 30000.1 {
|
||||
t.Fatalf("daily km = %v, want historical jump delta", candidate.DailyKM)
|
||||
if candidate.DailyKM != 0 {
|
||||
t.Fatalf("daily km = %v, want current-day first baseline", candidate.DailyKM)
|
||||
}
|
||||
if candidate.FirstTotalKM != 10009.7 || !candidate.FirstEventTime.Equal(baselineTime) {
|
||||
t.Fatalf("previous-day baseline not preserved: %#v", candidate)
|
||||
if candidate.FirstTotalKM != candidate.LatestTotalKM || !candidate.FirstEventTime.Equal(currentTime) {
|
||||
t.Fatalf("current-day boundary not retained: %#v", candidate)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
@@ -1815,6 +1868,9 @@ func TestWriterAppendMarksCandidateNoPreviousBaselineWhenPreviousBaselineMissing
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
|
||||
WithArgs(
|
||||
@@ -1894,7 +1950,7 @@ func TestWriterEnsuresSchemaAndUpsertsDailyMileage(t *testing.T) {
|
||||
t.Fatalf("Append() error = %v", err)
|
||||
}
|
||||
|
||||
schemaCalls := 6 + len(DailyMileageAlterSQL)
|
||||
schemaCalls := 8 + len(DailyMileageAlterSQL)
|
||||
if len(exec.calls) != schemaCalls+5 {
|
||||
t.Fatalf("exec calls = %d", len(exec.calls))
|
||||
}
|
||||
@@ -1904,7 +1960,9 @@ func TestWriterEnsuresSchemaAndUpsertsDailyMileage(t *testing.T) {
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_daily_mileage_source",
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_daily_mileage",
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_hydrogen_tank_capacity",
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_hydrogen_energy_parameter",
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_open_hydrogen_stream_state",
|
||||
"CREATE TABLE IF NOT EXISTS vehicle_open_hydrogen_segment_stream_state",
|
||||
} {
|
||||
if !strings.Contains(exec.calls[i].query, want) {
|
||||
t.Fatalf("schema call %d = %s, want %s", i, exec.calls[i].query, want)
|
||||
@@ -2294,6 +2352,9 @@ func TestWriterRollsBackMileageTransactionWhenProjectionFails(t *testing.T) {
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
|
||||
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}))
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
|
||||
WithArgs(
|
||||
|
||||
Reference in New Issue
Block a user