diff --git a/go/vehicle-gateway/internal/stats/query.go b/go/vehicle-gateway/internal/stats/query.go index 51f4c9ca..1cb3402a 100644 --- a/go/vehicle-gateway/internal/stats/query.go +++ b/go/vehicle-gateway/internal/stats/query.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "time" ) type Queryer interface { @@ -63,11 +64,14 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr var out []MetricRow for rows.Next() { var row MetricRow + var statDate scanDate + var createdAt scanDateTime + var updatedAt scanDateTime var first sql.NullFloat64 var latest sql.NullFloat64 if err := rows.Scan( &row.VIN, - &row.StatDate, + &statDate, &row.Protocol, &row.MetricKey, &row.MetricValue, @@ -76,11 +80,14 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr &latest, &row.SampleCount, &row.CalculationMethod, - &row.CreatedAt, - &row.UpdatedAt, + &createdAt, + &updatedAt, ); err != nil { return nil, err } + row.StatDate = statDate.String + row.CreatedAt = createdAt.String + row.UpdatedAt = updatedAt.String if first.Valid { row.FirstTotalMileageKM = &first.Float64 } @@ -239,3 +246,36 @@ func writeMetricError(w http.ResponseWriter, status int, message string) { w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{"error": message}) } + +type scanDate struct { + String string +} + +func (s *scanDate) Scan(value any) error { + s.String = formatSQLTime(value, "2006-01-02") + return nil +} + +type scanDateTime struct { + String string +} + +func (s *scanDateTime) Scan(value any) error { + s.String = formatSQLTime(value, "2006-01-02 15:04:05") + return nil +} + +func formatSQLTime(value any, layout string) string { + switch typed := value.(type) { + case nil: + return "" + case time.Time: + return typed.Format(layout) + case []byte: + return strings.TrimSpace(string(typed)) + case string: + return strings.TrimSpace(typed) + default: + return strings.TrimSpace(fmt.Sprint(typed)) + } +} diff --git a/go/vehicle-gateway/internal/stats/query_test.go b/go/vehicle-gateway/internal/stats/query_test.go index b9da7df6..75ce6f22 100644 --- a/go/vehicle-gateway/internal/stats/query_test.go +++ b/go/vehicle-gateway/internal/stats/query_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/DATA-DOG/go-sqlmock" ) @@ -24,8 +25,8 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) { "first_total_mileage_km", "latest_total_mileage_km", "sample_count", "calculation_method", "created_at", "updated_at", }).AddRow( - "LKLG7C4E3NA774736", "2026-07-01", "JT808", MetricDailyTotalMileageKM, 12345.6, "km", - 12345.6, 12345.6, 13, "TOTAL_MILEAGE_DIFF", "2026-07-01 22:49:11", "2026-07-01 23:09:36", + "LKLG7C4E3NA774736", time.Date(2026, 7, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "JT808", MetricDailyTotalMileageKM, 12345.6, "km", + 12345.6, 12345.6, 13, "TOTAL_MILEAGE_DIFF", time.Date(2026, 7, 1, 22, 49, 11, 0, time.FixedZone("Asia/Shanghai", 8*3600)), time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), )) repository := NewMetricRepository(db) @@ -45,6 +46,9 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) { if rows[0].MetricKey != MetricDailyTotalMileageKM || rows[0].MetricValue != 12345.6 { t.Fatalf("unexpected row: %#v", rows[0]) } + if rows[0].StatDate != "2026-07-01" || rows[0].UpdatedAt != "2026-07-01 23:09:36" { + t.Fatalf("unexpected time formatting: %#v", rows[0]) + } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) }