package realtime import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "github.com/redis/go-redis/v9" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) type Handler struct { repository *Repository } type OnlineIndexHandler struct { repository *Repository } type PipelineDebugHandler struct { repository *Repository } func NewHandler(repository *Repository) *Handler { if repository == nil { panic("realtime repository must not be nil") } return &Handler{repository: repository} } func NewOnlineIndexHandler(repository *Repository) *OnlineIndexHandler { if repository == nil { panic("online index repository must not be nil") } return &OnlineIndexHandler{repository: repository} } func NewPipelineDebugHandler(repository *Repository) *PipelineDebugHandler { if repository == nil { panic("pipeline debug repository must not be nil") } return &PipelineDebugHandler{repository: repository} } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } path := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/realtime/vehicles/"), "/") parts := strings.Split(path, "/") if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" { writeError(w, http.StatusNotFound, "vehicle not found") return } vin := parts[0] switch { case len(parts) == 1: snapshot, err := h.repository.GetMerged(r.Context(), vin) writeResult(w, snapshot, err) case len(parts) == 2 && parts[1] == "online": status, err := h.repository.IsOnline(r.Context(), vin) writeResult(w, status, err) case len(parts) == 3 && parts[1] == "protocols": snapshot, err := h.repository.GetProtocol(r.Context(), vin, envelope.Protocol(strings.ToUpper(parts[2]))) writeResult(w, snapshot, err) case len(parts) == 3 && parts[1] == "realtime-raw": payload, err := h.repository.GetRealtimeRaw(r.Context(), vin, envelope.Protocol(strings.ToUpper(parts[2]))) writeResult(w, payload, err) default: writeError(w, http.StatusNotFound, "route not found") } } func (h *OnlineIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } if strings.Trim(r.URL.Path, "/") != "api/realtime/online" { writeError(w, http.StatusNotFound, "route not found") return } query, err := parseOnlineListQuery(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } items, total, err := h.repository.ListOnline(r.Context(), query) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "items": items, "total": total, "limit": query.Limit, "offset": query.Offset, }) } func (h *PipelineDebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } if strings.Trim(r.URL.Path, "/") != "api/debug/pipeline" { writeError(w, http.StatusNotFound, "route not found") return } summary, err := h.repository.PipelineSummary(r.Context()) writeResult(w, summary, err) } func parseOnlineListQuery(r *http.Request) (OnlineListQuery, error) { values := r.URL.Query() limit, err := parseBoundedInt(values.Get("limit"), 100, 1, 1000, "limit") if err != nil { return OnlineListQuery{}, err } offset, err := parseBoundedInt(values.Get("offset"), 0, 0, 1_000_000, "offset") if err != nil { return OnlineListQuery{}, err } return normalizeOnlineListQuery(OnlineListQuery{ Protocol: envelope.Protocol(values.Get("protocol")), Limit: limit, Offset: offset, }), nil } func parseBoundedInt(raw string, fallback int, min int, max int, name string) (int, error) { raw = strings.TrimSpace(raw) if raw == "" { return fallback, nil } value, err := strconv.Atoi(raw) if err != nil || value < min || value > max { return 0, fmt.Errorf("%s must be between %d and %d", name, min, max) } return value, nil } func writeResult(w http.ResponseWriter, value any, err error) { if err != nil { if errors.Is(err, redis.Nil) { writeError(w, http.StatusNotFound, "not found") return } writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(value) } func writeError(w http.ResponseWriter, status int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{"error": message}) }