feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -52,6 +52,258 @@ func (h *Handler) routes() {
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/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("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("POST /api/v2/exports", h.handleCreateHistoryExport)
h.mux.HandleFunc("GET /api/v2/exports", h.handleListHistoryExports)
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("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("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/notifications", h.handleAlertNotifications)
h.mux.HandleFunc("POST /api/v2/alerts/notifications/read", h.handleAlertNotificationsRead)
}
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) 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) 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) 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) 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, 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) 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(request)
h.write(w, r, data, err)
}
func (h *Handler) handleListHistoryExports(w http.ResponseWriter, r *http.Request) {
h.write(w, r, h.service.ListHistoryExports(), nil)
}
func (h *Handler) handleDownloadHistoryExport(w http.ResponseWriter, r *http.Request) {
path, name, err := h.service.HistoryExportFile(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"`)
w.Header().Set("X-Export-Name", 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) handleDashboardSummary(w http.ResponseWriter, r *http.Request) {
@@ -214,7 +466,14 @@ func (h *Handler) handleSourceReadiness(w http.ResponseWriter, r *http.Request)
func (h *Handler) write(w http.ResponseWriter, r *http.Request, data any, err error) {
if err != nil {
if clientErr, ok := asClientError(err); ok {
httpx.WriteError(w, http.StatusBadRequest, clientErr.Code, clientErr.Message, "", traceID(r))
status := http.StatusBadRequest
switch {
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))