feat(platform-api): resolve vehicle keywords for data queries
This commit is contained in:
@@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -220,26 +220,49 @@ func isLikelyVIN(value string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
|
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) {
|
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) {
|
func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
|
||||||
if query.Limit <= 0 || query.Limit > 500 {
|
if query.Limit <= 0 || query.Limit > 500 {
|
||||||
query.Limit = 100
|
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)
|
return s.store.RawFrames(ctx, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) MileageSummary(ctx context.Context, query url.Values) (MileageSummary, error) {
|
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) {
|
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) {
|
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)
|
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 {
|
func vehicleSourceStatus(vehicles []VehicleRow, realtime []RealtimeLocationRow, history []HistoryLocationRow, raw []RawFrameRow, mileage []DailyMileageRow) []VehicleSourceStatus {
|
||||||
statusByProtocol := map[string]*VehicleSourceStatus{}
|
statusByProtocol := map[string]*VehicleSourceStatus{}
|
||||||
ensure := func(protocol string) *VehicleSourceStatus {
|
ensure := func(protocol string) *VehicleSourceStatus {
|
||||||
|
|||||||
Reference in New Issue
Block a user