fix(stats): calculate mileage without previous-day baseline

This commit is contained in:
lingniu
2026-07-08 17:26:34 +08:00
parent 536b0bfee6
commit 469e9c75f6
5 changed files with 315 additions and 101 deletions

View File

@@ -182,15 +182,18 @@ func (w *Writer) applyRealtimeBaseline(ctx context.Context, candidate *SourceMil
return err
}
if !found {
candidate.QualityStatus = QualityNoPreviousBaseline
candidate.QualityReason = "missing_previous_source"
candidate.QualityStatus = QualityOK
candidate.QualityReason = "current_day_first_sample"
return nil
}
candidate.FirstTotalKM = baseline.LatestTotalKM
candidate.FirstEventTime = baseline.LatestEventTime
candidate.DailyKM = candidate.LatestTotalKM - baseline.LatestTotalKM
candidate.QualityStatus = QualityOK
candidate.QualityReason = "same_source_previous_day"
candidate.QualityReason = baseline.QualityReason
if candidate.QualityReason == "" {
candidate.QualityReason = "historical_source_baseline"
}
if candidate.DailyKM < 0 || candidate.DailyKM > maxSelectedDailyMileageKM {
candidate.QualityStatus = QualityInvalidDelta
candidate.QualityReason = "outside_daily_range"

View File

@@ -278,7 +278,7 @@ func TestWriterAppendDedupesRealtimeMileagePerSource(t *testing.T) {
}
}
func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *testing.T) {
func TestWriterAppendUsesCurrentSampleWhenNoHistoricalBaseline(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New() error = %v", err)
@@ -303,7 +303,7 @@ func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *tes
WithArgs("JT808", "115.231.168.135", "115.231.168.135:20215", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-07", "JT808", "JT808:13307765812@115.231.168.135").
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 first_total_mileage_km, first_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
@@ -325,8 +325,8 @@ func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *tes
int64(1),
eventTime,
eventTime,
QualityNoPreviousBaseline,
"missing_previous_source",
QualityOK,
"current_day_first_sample",
).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).
@@ -334,7 +334,7 @@ func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *tes
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", int64(1000)).
WillReturnResult(sqlmock.NewResult(0, 0))
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source s`).
WithArgs(
"LA9GG64L7PBAF4001",
@@ -345,7 +345,7 @@ func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *tes
"2026-07-08",
"JT808",
).
WillReturnResult(sqlmock.NewResult(0, 0))
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_daily_mileage`).
WithArgs(
"LA9GG64L7PBAF4001",
@@ -355,7 +355,7 @@ func TestWriterAppendWritesNoPreviousBaselineCandidateAndCleansProjection(t *tes
"2026-07-08",
"JT808",
).
WillReturnResult(sqlmock.NewResult(0, 1))
WillReturnResult(sqlmock.NewResult(0, 0))
if err := writer.Append(context.Background(), event); err != nil {
t.Fatalf("Append() error = %v", err)
@@ -391,7 +391,7 @@ func TestWriterAppendUsesPreviousSourceBaselineForRealtimeCandidate(t *testing.T
WithArgs("JT808", "115.231.168.135", "115.231.168.135:20215", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-07", "JT808", "JT808:13307765812@115.231.168.135").
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}).
AddRow(4100.8, previousTime))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
@@ -412,7 +412,7 @@ func TestWriterAppendUsesPreviousSourceBaselineForRealtimeCandidate(t *testing.T
previousTime,
currentTime,
QualityOK,
"same_source_previous_day",
"historical_source_baseline",
).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).
@@ -451,6 +451,92 @@ func TestWriterAppendUsesPreviousSourceBaselineForRealtimeCandidate(t *testing.T
}
}
func TestWriterAppendUsesOlderHistoricalSourceBaseline(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)
historicalTime := time.Date(2026, 7, 4, 23, 58, 0, 0, loc)
currentTime := time.Date(2026, 7, 8, 13, 20, 0, 0, loc)
event := envelope.FrameEnvelope{
Protocol: envelope.ProtocolYutongMQTT,
VIN: "LMRKH9AC2R1004087",
DeviceID: "LMRKH9AC2R1004087",
SourceEndpoint: "mqtt://yutong/ytforward/shln/3",
EventTimeMS: currentTime.UnixMilli(),
Fields: map[string]any{
"yutong_mqtt.data.total_mileage": 120788000,
},
}
mock.ExpectExec(`INSERT INTO vehicle_data_source`).
WithArgs("YUTONG_MQTT", "mqtt", "mqtt://yutong/ytforward/shln/3", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LMRKH9AC2R1004087", "2026-07-08", "YUTONG_MQTT", "YUTONG_MQTT:LMRKH9AC2R1004087@mqtt").
WillReturnRows(sqlmock.NewRows([]string{"latest_total_mileage_km", "latest_event_time"}).
AddRow(120672.0, historicalTime))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
WithArgs(
"LMRKH9AC2R1004087",
"2026-07-08",
"YUTONG_MQTT",
"YUTONG_MQTT:LMRKH9AC2R1004087@mqtt",
"mqtt",
"mqtt://yutong/ytforward/shln/3",
"",
"LMRKH9AC2R1004087",
"",
float64(120672.0),
float64(120788.0),
approxFloat64{want: 116.0, tolerance: 0.000001},
int64(1),
historicalTime,
currentTime,
QualityOK,
"historical_source_baseline",
).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).
WithArgs("LMRKH9AC2R1004087", "2026-07-08", "YUTONG_MQTT").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage`).
WithArgs("LMRKH9AC2R1004087", "2026-07-08", "YUTONG_MQTT", int64(1000)).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source s`).
WithArgs(
"LMRKH9AC2R1004087",
"2026-07-08",
"YUTONG_MQTT",
int64(1000),
"LMRKH9AC2R1004087",
"2026-07-08",
"YUTONG_MQTT",
).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`DELETE FROM vehicle_daily_mileage`).
WithArgs(
"LMRKH9AC2R1004087",
"2026-07-08",
"YUTONG_MQTT",
"LMRKH9AC2R1004087",
"2026-07-08",
"YUTONG_MQTT",
).
WillReturnResult(sqlmock.NewResult(0, 0))
if err := writer.Append(context.Background(), event); err != nil {
t.Fatalf("Append() error = %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations: %v", err)
}
}
func TestWriterAppendUsesCurrentOKCandidateWhenPreviousBaselineMissing(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
@@ -477,7 +563,7 @@ func TestWriterAppendUsesCurrentOKCandidateWhenPreviousBaselineMissing(t *testin
WithArgs("JT808", "115.231.168.135", "115.231.168.135:20215", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(`SELECT latest_total_mileage_km, latest_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-07", "JT808", "JT808:13307765812@115.231.168.135").
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 first_total_mileage_km, first_event_time FROM vehicle_daily_mileage_source`).
WithArgs("LA9GG64L7PBAF4001", "2026-07-08", "JT808", "JT808:13307765812@115.231.168.135").
@@ -501,7 +587,7 @@ func TestWriterAppendUsesCurrentOKCandidateWhenPreviousBaselineMissing(t *testin
firstTime,
currentTime,
QualityOK,
"same_source_previous_day",
"current_day_first_sample",
).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).

View File

@@ -273,17 +273,14 @@ WHERE vin = ? AND stat_date = ? AND protocol = ?
type sourceBaseline struct {
LatestTotalKM float64
LatestEventTime time.Time
QualityReason string
}
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)
rows, err := query.QueryContext(ctx, previousSourceBaselineSQL, vin, statDate, string(protocol), sourceKey)
if err != nil {
return sourceBaseline{}, false, err
}
@@ -305,6 +302,7 @@ func lookupPreviousSourceBaseline(ctx context.Context, query Queryer, vin string
return sourceBaseline{
LatestTotalKM: latestTotal.Float64,
LatestEventTime: latestEvent.Time,
QualityReason: "historical_source_baseline",
}, latestTotal.Valid, nil
}
@@ -334,25 +332,19 @@ func lookupCurrentSourceBaseline(ctx context.Context, query Queryer, vin string,
return sourceBaseline{
LatestTotalKM: firstTotal.Float64,
LatestEventTime: firstEvent.Time,
QualityReason: "current_day_first_sample",
}, firstTotal.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 stat_date < ?
AND protocol = ?
AND source_key = ?
ORDER BY latest_event_time DESC
AND quality_status = '` + QualityOK + `'
ORDER BY stat_date DESC, latest_event_time DESC
LIMIT 1
`