// Package account 提供账号管理的业务逻辑服务 // 包含账号创建、查询、更新、删除、密码管理等功能 package account import ( "context" "fmt" "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/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // Service 账号业务服务 type Service struct { accountStore *postgres.AccountStore roleStore *postgres.RoleStore accountRoleStore *postgres.AccountRoleStore shopRoleStore *postgres.ShopRoleStore shopStore middleware.ShopStoreInterface enterpriseStore middleware.EnterpriseStoreInterface auditService AuditServiceInterface } type AuditServiceInterface interface { LogOperation(ctx context.Context, log *model.AccountOperationLog) } // New 创建账号服务 func New( accountStore *postgres.AccountStore, roleStore *postgres.RoleStore, accountRoleStore *postgres.AccountRoleStore, shopRoleStore *postgres.ShopRoleStore, shopStore middleware.ShopStoreInterface, enterpriseStore middleware.EnterpriseStoreInterface, auditService AuditServiceInterface, ) *Service { return &Service{ accountStore: accountStore, roleStore: roleStore, accountRoleStore: accountRoleStore, shopRoleStore: shopRoleStore, shopStore: shopStore, enterpriseStore: enterpriseStore, auditService: auditService, } } // 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, s.shopStore); err != nil { return nil, err } } if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil { if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore, s.shopStore); 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.accountStore.Create(ctx, account); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败") } currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID) operatorName := "" if currentAccount != nil { operatorName = currentAccount.Username } afterData := model.JSONB{ "id": account.ID, "username": account.Username, "phone": account.Phone, "user_type": account.UserType, "shop_id": account.ShopID, "enterprise_id": account.EnterpriseID, "status": account.Status, } requestID := middleware.GetRequestIDFromContext(ctx) ipAddress := middleware.GetIPFromContext(ctx) userAgent := middleware.GetUserAgentFromContext(ctx) s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: currentUserID, OperatorType: userType, OperatorName: operatorName, TargetAccountID: &account.ID, TargetUsername: &account.Username, TargetUserType: &account.UserType, OperationType: "create", OperationDesc: fmt.Sprintf("创建账号: %s", account.Username), AfterData: afterData, RequestID: requestID, IPAddress: ipAddress, UserAgent: userAgent, }) return account, nil } // Get 获取账号 func (s *Service) Get(ctx context.Context, id uint) (*model.Account, 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, "获取账号失败") } return account, 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 { return nil, errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } beforeData := model.JSONB{ "username": account.Username, "phone": account.Phone, "status": account.Status, } if req.Username != nil { existing, err := s.accountStore.GetByUsername(ctx, *req.Username) if err == nil && existing != nil && existing.ID != id { 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 { 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 { 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.accountStore.Update(ctx, account); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败") } currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID) operatorName := "" if currentAccount != nil { operatorName = currentAccount.Username } afterData := model.JSONB{ "username": account.Username, "phone": account.Phone, "status": account.Status, } requestID := middleware.GetRequestIDFromContext(ctx) ipAddress := middleware.GetIPFromContext(ctx) userAgent := middleware.GetUserAgentFromContext(ctx) s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: currentUserID, OperatorType: userType, OperatorName: operatorName, TargetAccountID: &account.ID, TargetUsername: &account.Username, TargetUserType: &account.UserType, OperationType: "update", OperationDesc: fmt.Sprintf("更新账号: %s", account.Username), BeforeData: beforeData, AfterData: afterData, RequestID: requestID, IPAddress: ipAddress, UserAgent: userAgent, }) 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 { return errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } beforeData := model.JSONB{ "id": account.ID, "username": account.Username, "phone": account.Phone, "status": account.Status, } if err := s.accountStore.Delete(ctx, id); err != nil { return errors.Wrap(errors.CodeInternalError, err, "删除账号失败") } currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID) operatorName := "" if currentAccount != nil { operatorName = currentAccount.Username } requestID := middleware.GetRequestIDFromContext(ctx) ipAddress := middleware.GetIPFromContext(ctx) userAgent := middleware.GetUserAgentFromContext(ctx) s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: currentUserID, OperatorType: userType, OperatorName: operatorName, TargetAccountID: &account.ID, TargetUsername: &account.Username, TargetUserType: &account.UserType, OperationType: "delete", OperationDesc: fmt.Sprintf("删除账号: %s", account.Username), BeforeData: beforeData, RequestID: requestID, IPAddress: ipAddress, UserAgent: userAgent, }) 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 { return nil, errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } if account.UserType == constants.UserTypeSuperAdmin { return nil, errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色") } // 空数组:清空所有角色 if len(roleIDs) == 0 { if err := s.accountRoleStore.DeleteByAccountID(ctx, accountID); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "清空账号角色失败") } return []*model.AccountRole{}, nil } maxRoles := constants.GetMaxRolesForUserType(account.UserType) if maxRoles == 0 { return nil, errors.New(errors.CodeInvalidParam, "该用户类型不需要分配角色") } existingCount, err := s.accountRoleStore.CountByAccountID(ctx, accountID) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "统计现有角色数量失败") } newRoleCount := 0 for _, roleID := range roleIDs { exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID) if !exists { newRoleCount++ } } if maxRoles != -1 && int(existingCount)+newRoleCount > maxRoles { return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("该用户类型最多只能分配 %d 个角色", maxRoles)) } for _, roleID := range roleIDs { role, err := s.roleStore.GetByID(ctx, roleID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeRoleNotFound, fmt.Sprintf("角色 %d 不存在", roleID)) } return nil, errors.Wrap(errors.CodeInternalError, err, "获取角色失败") } if !constants.IsRoleTypeMatchUserType(role.RoleType, account.UserType) { return nil, errors.New(errors.CodeInvalidParam, "角色类型与账号类型不匹配") } } var ars []*model.AccountRole for _, roleID := range roleIDs { exists, _ := s.accountRoleStore.Exists(ctx, accountID, roleID) if exists { continue } ar := &model.AccountRole{ AccountID: accountID, RoleID: roleID, Status: constants.StatusEnabled, Creator: currentUserID, Updater: currentUserID, } if err := s.accountRoleStore.Create(ctx, ar); err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号-角色关联失败") } ars = append(ars, ar) } currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID) operatorName := "" if currentAccount != nil { operatorName = currentAccount.Username } afterData := model.JSONB{ "role_ids": roleIDs, } requestID := middleware.GetRequestIDFromContext(ctx) ipAddress := middleware.GetIPFromContext(ctx) userAgent := middleware.GetUserAgentFromContext(ctx) s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: currentUserID, OperatorType: userType, OperatorName: operatorName, TargetAccountID: &account.ID, TargetUsername: &account.Username, TargetUserType: &account.UserType, OperationType: "assign_roles", OperationDesc: fmt.Sprintf("为账号 %s 分配角色", account.Username), AfterData: afterData, RequestID: requestID, IPAddress: ipAddress, UserAgent: userAgent, }) return ars, 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 { return errors.New(errors.CodeForbidden, "无权限操作该账号") } if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } if err := s.accountRoleStore.Delete(ctx, accountID, roleID); err != nil { return errors.Wrap(errors.CodeInternalError, err, "删除账号-角色关联失败") } currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID) operatorName := "" if currentAccount != nil { operatorName = currentAccount.Username } afterData := model.JSONB{ "removed_role_id": roleID, } requestID := middleware.GetRequestIDFromContext(ctx) ipAddress := middleware.GetIPFromContext(ctx) userAgent := middleware.GetUserAgentFromContext(ctx) s.auditService.LogOperation(ctx, &model.AccountOperationLog{ OperatorID: currentUserID, OperatorType: userType, OperatorName: operatorName, TargetAccountID: &account.ID, TargetUsername: &account.Username, TargetUserType: &account.UserType, OperationType: "remove_role", OperationDesc: fmt.Sprintf("移除账号 %s 的角色", account.Username), AfterData: afterData, RequestID: requestID, IPAddress: ipAddress, UserAgent: userAgent, }) return nil } // 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, "未授权访问") } _, 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 { return errors.Wrap(errors.CodeInternalError, err, "密码哈希失败") } if err := s.accountStore.UpdatePassword(ctx, accountID, string(hashedPassword), currentUserID); err != nil { return errors.Wrap(errors.CodeInternalError, err, "更新密码失败") } return nil } // 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, "未授权访问") } _, 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 err := s.accountStore.UpdateStatus(ctx, accountID, status, currentUserID); err != nil { return errors.Wrap(errors.CodeInternalError, err, "更新状态失败") } return nil } // 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, Status: 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 }