feat: expand vehicle data platform capabilities

This commit is contained in:
lingniu
2026-07-27 16:46:15 +08:00
parent e3a1f80f86
commit 3c4bece72c
650 changed files with 62155 additions and 2552 deletions

View File

@@ -0,0 +1,729 @@
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",
},
}
}