feat: expand vehicle data platform capabilities

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

View File

@@ -123,6 +123,41 @@ type userMutation struct {
VehicleGrants []userVehicleGrantInput `json:"vehicleGrants"`
}
const maxUserBatchItems = 50
type userBatchRequest struct {
Mode string `json:"mode"`
Items []userBatchItem `json:"items"`
}
type userBatchItem struct {
Row int `json:"row"`
Input userMutation `json:"input"`
}
type userBatchResultItem struct {
Row int `json:"row"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
Status string `json:"status"`
Code string `json:"code,omitempty"`
Message string `json:"message"`
ID int64 `json:"id,omitempty"`
}
type userBatchSummary struct {
Received int `json:"received"`
Ready int `json:"ready"`
Created int `json:"created"`
Failed int `json:"failed"`
}
type userBatchResult struct {
Mode string `json:"mode"`
Summary userBatchSummary `json:"summary"`
Items []userBatchResultItem `json:"items"`
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -139,6 +174,17 @@ type loginResponse struct {
Session platform.Principal `json:"session"`
}
type oneOSTicketExchangeRequest struct {
Ticket string `json:"ticket"`
}
type oneOSTicketExchangeResponse struct {
AccessToken string `json:"accessToken"`
ExpiresAt time.Time `json:"expiresAt"`
Session platform.Principal `json:"session"`
ReturnTo string `json:"returnTo"`
}
type cachedSession struct {
principal platform.Principal
expiresAt time.Time
@@ -146,10 +192,11 @@ type cachedSession struct {
}
type authStore struct {
db *sql.DB
sessionTTL time.Duration
cacheMu sync.RWMutex
cache map[string]cachedSession
db *sql.DB
sessionTTL time.Duration
oneOSSessionTTL time.Duration
cacheMu sync.RWMutex
cache map[string]cachedSession
}
func newAuthStore(db *sql.DB, cfg config.Config) (*authStore, error) {
@@ -160,7 +207,11 @@ func newAuthStore(db *sql.DB, cfg config.Config) (*authStore, error) {
if ttl <= 0 {
ttl = 12 * time.Hour
}
store := &authStore{db: db, sessionTTL: ttl, cache: map[string]cachedSession{}}
oneOSTTL := cfg.OneOSSessionTTL
if oneOSTTL <= 0 {
oneOSTTL = 30 * time.Minute
}
store := &authStore{db: db, sessionTTL: ttl, oneOSSessionTTL: oneOSTTL, cache: map[string]cachedSession{}}
if err := store.ensureBootstrapAdmin(context.Background(), cfg.BootstrapAdminUsername, cfg.BootstrapAdminPassword); err != nil {
return nil, err
}
@@ -243,28 +294,168 @@ func (s *authStore) login(w http.ResponseWriter, r *http.Request) {
httpx.WriteError(w, http.StatusInternalServerError, "AUTH_STORE_FAILED", "登录服务暂时不可用", "", requestTraceID(r))
return
}
accessToken, tokenHash, err := randomSessionToken()
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
return
}
sessionID, err := randomHex(16)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
return
}
expiresAt := now.Add(s.sessionTTL)
_, err = s.db.ExecContext(r.Context(), `INSERT INTO platform_user_session(id,user_id,token_hash,issued_at,expires_at,last_seen_at,remote_addr,user_agent) VALUES(?,?,?,?,?,?,?,?)`, sessionID, credential.ID, tokenHash[:], now, expiresAt, now, remoteAddress(r), truncateUTF8(r.UserAgent(), 255))
response, err := s.issueSession(r, credential.ID, principal, s.sessionTTL)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
return
}
_, _ = s.db.ExecContext(r.Context(), `UPDATE platform_user SET failed_login_count=0,locked_until=NULL,last_login_at=? WHERE id=?`, now, credential.ID)
principal.SessionID = sessionID
s.cachePut(hex.EncodeToString(tokenHash[:]), principal, expiresAt)
s.audit(r.Context(), principal.Name, "login", "user", strconv.FormatUint(credential.ID, 10), "success", nil, remoteAddress(r))
w.Header().Set("Cache-Control", "no-store")
httpx.WriteOK(w, requestTraceID(r), loginResponse{AccessToken: accessToken, ExpiresAt: expiresAt, Session: principal})
httpx.WriteOK(w, requestTraceID(r), response)
}
func (s *authStore) exchangeOneOSTicket(w http.ResponseWriter, r *http.Request, adapter *oneOSIdentityAdapter) {
var input oneOSTicketExchangeRequest
if !decodeAuthJSON(w, r, &input) {
return
}
identity, err := adapter.ExchangeTicket(r.Context(), input.Ticket)
if err != nil {
s.audit(r.Context(), "oneos", "sso.exchange", "ticket", "", "denied", map[string]any{"reason": err.Error()}, remoteAddress(r))
httpx.WriteError(w, http.StatusUnauthorized, "ONEOS_TICKET_REJECTED", "OneOS 登录票据无效、已过期或已使用", "", requestTraceID(r))
return
}
user, err := s.syncOneOSIdentity(r.Context(), identity, adapter.defaultMenus)
if err != nil {
s.audit(r.Context(), identity.Username, "sso.exchange", "user", identity.Subject, "failed", map[string]any{"reason": err.Error()}, remoteAddress(r))
httpx.WriteError(w, http.StatusInternalServerError, "ONEOS_IDENTITY_SYNC_FAILED", "无法同步 OneOS 用户权限", "", requestTraceID(r))
return
}
principal, err := s.principalForUser(r.Context(), user)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "ONEOS_IDENTITY_SYNC_FAILED", "无法加载 OneOS 用户权限", "", requestTraceID(r))
return
}
response, err := s.issueSession(r, user.ID, principal, s.oneOSSessionTTL)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "SESSION_CREATE_FAILED", "无法创建登录会话", "", requestTraceID(r))
return
}
_, _ = s.db.ExecContext(r.Context(), `UPDATE platform_user SET last_login_at=NOW(3),failed_login_count=0,locked_until=NULL WHERE id=?`, user.ID)
s.audit(r.Context(), principal.Name, "sso.exchange", "user", identity.Subject, "success", map[string]any{
"scopeLevel": identity.ScopeLevel, "departmentIds": identity.DepartmentIDs,
}, remoteAddress(r))
w.Header().Set("Cache-Control", "no-store")
httpx.WriteOK(w, requestTraceID(r), oneOSTicketExchangeResponse{
AccessToken: response.AccessToken,
ExpiresAt: response.ExpiresAt,
Session: response.Session,
ReturnTo: identity.ReturnTo,
})
}
func (s *authStore) syncOneOSIdentity(ctx context.Context, identity oneOSIdentity, menus []string) (authUser, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return authUser{}, err
}
defer tx.Rollback()
var userID uint64
err = tx.QueryRowContext(ctx, `SELECT id FROM platform_user WHERE auth_provider='oneos' AND external_subject=? FOR UPDATE`, identity.Subject).Scan(&userID)
switch {
case errors.Is(err, sql.ErrNoRows):
result, insertErr := tx.ExecContext(ctx, `INSERT INTO platform_user(
username,display_name,password_hash,user_type,status,customer_ref,tenant_ref,auth_provider,external_subject,created_by,updated_by
) VALUES(?,?,?,'customer','enabled','',?,'oneos',?,'oneos-sso','oneos-sso')`,
oneOSPlatformUsername(identity.Subject), oneOSDisplayName(identity), "", identity.TenantID, identity.Subject,
)
if insertErr != nil {
return authUser{}, insertErr
}
insertedID, insertErr := result.LastInsertId()
if insertErr != nil {
return authUser{}, insertErr
}
userID = uint64(insertedID)
case err != nil:
return authUser{}, err
default:
if _, err = tx.ExecContext(ctx, `UPDATE platform_user SET display_name=?,tenant_ref=?,status='enabled',updated_by='oneos-sso' WHERE id=?`,
oneOSDisplayName(identity), identity.TenantID, userID); err != nil {
return authUser{}, err
}
}
if _, err = tx.ExecContext(ctx, `DELETE FROM platform_user_menu WHERE user_id=?`, userID); err != nil {
return authUser{}, err
}
for _, menu := range menus {
if !customerMenuSet[menu] {
continue
}
if _, err = tx.ExecContext(ctx, `INSERT INTO platform_user_menu(user_id,menu_key,granted_by) VALUES(?,?, 'oneos-sso')`, userID, menu); err != nil {
return authUser{}, err
}
}
departmentIDs := strings.Join(normalizeStringList(identity.DepartmentIDs, 100), ",")
if _, err = tx.ExecContext(ctx, `INSERT INTO platform_user_business_scope(
user_id,scope_level,department_ids,responsible_user_id,enabled,source_system,source_updated_at
) VALUES(?,?,?,?,1,'oneos',?)
ON DUPLICATE KEY UPDATE scope_level=VALUES(scope_level),department_ids=VALUES(department_ids),
responsible_user_id=VALUES(responsible_user_id),enabled=1,source_system='oneos',
source_updated_at=VALUES(source_updated_at)`,
userID, identity.ScopeLevel, departmentIDs, strings.TrimSpace(identity.ResponsibleUserID), identity.IssuedAt); err != nil {
return authUser{}, err
}
if err = tx.Commit(); err != nil {
return authUser{}, err
}
s.invalidateUser(userID)
return authUser{
ID: userID, Username: oneOSPlatformUsername(identity.Subject), DisplayName: oneOSDisplayName(identity),
UserType: "customer", Status: "enabled", TenantRef: identity.TenantID,
AuthProvider: "oneos", ExternalSubject: identity.Subject,
}, nil
}
func (s *authStore) issueSession(r *http.Request, userID uint64, principal platform.Principal, ttl time.Duration) (loginResponse, error) {
accessToken, tokenHash, err := randomSessionToken()
if err != nil {
return loginResponse{}, err
}
sessionID, err := randomHex(16)
if err != nil {
return loginResponse{}, err
}
now := time.Now()
if ttl <= 0 {
ttl = 30 * time.Minute
}
expiresAt := now.Add(ttl)
if _, err = s.db.ExecContext(r.Context(), `INSERT INTO platform_user_session(id,user_id,token_hash,issued_at,expires_at,last_seen_at,remote_addr,user_agent) VALUES(?,?,?,?,?,?,?,?)`,
sessionID, userID, tokenHash[:], now, expiresAt, now, remoteAddress(r), truncateUTF8(r.UserAgent(), 255)); err != nil {
return loginResponse{}, err
}
principal.SessionID = sessionID
s.cachePut(hex.EncodeToString(tokenHash[:]), principal, expiresAt)
return loginResponse{AccessToken: accessToken, ExpiresAt: expiresAt, Session: principal}, nil
}
func oneOSPlatformUsername(subject string) string {
normalized := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
return r
}
return '-'
}, strings.TrimSpace(subject))
normalized = strings.Trim(normalized, "-")
if normalized == "" {
sum := sha256.Sum256([]byte(subject))
normalized = hex.EncodeToString(sum[:8])
}
value := "oneos-" + normalized
if len(value) > 64 {
sum := sha256.Sum256([]byte(subject))
value = "oneos-" + hex.EncodeToString(sum[:16])
}
return value
}
func oneOSDisplayName(identity oneOSIdentity) string {
if value := strings.TrimSpace(identity.DisplayName); value != "" {
return truncateUTF8(value, 96)
}
return truncateUTF8(identity.Username, 96)
}
func (s *authStore) authenticate(ctx context.Context, token string) (platform.Principal, bool) {
@@ -363,6 +554,9 @@ func (s *authStore) principalForUser(ctx context.Context, user authUser) (platfo
menus := append([]string(nil), adminMenus...)
vehicles := []string{}
vehicleGrants := []platform.VehicleGrant{}
businessScopeLevel := ""
departmentIDs := []string{}
responsibleUserID := ""
if user.UserType == "customer" {
menus = []string{}
rows, err := s.db.QueryContext(ctx, `SELECT menu_key FROM platform_user_menu WHERE user_id=? ORDER BY menu_key`, user.ID)
@@ -404,14 +598,96 @@ func (s *authStore) principalForUser(ctx context.Context, user authUser) (platfo
if err := rows.Close(); err != nil {
return platform.Principal{}, err
}
if strings.EqualFold(strings.TrimSpace(user.AuthProvider), "oneos") {
var rawDepartmentIDs string
err := s.db.QueryRowContext(ctx, `SELECT scope_level,department_ids,responsible_user_id
FROM platform_user_business_scope WHERE user_id=? AND enabled=1`, user.ID).Scan(
&businessScopeLevel, &rawDepartmentIDs, &responsibleUserID,
)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return platform.Principal{}, err
}
if err == nil {
departmentIDs = normalizeScopeValues(rawDepartmentIDs)
businessVINs, err := s.loadOneOSBusinessVINs(ctx, businessScopeLevel, departmentIDs, responsibleUserID)
if err != nil {
return platform.Principal{}, err
}
vehicles = businessVINs
vehicleGrants = make([]platform.VehicleGrant, 0, len(businessVINs))
for _, vin := range businessVINs {
vehicleGrants = append(vehicleGrants, platform.VehicleGrant{VIN: vin})
}
}
}
}
return platform.Principal{
SubjectID: strconv.FormatUint(user.ID, 10), Name: user.DisplayName, Username: user.Username,
Role: user.UserType, UserType: user.UserType, CustomerRef: user.CustomerRef, TenantRef: user.TenantRef,
AuthProvider: user.AuthProvider, MenuKeys: menus, VehicleVINs: vehicles, VehicleGrants: vehicleGrants,
BusinessScopeLevel: businessScopeLevel, DepartmentIDs: departmentIDs, ResponsibleUserID: strings.TrimSpace(responsibleUserID),
}, nil
}
func normalizeScopeValues(raw string) []string {
seen := map[string]bool{}
result := []string{}
for _, value := range strings.Split(raw, ",") {
value = strings.TrimSpace(value)
if value == "" || seen[value] || len(result) >= 100 {
continue
}
seen[value] = true
result = append(result, value)
}
sort.Strings(result)
return result
}
func (s *authStore) loadOneOSBusinessVINs(ctx context.Context, level string, departmentIDs []string, responsibleUserID string) ([]string, error) {
where := []string{"st.id=1"}
args := []any{}
switch strings.ToLower(strings.TrimSpace(level)) {
case "department":
if len(departmentIDs) == 0 {
return []string{}, nil
}
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(departmentIDs)), ",")
where = append(where, "scope.department_id IN ("+placeholders+")")
for _, id := range departmentIDs {
args = append(args, id)
}
case "responsible":
responsibleUserID = strings.TrimSpace(responsibleUserID)
if responsibleUserID == "" {
return []string{}, nil
}
where = append(where, "scope.responsible_user_id=?")
args = append(args, responsibleUserID)
default:
return []string{}, nil
}
rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT UPPER(TRIM(scope.vin))
FROM business_scope_state st
JOIN business_customer_vehicle_scope scope ON BINARY scope.source_version=BINARY st.active_version
WHERE `+strings.Join(where, " AND ")+` AND scope.vin<>'' ORDER BY UPPER(TRIM(scope.vin))`, args...)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var vin string
if err := rows.Scan(&vin); err != nil {
return nil, err
}
if vin = strings.TrimSpace(vin); vin != "" {
result = append(result, vin)
}
}
return result, rows.Err()
}
func (s *authStore) handleAdmin(w http.ResponseWriter, r *http.Request, principal platform.Principal) bool {
if r.URL.Path != "/api/v2/admin/users" && !strings.HasPrefix(r.URL.Path, "/api/v2/admin/users/") {
return false
@@ -423,6 +699,8 @@ func (s *authStore) handleAdmin(w http.ResponseWriter, r *http.Request, principa
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/admin/users":
s.listUsers(w, r)
case r.Method == http.MethodPost && r.URL.Path == "/api/v2/admin/users/batch":
s.batchCustomers(w, r, principal)
case r.Method == http.MethodPost && r.URL.Path == "/api/v2/admin/users":
s.createCustomer(w, r, principal)
case r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/api/v2/admin/users/"):
@@ -598,32 +876,127 @@ func (s *authStore) createCustomer(w http.ResponseWriter, r *http.Request, actor
httpx.WriteError(w, http.StatusBadRequest, "USER_INPUT_INVALID", err.Error(), "", requestTraceID(r))
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
tx, err := s.db.BeginTx(r.Context(), nil)
id, grantChanges, err := s.createCustomerRecord(r.Context(), input, menus, grants, actor.Name)
if err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法创建客户账号", "", requestTraceID(r))
return
}
defer tx.Rollback()
result, err := tx.ExecContext(r.Context(), `INSERT INTO platform_user(username,display_name,password_hash,user_type,status,customer_ref,tenant_ref,auth_provider,created_by,updated_by) VALUES(?,?,?,'customer',?,?,?,'local',?,?)`, strings.TrimSpace(input.Username), strings.TrimSpace(input.DisplayName), string(hash), input.Status, strings.TrimSpace(input.CustomerRef), strings.TrimSpace(input.TenantRef), actor.Name, actor.Name)
if err != nil {
writeUserMutationError(w, r, err, "创建")
return
}
id, _ := result.LastInsertId()
grantChanges := []vehicleGrantAuditChange{}
if err := replaceGrants(r.Context(), tx, uint64(id), menus, grants, actor.Name, &grantChanges); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法保存客户权限", "", requestTraceID(r))
return
}
if err := tx.Commit(); err != nil {
httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法创建客户账号", "", requestTraceID(r))
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
writeUserMutationError(w, r, err, "创建")
} else {
httpx.WriteError(w, http.StatusInternalServerError, "USER_CREATE_FAILED", "无法创建客户账号", "", requestTraceID(r))
}
return
}
s.audit(r.Context(), actor.Name, "user.create", "user", strconv.FormatInt(id, 10), "success", map[string]any{"menus": menus, "vehicleGrantChanges": grantChanges}, remoteAddress(r))
httpx.WriteOK(w, requestTraceID(r), map[string]any{"id": id})
}
func (s *authStore) batchCustomers(w http.ResponseWriter, r *http.Request, actor platform.Principal) {
var input userBatchRequest
if !decodeAuthJSON(w, r, &input) {
return
}
input.Mode = strings.ToLower(strings.TrimSpace(input.Mode))
if input.Mode != "preview" && input.Mode != "create" {
httpx.WriteError(w, http.StatusBadRequest, "USER_BATCH_MODE_INVALID", "批量操作模式必须为 preview 或 create", "", requestTraceID(r))
return
}
if len(input.Items) == 0 || len(input.Items) > maxUserBatchItems {
httpx.WriteError(w, http.StatusBadRequest, "USER_BATCH_SIZE_INVALID", fmt.Sprintf("每次需导入 1-%d 个客户账号", maxUserBatchItems), "", requestTraceID(r))
return
}
result := userBatchResult{Mode: input.Mode, Summary: userBatchSummary{Received: len(input.Items)}, Items: make([]userBatchResultItem, 0, len(input.Items))}
seen := map[string]bool{}
for index, item := range input.Items {
row := item.Row
if row <= 1 {
row = index + 2
}
item.Input.Username = strings.TrimSpace(item.Input.Username)
item.Input.DisplayName = strings.TrimSpace(item.Input.DisplayName)
item.Input.Status = firstNonEmpty(strings.TrimSpace(item.Input.Status), "enabled")
entry := userBatchResultItem{Row: row, Username: item.Input.Username, DisplayName: item.Input.DisplayName}
usernameKey := strings.ToLower(item.Input.Username)
if seen[usernameKey] {
entry.Status, entry.Code, entry.Message = "invalid", "DUPLICATE_IN_FILE", "文件内用户名重复"
result.Summary.Failed++
result.Items = append(result.Items, entry)
continue
}
seen[usernameKey] = true
menus, grants, err := s.validateMutation(r.Context(), item.Input, true)
if err != nil {
entry.Status, entry.Code, entry.Message = "invalid", "USER_INPUT_INVALID", err.Error()
result.Summary.Failed++
result.Items = append(result.Items, entry)
continue
}
var existingID uint64
err = s.db.QueryRowContext(r.Context(), `SELECT id FROM platform_user WHERE LOWER(username)=LOWER(?) LIMIT 1`, item.Input.Username).Scan(&existingID)
if err == nil {
entry.Status, entry.Code, entry.Message = "conflict", "USERNAME_EXISTS", "用户名已存在"
result.Summary.Failed++
result.Items = append(result.Items, entry)
continue
}
if !errors.Is(err, sql.ErrNoRows) {
entry.Status, entry.Code, entry.Message = "failed", "USER_CHECK_FAILED", "暂时无法校验用户名"
result.Summary.Failed++
result.Items = append(result.Items, entry)
continue
}
if input.Mode == "preview" {
entry.Status, entry.Message = "ready", "校验通过,可以创建"
result.Summary.Ready++
result.Items = append(result.Items, entry)
continue
}
id, grantChanges, err := s.createCustomerRecord(r.Context(), item.Input, menus, grants, actor.Name)
if err != nil {
entry.Status, entry.Code, entry.Message = "failed", "USER_CREATE_FAILED", "创建失败,请重试"
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
entry.Status, entry.Code, entry.Message = "conflict", "USERNAME_EXISTS", "用户名已存在"
}
result.Summary.Failed++
result.Items = append(result.Items, entry)
continue
}
entry.Status, entry.Message, entry.ID = "created", "账号与权限已创建", id
result.Summary.Created++
result.Items = append(result.Items, entry)
s.audit(r.Context(), actor.Name, "user.create", "user", strconv.FormatInt(id, 10), "success", map[string]any{"batch": true, "row": row, "menus": menus, "vehicleGrantChanges": grantChanges}, remoteAddress(r))
}
s.audit(r.Context(), actor.Name, "user.batch."+input.Mode, "user_batch", strconv.Itoa(len(input.Items)), "success", map[string]any{"received": result.Summary.Received, "ready": result.Summary.Ready, "created": result.Summary.Created, "failed": result.Summary.Failed}, remoteAddress(r))
httpx.WriteOK(w, requestTraceID(r), result)
}
func (s *authStore) createCustomerRecord(ctx context.Context, input userMutation, menus []string, grants []vehicleGrantMutation, actor string) (int64, []vehicleGrantAuditChange, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), 12)
if err != nil {
return 0, nil, err
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return 0, nil, err
}
defer tx.Rollback()
created, err := tx.ExecContext(ctx, `INSERT INTO platform_user(username,display_name,password_hash,user_type,status,customer_ref,tenant_ref,auth_provider,created_by,updated_by) VALUES(?,?,?,'customer',?,?,?,'local',?,?)`, strings.TrimSpace(input.Username), strings.TrimSpace(input.DisplayName), string(hash), input.Status, strings.TrimSpace(input.CustomerRef), strings.TrimSpace(input.TenantRef), actor, actor)
if err != nil {
return 0, nil, err
}
id, err := created.LastInsertId()
if err != nil {
return 0, nil, err
}
grantChanges := []vehicleGrantAuditChange{}
if err := replaceGrants(ctx, tx, uint64(id), menus, grants, actor, &grantChanges); err != nil {
return 0, nil, err
}
if err := tx.Commit(); err != nil {
return 0, nil, err
}
return id, grantChanges, nil
}
func (s *authStore) updateCustomer(w http.ResponseWriter, r *http.Request, actor platform.Principal) {
idText := strings.TrimPrefix(r.URL.Path, "/api/v2/admin/users/")
id, err := strconv.ParseUint(idText, 10, 64)
@@ -647,11 +1020,15 @@ func (s *authStore) updateCustomer(w http.ResponseWriter, r *http.Request, actor
return
}
defer tx.Rollback()
var userType string
if err := tx.QueryRowContext(r.Context(), `SELECT user_type FROM platform_user WHERE id=? FOR UPDATE`, id).Scan(&userType); err != nil || userType != "customer" {
var userType, authProvider string
if err := tx.QueryRowContext(r.Context(), `SELECT user_type,auth_provider FROM platform_user WHERE id=? FOR UPDATE`, id).Scan(&userType, &authProvider); err != nil || userType != "customer" {
httpx.WriteError(w, http.StatusBadRequest, "CUSTOMER_USER_REQUIRED", "只能通过此功能维护客户账号", "", requestTraceID(r))
return
}
if authProvider = strings.TrimSpace(authProvider); authProvider != "" && !strings.EqualFold(authProvider, "local") && input.Password != "" {
httpx.WriteError(w, http.StatusBadRequest, "EXTERNAL_IDENTITY_PASSWORD_READ_ONLY", "外部身份的登录凭据必须在身份源中维护", authProvider, requestTraceID(r))
return
}
args := []any{strings.TrimSpace(input.DisplayName), input.Status, strings.TrimSpace(input.CustomerRef), strings.TrimSpace(input.TenantRef), actor.Name}
query := `UPDATE platform_user SET display_name=?,status=?,customer_ref=?,tenant_ref=?,updated_by=?`
if input.Password != "" {