Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/platform/handler.go
2026-07-27 16:46:15 +08:00

990 lines
38 KiB
Go

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
}