feat(platform-api): resolve vehicle keywords for data queries

This commit is contained in:
lingniu
2026-07-04 00:37:47 +08:00
parent 0f371ab3c9
commit f62fe13647
2 changed files with 96 additions and 4 deletions

View File

@@ -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())
}
})
}
}

View File

@@ -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 {