feat(platform): separate vehicle lookup key from VIN

This commit is contained in:
lingniu
2026-07-04 00:16:04 +08:00
parent 52b3040506
commit cb1a3097d8
5 changed files with 66 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
package platform
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
@@ -56,7 +57,7 @@ func TestHandlerVehicleDetail(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
for _, want := range []string{"identity", "realtimeSummary", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus"} {
for _, want := range []string{"lookupKey", "lookupResolved", "identity", "realtimeSummary", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus"} {
if !strings.Contains(rec.Body.String(), want) {
t.Fatalf("response missing %q: %s", want, rec.Body.String())
}
@@ -89,6 +90,30 @@ func TestHandlerVehicleDetailAcceptsKeyword(t *testing.T) {
}
}
func TestHandlerVehicleDetailKeepsUnresolvedLookupOutOfVIN(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/vehicles/detail?keyword=64646848247", nil)
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
for _, want := range []string{`"lookupKey":"64646848247"`, `"lookupResolved":false`, `"vin":""`} {
if !strings.Contains(rec.Body.String(), want) {
t.Fatalf("response missing %q: %s", want, rec.Body.String())
}
}
var body struct {
Data VehicleDetail `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
}
if body.Data.VIN == "64646848247" {
t.Fatalf("unresolved lookup should not be returned as top-level VIN: %s", rec.Body.String())
}
}
func TestHandlerRealtimeLocations(t *testing.T) {
handler := NewHandler(NewService(NewMockStore()))
rec := httptest.NewRecorder()

View File

@@ -56,6 +56,8 @@ type VehicleCoverageRow struct {
type VehicleDetail struct {
VIN string `json:"vin"`
LookupKey string `json:"lookupKey"`
LookupResolved bool `json:"lookupResolved"`
Identity *VehicleRow `json:"identity,omitempty"`
RealtimeSummary *VehicleRealtimeRow `json:"realtimeSummary,omitempty"`
Sources []string `json:"sources"`

View File

@@ -67,23 +67,29 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
return VehicleDetail{}, err
}
identity := resolveVehicleIdentity(keyword, vehicles.Items)
resolvedVIN := keyword
resolvedVIN := ""
if identity != nil && strings.TrimSpace(identity.VIN) != "" {
resolvedVIN = identity.VIN
} else if isLikelyVIN(keyword) {
resolvedVIN = keyword
}
queryVIN := resolvedVIN
if queryVIN == "" {
queryVIN = keyword
}
realtimeQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
historyQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
rawQuery := RawFrameQuery{VIN: resolvedVIN, IncludeFields: true, Limit: 10}
mileageQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
qualityQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
realtimeQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
historyQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
rawQuery := RawFrameQuery{VIN: queryVIN, IncludeFields: true, Limit: 10}
mileageQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
qualityQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
if protocol != "" {
realtimeQuery.Set("protocol", protocol)
historyQuery.Set("protocol", protocol)
rawQuery.Protocol = protocol
qualityQuery.Set("protocol", protocol)
}
realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {resolvedVIN}, "limit": {"1"}})
realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {queryVIN}, "limit": {"1"}})
if err != nil {
return VehicleDetail{}, err
}
@@ -112,9 +118,11 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
return VehicleDetail{}, err
}
sourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items)
sourceStatus = s.enrichVehicleSourceStatus(ctx, resolvedVIN, sourceStatus)
sourceStatus = s.enrichVehicleSourceStatus(ctx, queryVIN, sourceStatus)
return VehicleDetail{
VIN: resolvedVIN,
LookupKey: keyword,
LookupResolved: resolvedVIN != "",
Identity: identity,
RealtimeSummary: summary,
Sources: sourceNames(sourceStatus),
@@ -172,6 +180,23 @@ func resolveVehicleIdentity(keyword string, vehicles []VehicleRow) *VehicleRow {
return nil
}
func isLikelyVIN(value string) bool {
value = strings.ToUpper(strings.TrimSpace(value))
if len(value) != 17 {
return false
}
for _, char := range value {
if char >= '0' && char <= '9' {
continue
}
if char >= 'A' && char <= 'Z' && char != 'I' && char != 'O' && char != 'Q' {
continue
}
return false
}
return true
}
func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
return s.store.RealtimeLocations(ctx, query)
}