feat(platform): add vehicle detail aggregate api
This commit is contained in:
@@ -27,6 +27,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) routes() {
|
||||
h.mux.HandleFunc("GET /api/dashboard/summary", h.handleDashboardSummary)
|
||||
h.mux.HandleFunc("GET /api/vehicles", h.handleVehicles)
|
||||
h.mux.HandleFunc("GET /api/vehicles/detail", h.handleVehicleDetail)
|
||||
h.mux.HandleFunc("GET /api/realtime/locations", h.handleRealtimeLocations)
|
||||
h.mux.HandleFunc("GET /api/history/locations", h.handleHistoryLocations)
|
||||
h.mux.HandleFunc("GET /api/history/raw-frames", h.handleRawFramesGet)
|
||||
@@ -46,6 +47,16 @@ func (h *Handler) handleVehicles(w http.ResponseWriter, r *http.Request) {
|
||||
h.write(w, r, data, err)
|
||||
}
|
||||
|
||||
func (h *Handler) handleVehicleDetail(w http.ResponseWriter, r *http.Request) {
|
||||
vin := strings.TrimSpace(r.URL.Query().Get("vin"))
|
||||
if vin == "" {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "VIN_REQUIRED", "VIN 不能为空", "", traceID(r))
|
||||
return
|
||||
}
|
||||
data, err := h.service.VehicleDetail(r.Context(), vin, r.URL.Query().Get("protocol"))
|
||||
h.write(w, r, data, err)
|
||||
}
|
||||
|
||||
func (h *Handler) handleRealtimeLocations(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := h.service.RealtimeLocations(r.Context(), r.URL.Query())
|
||||
h.write(w, r, data, err)
|
||||
|
||||
@@ -33,6 +33,21 @@ func TestHandlerVehicles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleDetail(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/vehicles/detail?vin=LB9A32A24R0LS1426", 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{"identity", "realtime", "history", "raw", "mileage", "sources"} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Fatalf("response missing %q: %s", want, rec.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRealtimeLocations(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -41,6 +41,16 @@ type VehicleRow struct {
|
||||
BindingScore int `json:"bindingScore"`
|
||||
}
|
||||
|
||||
type VehicleDetail struct {
|
||||
VIN string `json:"vin"`
|
||||
Identity *VehicleRow `json:"identity,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
Realtime []RealtimeLocationRow `json:"realtime"`
|
||||
History Page[HistoryLocationRow] `json:"history"`
|
||||
Raw Page[RawFrameRow] `json:"raw"`
|
||||
Mileage Page[DailyMileageRow] `json:"mileage"`
|
||||
}
|
||||
|
||||
type RealtimeLocationRow struct {
|
||||
VIN string `json:"vin"`
|
||||
Plate string `json:"plate"`
|
||||
|
||||
@@ -3,6 +3,8 @@ package platform
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
@@ -44,6 +46,60 @@ func (s *Service) Vehicles(ctx context.Context, query url.Values) (Page[VehicleR
|
||||
return s.store.Vehicles(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string) (VehicleDetail, error) {
|
||||
vin = strings.TrimSpace(vin)
|
||||
protocol = strings.TrimSpace(protocol)
|
||||
vehicleQuery := url.Values{"keyword": {vin}, "limit": {"10"}}
|
||||
realtimeQuery := url.Values{"vin": {vin}, "limit": {"20"}}
|
||||
historyQuery := url.Values{"vin": {vin}, "limit": {"20"}}
|
||||
rawQuery := RawFrameQuery{VIN: vin, IncludeFields: true, Limit: 10}
|
||||
mileageQuery := url.Values{"vin": {vin}, "limit": {"20"}}
|
||||
if protocol != "" {
|
||||
realtimeQuery.Set("protocol", protocol)
|
||||
historyQuery.Set("protocol", protocol)
|
||||
rawQuery.Protocol = protocol
|
||||
}
|
||||
vehicles, err := s.store.Vehicles(ctx, vehicleQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
realtime, err := s.store.RealtimeLocations(ctx, realtimeQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
history, err := s.store.HistoryLocationsFromTDengine(ctx, historyQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
raw, err := s.RawFrames(ctx, rawQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
mileage, err := s.store.DailyMileage(ctx, mileageQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
var identity *VehicleRow
|
||||
for index := range vehicles.Items {
|
||||
if strings.EqualFold(vehicles.Items[index].VIN, vin) {
|
||||
identity = &vehicles.Items[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
if identity == nil && len(vehicles.Items) > 0 {
|
||||
identity = &vehicles.Items[0]
|
||||
}
|
||||
return VehicleDetail{
|
||||
VIN: vin,
|
||||
Identity: identity,
|
||||
Sources: vehicleSources(vehicles.Items, realtime.Items, raw.Items, mileage.Items),
|
||||
Realtime: realtime.Items,
|
||||
History: history,
|
||||
Raw: raw,
|
||||
Mileage: mileage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
|
||||
return s.store.RealtimeLocations(ctx, query)
|
||||
}
|
||||
@@ -70,3 +126,32 @@ func (s *Service) QualityIssues(ctx context.Context, query url.Values) (Page[Qua
|
||||
func (s *Service) OpsHealth(ctx context.Context) (OpsHealth, error) {
|
||||
return s.store.OpsHealth(ctx)
|
||||
}
|
||||
|
||||
func vehicleSources(vehicles []VehicleRow, realtime []RealtimeLocationRow, raw []RawFrameRow, mileage []DailyMileageRow) []string {
|
||||
seen := map[string]struct{}{}
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
}
|
||||
for _, row := range vehicles {
|
||||
add(row.Protocol)
|
||||
}
|
||||
for _, row := range realtime {
|
||||
add(row.Protocol)
|
||||
}
|
||||
for _, row := range raw {
|
||||
add(row.Protocol)
|
||||
}
|
||||
for _, row := range mileage {
|
||||
add(row.Source)
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for source := range seen {
|
||||
out = append(out, source)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user