fix(stats): recover jt808 mileage from safe gps tracks
This commit is contained in:
188
go/vehicle-gateway/internal/stats/gps_mileage_test.go
Normal file
188
go/vehicle-gateway/internal/stats/gps_mileage_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
||||
)
|
||||
|
||||
func TestCalculateGPSMileageSegmentMatchesTrackSafetyWindow(t *testing.T) {
|
||||
start := time.Date(2026, 7, 19, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
||||
previous := GPSMileagePoint{EventTime: start, Longitude: 121.4737, Latitude: 31.2304}
|
||||
|
||||
valid := CalculateGPSMileageSegment(previous, GPSMileagePoint{
|
||||
EventTime: start.Add(5 * time.Minute),
|
||||
Longitude: 121.4837,
|
||||
Latitude: 31.2304,
|
||||
})
|
||||
if !valid.Usable || valid.DistanceKM <= 0.9 || valid.DistanceKM >= 1.1 {
|
||||
t.Fatalf("valid segment = %+v, want about 0.95 km", valid)
|
||||
}
|
||||
|
||||
longGap := CalculateGPSMileageSegment(previous, GPSMileagePoint{
|
||||
EventTime: start.Add(11 * time.Minute),
|
||||
Longitude: 121.4837,
|
||||
Latitude: 31.2304,
|
||||
})
|
||||
if !longGap.LongGap || longGap.Usable {
|
||||
t.Fatalf("long gap segment = %+v", longGap)
|
||||
}
|
||||
|
||||
jump := CalculateGPSMileageSegment(previous, GPSMileagePoint{
|
||||
EventTime: start.Add(time.Minute),
|
||||
Longitude: 121.5737,
|
||||
Latitude: 31.2304,
|
||||
})
|
||||
if !jump.BadJump || jump.Usable {
|
||||
t.Fatalf("jump segment = %+v", jump)
|
||||
}
|
||||
|
||||
outOfOrder := CalculateGPSMileageSegment(previous, GPSMileagePoint{
|
||||
EventTime: start,
|
||||
Longitude: 121.4747,
|
||||
Latitude: 31.2304,
|
||||
})
|
||||
if !outOfOrder.OutOfOrder || outOfOrder.Usable {
|
||||
t.Fatalf("out-of-order segment = %+v", outOfOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPSMileagePointFromEnvelopeUsesNaturalDayAndStableSource(t *testing.T) {
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
eventTime := time.Date(2026, 7, 19, 23, 59, 58, 0, loc)
|
||||
env := envelope.FrameEnvelope{
|
||||
EventID: "evt-gps-1",
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
VIN: "LMRKH9AC7R1004120",
|
||||
Phone: "64341233712",
|
||||
SourceEndpoint: "122.152.221.156:33805",
|
||||
EventTimeMS: eventTime.UnixMilli(),
|
||||
Fields: map[string]any{
|
||||
"jt808.location.longitude": 121.4737,
|
||||
"jt808.location.latitude": 31.2304,
|
||||
},
|
||||
}
|
||||
identity := SourceIdentity{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
SourceIP: "122.152.221.156",
|
||||
SourceCode: "dongfang_beidou",
|
||||
SourceKind: "PLATFORM",
|
||||
PlatformName: "东方北斗",
|
||||
}
|
||||
point, ok := GPSMileagePointFromEnvelope(env, identity, loc)
|
||||
if !ok {
|
||||
t.Fatal("expected valid GPS point")
|
||||
}
|
||||
if point.StatDate != "2026-07-19" || point.SourceKey != "JT808:64341233712@PLATFORM:dongfang_beidou" {
|
||||
t.Fatalf("point = %+v", point)
|
||||
}
|
||||
candidateKey := gpsMileageCandidateSourceKey(point)
|
||||
if candidateKey != "JT808:64341233712#GPS_COORDINATE:122.152.221.156" || strings.Contains(candidateKey, platformSourceKeyPrefix) {
|
||||
t.Fatalf("coordinate candidate key must remain lower-trust than an odometer platform source: %q", candidateKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccumulateJT808GPSMileagePersistsEstimateAndProjects(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)
|
||||
firstTime := time.Date(2026, 7, 19, 8, 0, 0, 0, loc)
|
||||
point := GPSMileagePoint{
|
||||
VIN: "LMRKH9AC7R1004120",
|
||||
StatDate: "2026-07-19",
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
SourceKey: "JT808:64341233712@PLATFORM:dongfang_beidou",
|
||||
SourceIP: "122.152.221.156",
|
||||
SourceEndpoint: "122.152.221.156:33805",
|
||||
Phone: "64341233712",
|
||||
EventID: "evt-2",
|
||||
EventTime: firstTime.Add(5 * time.Minute),
|
||||
Longitude: 121.4837,
|
||||
Latitude: 31.2304,
|
||||
}
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery("SELECT first_event_time, latest_event_time").
|
||||
WithArgs(point.VIN, point.StatDate, string(point.Protocol), point.SourceKey).
|
||||
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(firstTime, firstTime, "evt-1", 121.4737, 31.2304, 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()
|
||||
|
||||
state, recovered, err := AccumulateJT808GPSMileage(context.Background(), db, point)
|
||||
if err != nil {
|
||||
t.Fatalf("AccumulateJT808GPSMileage() error = %v", err)
|
||||
}
|
||||
if !recovered || state.UsableSegmentCount != 1 || math.Abs(state.DailyMileageKM-0.9529) > 0.02 {
|
||||
t.Fatalf("state = %+v recovered=%v", state, recovered)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet SQL expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccumulateJT808GPSMileageNeedsUsableSegmentBeforeProjection(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
point := GPSMileagePoint{
|
||||
VIN: "LMRKH9AC7R1004120",
|
||||
StatDate: "2026-07-19",
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
SourceKey: "JT808:64341233712@PLATFORM:dongfang_beidou",
|
||||
SourceIP: "122.152.221.156",
|
||||
EventID: "evt-1",
|
||||
EventTime: time.Date(2026, 7, 19, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
|
||||
Longitude: 121.4737,
|
||||
Latitude: 31.2304,
|
||||
}
|
||||
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",
|
||||
}))
|
||||
mock.ExpectQuery("SELECT daily_mileage_km, first_event_time, sample_count").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"daily_mileage_km", "first_event_time", "sample_count"}))
|
||||
mock.ExpectExec("INSERT INTO vehicle_daily_gps_mileage_state").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
_, recovered, err := AccumulateJT808GPSMileage(context.Background(), db, point)
|
||||
if err != nil {
|
||||
t.Fatalf("AccumulateJT808GPSMileage() error = %v", err)
|
||||
}
|
||||
if recovered {
|
||||
t.Fatal("first point must not project an estimated mileage")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet SQL expectations: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user