package platform import ( "encoding/json" "net/http" "strings" "time" "lingniu/vehicle-data-platform/apps/api/internal/httpx" ) type Handler struct { service *Service mux *http.ServeMux } func NewHandler(service *Service) *Handler { h := &Handler{service: service, mux: http.NewServeMux()} h.routes() return h } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } 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) h.mux.HandleFunc("POST /api/history/raw-frames/query", h.handleRawFramesPost) h.mux.HandleFunc("GET /api/mileage/daily", h.handleDailyMileage) h.mux.HandleFunc("GET /api/quality/issues", h.handleQualityIssues) h.mux.HandleFunc("GET /api/ops/health", h.handleOpsHealth) } func (h *Handler) handleDashboardSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.DashboardSummary(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleVehicles(w http.ResponseWriter, r *http.Request) { data, err := h.service.Vehicles(r.Context(), r.URL.Query()) 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) } func (h *Handler) handleHistoryLocations(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistoryLocations(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleRawFramesGet(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() query := RawFrameQuery{ Protocol: q.Get("protocol"), VIN: q.Get("vin"), DateFrom: q.Get("dateFrom"), DateTo: q.Get("dateTo"), Fields: splitCSV(q.Get("fields")), IncludeFields: q.Get("includeFields") == "true", Limit: parsePositive(q.Get("limit"), 100), Offset: parsePositive(q.Get("offset"), 0), } data, err := h.service.RawFrames(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleRawFramesPost(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var query RawFrameQuery if err := json.NewDecoder(r.Body).Decode(&query); err != nil { httpx.WriteError(w, http.StatusBadRequest, "BAD_JSON", "请求 JSON 解析失败", err.Error(), traceID(r)) return } data, err := h.service.RawFrames(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleDailyMileage(w http.ResponseWriter, r *http.Request) { data, err := h.service.DailyMileage(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleQualityIssues(w http.ResponseWriter, r *http.Request) { data, err := h.service.QualityIssues(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleOpsHealth(w http.ResponseWriter, r *http.Request) { data, err := h.service.OpsHealth(r.Context()) h.write(w, r, data, err) } func (h *Handler) write(w http.ResponseWriter, r *http.Request, data any, err error) { if err != nil { httpx.WriteError(w, http.StatusInternalServerError, "INTERNAL", "服务处理失败", err.Error(), traceID(r)) return } httpx.WriteOK(w, traceID(r), data) } func traceID(r *http.Request) string { if value := r.Header.Get("X-Trace-Id"); value != "" { return value } return "trace-" + time.Now().Format("20060102150405.000000") } func splitCSV(value string) []string { if strings.TrimSpace(value) == "" { return nil } parts := strings.Split(value, ",") out := make([]string, 0, len(parts)) for _, part := range parts { if trimmed := strings.TrimSpace(part); trimmed != "" { out = append(out, trimmed) } } return out }