Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
811 lines
28 KiB
Go
811 lines
28 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"
|
|
StationaryVehicleQueryPath = "/api/v1/vehicles/stationary/query"
|
|
RealtimeVehicleQueryPath = "/api/v1/vehicles/realtime/query"
|
|
HydrogenStationQueryPath = "/api/v1/hydrogen-stations/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 "+OperatingDataQueryPath, h.operatingData)
|
|
h.mux.HandleFunc("POST "+HydrogenQueryPath, h.hydrogen)
|
|
h.mux.HandleFunc("POST "+HistoricalHydrogenQueryPath, h.historicalHydrogen)
|
|
h.mux.HandleFunc("POST "+MileageQueryPath, h.mileage)
|
|
h.mux.HandleFunc("POST "+MileageRangeQueryPath, h.mileageRange)
|
|
h.mux.HandleFunc("POST "+TotalMileageQueryPath, h.totalMileage)
|
|
h.mux.HandleFunc("POST "+StationaryVehicleQueryPath, h.stationaryVehicles)
|
|
h.mux.HandleFunc("POST "+RealtimeVehicleQueryPath, h.realtimeVehicles)
|
|
h.mux.HandleFunc("POST "+HydrogenStationQueryPath, h.hydrogenStations)
|
|
}
|
|
|
|
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 "+OperatingDataQueryPath, handler.operatingData)
|
|
handler.mux.HandleFunc("POST "+HydrogenQueryPath, handler.hydrogen)
|
|
handler.mux.HandleFunc("POST "+HistoricalHydrogenQueryPath, handler.historicalHydrogen)
|
|
handler.mux.HandleFunc("POST "+MileageQueryPath, handler.mileage)
|
|
handler.mux.HandleFunc("POST "+MileageRangeQueryPath, handler.mileageRange)
|
|
handler.mux.HandleFunc("POST "+TotalMileageQueryPath, handler.totalMileage)
|
|
handler.mux.HandleFunc("POST "+StationaryVehicleQueryPath, handler.stationaryVehicles)
|
|
handler.mux.HandleFunc("POST "+RealtimeVehicleQueryPath, handler.realtimeVehicles)
|
|
handler.mux.HandleFunc("POST "+HydrogenStationQueryPath, handler.hydrogenStations)
|
|
return handler
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
h.mux.ServeHTTP(w, r)
|
|
}
|
|
|
|
func IsPublicPath(path string) bool {
|
|
return path == OperatingDataQueryPath || path == HistoricalHydrogenQueryPath || path == HydrogenQueryPath || path == MileageQueryPath || path == MileageRangeQueryPath || path == TotalMileageQueryPath || path == StationaryVehicleQueryPath || path == RealtimeVehicleQueryPath || path == HydrogenStationQueryPath
|
|
}
|
|
|
|
func (h *Handler) stationaryVehicles(w http.ResponseWriter, r *http.Request) {
|
|
traceID := externalTraceID(r)
|
|
var request StationaryVehicleQueryRequest
|
|
if !decodeExternalBody(w, r, traceID, &request) {
|
|
return
|
|
}
|
|
data, err := h.service.QueryStationaryVehicles(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) realtimeVehicles(w http.ResponseWriter, r *http.Request) {
|
|
traceID := externalTraceID(r)
|
|
var request RealtimeVehicleRequest
|
|
if !decodeExternalBody(w, r, traceID, &request) {
|
|
return
|
|
}
|
|
data, err := h.service.QueryRealtimeVehicles(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) hydrogenStations(w http.ResponseWriter, r *http.Request) {
|
|
traceID := externalTraceID(r)
|
|
var request HydrogenStationRequest
|
|
if !decodeExternalBody(w, r, traceID, &request) {
|
|
return
|
|
}
|
|
data, err := h.service.QueryHydrogenStations(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) 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)
|
|
encoder := json.NewEncoder(w)
|
|
encoder.SetIndent("", " ")
|
|
_ = encoder.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
|
|
}
|
|
return newExternalTraceID()
|
|
}
|
|
|
|
func newExternalTraceID() string {
|
|
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: "operating_data", Name: "车辆综合运营数据", Description: "批量聚合实时、单日里程与单日用氢,分别保留质量、时间和执行状态。", Version: "v1", Status: "available", Method: http.MethodPost, Path: OperatingDataQueryPath, Unit: ""},
|
|
{Code: "historical_hydrogen_remaining", Name: "历史剩余氢质量", Description: "按VIN与历史北京时间批量查询全车剩余氢质量、采样时间及来源证据。", Version: "v1", Status: "available", Method: http.MethodPost, Path: HistoricalHydrogenQueryPath, Unit: "kg"},
|
|
{
|
|
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",
|
|
},
|
|
{
|
|
Code: "stationary_vehicle_verification", Name: "加氢车辆停留核验",
|
|
Description: "按加氢站坐标与时间区间,核验授权车辆是否在 5 米内低速停留,并按匹配度排序。",
|
|
Version: "v1", Status: "available", Method: http.MethodPost,
|
|
Path: StationaryVehicleQueryPath, Unit: "辆",
|
|
},
|
|
{
|
|
Code: "realtime_vehicle", Name: "车辆实时位置与状态",
|
|
Description: "查询应用授权车辆的最新位置、在线状态、速度、总里程和采集协议。",
|
|
Version: "v1", Status: "available", Method: http.MethodPost,
|
|
Path: RealtimeVehicleQueryPath, Unit: "",
|
|
},
|
|
{
|
|
Code: "hydrogen_station", Name: "加氢站地图点位",
|
|
Description: "只读查询资产管理库中的加氢站名称、经纬度、行政区划和合作状态。",
|
|
Version: "v1", Status: "available", Method: http.MethodPost,
|
|
Path: HydrogenStationQueryPath, Unit: "座",
|
|
},
|
|
}
|
|
}
|