feat: sync vehicle monitoring and accumulated platform updates

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.
This commit is contained in:
lingniu
2026-10-26 18:05:59 +08:00
parent 8b32ae2988
commit d6b957dfc8
290 changed files with 28238 additions and 1647 deletions
@@ -5,10 +5,12 @@ import (
"crypto/subtle"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
"lingniu/vehicle-data-platform/apps/api/internal/config"
"lingniu/vehicle-data-platform/apps/api/internal/httpx"
@@ -27,12 +29,14 @@ type tokenPrincipal struct {
}
type apiAuthenticator struct {
mode string
tokens []tokenPrincipal
local *authStore
oneOS *oneOSIdentityAdapter
demo *demoAuthDirectory
adapters []IdentityAdapter
mode string
tokens []tokenPrincipal
local *authStore
profile *oneOSProfileAuth
oneOS *oneOSIdentityAdapter
demo *demoAuthDirectory
localBackend bool
adapters []IdentityAdapter
}
func withAPIAuth(next http.Handler, cfg config.Config, databases ...*sql.DB) http.Handler {
@@ -62,14 +66,32 @@ func newAPIAuthenticator(cfg config.Config, db *sql.DB) (*apiAuthenticator, erro
if err != nil {
return nil, err
}
oneOS, err := newOneOSIdentityAdapter(cfg)
legacyConfig := cfg
if cfg.OneOSAuthEnabled {
legacyConfig.OneOSSSOEnabled = false
}
oneOS, err := newOneOSIdentityAdapter(legacyConfig)
if err != nil {
return nil, err
}
if oneOS != nil && local == nil {
return nil, fmt.Errorf("ONEOS_SSO_ENABLED requires MYSQL_DSN and the platform identity schema")
}
authenticator := &apiAuthenticator{mode: mode, local: local, oneOS: oneOS}
authenticator := &apiAuthenticator{mode: mode, local: local, oneOS: oneOS, localBackend: cfg.OneOSLocalBackend}
if cfg.OneOSAuthEnabled {
localMock := (cfg.OneOSLocalOnly || cfg.OneOSDevIntegration) && strings.EqualFold(cfg.DataMode, "mock")
if mode != "enforce" || (local == nil && !localMock) {
return nil, fmt.Errorf("OneOS auth requires AUTH_MODE=enforce and identity DB or explicit isolated local mock")
}
if !validOneOSOrigin(cfg.OneOSPublicOrigin, cfg.OneOSLocalOnly || cfg.OneOSDevIntegration) {
return nil, fmt.Errorf("ONEOS_PUBLIC_ORIGIN must be a trusted HTTPS origin or explicit local loopback origin")
}
client, e := newOneOSProfileClient(cfg)
if e != nil {
return nil, e
}
authenticator.profile = &oneOSProfileAuth{localBackend: cfg.OneOSLocalBackend, vehicleQueryScope: cfg.OneOSVehicleQueryScope, devIntegration: cfg.OneOSDevIntegration, devDataProxy: cfg.OneOSDevDataProxyURL != "", client: client, store: local, origin: strings.TrimRight(cfg.OneOSPublicOrigin, "/"), issuer: cfg.OneOSProfileIssuer, tenant: cfg.OneOSProfileAllowedTenant, passwordEnabled: cfg.OneOSPasswordEnabled, scopeEnabled: cfg.OneOSVehicleScopePath != "" || cfg.OneOSVehicleQueryScope, localOnly: localMock, ssoEnabled: localMock && cfg.OneOSLoginTicketExchangePath != ""}
}
if mode == "disabled" && local == nil && strings.EqualFold(strings.TrimSpace(cfg.DataMode), "mock") {
authenticator.demo = newDemoAuthDirectory()
}
@@ -101,7 +123,7 @@ func newAPIAuthenticator(cfg config.Config, db *sql.DB) (*apiAuthenticator, erro
principal := platform.Principal{Name: item.Name, Username: item.Name, Role: item.Role, UserType: item.Role, AuthProvider: "legacy-token", MenuKeys: append([]string(nil), adminMenus...)}
authenticator.tokens = append(authenticator.tokens, tokenPrincipal{hash: hash, principal: principal})
}
if mode == "enforce" && len(authenticator.tokens) == 0 && authenticator.local == nil && len(authenticator.adapters) == 0 {
if mode == "enforce" && len(authenticator.tokens) == 0 && authenticator.local == nil && authenticator.profile == nil && len(authenticator.adapters) == 0 {
return nil, fmt.Errorf("enforce mode requires a local identity store, identity adapter, AUTH_TOKEN, or AUTH_TOKENS_JSON")
}
return authenticator, nil
@@ -109,6 +131,18 @@ func newAPIAuthenticator(cfg config.Config, db *sql.DB) (*apiAuthenticator, erro
func (a *apiAuthenticator) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/auth/providers" {
w.Header().Set("Cache-Control", "no-store")
if r.Method != http.MethodGet {
oneOSAuthError(w, r, 405, "METHOD_NOT_ALLOWED")
return
}
httpx.WriteOK(w, requestTraceID(r), map[string]any{"oneos": map[string]bool{"enabled": a.profile != nil, "devIntegration": a.profile != nil && a.profile.devIntegration, "realDataEnabled": a.profile != nil && (a.profile.devDataProxy || a.profile.localBackend), "passwordEnabled": a.profile != nil && a.profile.passwordEnabled, "authorizationReady": a.profile != nil && a.profile.scopeEnabled, "ssoEnabled": a.profile != nil && a.profile.ssoEnabled}, "local": map[string]bool{"enabled": a.local != nil}})
return
}
if a.profile != nil && a.profile.handle(w, r) {
return
}
if r.URL.Path == "/api/v2/auth/oneos/exchange" {
if r.Method != http.MethodPost {
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "OneOS 登录兑换接口仅支持 POST", "", requestTraceID(r))
@@ -133,13 +167,79 @@ func (a *apiAuthenticator) middleware(next http.Handler) http.Handler {
a.local.login(w, r)
return
}
// This route resolves the caller's own provider; request parameters never select it.
if r.URL.Path == "/api/v2/auth/vehicle-scope" {
w.Header().Set("Cache-Control", "no-store")
if r.Method != http.MethodGet {
oneOSAuthError(w, r, 405, "METHOD_NOT_ALLOWED")
return
}
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" && a.profile.vehicleQueryScope {
token, clientID := bearerToken(r), r.Header.Get("X-OneOS-Client-ID")
current, err := a.profile.client.CurrentUser(r.Context(), token, clientID)
if err != nil || !a.profile.validProfile(current.Profile) {
oneOSAuthError(w, r, 401, "AUTH_REQUIRED")
return
}
p, _, err := a.profile.resolveVehicleQueryScope(r.Context(), token, clientID, current)
if err != nil {
httpx.WriteError(w, 503, "VEHICLE_SCOPE_UNAVAILABLE", "车辆权限暂时加载失败,请重试", "", requestTraceID(r))
return
}
httpx.WriteOK(w, requestTraceID(r), struct {
platform.Principal
AuthMode string `json:"authMode"`
}{p, a.mode})
return
}
p, ok := a.authenticate(r)
if !ok {
oneOSAuthError(w, r, 401, "AUTH_REQUIRED")
return
}
httpx.WriteOK(w, requestTraceID(r), struct {
platform.Principal
AuthMode string `json:"authMode"`
}{p, a.mode})
return
}
principal, ok := a.authenticate(r)
if !ok {
w.Header().Set("WWW-Authenticate", `Bearer realm="lingniu-vehicle-platform"`)
httpx.WriteError(w, http.StatusUnauthorized, "AUTH_REQUIRED", "需要有效的访问令牌", "", requestTraceID(r))
return
}
if principal.UserType == "customer" && r.URL.Path != "/api/v2/session" && r.URL.Path != "/api/v2/auth/logout" && r.URL.Path != "/api/v2/auth/password" {
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" {
limited := principal.AuthorizationStatus == "pending" || (a.profile.devIntegration && principal.AuthorizationStatus == "asset-catalog")
identityRoute := (r.Method == http.MethodGet && (r.URL.Path == "/api/v2/session" || r.URL.Path == "/api/v2/auth/oneos/me")) || (r.Method == http.MethodPost && r.URL.Path == "/api/v2/auth/logout")
catalogRoute := principal.AuthorizationStatus != "pending" && r.Method == http.MethodGet && r.URL.Path == "/api/v2/auth/oneos/vehicles" && (principal.AuthorizationStatus == "asset-catalog" || (a.profile.vehicleQueryScope || a.profile.devDataProxy && principal.Role == "admin"))
action := oneOSReadAction(r)
admin := oneOSHasAdminPermission(principal.PermissionKeys) && principal.Role == "admin"
if limited && admin && !identityRoute && !catalogRoute {
httpx.WriteError(w, http.StatusServiceUnavailable, "ONEOS_DATA_UNAVAILABLE", "管理员权限已开通,本地开发环境尚未连接中台业务数据源", "", requestTraceID(r))
return
}
if (limited && !identityRoute && !catalogRoute) || (!limited && !admin && !identityRoute && !catalogRoute && (action == "" || !oneOSPermissions(principal.PermissionKeys)[action])) {
oneOSAuthError(w, r, 403, "ONEOS_PERMISSION_DENIED")
return
}
if catalogRoute {
page, err := a.profile.client.VehicleAssetPage(r.Context(), bearerToken(r), r.Header.Get("X-OneOS-Client-ID"), r.URL.Query())
if err != nil {
w.Header().Set("Cache-Control", "no-store")
if errors.Is(err, errOneOSAssetQuery) {
httpx.WriteError(w, http.StatusBadRequest, "ONEOS_CATALOG_QUERY_INVALID", "车辆目录查询参数无效,请检查筛选条件及分页范围", "", requestTraceID(r))
} else {
httpx.WriteError(w, http.StatusBadGateway, "ONEOS_CATALOG_UNAVAILABLE", "OneOS 车辆目录暂不可用,请稍后重试", "", requestTraceID(r))
}
return
}
w.Header().Set("Cache-Control", "no-store")
httpx.WriteOK(w, requestTraceID(r), map[string]any{"records": page.Records, "total": page.Total, "current": page.Current, "size": page.Size, "telemetryAvailable": false, "historyAvailable": false, "source": "oneos-asset-catalog"})
return
}
}
if principal.UserType == "customer" && r.URL.Path != "/api/v2/session" && r.URL.Path != "/api/v2/auth/logout" && r.URL.Path != "/api/v2/auth/password" && r.URL.Path != "/api/v2/auth/oneos/me" {
menu := requiredMenu(r)
allowed := principal.CanMenu(menu)
if menu == "shared" {
@@ -159,8 +259,45 @@ func (a *apiAuthenticator) middleware(next http.Handler) http.Handler {
httpx.WriteError(w, http.StatusForbidden, "PERMISSION_DENIED", "当前角色无权执行该操作", "需要 "+required+" 角色", requestTraceID(r))
return
}
if a.localBackend {
principal.DataSource = "production-local"
}
ctx := platform.WithPrincipal(r.Context(), principal)
if strings.HasPrefix(r.URL.Path, "/api/v2/customer-mileage") || strings.HasPrefix(r.URL.Path, "/api/v2/operation-compliance") {
snapshot := platform.CustomerAssignmentSnapshot{Tenant: principal.TenantRef}
if principal.AuthProvider != "oneos" && a.profile != nil {
snapshot.Tenant = a.profile.tenant
}
if principal.AuthProvider == "oneos" && (r.URL.Path == "/api/v2/customer-mileage" || strings.HasPrefix(r.URL.Path, "/api/v2/operation-compliance")) && r.Method == http.MethodGet && a.profile != nil && a.profile.vehicleQueryScope {
token, clientID := bearerToken(r), r.Header.Get("X-OneOS-Client-ID")
rows, err := a.profile.vehicleQuerySnapshot(ctx, token, clientID)
if err != nil {
httpx.WriteError(w, 503, "ONEOS_CATALOG_UNAVAILABLE", "OneOS 客户车辆归属暂不可用,请重试", "", requestTraceID(r))
return
}
snapshot.Live, snapshot.ObservedAt = true, time.Now()
key := sha256.Sum256([]byte(clientID + "\x00" + token))
a.profile.scopeMu.Lock()
if entry := a.profile.queryScopes[key]; entry != nil && !entry.expires.IsZero() {
snapshot.ObservedAt = entry.expires.Add(-oneOSQueryScopeTTL)
}
a.profile.scopeMu.Unlock()
for _, row := range rows {
snapshot.Rows = append(snapshot.Rows, platform.CustomerVehicleAssignment{VIN: row.VIN, Plate: row.PlateNumber, Customer: row.CustomerName, Department: row.BusinessDepartment, ResponsiblePerson: row.BusinessResponsiblePerson})
}
}
ctx = platform.WithCustomerAssignments(ctx, snapshot)
}
r = r.WithContext(ctx)
if r.URL.Path == "/api/v2/auth/oneos/me" {
if a.profile == nil || principal.AuthProvider != "oneos" || r.Method != http.MethodGet {
oneOSAuthError(w, r, 403, "ONEOS_REQUIRED")
return
}
w.Header().Set("Cache-Control", "no-store")
httpx.WriteOK(w, requestTraceID(r), map[string]any{"username": principal.Username, "displayName": principal.Name, "tenantId": principal.TenantRef, "menuKeys": principal.MenuKeys, "authorizationStatus": principal.AuthorizationStatus})
return
}
if r.URL.Path == "/api/v2/session" {
principal.VehicleCount = len(principal.VehicleVINs)
httpx.WriteOK(w, requestTraceID(r), struct {
@@ -174,7 +311,7 @@ func (a *apiAuthenticator) middleware(next http.Handler) http.Handler {
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "退出接口仅支持 POST", "", requestTraceID(r))
return
}
if a.local != nil && (principal.AuthProvider == "local" || principal.AuthProvider == "oneos") {
if a.local != nil && (principal.AuthProvider == "local" || principal.AuthProvider == "oneos" || principal.AuthProvider == "oneos-profile") {
a.local.logout(r.Context(), bearerToken(r))
}
httpx.WriteOK(w, requestTraceID(r), map[string]bool{"loggedOut": true})
@@ -206,8 +343,14 @@ func (a *apiAuthenticator) authenticate(r *http.Request) (platform.Principal, bo
if token == "" {
return platform.Principal{}, false
}
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" {
return a.profile.authenticate(r, token)
}
if a.local != nil {
if principal, ok := a.local.authenticate(r.Context(), token); ok {
if principal.AuthProvider == "oneos-profile" || (a.profile != nil && principal.AuthProvider == "oneos") {
return platform.Principal{}, false
}
return principal, true
}
}
@@ -245,13 +388,17 @@ func bearerToken(r *http.Request) string {
func requiredMenu(r *http.Request) string {
path := r.URL.Path
switch {
case strings.HasPrefix(path, "/api/v2/operation-compliance"):
return "operation-compliance"
case strings.HasPrefix(path, "/api/v2/customer-mileage"):
return "customer-mileage"
case strings.HasPrefix(path, "/api/v2/monitor"), path == "/api/v2/alerts/events":
return "monitor"
case path == "/api/map/reverse-geocode", path == "/api/realtime/vehicles", path == "/api/realtime/locations", path == "/api/vehicle-service", path == "/api/vehicle-service/overview", path == "/api/vehicle-service/overviews", strings.HasSuffix(path, "/telemetry/latest"):
return "shared"
case path == "/api/v2/tracks":
return "tracks"
case path == "/api/mileage/daily", path == "/api/mileage/summary", path == "/api/v2/statistics/mileage":
case path == "/api/mileage/daily", path == "/api/mileage/summary", path == "/api/v2/statistics/mileage", path == "/api/v2/statistics/vehicles", path == "/api/v2/statistics/vehicle-options", path == "/api/v2/statistics/vehicle-resolve":
return "statistics"
case path == "/api/vehicles", path == "/api/vehicles/resolve", path == "/api/vehicles/coverage", path == "/api/vehicles/coverage/summary", path == "/api/vehicles/business-filters":
return "shared"
@@ -263,6 +410,9 @@ func requiredMenu(r *http.Request) string {
}
func requiredRole(r *http.Request) string {
if r.URL.Path == "/api/v2/customer-mileage/rules" && r.Method != http.MethodGet {
return "admin"
}
if strings.HasPrefix(r.URL.Path, "/api/v2/open-platform/") {
return "admin"
}
@@ -293,7 +443,7 @@ func requiredRole(r *http.Request) string {
switch path {
case "/api/v2/auth/logout":
return "viewer"
case "/api/vehicle-service/overviews", "/api/history/raw-frames/query", "/api/mileage/daily", "/api/v2/statistics/mileage", "/api/v2/access/summary", "/api/v2/access/vehicles", "/api/v2/alerts/summary", "/api/v2/alerts/events", "/api/v2/exports":
case "/api/v2/statistics/vehicle-resolve", "/api/vehicle-service/overviews", "/api/history/raw-frames/query", "/api/mileage/daily", "/api/v2/statistics/mileage", "/api/v2/access/summary", "/api/v2/access/vehicles", "/api/v2/alerts/summary", "/api/v2/alerts/events", "/api/v2/exports":
return "viewer"
case "/api/v2/alerts/notifications/read":
return "operator"