From fdcac05559bdbbb5971dd7ec3b17916853dbd682 Mon Sep 17 00:00:00 2001 From: lingniu Date: Wed, 8 Jul 2026 11:23:06 +0800 Subject: [PATCH] fix(stats): map protocol mileage fields --- go/vehicle-gateway/internal/realtime/kv.go | 19 +------ .../internal/realtime/kv_test.go | 14 ++--- .../internal/stats/daily_metric.go | 37 ++++++++++++- .../internal/stats/daily_metric_test.go | 54 +++++++++++++++++++ 4 files changed, 94 insertions(+), 30 deletions(-) diff --git a/go/vehicle-gateway/internal/realtime/kv.go b/go/vehicle-gateway/internal/realtime/kv.go index 78db21c9..5325dcc1 100644 --- a/go/vehicle-gateway/internal/realtime/kv.go +++ b/go/vehicle-gateway/internal/realtime/kv.go @@ -30,7 +30,6 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo if !ok { return envelope.FrameEnvelope{}, false } - statFields := fieldsWithCanonicalStats(fields, env.Fields) out := envelope.FrameEnvelope{ EventID: env.StableEventID() + ":fields", TraceID: env.TraceID, @@ -45,7 +44,7 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo SourceEndpoint: env.SourceEndpoint, EventTimeMS: env.EventTimeMS, ReceivedAtMS: env.ReceivedAtMS, - Fields: statFields, + Fields: fields, ParsedFields: fields, ParsedFieldTypes: fieldTypes, Parsed: map[string]any{ @@ -57,22 +56,6 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo return out, true } -func fieldsWithCanonicalStats(mapped map[string]any, canonical map[string]any) map[string]any { - out := cloneAnyMap(mapped) - for _, key := range []string{ - envelope.FieldTotalMileageKM, - envelope.FieldSpeedKMH, - envelope.FieldSOCPercent, - envelope.FieldLongitude, - envelope.FieldLatitude, - } { - if value, ok := canonical[key]; ok && value != nil { - out[key] = value - } - } - return out -} - func EnsureParsedFields(env *envelope.FrameEnvelope) bool { if env == nil { return false diff --git a/go/vehicle-gateway/internal/realtime/kv_test.go b/go/vehicle-gateway/internal/realtime/kv_test.go index d61eee15..43610d75 100644 --- a/go/vehicle-gateway/internal/realtime/kv_test.go +++ b/go/vehicle-gateway/internal/realtime/kv_test.go @@ -39,7 +39,7 @@ func TestRealtimeKVFieldsFromGB32960ParsedDomains(t *testing.T) { } } -func TestBuildFieldsEnvelopeKeepsMappedAndCanonicalStatFields(t *testing.T) { +func TestBuildFieldsEnvelopeUsesOnlyProtocolMappedFieldNames(t *testing.T) { fieldsEnv, ok := BuildFieldsEnvelope(envelope.FrameEnvelope{ Protocol: envelope.ProtocolGB32960, VIN: "VIN001", @@ -65,9 +65,9 @@ func TestBuildFieldsEnvelopeKeepsMappedAndCanonicalStatFields(t *testing.T) { if !ok { t.Fatal("BuildFieldsEnvelope() should emit mapped fields") } - for _, bareKey := range []string{"charge_status"} { + for _, bareKey := range []string{"charge_status", "soc_percent", "total_mileage_km"} { if _, exists := fieldsEnv.Fields[bareKey]; exists { - t.Fatalf("fields envelope should not expose non-canonical bare env.Fields key %q: %#v", bareKey, fieldsEnv.Fields) + t.Fatalf("fields envelope should not expose non-protocol bare key %q: %#v", bareKey, fieldsEnv.Fields) } } for _, mappedKey := range []string{ @@ -79,14 +79,6 @@ func TestBuildFieldsEnvelopeKeepsMappedAndCanonicalStatFields(t *testing.T) { t.Fatalf("fields envelope missing mapped key %q: %#v", mappedKey, fieldsEnv.Fields) } } - for _, canonicalKey := range []string{envelope.FieldSOCPercent, envelope.FieldTotalMileageKM} { - if _, exists := fieldsEnv.Fields[canonicalKey]; !exists { - t.Fatalf("fields envelope should keep canonical stat key %q: %#v", canonicalKey, fieldsEnv.Fields) - } - if _, exists := fieldsEnv.ParsedFields[canonicalKey]; exists { - t.Fatalf("parsed fields should stay protocol-mapped without canonical duplicate %q: %#v", canonicalKey, fieldsEnv.ParsedFields) - } - } } func TestRealtimeKVFieldsFromJT808ParsedFields(t *testing.T) { diff --git a/go/vehicle-gateway/internal/stats/daily_metric.go b/go/vehicle-gateway/internal/stats/daily_metric.go index 995ec0c5..b4e03912 100644 --- a/go/vehicle-gateway/internal/stats/daily_metric.go +++ b/go/vehicle-gateway/internal/stats/daily_metric.go @@ -99,7 +99,7 @@ func SamplesFromEnvelope(env envelope.FrameEnvelope, loc *time.Location) ([]Metr if vin == "" { return nil, nil } - totalMileage, ok := floatField(env, envelope.FieldTotalMileageKM) + totalMileage, ok := totalMileageKMFromEnvelope(env) if !ok { return nil, nil } @@ -125,6 +125,41 @@ func SamplesFromEnvelope(env envelope.FrameEnvelope, loc *time.Location) ([]Metr }}, nil } +type mileageFieldMapping struct { + key string + scale float64 +} + +func totalMileageKMFromEnvelope(env envelope.FrameEnvelope) (float64, bool) { + if value, ok := floatField(env, envelope.FieldTotalMileageKM); ok { + return value, true + } + for _, mapping := range mileageMappingsByProtocol(env.Protocol) { + value, ok := floatField(env, mapping.key) + if !ok { + continue + } + return value * mapping.scale, true + } + return 0, false +} + +func mileageMappingsByProtocol(protocol envelope.Protocol) []mileageFieldMapping { + switch protocol { + case envelope.ProtocolGB32960: + return []mileageFieldMapping{{key: "gb32960.vehicle.total_mileage_km", scale: 1}} + case envelope.ProtocolJT808: + return []mileageFieldMapping{{key: "jt808.location.total_mileage_km", scale: 1}} + case envelope.ProtocolYutongMQTT: + return []mileageFieldMapping{ + {key: "yutong_mqtt.data.total_mileage", scale: 0.001}, + {key: "yutong_mqtt.root.data.total_mileage", scale: 0.001}, + } + default: + return nil + } +} + const upsertDailyMileageSQL = ` INSERT INTO vehicle_daily_mileage (vin, stat_date, protocol, daily_mileage_km, diff --git a/go/vehicle-gateway/internal/stats/daily_metric_test.go b/go/vehicle-gateway/internal/stats/daily_metric_test.go index 6c72e9c1..f2c13e2a 100644 --- a/go/vehicle-gateway/internal/stats/daily_metric_test.go +++ b/go/vehicle-gateway/internal/stats/daily_metric_test.go @@ -40,6 +40,60 @@ func TestSamplesFromEnvelopeDerivesDailyMileageSample(t *testing.T) { } } +func TestSamplesFromEnvelopeMapsProtocolMileageFields(t *testing.T) { + loc := time.FixedZone("Asia/Shanghai", 8*3600) + tests := []struct { + name string + protocol envelope.Protocol + fields map[string]any + wantKM float64 + }{ + { + name: "gb32960", + protocol: envelope.ProtocolGB32960, + fields: map[string]any{"gb32960.vehicle.total_mileage_km": "38587"}, + wantKM: 38587, + }, + { + name: "jt808", + protocol: envelope.ProtocolJT808, + fields: map[string]any{"jt808.location.total_mileage_km": "12432.4"}, + wantKM: 12432.4, + }, + { + name: "yutong mqtt meters", + protocol: envelope.ProtocolYutongMQTT, + fields: map[string]any{"yutong_mqtt.data.total_mileage": "120756000"}, + wantKM: 120756, + }, + { + name: "yutong mqtt root meters", + protocol: envelope.ProtocolYutongMQTT, + fields: map[string]any{"yutong_mqtt.root.data.total_mileage": float64(22858000)}, + wantKM: 22858, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + samples, err := SamplesFromEnvelope(envelope.FrameEnvelope{ + Protocol: tt.protocol, + VIN: "LNBVIN00000000001", + EventTimeMS: time.Date(2026, 7, 1, 8, 30, 0, 0, loc).UnixMilli(), + Fields: tt.fields, + }, loc) + if err != nil { + t.Fatalf("SamplesFromEnvelope() error = %v", err) + } + if len(samples) != 1 { + t.Fatalf("sample count = %d", len(samples)) + } + if samples[0].TotalMileageKM != tt.wantKM { + t.Fatalf("total mileage = %v, want %v", samples[0].TotalMileageKM, tt.wantKM) + } + }) + } +} + func TestSamplesFromEnvelopeSkipsMissingVIN(t *testing.T) { loc := time.FixedZone("Asia/Shanghai", 8*3600) samples, err := SamplesFromEnvelope(envelope.FrameEnvelope{