package platform import ( "encoding/json" "net/http" "net/url" "strconv" "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/resolve", h.handleVehicleResolve) h.mux.HandleFunc("GET /api/vehicles/coverage", h.handleVehicleCoverage) h.mux.HandleFunc("GET /api/vehicles/coverage/summary", h.handleVehicleCoverageSummary) h.mux.HandleFunc("GET /api/vehicles/business-filters", h.handleVehicleBusinessFilters) h.mux.HandleFunc("GET /api/vehicle-service", h.handleVehicleDetail) h.mux.HandleFunc("GET /api/vehicle-service/summary", h.handleVehicleServiceSummary) h.mux.HandleFunc("GET /api/vehicle-service/overview", h.handleVehicleServiceOverview) h.mux.HandleFunc("POST /api/vehicle-service/overviews", h.handleVehicleServiceOverviews) 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) 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/summary", h.handleMileageSummary) h.mux.HandleFunc("GET /api/mileage/daily", h.handleDailyMileage) h.mux.HandleFunc("POST /api/mileage/daily", h.handleDailyMileagePost) h.mux.HandleFunc("GET /api/v2/statistics/mileage", h.handleMileageStatistics) h.mux.HandleFunc("POST /api/v2/statistics/mileage", h.handleMileageStatisticsPost) h.mux.HandleFunc("GET /api/statistics/online-summary", h.handleOnlineStatisticsSummary) h.mux.HandleFunc("GET /api/statistics/online-vehicles", h.handleOnlineVehicleStatuses) h.mux.HandleFunc("GET /api/quality/summary", h.handleQualitySummary) h.mux.HandleFunc("GET /api/quality/issues", h.handleQualityIssues) h.mux.HandleFunc("GET /api/quality/notification-plan", h.handleQualityNotificationPlan) h.mux.HandleFunc("GET /api/alert-events/summary", h.handleQualitySummary) h.mux.HandleFunc("GET /api/alert-events", h.handleQualityIssues) h.mux.HandleFunc("GET /api/alert-events/notification-plan", h.handleQualityNotificationPlan) h.mux.HandleFunc("GET /api/ops/health", h.handleOpsHealth) h.mux.HandleFunc("GET /api/ops/source-readiness", h.handleSourceReadiness) h.mux.HandleFunc("GET /api/v2/monitor/summary", h.handleMonitorSummary) h.mux.HandleFunc("GET /api/v2/monitor/map", h.handleMonitorMap) h.mux.HandleFunc("GET /api/v2/monitor/workspace", h.handleMonitorWorkspace) h.mux.HandleFunc("GET /api/v2/vehicles/{vin}/profile", h.handleVehicleProfile) h.mux.HandleFunc("PUT /api/v2/vehicles/{vin}/profile", h.handleSaveVehicleProfile) h.mux.HandleFunc("GET /api/v2/vehicles/{vin}/telemetry/latest", h.handleLatestTelemetry) h.mux.HandleFunc("GET /api/v2/vehicles/{vin}/source-evidence", h.handleVehicleSourceEvidence) h.mux.HandleFunc("GET /api/v2/operations/vehicles/{vin}/sources", h.handleVehicleSourceDiagnostic) h.mux.HandleFunc("PUT /api/v2/operations/vehicles/{vin}/sources/{sourceRef}", h.handleUpdateVehicleSourcePolicy) h.mux.HandleFunc("GET /api/v2/reconciliation/summary", h.handleReconciliationSummary) h.mux.HandleFunc("GET /api/v2/reconciliation/assignees", h.handleReconciliationAssignees) h.mux.HandleFunc("POST /api/v2/reconciliation/issues", h.handleReconciliationIssues) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/export", h.handleReconciliationIssuesExport) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/batch-actions", h.handleReconciliationBatchAction) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/batch-assignments", h.handleReconciliationBatchAssignment) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/batch-archive", h.handleReconciliationBatchArchive) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/batch-restore", h.handleReconciliationBatchRestore) h.mux.HandleFunc("GET /api/v2/reconciliation/issues/{id}", h.handleReconciliationIssue) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/{id}/actions", h.handleReconciliationAction) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/{id}/assignment", h.handleReconciliationAssignment) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/{id}/archive", h.handleReconciliationArchive) h.mux.HandleFunc("POST /api/v2/reconciliation/issues/{id}/restore", h.handleReconciliationRestore) h.mux.HandleFunc("POST /api/v2/vehicle-profiles/sync", h.handleSyncVehicleProfiles) h.mux.HandleFunc("GET /api/v2/tracks", h.handleTrackPlayback) h.mux.HandleFunc("GET /api/v2/metrics", h.handleMetricCatalog) h.mux.HandleFunc("GET /api/v2/history/metrics", h.handleHistoryMetricCatalog) h.mux.HandleFunc("GET /api/v2/history/query", h.handleHistoryData) h.mux.HandleFunc("GET /api/v2/history/series", h.handleHistorySeries) h.mux.HandleFunc("GET /api/v2/history/preferences", h.handleHistoryPreferences) h.mux.HandleFunc("PUT /api/v2/history/preferences", h.handleUpdateHistoryPreferences) h.mux.HandleFunc("POST /api/v2/exports", h.handleCreateHistoryExport) h.mux.HandleFunc("GET /api/v2/exports", h.handleListHistoryExports) h.mux.HandleFunc("GET /api/v2/exports/page", h.handleListHistoryExportsPage) h.mux.HandleFunc("GET /api/v2/exports/cleanup/preview", h.handlePreviewHistoryExportCleanup) h.mux.HandleFunc("GET /api/v2/exports/cleanup/audit", h.handleHistoryExportCleanupAudit) h.mux.HandleFunc("POST /api/v2/exports/cleanup", h.handleCleanupHistoryExports) h.mux.HandleFunc("GET /api/v2/exports/cleanup/automation", h.handleHistoryExportCleanupAutomation) h.mux.HandleFunc("PUT /api/v2/exports/cleanup/automation", h.handleUpdateHistoryExportCleanupAutomation) h.mux.HandleFunc("POST /api/v2/exports/cleanup/automation/{id}/approve", h.handleApproveHistoryExportCleanupAutomation) h.mux.HandleFunc("POST /api/v2/exports/cleanup/automation/{id}/reject", h.handleRejectHistoryExportCleanupAutomation) h.mux.HandleFunc("POST /api/v2/exports/cleanup/automation/{id}/retry", h.handleRetryHistoryExportCleanupAutomation) h.mux.HandleFunc("POST /api/v2/exports/batch", h.handleBatchHistoryExports) h.mux.HandleFunc("POST /api/v2/exports/{id}/cancel", h.handleCancelHistoryExport) h.mux.HandleFunc("POST /api/v2/exports/{id}/rebuild", h.handleRebuildHistoryExport) h.mux.HandleFunc("POST /api/v2/exports/{id}/archive", h.handleArchiveHistoryExport) h.mux.HandleFunc("POST /api/v2/exports/{id}/restore", h.handleRestoreHistoryExport) h.mux.HandleFunc("PUT /api/v2/exports/{id}/cleanup-protection", h.handleHistoryExportCleanupProtection) h.mux.HandleFunc("GET /api/v2/exports/{id}/download", h.handleDownloadHistoryExport) h.mux.HandleFunc("POST /api/v2/access/summary", h.handleAccessSummary) h.mux.HandleFunc("POST /api/v2/access/vehicles", h.handleAccessVehicles) h.mux.HandleFunc("POST /api/v2/access/unresolved-identities", h.handleAccessUnresolvedIdentities) h.mux.HandleFunc("POST /api/v2/access/unresolved-identities/{id}/claim", h.handleClaimAccessIdentity) h.mux.HandleFunc("GET /api/v2/access/thresholds", h.handleAccessThresholds) h.mux.HandleFunc("PUT /api/v2/access/thresholds", h.handleUpdateAccessThresholds) h.mux.HandleFunc("POST /api/v2/alerts/summary", h.handleAlertSummary) h.mux.HandleFunc("POST /api/v2/alerts/events", h.handleAlertEvents) h.mux.HandleFunc("GET /api/v2/alerts/events/{id}", h.handleAlertEvent) h.mux.HandleFunc("POST /api/v2/alerts/events/{id}/actions", h.handleAlertAction) h.mux.HandleFunc("GET /api/v2/alerts/rules", h.handleAlertRules) h.mux.HandleFunc("GET /api/v2/alerts/rules/library", h.handleAlertRuleLibrary) h.mux.HandleFunc("POST /api/v2/alerts/rules", h.handleSaveAlertRule) h.mux.HandleFunc("PUT /api/v2/alerts/rules/{id}", h.handleSaveAlertRule) h.mux.HandleFunc("PUT /api/v2/alerts/rules/{id}/enabled", h.handleAlertRuleEnabled) h.mux.HandleFunc("GET /api/v2/alerts/rules/{id}/revisions", h.handleAlertRuleRevisions) h.mux.HandleFunc("POST /api/v2/alerts/rules/{id}/rollback", h.handleAlertRuleRollback) h.mux.HandleFunc("POST /api/v2/alerts/rules/{id}/archive", h.handleArchiveAlertRule) h.mux.HandleFunc("POST /api/v2/alerts/rules/{id}/restore", h.handleRestoreAlertRule) h.mux.HandleFunc("GET /api/v2/alerts/notification-config", h.handleAlertNotificationConfig) h.mux.HandleFunc("GET /api/v2/alerts/notifications/health", h.handleAlertNotificationDeliveryHealth) h.mux.HandleFunc("GET /api/v2/alerts/notifications", h.handleAlertNotifications) h.mux.HandleFunc("POST /api/v2/alerts/notifications/read", h.handleAlertNotificationsRead) h.mux.HandleFunc("POST /api/v2/alerts/notifications/{id}/retry", h.handleAlertNotificationRetry) h.mux.HandleFunc("GET /api/v2/alerts/notifications/{id}/retry-audit", h.handleAlertNotificationRetryAudit) } func (h *Handler) handleAlertNotificationConfig(w http.ResponseWriter, r *http.Request) { data, err := h.service.AlertNotificationConfig(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleAlertNotificationDeliveryHealth(w http.ResponseWriter, r *http.Request) { data, err := h.service.AlertNotificationDeliveryHealth(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleReconciliationSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.ReconciliationSummary(r.Context(), parsePositive(r.URL.Query().Get("days"), 30)) h.write(w, r, data, err) } func (h *Handler) handleReconciliationIssues(w http.ResponseWriter, r *http.Request) { var query ReconciliationQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.ReconciliationIssues(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleReconciliationAssignees(w http.ResponseWriter, r *http.Request) { data, err := h.service.ReconciliationAssignees(r.Context(), r.URL.Query().Get("search")) h.write(w, r, data, err) } func (h *Handler) handleReconciliationIssuesExport(w http.ResponseWriter, r *http.Request) { var query ReconciliationQuery if !decodeJSONBody(w, r, &query) { return } file, err := h.service.ExportReconciliationIssues(r.Context(), query) if err != nil { h.write(w, r, nil, err) return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="quality-issues.csv"; filename*=UTF-8''`+url.PathEscape(file.Name)) w.Header().Set("X-Export-Name", url.PathEscape(file.Name)) w.Header().Set("X-Export-Count", strconv.Itoa(file.RowCount)) w.WriteHeader(http.StatusOK) _, _ = w.Write(file.Content) } func (h *Handler) handleReconciliationIssue(w http.ResponseWriter, r *http.Request) { data, err := h.service.ReconciliationIssue(r.Context(), r.PathValue("id")) h.write(w, r, data, err) } func (h *Handler) handleReconciliationAction(w http.ResponseWriter, r *http.Request) { var request ReconciliationActionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.UpdateReconciliationIssue(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleReconciliationAssignment(w http.ResponseWriter, r *http.Request) { var request ReconciliationAssignmentRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.AssignReconciliationIssue(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleReconciliationBatchAssignment(w http.ResponseWriter, r *http.Request) { var request ReconciliationBatchAssignmentRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.BatchAssignReconciliationIssues(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleReconciliationBatchAction(w http.ResponseWriter, r *http.Request) { var request ReconciliationBatchActionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.BatchUpdateReconciliationIssues(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleReconciliationArchive(w http.ResponseWriter, r *http.Request) { h.handleReconciliationLifecycle(w, r, true) } func (h *Handler) handleReconciliationRestore(w http.ResponseWriter, r *http.Request) { h.handleReconciliationLifecycle(w, r, false) } func (h *Handler) handleReconciliationLifecycle(w http.ResponseWriter, r *http.Request, archived bool) { var request ReconciliationLifecycleRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.SetReconciliationIssueArchived(r.Context(), r.PathValue("id"), archived, request) h.write(w, r, data, err) } func (h *Handler) handleReconciliationBatchArchive(w http.ResponseWriter, r *http.Request) { h.handleReconciliationBatchLifecycle(w, r, true) } func (h *Handler) handleReconciliationBatchRestore(w http.ResponseWriter, r *http.Request) { h.handleReconciliationBatchLifecycle(w, r, false) } func (h *Handler) handleReconciliationBatchLifecycle(w http.ResponseWriter, r *http.Request, archived bool) { var request ReconciliationBatchLifecycleRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.BatchSetReconciliationIssuesArchived(r.Context(), archived, request) h.write(w, r, data, err) } func (h *Handler) handleAccessUnresolvedIdentities(w http.ResponseWriter, r *http.Request) { var query AccessUnresolvedIdentityQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.AccessUnresolvedIdentities(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleClaimAccessIdentity(w http.ResponseWriter, r *http.Request) { var input AccessIdentityClaimInput if !decodeJSONBody(w, r, &input) { return } input.Actor = ActorFromContext(r.Context()) data, err := h.service.ClaimAccessIdentity(r.Context(), r.PathValue("id"), input) h.write(w, r, data, err) } func (h *Handler) handleVehicleProfile(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleProfile(r.Context(), r.PathValue("vin")) h.write(w, r, data, err) } func (h *Handler) handleLatestTelemetry(w http.ResponseWriter, r *http.Request) { data, err := h.service.LatestTelemetry(r.Context(), r.PathValue("vin")) h.write(w, r, data, err) } func (h *Handler) handleVehicleSourceEvidence(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleSourceEvidence(r.Context(), r.PathValue("vin"), r.URL.Query().Get("date")) h.write(w, r, data, err) } func (h *Handler) handleVehicleSourceDiagnostic(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleSourceDiagnostic(r.Context(), r.PathValue("vin")) h.write(w, r, data, err) } func (h *Handler) handleUpdateVehicleSourcePolicy(w http.ResponseWriter, r *http.Request) { var update VehicleSourcePolicyUpdate if !decodeJSONBody(w, r, &update) { return } update.SourceRef = r.PathValue("sourceRef") update.Actor = ActorFromContext(r.Context()) data, err := h.service.UpdateVehicleSourcePolicy(r.Context(), r.PathValue("vin"), update) h.write(w, r, data, err) } func (h *Handler) handleSaveVehicleProfile(w http.ResponseWriter, r *http.Request) { var input VehicleProfileInput if !decodeJSONBody(w, r, &input) { return } input.Actor = ActorFromContext(r.Context()) data, err := h.service.SaveVehicleProfile(r.Context(), r.PathValue("vin"), input) h.write(w, r, data, err) } func (h *Handler) handleSyncVehicleProfiles(w http.ResponseWriter, r *http.Request) { var request VehicleProfileSyncRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) data, err := h.service.SyncVehicleProfiles(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleMetricCatalog(w http.ResponseWriter, r *http.Request) { data, err := h.service.MetricCatalog(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleAlertSummary(w http.ResponseWriter, r *http.Request) { var query AlertQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.AlertSummary(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleAlertEvents(w http.ResponseWriter, r *http.Request) { var query AlertQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.AlertEvents(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleAlertEvent(w http.ResponseWriter, r *http.Request) { data, err := h.service.AlertEvent(r.Context(), r.PathValue("id")) h.write(w, r, data, err) } func (h *Handler) handleAlertAction(w http.ResponseWriter, r *http.Request) { var request AlertActionRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) data, err := h.service.ActOnAlert(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleAlertRules(w http.ResponseWriter, r *http.Request) { data, err := h.service.AlertRules(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleAlertRuleLibrary(w http.ResponseWriter, r *http.Request) { query := AlertRuleQuery{ Keyword: r.URL.Query().Get("keyword"), Status: r.URL.Query().Get("status"), Protocol: r.URL.Query().Get("protocol"), Lifecycle: r.URL.Query().Get("lifecycle"), Limit: parsePositive(r.URL.Query().Get("limit"), 10), Offset: parsePositive(r.URL.Query().Get("offset"), 0), } data, err := h.service.AlertRulePage(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleSaveAlertRule(w http.ResponseWriter, r *http.Request) { var input AlertRuleInput if !decodeJSONBody(w, r, &input) { return } if pathID := r.PathValue("id"); pathID != "" { input.ID = pathID } input.Actor = ActorFromContext(r.Context()) data, err := h.service.SaveAlertRule(r.Context(), input) h.write(w, r, data, err) } func (h *Handler) handleAlertRuleEnabled(w http.ResponseWriter, r *http.Request) { var update AlertRuleEnabledUpdate if !decodeJSONBody(w, r, &update) { return } update.Actor = ActorFromContext(r.Context()) data, err := h.service.SetAlertRuleEnabled(r.Context(), r.PathValue("id"), update) h.write(w, r, data, err) } func (h *Handler) handleAlertRuleRevisions(w http.ResponseWriter, r *http.Request) { data, err := h.service.AlertRuleRevisions(r.Context(), r.PathValue("id")) h.write(w, r, data, err) } func (h *Handler) handleAlertRuleRollback(w http.ResponseWriter, r *http.Request) { var request AlertRuleRollbackRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) data, err := h.service.RollbackAlertRule(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleArchiveAlertRule(w http.ResponseWriter, r *http.Request) { var request AlertRuleLifecycleRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) data, err := h.service.SetAlertRuleArchived(r.Context(), r.PathValue("id"), true, request) h.write(w, r, data, err) } func (h *Handler) handleRestoreAlertRule(w http.ResponseWriter, r *http.Request) { var request AlertRuleLifecycleRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) data, err := h.service.SetAlertRuleArchived(r.Context(), r.PathValue("id"), false, request) h.write(w, r, data, err) } func (h *Handler) handleAlertNotifications(w http.ResponseWriter, r *http.Request) { limit := parsePositive(r.URL.Query().Get("limit"), 20) offset := parsePositive(r.URL.Query().Get("offset"), 0) unread := strings.EqualFold(r.URL.Query().Get("unreadOnly"), "true") || r.URL.Query().Get("unreadOnly") == "1" data, err := h.service.AlertNotifications(r.Context(), AlertNotificationQuery{ UnreadOnly: unread, Search: strings.TrimSpace(r.URL.Query().Get("search")), DeliveryStatus: strings.TrimSpace(r.URL.Query().Get("deliveryStatus")), Limit: limit, Offset: offset, }) h.write(w, r, data, err) } func (h *Handler) handleAlertNotificationsRead(w http.ResponseWriter, r *http.Request) { var request AlertNotificationReadRequest if !decodeJSONBody(w, r, &request) { return } request.Actor = ActorFromContext(r.Context()) count, err := h.service.MarkAlertNotificationsRead(r.Context(), request) h.write(w, r, map[string]int{"updated": count}, err) } func (h *Handler) handleAlertNotificationRetry(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(strings.TrimSpace(r.PathValue("id")), 10, 64) if err != nil || id <= 0 { h.write(w, r, nil, clientError{Code: "ALERT_NOTIFICATION_ID_INVALID", Message: "通知记录 ID 无效"}) return } var request AlertNotificationRetryRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.RetryAlertNotification(r.Context(), id, request) h.write(w, r, data, err) } func (h *Handler) handleAlertNotificationRetryAudit(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(strings.TrimSpace(r.PathValue("id")), 10, 64) if err != nil || id <= 0 { h.write(w, r, nil, clientError{Code: "ALERT_NOTIFICATION_ID_INVALID", Message: "通知记录 ID 无效"}) return } data, err := h.service.AlertNotificationRetryAudits(r.Context(), id) h.write(w, r, data, err) } func (h *Handler) handleAccessSummary(w http.ResponseWriter, r *http.Request) { var query AccessQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.AccessSummary(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleAccessVehicles(w http.ResponseWriter, r *http.Request) { var query AccessQuery if !decodeJSONBody(w, r, &query) { return } data, err := h.service.AccessVehicles(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleAccessThresholds(w http.ResponseWriter, r *http.Request) { data, err := h.service.AccessThresholds(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleUpdateAccessThresholds(w http.ResponseWriter, r *http.Request) { var update AccessThresholdUpdate if !decodeJSONBody(w, r, &update) { return } update.Actor = ActorFromContext(r.Context()) data, err := h.service.UpdateAccessThresholds(r.Context(), update) h.write(w, r, data, err) } func decodeJSONBody(w http.ResponseWriter, r *http.Request, target any) bool { defer r.Body.Close() decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { httpx.WriteError(w, http.StatusBadRequest, "BAD_JSON", "请求 JSON 解析失败", err.Error(), traceID(r)) return false } return true } func (h *Handler) handleCreateHistoryExport(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var request HistoryExportRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { httpx.WriteError(w, http.StatusBadRequest, "BAD_JSON", "请求 JSON 解析失败", err.Error(), traceID(r)) return } data, err := h.service.CreateHistoryExport(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleListHistoryExports(w http.ResponseWriter, r *http.Request) { h.write(w, r, h.service.ListHistoryExports(r.Context()), nil) } func (h *Handler) handleListHistoryExportsPage(w http.ResponseWriter, r *http.Request) { limit := parsePositive(r.URL.Query().Get("limit"), 10) offset := parsePositive(r.URL.Query().Get("offset"), 0) h.write(w, r, h.service.ListHistoryExportsPage(r.Context(), HistoryExportQuery{ Search: r.URL.Query().Get("search"), Status: r.URL.Query().Get("status"), Scope: r.URL.Query().Get("scope"), OwnerScope: r.URL.Query().Get("ownerScope"), Sort: r.URL.Query().Get("sort"), Limit: limit, Offset: offset, }), nil) } func (h *Handler) handlePreviewHistoryExportCleanup(w http.ResponseWriter, r *http.Request) { data, err := h.service.PreviewHistoryExportCleanup(r.Context(), HistoryExportCleanupQuery{ OlderThanDays: parsePositive(r.URL.Query().Get("olderThanDays"), 180), OwnerScope: r.URL.Query().Get("ownerScope"), }) h.write(w, r, data, err) } func (h *Handler) handleHistoryExportCleanupAudit(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistoryExportCleanupAudit(r.Context(), parsePositive(r.URL.Query().Get("limit"), 20)) h.write(w, r, data, err) } func (h *Handler) handleCleanupHistoryExports(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.CleanupHistoryExports(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleHistoryExportCleanupAutomation(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistoryExportCleanupAutomation(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleUpdateHistoryExportCleanupAutomation(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupAutomationPolicyRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.UpdateHistoryExportCleanupAutomation(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleApproveHistoryExportCleanupAutomation(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupAutomationActionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.ApproveHistoryExportCleanupAutomation(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleRejectHistoryExportCleanupAutomation(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupAutomationActionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.RejectHistoryExportCleanupAutomation(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleRetryHistoryExportCleanupAutomation(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupAutomationActionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.RetryHistoryExportCleanupAutomation(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleHistoryPreferences(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistoryPreferences(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleUpdateHistoryPreferences(w http.ResponseWriter, r *http.Request) { var input HistoryPreferences if !decodeJSONBody(w, r, &input) { return } data, err := h.service.UpdateHistoryPreferences(r.Context(), input) h.write(w, r, data, err) } func (h *Handler) handleCancelHistoryExport(w http.ResponseWriter, r *http.Request) { data, err := h.service.CancelHistoryExport(r.Context(), r.PathValue("id")) h.write(w, r, data, err) } func (h *Handler) handleRebuildHistoryExport(w http.ResponseWriter, r *http.Request) { data, err := h.service.RebuildHistoryExport(r.Context(), r.PathValue("id")) h.write(w, r, data, err) } func (h *Handler) handleArchiveHistoryExport(w http.ResponseWriter, r *http.Request) { data, err := h.service.SetHistoryExportArchived(r.Context(), r.PathValue("id"), true) h.write(w, r, data, err) } func (h *Handler) handleRestoreHistoryExport(w http.ResponseWriter, r *http.Request) { data, err := h.service.SetHistoryExportArchived(r.Context(), r.PathValue("id"), false) h.write(w, r, data, err) } func (h *Handler) handleHistoryExportCleanupProtection(w http.ResponseWriter, r *http.Request) { var request HistoryExportCleanupProtectionRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.SetHistoryExportCleanupProtection(r.Context(), r.PathValue("id"), request) h.write(w, r, data, err) } func (h *Handler) handleBatchHistoryExports(w http.ResponseWriter, r *http.Request) { var request HistoryExportBatchRequest if !decodeJSONBody(w, r, &request) { return } data, err := h.service.BatchHistoryExports(r.Context(), request) h.write(w, r, data, err) } func (h *Handler) handleDownloadHistoryExport(w http.ResponseWriter, r *http.Request) { path, name, err := h.service.HistoryExportFile(r.Context(), r.PathValue("id")) if err != nil { h.write(w, r, nil, err) return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="history-export.csv"; filename*=UTF-8''`+url.PathEscape(name)) w.Header().Set("X-Export-Name", url.PathEscape(name)) http.ServeFile(w, r, path) } func (h *Handler) handleHistoryMetricCatalog(w http.ResponseWriter, r *http.Request) { h.write(w, r, h.service.HistoryMetricCatalog(), nil) } func (h *Handler) handleHistoryData(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistoryData(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleHistorySeries(w http.ResponseWriter, r *http.Request) { data, err := h.service.HistorySeries(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleTrackPlayback(w http.ResponseWriter, r *http.Request) { data, err := h.service.TrackPlayback(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleMonitorSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.MonitorSummary(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleMonitorMap(w http.ResponseWriter, r *http.Request) { data, err := h.service.MonitorMap(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleMonitorWorkspace(w http.ResponseWriter, r *http.Request) { data, err := h.service.MonitorWorkspace(r.Context(), r.URL.Query()) h.write(w, r, data, err) } 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) handleVehicleResolve(w http.ResponseWriter, r *http.Request) { keyword := firstNonEmpty(r.URL.Query().Get("keyword"), r.URL.Query().Get("vin")) keyword = strings.TrimSpace(keyword) if keyword == "" { httpx.WriteError(w, http.StatusBadRequest, "VEHICLE_KEY_REQUIRED", "车辆关键词不能为空", "", traceID(r)) return } data, err := h.service.ResolveVehicleIdentity(r.Context(), keyword, r.URL.Query().Get("protocol")) h.write(w, r, data, err) } func (h *Handler) handleVehicleCoverage(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleCoverage(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleVehicleCoverageSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleCoverageSummary(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleVehicleBusinessFilters(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleBusinessFilters(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleVehicleServiceSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleServiceSummary(r.Context()) h.write(w, r, data, err) } func (h *Handler) handleVehicleDetail(w http.ResponseWriter, r *http.Request) { keyword := firstNonEmpty(r.URL.Query().Get("keyword"), r.URL.Query().Get("vin")) keyword = strings.TrimSpace(keyword) if keyword == "" { httpx.WriteError(w, http.StatusBadRequest, "VEHICLE_KEY_REQUIRED", "车辆关键词不能为空", "", traceID(r)) return } data, err := h.service.VehicleDetail(r.Context(), keyword, r.URL.Query().Get("protocol")) h.write(w, r, data, err) } func (h *Handler) handleVehicleServiceOverview(w http.ResponseWriter, r *http.Request) { keyword := firstNonEmpty(r.URL.Query().Get("keyword"), r.URL.Query().Get("vin")) keyword = strings.TrimSpace(keyword) if keyword == "" { httpx.WriteError(w, http.StatusBadRequest, "VEHICLE_KEY_REQUIRED", "车辆关键词不能为空", "", traceID(r)) return } data, err := h.service.VehicleServiceOverview(r.Context(), keyword, r.URL.Query().Get("protocol")) h.write(w, r, data, err) } func (h *Handler) handleVehicleServiceOverviews(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var query VehicleOverviewBatchQuery 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.VehicleServiceOverviews(r.Context(), query) h.write(w, r, data, err) } func (h *Handler) handleVehicleRealtime(w http.ResponseWriter, r *http.Request) { data, err := h.service.VehicleRealtime(r.Context(), r.URL.Query()) 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: firstNonEmpty(q.Get("vin"), q.Get("keyword")), Keyword: q.Get("keyword"), 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) handleDailyMileagePost(w http.ResponseWriter, r *http.Request) { query, ok := decodeMileageQuery(w, r) if !ok { return } data, err := h.service.DailyMileage(r.Context(), mileageQueryValues(query)) h.write(w, r, data, err) } func (h *Handler) handleMileageSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.MileageSummary(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleMileageStatistics(w http.ResponseWriter, r *http.Request) { data, err := h.service.MileageStatistics(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleMileageStatisticsPost(w http.ResponseWriter, r *http.Request) { query, ok := decodeMileageQuery(w, r) if !ok { return } data, err := h.service.MileageStatistics(r.Context(), mileageQueryValues(query)) h.write(w, r, data, err) } func decodeMileageQuery(w http.ResponseWriter, r *http.Request) (MileageQuery, bool) { defer r.Body.Close() var query MileageQuery decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&query); err != nil { httpx.WriteError(w, http.StatusBadRequest, "BAD_JSON", "里程查询 JSON 解析失败", err.Error(), traceID(r)) return MileageQuery{}, false } return query, true } func mileageQueryValues(query MileageQuery) url.Values { values := url.Values{} if query.DateFrom != "" { values.Set("dateFrom", query.DateFrom) } if query.DateTo != "" { values.Set("dateTo", query.DateTo) } if query.Keyword != "" { values.Set("keyword", query.Keyword) } if len(query.VINs) > 0 { values.Set("vins", strings.Join(query.VINs, ",")) } if query.VehicleScope != "" { values.Set("vehicleScope", query.VehicleScope) } if query.Protocol != "" { values.Set("protocol", query.Protocol) } if len(query.Protocols) > 0 { values.Set("protocols", strings.Join(query.Protocols, ",")) } if query.Deduplicate { values.Set("deduplicate", "1") } if query.Limit > 0 { values.Set("limit", strconv.Itoa(query.Limit)) } if query.Offset > 0 { values.Set("offset", strconv.Itoa(query.Offset)) } return values } func (h *Handler) handleOnlineStatisticsSummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.OnlineStatisticsSummary(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleOnlineVehicleStatuses(w http.ResponseWriter, r *http.Request) { data, err := h.service.OnlineVehicleStatuses(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) handleQualitySummary(w http.ResponseWriter, r *http.Request) { data, err := h.service.QualitySummary(r.Context(), r.URL.Query()) h.write(w, r, data, err) } func (h *Handler) handleQualityNotificationPlan(w http.ResponseWriter, r *http.Request) { data, err := h.service.QualityNotificationPlan(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) handleSourceReadiness(w http.ResponseWriter, r *http.Request) { data, err := h.service.SourceReadiness(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 { if clientErr, ok := asClientError(err); ok { status := http.StatusBadRequest switch { case clientErr.Code == "VEHICLE_PERMISSION_DENIED": status = http.StatusForbidden case strings.HasPrefix(clientErr.Code, "HISTORY_SCOPE_"), clientErr.Code == "HISTORY_BEFORE_AUTHORIZATION": status = http.StatusForbidden case strings.HasSuffix(clientErr.Code, "_NOT_FOUND"): status = http.StatusNotFound case strings.HasSuffix(clientErr.Code, "_CONFLICT"), clientErr.Code == "ALERT_ACTION_NOT_ALLOWED": status = http.StatusConflict } httpx.WriteError(w, status, clientErr.Code, clientErr.Message, "", traceID(r)) return } 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 []string{} } 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 }