From f62fe136472ab05ea7d4065b9e41b40503583fcb Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 00:37:47 +0800 Subject: [PATCH] feat(platform-api): resolve vehicle keywords for data queries --- .../api/internal/platform/handler_test.go | 25 +++++++ .../apps/api/internal/platform/service.go | 75 ++++++++++++++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index f5831298..fdcaeb50 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -173,3 +173,28 @@ func TestHandlerHistoryMileageQualityOps(t *testing.T) { }) } } + +func TestHandlerVehicleDataAPIsResolveVehicleKeyword(t *testing.T) { + handler := NewHandler(NewService(NewMockStore())) + cases := []struct { + name string + path string + }{ + {name: "history locations", path: "/api/history/locations?vin=粤AG18312&limit=10"}, + {name: "raw frames", path: "/api/history/raw-frames?vin=粤AG18312&limit=1"}, + {name: "daily mileage", path: "/api/mileage/daily?vin=粤AG18312&limit=10"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) + } + if !strings.Contains(rec.Body.String(), `"vin":"LB9A32A24R0LS1426"`) { + t.Fatalf("vehicle data API should resolve plate keyword to VIN: %s", rec.Body.String()) + } + }) + } +} diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index d876e773..41760217 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -220,26 +220,49 @@ func isLikelyVIN(value string) bool { } func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) { - return s.store.RealtimeLocations(ctx, query) + resolvedQuery, err := s.resolveVehicleQuery(ctx, query) + if err != nil { + return Page[RealtimeLocationRow]{}, err + } + return s.store.RealtimeLocations(ctx, resolvedQuery) } func (s *Service) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) { - return s.store.HistoryLocationsFromTDengine(ctx, query) + resolvedQuery, err := s.resolveVehicleQuery(ctx, query) + if err != nil { + return Page[HistoryLocationRow]{}, err + } + return s.store.HistoryLocationsFromTDengine(ctx, resolvedQuery) } func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) { if query.Limit <= 0 || query.Limit > 500 { query.Limit = 100 } + resolvedVIN, err := s.resolveVehicleVIN(ctx, query.VIN, query.Protocol) + if err != nil { + return Page[RawFrameRow]{}, err + } + if resolvedVIN != "" { + query.VIN = resolvedVIN + } return s.store.RawFrames(ctx, query) } func (s *Service) MileageSummary(ctx context.Context, query url.Values) (MileageSummary, error) { - return s.store.MileageSummary(ctx, query) + resolvedQuery, err := s.resolveVehicleQuery(ctx, query) + if err != nil { + return MileageSummary{}, err + } + return s.store.MileageSummary(ctx, resolvedQuery) } func (s *Service) DailyMileage(ctx context.Context, query url.Values) (Page[DailyMileageRow], error) { - return s.store.DailyMileage(ctx, query) + resolvedQuery, err := s.resolveVehicleQuery(ctx, query) + if err != nil { + return Page[DailyMileageRow]{}, err + } + return s.store.DailyMileage(ctx, resolvedQuery) } func (s *Service) QualitySummary(ctx context.Context, query url.Values) (QualitySummary, error) { @@ -254,6 +277,50 @@ func (s *Service) OpsHealth(ctx context.Context) (OpsHealth, error) { return s.store.OpsHealth(ctx) } +func (s *Service) resolveVehicleQuery(ctx context.Context, query url.Values) (url.Values, error) { + resolved := cloneValues(query) + vin := strings.TrimSpace(resolved.Get("vin")) + if vin == "" { + return resolved, nil + } + resolvedVIN, err := s.resolveVehicleVIN(ctx, vin, resolved.Get("protocol")) + if err != nil { + return nil, err + } + if resolvedVIN != "" { + resolved.Set("vin", resolvedVIN) + } + return resolved, nil +} + +func (s *Service) resolveVehicleVIN(ctx context.Context, keyword string, protocol string) (string, error) { + keyword = strings.TrimSpace(keyword) + if keyword == "" || isLikelyVIN(keyword) { + return keyword, nil + } + vehicleQuery := url.Values{"keyword": {keyword}, "limit": {"10"}} + if protocol = strings.TrimSpace(protocol); protocol != "" { + vehicleQuery.Set("protocol", protocol) + } + vehicles, err := s.store.Vehicles(ctx, vehicleQuery) + if err != nil { + return "", err + } + identity := resolveVehicleIdentity(keyword, vehicles.Items) + if identity == nil || strings.TrimSpace(identity.VIN) == "" { + return keyword, nil + } + return identity.VIN, nil +} + +func cloneValues(values url.Values) url.Values { + cloned := make(url.Values, len(values)) + for key, current := range values { + cloned[key] = append([]string(nil), current...) + } + return cloned +} + func vehicleSourceStatus(vehicles []VehicleRow, realtime []RealtimeLocationRow, history []HistoryLocationRow, raw []RawFrameRow, mileage []DailyMileageRow) []VehicleSourceStatus { statusByProtocol := map[string]*VehicleSourceStatus{} ensure := func(protocol string) *VehicleSourceStatus {