package service import ( "context" "errors" "fmt" "strings" "time" "github.com/user-management-system/internal/auth" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/pagination" "github.com/user-management-system/internal/repository" "gorm.io/gorm" ) // UserService 用户服务 type UserService struct { userRepo *repository.UserRepository userRoleRepo *repository.UserRoleRepository roleRepo *repository.RoleRepository passwordHistoryRepo *repository.PasswordHistoryRepository } const passwordHistoryLimit = 5 // 保留最近5条密码历史 // NewUserService 创建用户服务实例 func NewUserService( userRepo *repository.UserRepository, userRoleRepo *repository.UserRoleRepository, roleRepo *repository.RoleRepository, passwordHistoryRepo *repository.PasswordHistoryRepository, ) *UserService { return &UserService{ userRepo: userRepo, userRoleRepo: userRoleRepo, roleRepo: roleRepo, passwordHistoryRepo: passwordHistoryRepo, } } // ChangePassword 修改用户密码(含历史记录检查) func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error { if s.userRepo == nil { return errors.New("user repository is not configured") } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return errors.New("用户不存在") } // 验证旧密码 if strings.TrimSpace(oldPassword) == "" { return errors.New("请输入当前密码") } if !auth.VerifyPassword(user.Password, oldPassword) { return errors.New("当前密码不正确") } // 检查新密码强度 if strings.TrimSpace(newPassword) == "" { return errors.New("新密码不能为空") } if err := validatePasswordStrength(newPassword, 8, false); err != nil { return err } // 检查密码历史(需要明文密码比对,必须在哈希之前) if s.passwordHistoryRepo != nil { histories, err := s.passwordHistoryRepo.GetByUserID(ctx, userID, passwordHistoryLimit) if err == nil && len(histories) > 0 { for _, h := range histories { if auth.VerifyPassword(h.PasswordHash, newPassword) { return errors.New("新密码不能与最近5次密码相同") } } } } // 计算一次哈希,用于更新密码和保存历史(避免 Argon2id 重复计算的高成本) newHashedPassword, hashErr := auth.HashPassword(newPassword) if hashErr != nil { return errors.New("密码哈希失败") } // 保存新密码到历史记录(异步,不阻塞密码更新) if s.passwordHistoryRepo != nil { // #nosec G118 - 使用带超时的独立 context(不能使用请求 ctx,该 goroutine 在请求完成后仍可能运行) go func(hashedPw string) { // #nosec G118 bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = s.passwordHistoryRepo.Create(bgCtx, &domain.PasswordHistory{ UserID: userID, PasswordHash: hashedPw, }) _ = s.passwordHistoryRepo.DeleteOldRecords(bgCtx, userID, passwordHistoryLimit) }(newHashedPassword) } // 更新密码(使用同一哈希值) user.Password = newHashedPassword return s.userRepo.Update(ctx, user) } // GetByID 根据ID获取用户 func (s *UserService) GetByID(ctx context.Context, id int64) (*domain.User, error) { return s.userRepo.GetByID(ctx, id) } // GetByEmail 根据邮箱获取用户 func (s *UserService) GetByEmail(ctx context.Context, email string) (*domain.User, error) { return s.userRepo.GetByEmail(ctx, email) } // Create 创建用户 func (s *UserService) Create(ctx context.Context, user *domain.User) error { return s.userRepo.Create(ctx, user) } // Update 更新用户 func (s *UserService) Update(ctx context.Context, user *domain.User) error { return s.userRepo.Update(ctx, user) } // Delete 删除用户 func (s *UserService) Delete(ctx context.Context, id int64) error { return s.userRepo.Delete(ctx, id) } // List 获取用户列表 func (s *UserService) List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error) { return s.userRepo.List(ctx, offset, limit) } // ListCursorRequest 用户游标分页请求 type ListCursorRequest struct { Keyword string `form:"keyword"` Status int `form:"status"` // -1=全部 RoleIDs []int64 CreatedFrom *time.Time CreatedTo *time.Time SortBy string // created_at, last_login_time, username SortOrder string // asc, desc Cursor string `form:"cursor"` Size int `form:"size"` } // ListCursor 游标分页获取用户列表(推荐使用) func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*CursorResult, error) { size := pagination.ClampPageSize(req.Size) cursor, err := pagination.Decode(req.Cursor) if err != nil { return nil, fmt.Errorf("invalid cursor: %w", err) } filter := &repository.AdvancedFilter{ Keyword: req.Keyword, Status: req.Status, RoleIDs: req.RoleIDs, CreatedFrom: req.CreatedFrom, CreatedTo: req.CreatedTo, SortBy: req.SortBy, SortOrder: req.SortOrder, } users, hasMore, err := s.userRepo.ListCursor(ctx, filter, size, cursor) if err != nil { return nil, err } nextCursor := "" if len(users) > 0 { last := users[len(users)-1] nextCursor = pagination.BuildNextCursor(last.ID, last.CreatedAt) } return &CursorResult{ Items: users, NextCursor: nextCursor, HasMore: hasMore, PageSize: size, }, nil } // UpdateStatus 更新用户状态 func (s *UserService) UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error { return s.userRepo.UpdateStatus(ctx, id, status) } // BatchUpdateStatusRequest 批量更新状态请求 type BatchUpdateStatusRequest struct { IDs []int64 `json:"ids" binding:"required,min=1"` Status domain.UserStatus `json:"status" binding:"required"` } // BatchDeleteRequest 批量删除请求 type BatchDeleteRequest struct { IDs []int64 `json:"ids" binding:"required,min=1"` } // BatchUpdateStatus 批量更新用户状态 func (s *UserService) BatchUpdateStatus(ctx context.Context, req *BatchUpdateStatusRequest) (int64, error) { err := s.userRepo.BatchUpdateStatus(ctx, req.IDs, req.Status) return int64(len(req.IDs)), err } // BatchDelete 批量删除用户 func (s *UserService) BatchDelete(ctx context.Context, req *BatchDeleteRequest) (int64, error) { err := s.userRepo.BatchDelete(ctx, req.IDs) return int64(len(req.IDs)), err } // GetUserRoles 获取用户的所有角色 func (s *UserService) GetUserRoles(ctx context.Context, userID int64) ([]*domain.Role, error) { // 检查用户是否存在 if _, err := s.userRepo.GetByID(ctx, userID); err != nil { return nil, err } // 获取用户角色关联 userRoles, err := s.userRoleRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } if len(userRoles) == 0 { return []*domain.Role{}, nil } // 获取角色ID列表 roleIDs := make([]int64, len(userRoles)) for i, ur := range userRoles { roleIDs[i] = ur.RoleID } // 批量获取角色详情(消除 N+1 查询) roles, err := s.roleRepo.GetByIDs(ctx, roleIDs) if err != nil { return nil, fmt.Errorf("failed to fetch roles: %w", err) } return roles, nil } // AssignRoles 分配用户角色 func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []int64) error { // 检查用户是否存在 if _, err := s.userRepo.GetByID(ctx, userID); err != nil { return err } // 验证所有角色存在(预先验证,避免在事务内做不必要的查询) for _, roleID := range roleIDs { if _, err := s.roleRepo.GetByID(ctx, roleID); err != nil { return fmt.Errorf("角色 %d 不存在", roleID) } } // 构建新的用户角色关联 var userRoles []*domain.UserRole for _, roleID := range roleIDs { userRoles = append(userRoles, &domain.UserRole{ UserID: userID, RoleID: roleID, }) } // 使用事务包装删旧建新操作,确保原子性 return s.userRoleRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.userRoleRepo.WithTx(tx).DeleteByUserID(ctx, userID); err != nil { return err } return s.userRoleRepo.WithTx(tx).BatchCreate(ctx, userRoles) }) } // getAdminRoleID looks up the admin role ID by code to avoid hardcoded magic numbers. func (s *UserService) getAdminRoleID(ctx context.Context) (int64, error) { adminRole, err := s.roleRepo.GetByCode(ctx, "admin") if err != nil { return 0, fmt.Errorf("failed to find admin role: %w", err) } return adminRole.ID, nil } // ListAdmins 获取所有管理员 func (s *UserService) ListAdmins(ctx context.Context) ([]*domain.User, error) { // 获取管理员角色ID列表 adminRoleID, err := s.getAdminRoleID(ctx) if err != nil { return nil, err } adminUserIDs, err := s.userRoleRepo.GetUserIDByRoleID(ctx, adminRoleID) if err != nil { return nil, err } if len(adminUserIDs) == 0 { return []*domain.User{}, nil } // 批量获取所有管理员用户(消除 N+1 查询) admins, err := s.userRepo.GetByIDs(ctx, adminUserIDs) if err != nil { return nil, fmt.Errorf("failed to fetch admin users: %w", err) } return admins, nil } // CreateAdmin 创建管理员(事务性) func (s *UserService) CreateAdmin(ctx context.Context, req *CreateAdminRequest) (*domain.User, error) { // 检查用户名是否已存在 existingUser, err := s.userRepo.GetByUsername(ctx, req.Username) if err == nil && existingUser != nil { return nil, errors.New("用户名已存在") } // 预先查询管理员角色 ID(避免在事务中使用 roleRepo) adminRoleID, err := s.getAdminRoleID(ctx) if err != nil { return nil, err } // 创建用户 hashedPassword, err := auth.HashPassword(req.Password) if err != nil { return nil, errors.New("密码哈希失败") } user := &domain.User{ Username: req.Username, Password: hashedPassword, Status: domain.UserStatusActive, } if req.Email != "" { user.Email = &req.Email } if req.Nickname != "" { user.Nickname = req.Nickname } // 使用事务创建用户和分配角色 err = s.userRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(user).Error; err != nil { return err } // 分配管理员角色 userRole := &domain.UserRole{ UserID: user.ID, RoleID: adminRoleID, } if err := tx.Create(userRole).Error; err != nil { return err } return nil }) if err != nil { return nil, err } return user, nil } // DeleteAdmin 删除管理员(移除管理员角色) func (s *UserService) DeleteAdmin(ctx context.Context, userID int64, currentUserID int64) error { // 检查用户是否存在 if _, err := s.userRepo.GetByID(ctx, userID); err != nil { return err } // 不能删除自己 if currentUserID == userID { return errors.New("不能删除自己") } // 检查是否是最后一个管理员(保护) adminRoleID, err := s.getAdminRoleID(ctx) if err != nil { return err } adminUserRoles, err := s.userRoleRepo.GetByRoleID(ctx, adminRoleID) if err != nil { return err } if len(adminUserRoles) <= 1 { return errors.New("不能删除最后一个管理员") } // 删除用户的管理员角色 return s.userRoleRepo.DeleteByUserAndRole(ctx, userID, adminRoleID) } // CreateAdminRequest 创建管理员请求 type CreateAdminRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` Email string `json:"email"` Nickname string `json:"nickname"` }