package openplatform import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "lingniu/vehicle-data-platform/apps/api/internal/httpx" "lingniu/vehicle-data-platform/apps/api/internal/platform" ) const ( HydrogenQueryPath = "/api/v1/vehicles/hydrogen-consumption/query" MileageQueryPath = "/api/v1/vehicles/mileage/query" MileageRangeQueryPath = "/api/v1/vehicles/mileage/range/query" TotalMileageQueryPath = "/api/v1/vehicles/total-mileage/query" ) type Handler struct { service *Service portal *PortalService mux *http.ServeMux } func NewHandler(service *Service) *Handler { handler := &Handler{service: service, mux: http.NewServeMux()} handler.registerExternalDataRoutes() handler.registerAdminAppRoutes() return handler } func NewAdminHandler(service *Service, portal *PortalService) *Handler { handler := &Handler{service: service, portal: portal, mux: http.NewServeMux()} handler.registerAdminAppRoutes() if portal != nil { handler.registerAdminUserRoutes() } return handler } func NewExternalHandler(service *Service, portal *PortalService) *Handler { handler := &Handler{service: service, portal: portal, mux: http.NewServeMux()} handler.registerExternalDataRoutes() if portal != nil { handler.registerPortalRoutes() } return handler } func (h *Handler) registerExternalDataRoutes() { h.mux.HandleFunc("POST "+HydrogenQueryPath, h.hydrogen) h.mux.HandleFunc("POST "+MileageQueryPath, h.mileage) h.mux.HandleFunc("POST "+MileageRangeQueryPath, h.mileageRange) h.mux.HandleFunc("POST "+TotalMileageQueryPath, h.totalMileage) } func (h *Handler) registerAdminAppRoutes() { h.mux.HandleFunc("GET /api/v2/open-platform/apps", h.listApps) h.mux.HandleFunc("POST /api/v2/open-platform/apps", h.createApp) h.mux.HandleFunc("PUT /api/v2/open-platform/apps/{id}", h.updateApp) h.mux.HandleFunc("POST /api/v2/open-platform/apps/{id}/rotate-key", h.rotateKey) h.mux.HandleFunc("GET /api/v2/open-platform/apps/{id}/vehicles", h.listVehicleGrants) h.mux.HandleFunc("PUT /api/v2/open-platform/apps/{id}/vehicles", h.replaceVehicleGrants) } func (h *Handler) registerAdminUserRoutes() { h.mux.HandleFunc("GET /api/v2/open-platform/users", h.listPortalUsers) h.mux.HandleFunc("POST /api/v2/open-platform/users", h.createPortalUser) h.mux.HandleFunc("PUT /api/v2/open-platform/users/{id}", h.updatePortalUser) h.mux.HandleFunc("GET /api/v2/open-platform/users/{id}/apps", h.listPortalUserApps) h.mux.HandleFunc("PUT /api/v2/open-platform/users/{id}/apps", h.replacePortalUserApps) } func (h *Handler) registerPortalRoutes() { h.mux.HandleFunc("POST /portal-api/auth/login", h.portalLogin) h.mux.HandleFunc("POST /portal-api/auth/logout", h.portalLogout) h.mux.HandleFunc("GET /portal-api/session", h.portalSession) h.mux.HandleFunc("GET /portal-api/catalog", h.portalCatalog) h.mux.HandleFunc("GET /portal-api/apps", h.portalApps) h.mux.HandleFunc("GET /portal-api/apps/{id}/vehicles", h.portalVehicles) h.mux.HandleFunc("GET /portal-api/apps/{id}/audit", h.portalAudit) h.mux.HandleFunc("POST /portal-api/apps/{id}/rotate-key", h.portalRotateKey) h.mux.HandleFunc("PUT /portal-api/account/password", h.portalChangePassword) h.mux.HandleFunc("GET /portal-api/admin/apps", h.portalAdminListApps) h.mux.HandleFunc("POST /portal-api/admin/apps", h.portalAdminCreateApp) h.mux.HandleFunc("PUT /portal-api/admin/apps/{id}", h.portalAdminUpdateApp) h.mux.HandleFunc("PUT /portal-api/admin/apps/{id}/vehicles", h.portalAdminReplaceVehicles) h.mux.HandleFunc("GET /portal-api/admin/vehicles", h.portalAdminVehicleCatalog) h.mux.HandleFunc("GET /portal-api/admin/users", h.portalAdminListUsers) h.mux.HandleFunc("POST /portal-api/admin/users", h.portalAdminCreateUser) h.mux.HandleFunc("PUT /portal-api/admin/users/{id}", h.portalAdminUpdateUser) h.mux.HandleFunc("GET /portal-api/admin/users/{id}/apps", h.portalAdminListUserApps) h.mux.HandleFunc("PUT /portal-api/admin/users/{id}/apps", h.portalAdminReplaceUserApps) } func NewDataHandler(service *Service) *Handler { handler := &Handler{service: service, mux: http.NewServeMux()} handler.mux.HandleFunc("POST "+HydrogenQueryPath, handler.hydrogen) handler.mux.HandleFunc("POST "+MileageQueryPath, handler.mileage) handler.mux.HandleFunc("POST "+MileageRangeQueryPath, handler.mileageRange) handler.mux.HandleFunc("POST "+TotalMileageQueryPath, handler.totalMileage) return handler } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } func IsPublicPath(path string) bool { return path == HydrogenQueryPath || path == MileageQueryPath || path == MileageRangeQueryPath || path == TotalMileageQueryPath } func (h *Handler) hydrogen(w http.ResponseWriter, r *http.Request) { traceID := externalTraceID(r) var request QueryRequest if !decodeExternalBody(w, r, traceID, &request) { return } data, err := h.service.QueryHydrogen(r.Context(), externalBearer(r), traceID, request) if err != nil { writeExternalError(w, traceID, err) return } writeExternal(w, http.StatusOK, ExternalResponse{Code: "SUCCESS", Message: "success", Data: data, TraceID: traceID}) } func (h *Handler) mileage(w http.ResponseWriter, r *http.Request) { traceID := externalTraceID(r) var request QueryRequest if !decodeExternalBody(w, r, traceID, &request) { return } data, err := h.service.QueryMileage(r.Context(), externalBearer(r), traceID, request) if err != nil { writeExternalError(w, traceID, err) return } writeExternal(w, http.StatusOK, ExternalResponse{Code: "SUCCESS", Message: "success", Data: data, TraceID: traceID}) } func (h *Handler) mileageRange(w http.ResponseWriter, r *http.Request) { traceID := externalTraceID(r) var request MileageRangeRequest if !decodeExternalBody(w, r, traceID, &request) { return } response, err := h.service.QueryMileageRange(r.Context(), externalBearer(r), traceID, request) if err != nil { writeExternalError(w, traceID, err) return } writeExternal(w, http.StatusOK, response) } func (h *Handler) totalMileage(w http.ResponseWriter, r *http.Request) { traceID := externalTraceID(r) var request TotalMileageQueryRequest if !decodeExternalBody(w, r, traceID, &request) { return } data, err := h.service.QueryTotalMileage(r.Context(), externalBearer(r), traceID, request) if err != nil { writeExternalError(w, traceID, err) return } writeExternal(w, http.StatusOK, ExternalResponse{Code: "SUCCESS", Message: "success", Data: data, TraceID: traceID}) } func (h *Handler) listApps(w http.ResponseWriter, r *http.Request) { data, err := h.service.ListApps(r.Context()) h.writeAdmin(w, r, data, err) } func (h *Handler) createApp(w http.ResponseWriter, r *http.Request) { var input AppInput if !decodeAdminBody(w, r, &input) { return } data, err := h.service.CreateApp(r.Context(), input, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) updateApp(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } var input AppInput if !decodeAdminBody(w, r, &input) { return } data, err := h.service.UpdateApp(r.Context(), id, input, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) rotateKey(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } data, err := h.service.RotateKey(r.Context(), id, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) listVehicleGrants(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } data, err := h.service.ListVehicleGrants(r.Context(), id) h.writeAdmin(w, r, data, err) } func (h *Handler) replaceVehicleGrants(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } var request VehicleGrantRequest if !decodeAdminBody(w, r, &request) { return } data, err := h.service.ReplaceVehicleGrants(r.Context(), id, request, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) listPortalUsers(w http.ResponseWriter, r *http.Request) { data, err := h.portal.ListUsers(r.Context()) h.writeAdmin(w, r, data, err) } func (h *Handler) createPortalUser(w http.ResponseWriter, r *http.Request) { var input PortalUserInput if !decodeAdminBody(w, r, &input) { return } data, err := h.portal.CreateUser(r.Context(), input, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) updatePortalUser(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } var input PortalUserInput if !decodeAdminBody(w, r, &input) { return } data, err := h.portal.UpdateUser(r.Context(), id, input, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) listPortalUserApps(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } data, err := h.portal.ListUserApps(r.Context(), id) h.writeAdmin(w, r, data, err) } func (h *Handler) replacePortalUserApps(w http.ResponseWriter, r *http.Request) { id, ok := parseID(w, r) if !ok { return } var request PortalUserAppRequest if !decodeAdminBody(w, r, &request) { return } data, err := h.portal.ReplaceUserApps(r.Context(), id, request, platform.ActorFromContext(r.Context())) h.writeAdmin(w, r, data, err) } func (h *Handler) portalLogin(w http.ResponseWriter, r *http.Request) { var input PortalLoginRequest if !decodePortalBody(w, r, &input) { return } data, err := h.portal.Login(r.Context(), input, requestRemoteAddress(r), r.UserAgent()) h.writePortal(w, r, data, err) } func (h *Handler) portalLogout(w http.ResponseWriter, r *http.Request) { h.portal.Logout(r.Context(), externalBearer(r)) h.writePortal(w, r, map[string]bool{"loggedOut": true}, nil) } func (h *Handler) portalSession(w http.ResponseWriter, r *http.Request) { session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) h.writePortal(w, r, session, err) } func (h *Handler) portalCatalog(w http.ResponseWriter, r *http.Request) { h.writePortal(w, r, dataProducts(), nil) } func (h *Handler) portalApps(w http.ResponseWriter, r *http.Request) { session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err != nil { h.writePortal(w, r, nil, err) return } data, err := h.portal.Apps(r.Context(), session) h.writePortal(w, r, data, err) } func (h *Handler) portalVehicles(w http.ResponseWriter, r *http.Request) { appID, ok := parsePortalID(w, r) if !ok { return } session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err != nil { h.writePortal(w, r, nil, err) return } data, err := h.portal.Vehicles(r.Context(), session, appID) h.writePortal(w, r, data, err) } func (h *Handler) portalAudit(w http.ResponseWriter, r *http.Request) { appID, ok := parsePortalID(w, r) if !ok { return } session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err != nil { h.writePortal(w, r, nil, err) return } data, err := h.portal.Audit(r.Context(), session, appID) h.writePortal(w, r, data, err) } func (h *Handler) portalRotateKey(w http.ResponseWriter, r *http.Request) { appID, ok := parsePortalID(w, r) if !ok { return } session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err == nil { err = h.portal.CanRotateKey(r.Context(), session, appID) } if err != nil { h.writePortal(w, r, nil, err) return } data, err := h.service.RotateKey(r.Context(), appID, "portal:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalChangePassword(w http.ResponseWriter, r *http.Request) { session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err != nil { h.writePortal(w, r, nil, err) return } var input struct { CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` } if !decodePortalBody(w, r, &input) { return } err = h.portal.ChangePassword(r.Context(), session, input.CurrentPassword, input.NewPassword) h.writePortal(w, r, map[string]bool{"changed": err == nil}, err) } func (h *Handler) portalAdminListApps(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePortalAdmin(w, r); !ok { return } data, err := h.service.ListApps(r.Context()) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminCreateApp(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } var input AppInput if !decodePortalBody(w, r, &input) { return } data, err := h.service.CreateApp(r.Context(), input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminUpdateApp(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } id, ok := parsePortalID(w, r) if !ok { return } var input AppInput if !decodePortalBody(w, r, &input) { return } data, err := h.service.UpdateApp(r.Context(), id, input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminReplaceVehicles(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } id, ok := parsePortalID(w, r) if !ok { return } var input VehicleGrantRequest if !decodePortalBody(w, r, &input) { return } data, err := h.service.ReplaceVehicleGrants(r.Context(), id, input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminVehicleCatalog(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePortalAdmin(w, r); !ok { return } data, err := h.portal.VehicleCatalog(r.Context()) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminListUsers(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePortalAdmin(w, r); !ok { return } data, err := h.portal.ListUsers(r.Context()) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminCreateUser(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } var input PortalUserInput if !decodePortalBody(w, r, &input) { return } data, err := h.portal.CreateUser(r.Context(), input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminUpdateUser(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } id, ok := parsePortalID(w, r) if !ok { return } var input PortalUserInput if !decodePortalBody(w, r, &input) { return } data, err := h.portal.UpdateUser(r.Context(), id, input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminListUserApps(w http.ResponseWriter, r *http.Request) { if _, ok := h.requirePortalAdmin(w, r); !ok { return } id, ok := parsePortalID(w, r) if !ok { return } data, err := h.portal.ListUserApps(r.Context(), id) h.writePortal(w, r, data, err) } func (h *Handler) portalAdminReplaceUserApps(w http.ResponseWriter, r *http.Request) { session, ok := h.requirePortalAdmin(w, r) if !ok { return } id, ok := parsePortalID(w, r) if !ok { return } var input PortalUserAppRequest if !decodePortalBody(w, r, &input) { return } data, err := h.portal.ReplaceUserApps(r.Context(), id, input, "portal-admin:"+session.Username) h.writePortal(w, r, data, err) } func (h *Handler) requirePortalAdmin(w http.ResponseWriter, r *http.Request) (PortalSession, bool) { session, err := h.portal.Authenticate(r.Context(), externalBearer(r)) if err == nil && session.UserType != "admin" { err = ErrForbidden } if err != nil { h.writePortal(w, r, nil, err) return PortalSession{}, false } return session, true } func (h *Handler) writeAdmin(w http.ResponseWriter, r *http.Request, data any, err error) { traceID := externalTraceID(r) switch { case err == nil: httpx.WriteOK(w, traceID, data) case errors.Is(err, ErrInvalidRequest): httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "请求参数不正确", err.Error(), traceID) case errors.Is(err, ErrNotFound): httpx.WriteError(w, http.StatusNotFound, "NOT_FOUND", "开放平台应用不存在", "", traceID) default: httpx.WriteError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "开放平台操作失败", err.Error(), traceID) } } func (h *Handler) writePortal(w http.ResponseWriter, r *http.Request, data any, err error) { traceID := externalTraceID(r) w.Header().Set("Cache-Control", "no-store") switch { case err == nil: httpx.WriteOK(w, traceID, data) case errors.Is(err, ErrUnauthorized): w.Header().Set("WWW-Authenticate", `Bearer realm="lingniu-open-platform-portal"`) httpx.WriteError(w, http.StatusUnauthorized, "UNAUTHORIZED", "登录状态无效或已过期", "", traceID) case errors.Is(err, ErrForbidden): httpx.WriteError(w, http.StatusForbidden, "FORBIDDEN", "当前账号无此操作权限", "", traceID) case errors.Is(err, ErrInvalidRequest): httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "请求参数不正确", err.Error(), traceID) case errors.Is(err, ErrNotFound): httpx.WriteError(w, http.StatusNotFound, "NOT_FOUND", "资源不存在", "", traceID) default: httpx.WriteError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "开放平台操作失败", "", traceID) } } func decodeExternalBody(w http.ResponseWriter, r *http.Request, traceID string, output any) bool { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(output); err != nil { message := "请求参数不正确" if strings.Contains(err.Error(), "protocolPriority") { message = "protocolPriority必须是非空字符串数组" } writeExternal(w, http.StatusBadRequest, ExternalResponse{Code: "INVALID_REQUEST", Message: message, TraceID: traceID}) return false } if err := decoder.Decode(&struct{}{}); err != io.EOF { writeExternal(w, http.StatusBadRequest, ExternalResponse{Code: "INVALID_REQUEST", Message: "请求体只能包含一个JSON对象", TraceID: traceID}) return false } return true } func decodeAdminBody(w http.ResponseWriter, r *http.Request, output any) bool { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(output); err != nil { httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "请求参数不正确", err.Error(), externalTraceID(r)) return false } return true } func decodePortalBody(w http.ResponseWriter, r *http.Request, output any) bool { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(output); err != nil { httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "请求参数不正确", portalDecodeDetail(err), externalTraceID(r)) return false } if err := decoder.Decode(&struct{}{}); err != io.EOF { httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "请求体只能包含一个JSON对象", "", externalTraceID(r)) return false } return true } func portalDecodeDetail(err error) string { var syntaxError *json.SyntaxError var typeError *json.UnmarshalTypeError switch { case errors.As(err, &syntaxError): return "请求内容不是有效的 JSON" case errors.As(err, &typeError): if typeError.Field != "" { return fmt.Sprintf("字段 %s 的数据类型不正确", typeError.Field) } return "请求字段的数据类型不正确" case errors.Is(err, io.EOF): return "请求内容不能为空" case strings.HasPrefix(err.Error(), "json: unknown field "): return "请求包含未支持的字段 " + strings.TrimPrefix(err.Error(), "json: unknown field ") default: return "请求内容无法解析" } } func writeExternalError(w http.ResponseWriter, traceID string, err error) { response := ExternalResponse{TraceID: traceID} status := http.StatusInternalServerError switch { case errors.Is(err, ErrUnauthorized): status, response.Code, response.Message = http.StatusUnauthorized, "UNAUTHORIZED", "身份认证失败" case errors.Is(err, ErrForbidden): status, response.Code, response.Message = http.StatusForbidden, "FORBIDDEN", "无数据访问权限" case errors.Is(err, ErrInvalidRequest): status, response.Code, response.Message = http.StatusBadRequest, "INVALID_REQUEST", "请求参数不正确" if strings.Contains(err.Error(), "invalid datetime") { response.Code, response.Message = "INVALID_DATETIME_FORMAT", "time格式必须为yyyy-MM-dd HH:mm:ss" } else if strings.Contains(err.Error(), "invalid date") { response.Code, response.Message = "INVALID_DATE_FORMAT", "date格式必须为yyyy-MM-dd" } else if strings.Contains(err.Error(), "protocolPriority must not be empty") { response.Message = "protocolPriority不能为空" } else if strings.Contains(err.Error(), "protocolPriority contains duplicate protocol") { response.Message = "protocolPriority不能包含重复协议" } else if strings.Contains(err.Error(), "protocolPriority contains unsupported protocol") { response.Message = "protocolPriority仅允许GB32960、MQTT、JT808" } default: response.Code, response.Message = "INTERNAL_ERROR", "服务内部异常" } if status == http.StatusUnauthorized { w.Header().Set("WWW-Authenticate", `Bearer realm="lingniu-vehicle-open-platform"`) } writeExternal(w, status, response) } func writeExternal(w http.ResponseWriter, status int, response any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(response) } func externalBearer(r *http.Request) string { header := strings.TrimSpace(r.Header.Get("Authorization")) if len(header) < 8 || !strings.EqualFold(header[:7], "Bearer ") { return "" } return strings.TrimSpace(header[7:]) } func externalTraceID(r *http.Request) string { if traceID := strings.TrimSpace(r.Header.Get("X-Trace-Id")); traceID != "" && len(traceID) <= 64 { return traceID } var value [16]byte if _, err := rand.Read(value[:]); err == nil { return hex.EncodeToString(value[:]) } return strconv.FormatInt(time.Now().UnixNano(), 10) } func parseID(w http.ResponseWriter, r *http.Request) (uint64, bool) { id, err := strconv.ParseUint(r.PathValue("id"), 10, 64) if err != nil || id == 0 { httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "应用ID无效", "", externalTraceID(r)) return 0, false } return id, true } func parsePortalID(w http.ResponseWriter, r *http.Request) (uint64, bool) { id, err := strconv.ParseUint(r.PathValue("id"), 10, 64) if err != nil || id == 0 { httpx.WriteError(w, http.StatusBadRequest, "INVALID_REQUEST", "应用ID无效", "", externalTraceID(r)) return 0, false } return id, true } func requestRemoteAddress(r *http.Request) string { for _, header := range []string{"X-Forwarded-For", "X-Real-Ip"} { if value := strings.TrimSpace(strings.Split(r.Header.Get(header), ",")[0]); value != "" { return value } } return r.RemoteAddr } func dataProducts() []DataProduct { return []DataProduct{ { Code: "daily_hydrogen", Name: "单日用氢量", Description: "按车牌和自然日查询授权车辆的氢气消耗量。", Version: "v1", Status: "available", Method: http.MethodPost, Path: HydrogenQueryPath, Unit: "kg", }, { Code: "daily_mileage", Name: "单日里程", Description: "按车牌和自然日查询日里程、累计里程、实际来源协议与数据时间,支持自定义协议优先级。", Version: "v1", Status: "available", Method: http.MethodPost, Path: MileageQueryPath, Unit: "km", }, { Code: "mileage_range", Name: "区间日里程", Description: "按最长366天区间分页查询逐日里程,支持逐车逐日自定义协议优先级。", Version: "v1", Status: "available", Method: http.MethodPost, Path: MileageRangeQueryPath, Unit: "km", }, { Code: "total_mileage_at_time", Name: "指定时刻总里程", Description: "按VIN和北京时间查询最近一条总里程、采集协议及记录时间差。", Version: "v1", Status: "available", Method: http.MethodPost, Path: TotalMileageQueryPath, Unit: "km", }, } }