730 lines
24 KiB
Go
730 lines
24 KiB
Go
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",
|
||
},
|
||
}
|
||
}
|