From 800ca1a8b1b25685bf8808d366bc2a37a69d380b Mon Sep 17 00:00:00 2001 From: lingniu Date: Wed, 8 Jul 2026 15:19:43 +0800 Subject: [PATCH] feat(stats): expose selected mileage source --- go/vehicle-gateway/internal/stats/query.go | 39 ++++++++--- .../internal/stats/query_test.go | 69 ++++++++++++++++--- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/go/vehicle-gateway/internal/stats/query.go b/go/vehicle-gateway/internal/stats/query.go index f6e04aa7..2e55592c 100644 --- a/go/vehicle-gateway/internal/stats/query.go +++ b/go/vehicle-gateway/internal/stats/query.go @@ -27,14 +27,17 @@ type MetricQuery struct { } type MetricRow struct { - VIN string `json:"vin"` - StatDate string `json:"stat_date"` - Protocol string `json:"protocol"` - DailyMileageKM float64 `json:"daily_mileage_km"` - FirstTotalMileageKM *float64 `json:"first_total_mileage_km,omitempty"` - LatestTotalMileageKM *float64 `json:"latest_total_mileage_km,omitempty"` - SampleCount int64 `json:"sample_count"` - UpdatedAt string `json:"updated_at"` + VIN string `json:"vin"` + StatDate string `json:"stat_date"` + Protocol string `json:"protocol"` + DailyMileageKM float64 `json:"daily_mileage_km"` + FirstTotalMileageKM *float64 `json:"first_total_mileage_km,omitempty"` + LatestTotalMileageKM *float64 `json:"latest_total_mileage_km,omitempty"` + TrustedSourceKey *string `json:"trusted_source_key,omitempty"` + TrustedPhone *string `json:"trusted_phone,omitempty"` + TrustedSourceEndpoint *string `json:"trusted_source_endpoint,omitempty"` + SampleCount int64 `json:"sample_count"` + UpdatedAt string `json:"updated_at"` } type MetricRepository struct { @@ -64,6 +67,9 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr var updatedAt scanDateTime var first sql.NullFloat64 var latest sql.NullFloat64 + var trustedSourceKey sql.NullString + var trustedPhone sql.NullString + var trustedSourceEndpoint sql.NullString if err := rows.Scan( &row.VIN, &statDate, @@ -71,6 +77,9 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr &row.DailyMileageKM, &first, &latest, + &trustedSourceKey, + &trustedPhone, + &trustedSourceEndpoint, &row.SampleCount, &updatedAt, ); err != nil { @@ -84,6 +93,15 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr if latest.Valid { row.LatestTotalMileageKM = &latest.Float64 } + if trustedSourceKey.Valid { + row.TrustedSourceKey = &trustedSourceKey.String + } + if trustedPhone.Valid { + row.TrustedPhone = &trustedPhone.String + } + if trustedSourceEndpoint.Valid { + row.TrustedSourceEndpoint = &trustedSourceEndpoint.String + } out = append(out, row) } return out, rows.Err() @@ -120,7 +138,10 @@ func normalizeMetricQuery(query MetricQuery) MetricQuery { func buildMetricSQL(query MetricQuery) (string, []any) { where, args := buildMetricWhere(query) - sqlText := `SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage` + sqlText := `SELECT vin, stat_date, protocol, daily_mileage_km, + first_total_mileage_km, latest_total_mileage_km, + trusted_source_key, trusted_phone, trusted_source_endpoint, + sample_count, updated_at FROM vehicle_daily_mileage` if len(where) > 0 { sqlText += " WHERE " + strings.Join(where, " AND ") } diff --git a/go/vehicle-gateway/internal/stats/query_test.go b/go/vehicle-gateway/internal/stats/query_test.go index 923b10c5..3f20f772 100644 --- a/go/vehicle-gateway/internal/stats/query_test.go +++ b/go/vehicle-gateway/internal/stats/query_test.go @@ -18,15 +18,19 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() - mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage"). + mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage"). WithArgs("LKLG7C4E3NA774736", "JT808", "2026-07-01", "2026-07-01", 20, 0). WillReturnRows(sqlmock.NewRows([]string{ "vin", "stat_date", "protocol", "daily_mileage_km", - "first_total_mileage_km", "latest_total_mileage_km", "sample_count", + "first_total_mileage_km", "latest_total_mileage_km", + "trusted_source_key", "trusted_phone", "trusted_source_endpoint", + "sample_count", "updated_at", }).AddRow( "LKLG7C4E3NA774736", time.Date(2026, 7, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "JT808", 12.3, - 12345.6, 12357.9, 13, time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), + 12345.6, 12357.9, + nil, nil, nil, + 13, time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), )) repository := NewMetricRepository(db) @@ -54,6 +58,39 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) { } } +func TestMetricQueryReturnsSelectedSourceFields(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer db.Close() + mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage"). + WillReturnRows(sqlmock.NewRows([]string{ + "vin", "stat_date", "protocol", "daily_mileage_km", + "first_total_mileage_km", "latest_total_mileage_km", + "trusted_source_key", "trusted_phone", "trusted_source_endpoint", + "sample_count", "updated_at", + }).AddRow( + "LA9GG64L7PBAF4001", "2026-07-08", "JT808", 23.1, + 4100.8, 4123.9, + "JT808:13307765812@115.231.168.135", "13307765812", "115.231.168.135:20215", + 1, "2026-07-08 13:30:57", + )) + + repo := NewMetricRepository(db) + got, err := repo.Query(context.Background(), MetricQuery{ + VIN: "LA9GG64L7PBAF4001", + Protocol: "JT808", + Limit: 1, + }) + if err != nil { + t.Fatalf("Query() error = %v", err) + } + if got[0].TrustedSourceKey == nil || *got[0].TrustedSourceKey != "JT808:13307765812@115.231.168.135" { + t.Fatalf("trusted source = %#v", got[0].TrustedSourceKey) + } +} + func TestMetricHandlerReturnsDailyMetrics(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { @@ -63,15 +100,19 @@ func TestMetricHandlerReturnsDailyMetrics(t *testing.T) { mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM vehicle_daily_mileage"). WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01"). WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(42)) - mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage"). + mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage"). WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01", 50, 0). WillReturnRows(sqlmock.NewRows([]string{ "vin", "stat_date", "protocol", "daily_mileage_km", - "first_total_mileage_km", "latest_total_mileage_km", "sample_count", + "first_total_mileage_km", "latest_total_mileage_km", + "trusted_source_key", "trusted_phone", "trusted_source_endpoint", + "sample_count", "updated_at", }).AddRow( "LB9A32A21R0LS1707", "2020-07-01", "GB32960", 0.0, - 53490.9, 53490.9, 3, "2026-07-01 22:28:25", + 53490.9, 53490.9, + nil, nil, nil, + 3, "2026-07-01 22:28:25", )) handler := NewMetricHandler(NewMetricRepository(db)) @@ -103,11 +144,13 @@ func TestMetricHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() - mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage"). + mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage"). WithArgs("YUTONG_MQTT", 50, 0). WillReturnRows(sqlmock.NewRows([]string{ "vin", "stat_date", "protocol", "daily_mileage_km", - "first_total_mileage_km", "latest_total_mileage_km", "sample_count", + "first_total_mileage_km", "latest_total_mileage_km", + "trusted_source_key", "trusted_phone", "trusted_source_endpoint", + "sample_count", "updated_at", })) @@ -135,15 +178,19 @@ func TestMetricHandlerSkipsTotalCountByDefault(t *testing.T) { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() - mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage"). + mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage"). WithArgs("JT808", 1, 0). WillReturnRows(sqlmock.NewRows([]string{ "vin", "stat_date", "protocol", "daily_mileage_km", - "first_total_mileage_km", "latest_total_mileage_km", "sample_count", + "first_total_mileage_km", "latest_total_mileage_km", + "trusted_source_key", "trusted_phone", "trusted_source_endpoint", + "sample_count", "updated_at", }).AddRow( "LKLG7C4E3NA774736", "2026-07-02", "JT808", 12.3, - 8792.8, 8805.1, 30, "2026-07-02 23:59:59", + 8792.8, 8805.1, + nil, nil, nil, + 30, "2026-07-02 23:59:59", )) handler := NewMetricHandler(NewMetricRepository(db))