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

730 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
},
}
}