// Package account 提供账号管理的业务逻辑服务 // 包含账号创建、查询、更新、删除、密码管理等功能 package account import ( "context" stdErrors "errors" "fmt" "slices" "strconv" "strings" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" accountauditapp "github.com/break/junhong_cmp_fiber/internal/application/accountaudit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auditfailure" pkgAuth "github.com/break/junhong_cmp_fiber/pkg/auth" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/logger" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/jackc/pgx/v5/pgconn" "github.com/redis/go-redis/v9" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ShopStoreInterface 店铺存储接口(仅用于获取店铺信息) type ShopStoreInterface interface { GetByIDs(ctx context.Context, ids []uint) ([]*model.Shop, error) } // Service 账号业务服务 type Service struct { db *gorm.DB lifecycleAudit accountauditapp.Writer accessAudit accessauditapp.Writer redisClient *redis.Client accountStore *postgres.AccountStore roleStore *postgres.RoleStore accountRoleStore *postgres.AccountRoleStore shopRoleStore *postgres.ShopRoleStore shopStore ShopStoreInterface enterpriseStore middleware.EnterpriseStoreInterface wecomMembers WeComMemberFinder tokenManager *pkgAuth.TokenManager } // SetLifecycleAudit 注入账号生命周期事务和统一审计边界。 func (s *Service) SetLifecycleAudit(db *gorm.DB, writer accountauditapp.Writer) { s.db = db s.lifecycleAudit = writer } // SetAccessAudit 注入账号角色授权的事务、缓存和统一审计边界。 func (s *Service) SetAccessAudit(db *gorm.DB, redisClient *redis.Client, writer accessauditapp.Writer) { s.db = db s.redisClient = redisClient s.accessAudit = writer } // SetTokenManager 注入改密后撤销现有会话所需的令牌管理器。 func (s *Service) SetTokenManager(tokenManager *pkgAuth.TokenManager) { s.tokenManager = tokenManager } // WeComMemberFinder 定义账号绑定时校验应用可见成员的边界。 type WeComMemberFinder interface { GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error) } // New 创建账号服务 func New( accountStore *postgres.AccountStore, roleStore *postgres.RoleStore, accountRoleStore *postgres.AccountRoleStore, shopRoleStore *postgres.ShopRoleStore, shopStore ShopStoreInterface, enterpriseStore middleware.EnterpriseStoreInterface, ) *Service { return &Service{ accountStore: accountStore, roleStore: roleStore, accountRoleStore: accountRoleStore, shopRoleStore: shopRoleStore, shopStore: shopStore, enterpriseStore: enterpriseStore, } } // SetWeComMemberFinder 注入企业微信应用可见成员查询边界。 func (s *Service) SetWeComMemberFinder(finder WeComMemberFinder) { s.wecomMembers = finder } // Create 创建账号 func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*model.Account, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeEnterprise { return nil, errors.New(errors.CodeForbidden, "企业账号不允许创建账号") } if userType == constants.UserTypeAgent && req.UserType == constants.UserTypePlatform { return nil, errors.New(errors.CodeForbidden, "无权限创建平台账号") } if req.UserType == constants.UserTypeAgent && req.ShopID == nil { return nil, errors.New(errors.CodeInvalidParam, "代理账号必须提供店铺ID") } if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID == nil { return nil, errors.New(errors.CodeInvalidParam, "企业账号必须提供企业ID") } if req.UserType == constants.UserTypeAgent && req.ShopID != nil { if err := middleware.CanManageShop(ctx, *req.ShopID); err != nil { return nil, err } } if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil { if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore); err != nil { return nil, err } } existing, err := s.accountStore.GetByUsername(ctx, req.Username) if err == nil && existing != nil { return nil, errors.New(errors.CodeUsernameExists, "用户名已存在") } existing, err = s.accountStore.GetByPhone(ctx, req.Phone) if err == nil && existing != nil { return nil, errors.New(errors.CodePhoneExists, "手机号已存在") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败") } account := &model.Account{ Username: req.Username, Phone: req.Phone, Password: string(hashedPassword), UserType: req.UserType, ShopID: req.ShopID, EnterpriseID: req.EnterpriseID, Status: constants.StatusEnabled, } if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewAccountStore(tx, nil).Create(ctx, account); err != nil { return err } if req.UserType == constants.UserTypeAgent && req.ShopID != nil { var roleIDs []uint _ = tx.Transaction(func(roleTx *gorm.DB) error { var err error roleIDs, err = postgres.NewShopRoleStore(roleTx, nil).GetRoleIDsByShopID(ctx, *req.ShopID) return err }) for _, roleID := range roleIDs { accountRole := &model.AccountRole{AccountID: account.ID, RoleID: roleID, Status: constants.StatusEnabled, Creator: currentUserID, Updater: currentUserID} _ = tx.Transaction(func(roleTx *gorm.DB) error { return postgres.NewAccountRoleStore(roleTx, nil).Create(ctx, accountRole) }) } } shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account) if err != nil { return err } return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{ ActionCode: constants.AuditActionAccountCreated, Summary: "创建账号", Result: constants.AuditResultSuccess, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, AfterData: accountLifecycleData(account), }) }); err != nil { account.ID = 0 s.recordLifecycleFailure(ctx, constants.AuditActionAccountCreated, "创建账号失败", constants.AuditResultFailed, account, nil, nil, err) return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败") } return account, nil } // Get 获取账号 func (s *Service) Get(ctx context.Context, id uint) (*dto.AccountResponse, error) { account, err := s.accountStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeAccountNotFound, "账号不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } accounts := []*model.Account{account} return s.toAccountResponse(account, s.loadShopNames(ctx, accounts), s.loadEnterpriseNames(ctx, accounts)), nil } // BindWeCom 将系统账号绑定到管理员明确选择的企微应用可见成员。 func (s *Service) BindWeCom(ctx context.Context, accountID uint, request dto.BindAccountWeComRequest) (*dto.AccountResponse, error) { operatorID := middleware.GetUserIDFromContext(ctx) operatorType := middleware.GetUserTypeFromContext(ctx) if operatorID == 0 { return nil, errors.New(errors.CodeUnauthorized) } if operatorType != constants.UserTypeSuperAdmin && operatorType != constants.UserTypePlatform { return nil, errors.New(errors.CodeForbidden) } if s.wecomMembers == nil || request.ApplicationID == 0 || strings.TrimSpace(request.UserID) == "" { return nil, errors.New(errors.CodeInvalidParam) } account, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待绑定账号失败") } member, err := s.wecomMembers.GetVisible(ctx, request.ApplicationID, request.UserID) if err != nil { s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "绑定账号企业微信身份失败", constants.AuditResultFailed, account, map[string]any{ "account_id": account.ID, "auth_method": "wecom", "state": "failed", }, nil, nil, err) return nil, err } beforeData := model.JSONB{ "wecom_corp_id": account.WeComCorpID, "wecom_userid": account.WeComUserID, "wecom_name": account.WeComName, } afterData := map[string]any{ "wecom_corp_id": member.CorpID, "wecom_userid": member.UserID, "wecom_name": member.Name, } updatedAccount := *account updatedAccount.WeComCorpID = member.CorpID updatedAccount.WeComUserID = member.UserID updatedAccount.WeComName = member.Name updatedAccount.Updater = operatorID if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewAccountStore(tx, nil).BindWeCom(ctx, accountID, member.CorpID, member.UserID, member.Name, operatorID); err != nil { return err } return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{ ActionCode: constants.AuditActionAccountWeComBound, Summary: "绑定账号企业微信身份", Result: constants.AuditResultSuccess, ActorID: operatorID, Account: &updatedAccount, AuthenticationKey: fmt.Sprintf("account:%d:wecom", account.ID), Authentication: map[string]any{ "account_id": account.ID, "auth_method": "wecom", "state": "bound", "wecom_corp_id": member.CorpID, "wecom_userid": member.UserID, "wecom_name": member.Name, }, BeforeData: beforeData, AfterData: afterData, }) }); err != nil { var pgErr *pgconn.PgError if stdErrors.As(err, &pgErr) && pgErr.Code == "23505" { appErr := errors.New(errors.CodeConflict, "该企业微信成员已绑定其他系统账号") s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "拒绝绑定账号企业微信身份", constants.AuditResultDenied, account, map[string]any{ "account_id": account.ID, "auth_method": "wecom", "state": "denied", }, beforeData, nil, appErr) return nil, appErr } s.recordSecurityFailure(ctx, constants.AuditActionAccountWeComBound, "绑定账号企业微信身份失败", constants.AuditResultFailed, account, map[string]any{ "account_id": account.ID, "auth_method": "wecom", "state": "failed", }, beforeData, nil, err) return nil, errors.Wrap(errors.CodeDatabaseError, err, "绑定企业微信成员失败") } account = &updatedAccount accounts := []*model.Account{account} return s.toAccountResponse(account, s.loadShopNames(ctx, accounts), s.loadEnterpriseNames(ctx, accounts)), nil } // Update 更新账号 func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountRequest) (*model.Account, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } account, err := s.accountStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { if account.ShopID == nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden)) return nil, errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号", constants.AuditResultDenied, account, nil, nil, err) return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } beforeData := accountLifecycleData(account) if req.Username != nil { existing, err := s.accountStore.GetByUsername(ctx, *req.Username) if err == nil && existing != nil && existing.ID != id { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新重复用户名", constants.AuditResultDenied, account, beforeData, nil, errors.New(errors.CodeUsernameExists)) return nil, errors.New(errors.CodeUsernameExists, "用户名已存在") } account.Username = *req.Username } if req.Phone != nil { existing, err := s.accountStore.GetByPhone(ctx, *req.Phone) if err == nil && existing != nil && existing.ID != id { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新重复手机号", constants.AuditResultDenied, account, beforeData, nil, errors.New(errors.CodePhoneExists)) return nil, errors.New(errors.CodePhoneExists, "手机号已存在") } account.Phone = *req.Phone } if req.Password != nil { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) if err != nil { s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "更新账号凭据处理失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), beforeData, nil, err) return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败") } account.Password = string(hashedPassword) } if req.Status != nil { account.Status = *req.Status } account.Updater = currentUserID if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewAccountStore(tx, nil).Update(ctx, account); err != nil { return err } if req.Password != nil { return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{ ActionCode: constants.AuditActionAccountPasswordReset, Summary: "更新账号安全资料", Result: constants.AuditResultSuccess, ActorID: currentUserID, Account: account, AuthenticationKey: fmt.Sprintf("account:%d:password", account.ID), Authentication: passwordAuthentication(account, "changed"), BeforeData: beforeData, AfterData: accountLifecycleData(account), }) } shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account) if err != nil { return err } return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{ ActionCode: constants.AuditActionAccountUpdated, Summary: "更新账号", Result: constants.AuditResultSuccess, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, BeforeData: beforeData, AfterData: accountLifecycleData(account), }) }); err != nil { if req.Password != nil { s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "更新账号安全资料失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), beforeData, nil, err) } else { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "更新账号失败", constants.AuditResultFailed, account, beforeData, nil, err) } return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败") } if req.Password != nil { s.revokeAccountTokens(ctx, account.ID) } return account, nil } // Delete 软删除账号 func (s *Service) Delete(ctx context.Context, id uint) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } account, err := s.accountStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { if account.ShopID == nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "拒绝删除账号", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden)) return errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "拒绝删除账号", constants.AuditResultDenied, account, nil, nil, err) return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } beforeData := accountLifecycleData(account) if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account) if err != nil { return err } if err := postgres.NewAccountStore(tx, nil).Delete(ctx, id); err != nil { return err } return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{ ActionCode: constants.AuditActionAccountDeleted, Summary: "删除账号", Result: constants.AuditResultSuccess, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, BeforeData: beforeData, }) }); err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountDeleted, "删除账号失败", constants.AuditResultFailed, account, beforeData, nil, err) return errors.Wrap(errors.CodeInternalError, err, "删除账号失败") } return nil } // List 查询账号列表 func (s *Service) List(ctx context.Context, req *dto.AccountListRequest) ([]*dto.AccountResponse, int64, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "id DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.Username != "" { filters["username"] = req.Username } if req.Phone != "" { filters["phone"] = req.Phone } if req.UserType != nil { filters["user_type"] = *req.UserType } if req.Status != nil { filters["status"] = *req.Status } if req.ShopID != nil { filters["shop_id"] = *req.ShopID } if req.EnterpriseID != nil { filters["enterprise_id"] = *req.EnterpriseID } accounts, total, err := s.accountStore.List(ctx, opts, filters) if err != nil { return nil, 0, err } shopMap := s.loadShopNames(ctx, accounts) enterpriseMap := s.loadEnterpriseNames(ctx, accounts) responses := make([]*dto.AccountResponse, 0, len(accounts)) for _, acc := range accounts { resp := s.toAccountResponse(acc, shopMap, enterpriseMap) responses = append(responses, resp) } return responses, total, nil } // AssignRoles 为账号分配角色(支持空数组清空所有角色,超级管理员禁止分配) func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uint) ([]*model.AccountRole, error) { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } account, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { if account.ShopID == nil { err := errors.New(errors.CodeForbidden, "无权限操作该账号") s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, err) return nil, err } if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil { appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, appErr) return nil, appErr } } if account.UserType == constants.UserTypeSuperAdmin { err := errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色") s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, nil, err) return nil, err } assigned, changedRoles, err := s.assignAccountRoles(ctx, account, currentUserID, roleIDs) if err != nil { s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRolesAssigned, account, changedRoles, err) return nil, err } return assigned, nil } // GetRoles 获取账号的所有角色 func (s *Service) GetRoles(ctx context.Context, accountID uint) ([]*model.Role, error) { // 检查账号存在 _, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeAccountNotFound, "账号不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } // 获取角色 ID 列表 roleIDs, err := s.accountRoleStore.GetRoleIDsByAccountID(ctx, accountID) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号角色 ID 失败") } if len(roleIDs) == 0 { return []*model.Role{}, nil } // 获取角色详情 return s.roleStore.GetByIDs(ctx, roleIDs) } // RemoveRole 移除账号的角色 func (s *Service) RemoveRole(ctx context.Context, accountID, roleID uint) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } account, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeAgent { if account.ShopID == nil { err := errors.New(errors.CodeForbidden, "无权限操作该账号") s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, nil, err) return err } if err := middleware.CanManageShop(ctx, *account.ShopID); err != nil { appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, nil, appErr) return appErr } } role, err := s.removeAccountRole(ctx, account, currentUserID, roleID) if err != nil { roles := []*model.Role(nil) if role != nil { roles = []*model.Role{role} } s.recordRoleAssignmentFailure(ctx, constants.AuditActionAccountRoleRemoved, account, roles, err) return err } return nil } func (s *Service) assignAccountRoles(ctx context.Context, account *model.Account, operatorID uint, requested []uint) ([]*model.AccountRole, []*model.Role, error) { if s.db == nil || s.accessAudit == nil { return nil, nil, errors.New(errors.CodeInvalidStatus, "账号角色审计接缝未配置") } assigned := make([]*model.AccountRole, 0, len(requested)) var changedRoles []*model.Role err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Account{}, account.ID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定账号角色关系失败") } accountRoles := postgres.NewAccountRoleStore(tx, nil) roles := postgres.NewRoleStore(tx) scopeShop, err := accountRoleScopeShop(ctx, tx, account.ShopID) if err != nil { return err } beforeIDs, err := accountRoles.GetRoleIDsByAccountID(ctx, account.ID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询账号现有角色失败") } afterIDs, changes, err := s.applyAccountRoleAssignment(ctx, accountRoles, roles, account, operatorID, beforeIDs, requested, &assigned) if err != nil { return err } changedRoles = changes if len(changes) == 0 { return nil } if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionAccountRolesAssigned, Summary: "为账号分配角色", OperatorID: operatorID, Account: account, Shop: scopeShop, Roles: roleAssignmentChanges(changes, beforeIDs, afterIDs), BeforeData: map[string]any{"role_ids": sortedRoleIDs(beforeIDs)}, AfterData: map[string]any{"role_ids": sortedRoleIDs(afterIDs)}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入账号角色审计失败") } return nil }) if err != nil { return nil, changedRoles, err } s.clearAccountPermissionCache(ctx, account.ID) return assigned, changedRoles, nil } func (s *Service) applyAccountRoleAssignment(ctx context.Context, accountRoles *postgres.AccountRoleStore, roles *postgres.RoleStore, account *model.Account, operatorID uint, beforeIDs, requested []uint, assigned *[]*model.AccountRole) ([]uint, []*model.Role, error) { if len(requested) == 0 { if len(beforeIDs) == 0 { return []uint{}, nil, nil } removed, err := roles.GetByIDs(ctx, beforeIDs) if err != nil { return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询待移除角色失败") } if err := accountRoles.DeleteByAccountID(ctx, account.ID); err != nil { return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "清空账号角色失败") } return []uint{}, removed, nil } requestedRoles := make([]*model.Role, 0, len(requested)) for _, roleID := range requested { role, err := roles.GetByID(ctx, roleID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil, errors.New(errors.CodeRoleNotFound, fmt.Sprintf("角色 %d 不存在", roleID)) } return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询角色失败") } if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) { return nil, nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配") } requestedRoles = append(requestedRoles, role) } existing := roleIDSet(beforeIDs) newRoleCount := 0 for _, roleID := range requested { if !existing[roleID] { newRoleCount++ } } maxRoles := constants.GetMaxRolesForUserType(account.UserType) if maxRoles == 0 { return nil, nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色") } if maxRoles != -1 && len(beforeIDs)+newRoleCount > maxRoles { return nil, nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles)) } changed := make([]*model.Role, 0, newRoleCount) addedIDs := make([]uint, 0, newRoleCount) for index, roleID := range requested { if existing[roleID] { continue } accountRole := &model.AccountRole{AccountID: account.ID, RoleID: roleID, Status: constants.StatusEnabled, Creator: operatorID, Updater: operatorID} if err := accountRoles.Create(ctx, accountRole); err != nil { return nil, changed, errors.Wrap(errors.CodeDatabaseError, err, "创建账号-角色关联失败") } *assigned = append(*assigned, accountRole) changed = append(changed, requestedRoles[index]) addedIDs = append(addedIDs, roleID) existing[roleID] = true } return append(append([]uint(nil), beforeIDs...), addedIDs...), changed, nil } func (s *Service) removeAccountRole(ctx context.Context, account *model.Account, operatorID, roleID uint) (*model.Role, error) { if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "账号角色审计接缝未配置") } var removed *model.Role err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id").First(&model.Account{}, account.ID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定账号角色关系失败") } accountRoles := postgres.NewAccountRoleStore(tx, nil) scopeShop, err := accountRoleScopeShop(ctx, tx, account.ShopID) if err != nil { return err } beforeIDs, err := accountRoles.GetRoleIDsByAccountID(ctx, account.ID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询账号现有角色失败") } if !slices.Contains(beforeIDs, roleID) { return nil } removed, err = postgres.NewRoleStore(tx).GetByID(ctx, roleID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询待移除角色失败") } if err := accountRoles.Delete(ctx, account.ID, roleID); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "删除账号-角色关联失败") } afterIDs := removeRoleID(beforeIDs, roleID) if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionAccountRoleRemoved, Summary: "移除账号角色", OperatorID: operatorID, Account: account, Shop: scopeShop, Roles: []accessauditapp.RoleChange{{Role: removed, BeforeData: map[string]any{"assigned": true}, AfterData: map[string]any{"assigned": false}}}, BeforeData: map[string]any{"role_ids": sortedRoleIDs(beforeIDs)}, AfterData: map[string]any{"role_ids": sortedRoleIDs(afterIDs)}, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入账号角色审计失败") } return nil }) if err == nil && removed != nil { s.clearAccountPermissionCache(ctx, account.ID) } return removed, err } func (s *Service) clearAccountPermissionCache(ctx context.Context, accountID uint) { if s.redisClient == nil { return } if err := s.redisClient.Del(ctx, constants.RedisUserPermissionsKey(accountID)).Err(); err != nil { logger.GetAppLogger().Warn("清理账号权限缓存失败", zap.Uint("account_id", accountID), zap.Error(err)) } } func (s *Service) recordRoleAssignmentFailure(ctx context.Context, action string, account *model.Account, roles []*model.Role, originalErr error) { changes := make([]accessauditapp.RoleChange, 0, len(roles)) for _, role := range roles { changes = append(changes, accessauditapp.RoleChange{Role: role}) } accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: action, Summary: "账号角色操作失败", Result: accessFailureResult(originalErr), OperatorID: middleware.GetUserIDFromContext(ctx), Account: account, Shop: s.loadAccountRoleScopeShop(ctx, account), Roles: changes, }, originalErr) } func accountRoleScopeShop(ctx context.Context, tx *gorm.DB, shopID *uint) (*model.Shop, error) { if shopID == nil { return nil, nil } shop, err := postgres.NewShopStore(tx, nil).GetByID(ctx, *shopID) if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询账号所属店铺失败") } return shop, nil } func (s *Service) loadAccountRoleScopeShop(ctx context.Context, account *model.Account) *model.Shop { if account == nil || account.ShopID == nil || s.shopStore == nil { return nil } shops, err := s.shopStore.GetByIDs(ctx, []uint{*account.ShopID}) if err != nil || len(shops) == 0 { return nil } return shops[0] } func roleAssignmentChanges(roles []*model.Role, beforeIDs, afterIDs []uint) []accessauditapp.RoleChange { before, after := roleIDSet(beforeIDs), roleIDSet(afterIDs) changes := make([]accessauditapp.RoleChange, 0, len(roles)) for _, role := range roles { changes = append(changes, accessauditapp.RoleChange{ Role: role, BeforeData: map[string]any{"assigned": before[role.ID]}, AfterData: map[string]any{"assigned": after[role.ID]}, }) } return changes } func accessFailureResult(err error) string { var appErr *errors.AppError if stdErrors.As(err, &appErr) { switch appErr.Code { case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeRoleNotFound: return constants.AuditResultDenied } } return constants.AuditResultFailed } func roleIDSet(ids []uint) map[uint]bool { set := make(map[uint]bool, len(ids)) for _, id := range ids { set[id] = true } return set } func sortedRoleIDs(ids []uint) []uint { result := append([]uint(nil), ids...) slices.Sort(result) return result } func removeRoleID(ids []uint, removed uint) []uint { result := make([]uint, 0, len(ids)) for _, id := range ids { if id != removed { result = append(result, id) } } return result } // ValidatePassword 验证密码 func (s *Service) ValidatePassword(plainPassword, hashedPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword)) return err == nil } // UpdatePassword 修改账号密码(管理员重置场景,无需旧密码) func (s *Service) UpdatePassword(ctx context.Context, accountID uint, newPassword string) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } account, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeAccountNotFound, "账号不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "重置账号密码失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), nil, nil, err) return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败") } if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewAccountStore(tx, nil).UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil { return err } return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{ ActionCode: constants.AuditActionAccountPasswordReset, Summary: "重置账号密码", Result: constants.AuditResultSuccess, ActorID: currentUserID, Account: account, AuthenticationKey: fmt.Sprintf("account:%d:password", account.ID), Authentication: passwordAuthentication(account, "changed"), BeforeData: map[string]any{"credentials_configured": account.Password != ""}, AfterData: map[string]any{"credentials_configured": true}, }) }); err != nil { s.recordSecurityFailure(ctx, constants.AuditActionAccountPasswordReset, "重置账号密码失败", constants.AuditResultFailed, account, passwordAuthentication(account, "failed"), nil, nil, err) return errors.Wrap(errors.CodeInternalError, err, "更新密码失败") } s.revokeAccountTokens(ctx, account.ID) return nil } func (s *Service) revokeAccountTokens(ctx context.Context, accountID uint) { if s.tokenManager == nil { return } if err := s.tokenManager.RevokeAllUserTokens(ctx, accountID); err != nil { logger.GetAppLogger().Warn("改密后撤销账号令牌失败", zap.Uint("account_id", accountID), zap.Error(err)) } } func (s *Service) recordSecurityFailure( ctx context.Context, actionCode, summary, result string, account *model.Account, authentication, beforeData, afterData map[string]any, originalErr error, ) { if s.db == nil || s.lifecycleAudit == nil || account == nil || account.ID == 0 { return } errorCode := strconv.Itoa(errors.CodeInternalError) var appErr *errors.AppError if stdErrors.As(originalErr, &appErr) { errorCode = strconv.Itoa(appErr.Code) } else if result == constants.AuditResultDenied { errorCode = strconv.Itoa(errors.CodeForbidden) } operatorID := middleware.GetUserIDFromContext(ctx) if operatorID == 0 { operatorID = account.ID } err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return s.lifecycleAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{ ActionCode: actionCode, Summary: summary, Result: result, ErrorCode: errorCode, ErrorSummary: summary, ActorID: operatorID, Account: account, AuthenticationKey: fmt.Sprintf("account:%d:security", account.ID), Authentication: authentication, BeforeData: beforeData, AfterData: afterData, }) }) if err != nil { requestID := "" if value := middleware.GetRequestIDFromContext(ctx); value != nil { requestID = *value } auditfailure.RecordSecondaryWriteFailure(actionCode, account.Username, requestID, requestID, errorCode, err) } } func passwordAuthentication(account *model.Account, state string) map[string]any { return map[string]any{"account_id": account.ID, "auth_method": "password", "state": state} } // UpdateStatus 修改账号状态(启用/禁用) func (s *Service) UpdateStatus(ctx context.Context, accountID uint, status int) error { currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return errors.New(errors.CodeUnauthorized, "未授权访问") } if status != constants.StatusDisabled && status != constants.StatusEnabled { return errors.New(errors.CodeInvalidParam, "账号状态无效") } account, err := s.accountStore.GetByID(ctx, accountID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeAccountNotFound, "账号不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取账号失败") } if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent { if account.ShopID == nil || middleware.CanManageShop(ctx, *account.ShopID) != nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "拒绝更新账号状态", constants.AuditResultDenied, account, nil, nil, errors.New(errors.CodeForbidden)) return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } beforeData := accountLifecycleData(account) if err := s.runLifecycleTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewAccountStore(tx, nil).UpdateStatus(ctx, accountID, status, currentUserID); err != nil { return err } account.Status = status account.Updater = currentUserID shop, enterprise, roles, err := loadLifecycleResources(ctx, tx, account) if err != nil { return err } return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{ ActionCode: constants.AuditActionAccountUpdated, Summary: "更新账号状态", Result: constants.AuditResultSuccess, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, BeforeData: beforeData, AfterData: accountLifecycleData(account), }) }); err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionAccountUpdated, "更新账号状态失败", constants.AuditResultFailed, account, beforeData, nil, err) return errors.Wrap(errors.CodeInternalError, err, "更新状态失败") } return nil } func (s *Service) runLifecycleTransaction(ctx context.Context, fn func(*gorm.DB) error) error { if s.db == nil || s.lifecycleAudit == nil { return errors.New(errors.CodeInvalidStatus, "账号生命周期审计接缝未配置") } return s.db.WithContext(ctx).Transaction(fn) } func (s *Service) recordLifecycleFailure( ctx context.Context, actionCode, summary, result string, account *model.Account, beforeData, afterData map[string]any, originalErr error, ) { if s.db == nil || s.lifecycleAudit == nil || account == nil { return } errorCode := strconv.Itoa(errors.CodeInternalError) var appErr *errors.AppError if stdErrors.As(originalErr, &appErr) { errorCode = strconv.Itoa(appErr.Code) } else if result == constants.AuditResultDenied { errorCode = strconv.Itoa(errors.CodeForbidden) } err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { shop, enterprise, roles, loadErr := loadLifecycleResources(ctx, tx, account) if loadErr != nil { return loadErr } return s.lifecycleAudit.WriteAccountLifecycle(ctx, tx, accountauditapp.LifecycleAudit{ ActionCode: actionCode, Summary: summary, Result: result, ErrorCode: errorCode, ErrorSummary: summary, Account: account, Shop: shop, Enterprise: enterprise, Roles: roles, BeforeData: beforeData, AfterData: afterData, }) }) if err != nil { requestID := "" if value := middleware.GetRequestIDFromContext(ctx); value != nil { requestID = *value } auditfailure.RecordSecondaryWriteFailure(actionCode, account.Username, requestID, requestID, errorCode, err) } } func loadLifecycleResources(ctx context.Context, tx *gorm.DB, account *model.Account) (*model.Shop, *model.Enterprise, []*model.Role, error) { var shop *model.Shop if account.ShopID != nil { shop = &model.Shop{} if err := tx.WithContext(ctx).Unscoped().First(shop, *account.ShopID).Error; err != nil { return nil, nil, nil, err } } var enterprise *model.Enterprise if account.EnterpriseID != nil { enterprise = &model.Enterprise{} if err := tx.WithContext(ctx).Unscoped().First(enterprise, *account.EnterpriseID).Error; err != nil { return nil, nil, nil, err } } var roles []*model.Role if account.ID != 0 { if err := tx.WithContext(ctx).Table("tb_role AS r"). Joins("JOIN tb_account_role AS ar ON ar.role_id = r.id AND ar.deleted_at IS NULL"). Where("ar.account_id = ?", account.ID).Order("r.id ASC").Find(&roles).Error; err != nil { return nil, nil, nil, err } } return shop, enterprise, roles, nil } func accountLifecycleData(account *model.Account) map[string]any { return map[string]any{ "id": account.ID, "username": account.Username, "phone": account.Phone, "user_type": account.UserType, "shop_id": account.ShopID, "enterprise_id": account.EnterpriseID, "status": account.Status, } } // ListPlatformAccounts 查询平台账号列表(自动筛选 user_type IN (1, 2)) func (s *Service) ListPlatformAccounts(ctx context.Context, req *dto.PlatformAccountListRequest) ([]*model.Account, int64, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "id DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.Username != "" { filters["username"] = req.Username } if req.Phone != "" { filters["phone"] = req.Phone } if req.Status != nil { filters["status"] = *req.Status } return s.accountStore.ListPlatformAccounts(ctx, opts, filters) } // CreateSystemAccount 系统内部创建账号方法,用于系统初始化场景(绕过当前用户检查) func (s *Service) CreateSystemAccount(ctx context.Context, account *model.Account) error { if account.Username == "" { return errors.New(errors.CodeInvalidParam, "用户名不能为空") } if account.Phone == "" { return errors.New(errors.CodeInvalidParam, "手机号不能为空") } if account.Password == "" { return errors.New(errors.CodeInvalidParam, "密码不能为空") } existing, err := s.accountStore.GetByUsername(ctx, account.Username) if err == nil && existing != nil { return errors.New(errors.CodeUsernameExists, "用户名已存在") } existing, err = s.accountStore.GetByPhone(ctx, account.Phone) if err == nil && existing != nil { return errors.New(errors.CodePhoneExists, "手机号已存在") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败") } account.Password = string(hashedPassword) if err := s.accountStore.Create(ctx, account); err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建账号失败") } return nil } // loadShopNames 批量加载店铺名称 func (s *Service) loadShopNames(ctx context.Context, accounts []*model.Account) map[uint]string { shopIDs := make([]uint, 0) shopIDSet := make(map[uint]bool) for _, acc := range accounts { if acc.ShopID != nil && *acc.ShopID > 0 && !shopIDSet[*acc.ShopID] { shopIDs = append(shopIDs, *acc.ShopID) shopIDSet[*acc.ShopID] = true } } shopMap := make(map[uint]string) if len(shopIDs) > 0 { shops, err := s.shopStore.GetByIDs(ctx, shopIDs) if err == nil { for _, shop := range shops { shopMap[shop.ID] = shop.ShopName } } } return shopMap } // loadEnterpriseNames 批量加载企业名称 func (s *Service) loadEnterpriseNames(ctx context.Context, accounts []*model.Account) map[uint]string { enterpriseIDs := make([]uint, 0) enterpriseIDSet := make(map[uint]bool) for _, acc := range accounts { if acc.EnterpriseID != nil && *acc.EnterpriseID > 0 && !enterpriseIDSet[*acc.EnterpriseID] { enterpriseIDs = append(enterpriseIDs, *acc.EnterpriseID) enterpriseIDSet[*acc.EnterpriseID] = true } } enterpriseMap := make(map[uint]string) if len(enterpriseIDs) > 0 { enterprises, err := s.enterpriseStore.GetByIDs(ctx, enterpriseIDs) if err == nil { for _, ent := range enterprises { enterpriseMap[ent.ID] = ent.EnterpriseName } } } return enterpriseMap } // toAccountResponse 组装账号响应,填充关联名称 func (s *Service) toAccountResponse(acc *model.Account, shopMap map[uint]string, enterpriseMap map[uint]string) *dto.AccountResponse { resp := &dto.AccountResponse{ ID: acc.ID, Username: acc.Username, Phone: acc.Phone, UserType: acc.UserType, ShopID: acc.ShopID, EnterpriseID: acc.EnterpriseID, WeComCorpID: acc.WeComCorpID, WeComUserID: acc.WeComUserID, WeComName: acc.WeComName, WeComBound: acc.WeComCorpID != "" && acc.WeComUserID != "", Status: acc.Status, StatusName: constants.GetStatusName(acc.Status), Creator: acc.Creator, Updater: acc.Updater, CreatedAt: acc.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: acc.UpdatedAt.Format("2006-01-02 15:04:05"), } if acc.ShopID != nil && *acc.ShopID > 0 { resp.ShopName = shopMap[*acc.ShopID] } if acc.EnterpriseID != nil && *acc.EnterpriseID > 0 { resp.EnterpriseName = enterpriseMap[*acc.EnterpriseID] } return resp }