feat(platform): add vehicle service API
This commit is contained in:
@@ -29,6 +29,7 @@ func (h *Handler) routes() {
|
||||
h.mux.HandleFunc("GET /api/vehicles", h.handleVehicles)
|
||||
h.mux.HandleFunc("GET /api/vehicles/resolve", h.handleVehicleResolve)
|
||||
h.mux.HandleFunc("GET /api/vehicles/coverage", h.handleVehicleCoverage)
|
||||
h.mux.HandleFunc("GET /api/vehicle-service", h.handleVehicleDetail)
|
||||
h.mux.HandleFunc("GET /api/vehicles/detail", h.handleVehicleDetail)
|
||||
h.mux.HandleFunc("GET /api/realtime/vehicles", h.handleVehicleRealtime)
|
||||
h.mux.HandleFunc("GET /api/realtime/locations", h.handleRealtimeLocations)
|
||||
|
||||
@@ -64,6 +64,30 @@ func TestHandlerVehicleDetail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleServiceCanonicalEndpoint(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/vehicle-service?keyword=粤AG18312&protocol=JT808", nil)
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s", rec.Code, 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.Resolution == nil || body.Data.Resolution.VIN != "LB9A32A24R0LS1426" {
|
||||
t.Fatalf("vehicle service should resolve canonical vehicle identity, got %+v body=%s", body.Data.Resolution, rec.Body.String())
|
||||
}
|
||||
for _, row := range body.Data.Realtime {
|
||||
if row.Protocol != "JT808" {
|
||||
t.Fatalf("vehicle service should keep source filter, got realtime row %+v body=%s", row, rec.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleDetailResolvesPlateToVIN(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -74,7 +74,7 @@ export const api = {
|
||||
vehicles: (params = new URLSearchParams()) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`),
|
||||
vehicleResolve: (params = new URLSearchParams()) => request<VehicleIdentityResolution>(`/api/vehicles/resolve?${params.toString()}`),
|
||||
vehicleCoverage: (params = new URLSearchParams()) => request<Page<VehicleCoverageRow>>(`/api/vehicles/coverage?${params.toString()}`),
|
||||
vehicleDetail: (params = new URLSearchParams()) => request<VehicleDetail>(`/api/vehicles/detail?${params.toString()}`),
|
||||
vehicleDetail: (params = new URLSearchParams()) => request<VehicleDetail>(`/api/vehicle-service?${params.toString()}`),
|
||||
vehicleRealtime: (params = new URLSearchParams()) => request<Page<VehicleRealtimeRow>>(`/api/realtime/vehicles?${params.toString()}`),
|
||||
realtimeLocations: (params = new URLSearchParams()) => request<Page<RealtimeLocationRow>>(`/api/realtime/locations?${params.toString()}`),
|
||||
historyLocations: (params = new URLSearchParams()) => request<Page<HistoryLocationRow>>(`/api/history/locations?${params.toString()}`),
|
||||
|
||||
@@ -249,7 +249,7 @@ test('switches vehicle detail to a source from the coverage cards', async () =>
|
||||
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/vehicles/detail')) {
|
||||
if (path.includes('/api/vehicle-service')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -320,14 +320,14 @@ test('switches vehicle detail to a source from the coverage cards', async () =>
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#/detail?keyword=VIN001&protocol=JT808');
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/detail?keyword=VIN001&protocol=JT808'), undefined);
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined);
|
||||
});
|
||||
|
||||
test('opens history with the currently selected vehicle detail source', async () => {
|
||||
window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/vehicles/detail')) {
|
||||
if (path.includes('/api/vehicle-service')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -446,9 +446,9 @@ test('opens vehicle service from an empty history query with the current filters
|
||||
|
||||
test('shows selected source scope on vehicle detail', async () => {
|
||||
window.history.replaceState(null, '', '/#/detail?keyword=VIN001&protocol=JT808');
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/vehicles/detail')) {
|
||||
if (path.includes('/api/vehicle-service')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -516,4 +516,5 @@ test('shows selected source scope on vehicle detail', async () => {
|
||||
|
||||
expect(await screen.findByText('当前查看范围')).toBeInTheDocument();
|
||||
expect(screen.getByText('单一来源:JT808')).toBeInTheDocument();
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined);
|
||||
});
|
||||
|
||||
@@ -35,18 +35,20 @@ The platform is vehicle-first. Query APIs should accept `keyword` as the user-fa
|
||||
|
||||
The BFF resolves `keyword` through vehicle identity data before querying realtime, history, RAW, or mileage data. `vin` remains accepted as a compatibility alias on data query APIs, but new UI and integrations should send `keyword`.
|
||||
|
||||
If a keyword cannot be resolved to a VIN, data APIs must not fabricate a VIN. They should return empty result pages for vehicle data, while `/api/vehicles/detail` exposes `lookupResolved=false` and quality issues for follow-up binding work.
|
||||
If a keyword cannot be resolved to a VIN, data APIs must not fabricate a VIN. They should return empty result pages for vehicle data, while `/api/vehicle-service` exposes `lookupResolved=false` and quality issues for follow-up binding work.
|
||||
|
||||
## Core Query APIs
|
||||
|
||||
### Vehicle Detail
|
||||
### Vehicle Service
|
||||
|
||||
```http
|
||||
GET /api/vehicles/detail?keyword=粤AG18312
|
||||
GET /api/vehicle-service?keyword=粤AG18312
|
||||
```
|
||||
|
||||
Returns one vehicle service view with identity, realtime summary, source coverage, history preview, RAW preview, mileage preview, and quality issues.
|
||||
|
||||
`/api/vehicles/detail` remains available as a compatibility alias. New UI and integrations should use `/api/vehicle-service` to make the vehicle-first boundary explicit.
|
||||
|
||||
### Realtime Vehicles
|
||||
|
||||
```http
|
||||
|
||||
Reference in New Issue
Block a user