package service import ( "context" "errors" "fmt" "strings" "time" "unicode/utf8" "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" ) // Repository interfaces for dependency inversion (DIP) — service layer depends on these abstractions, not concrete types. type userRepository interface { GetByID(ctx context.Context, id int64) (*domain.User, error) GetByUsername(ctx context.Context, username string) (*domain.User, error) GetByEmail(ctx context.Context, email string) (*domain.User, error) Create(ctx context.Context, user *domain.User) error Update(ctx context.Context, user *domain.User) error Delete(ctx context.Context, id int64) error List(ctx context.Context, offset, limit int) ([]*domain.User, int64, error) ListCursor(ctx context.Context, filter *repository.AdvancedFilter, limit int, cursor *pagination.Cursor) ([]*domain.User, bool, error) GetByIDs(ctx context.Context, ids []int64) ([]*domain.User, error) UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error BatchUpdateStatus(ctx context.Context, ids []int64, status domain.UserStatus) error BatchDelete(ctx context.Context, ids []int64) error DB() *gorm.DB } type userRoleRepository interface { GetByUserID(ctx context.Context, userID int64) ([]*domain.UserRole, error) DeleteByUserID(ctx context.Context, userID int64) error DeleteByUserAndRole(ctx context.Context, userID, roleID int64) error GetByRoleID(ctx context.Context, roleID int64) ([]*domain.UserRole, error) GetUserIDByRoleID(ctx context.Context, roleID int64) ([]int64, error) BatchCreate(ctx context.Context, userRoles []*domain.UserRole) error ReplaceUserRoles(ctx context.Context, userID int64, roleIDs []int64) error DB() *gorm.DB } type roleRepository interface { GetByCode(ctx context.Context, code string) (*domain.Role, error) GetByID(ctx context.Context, id int64) (*domain.Role, error) GetByIDs(ctx context.Context, ids []int64) ([]*domain.Role, error) } type passwordHistoryRepository interface { GetByUserID(ctx context.Context, userID int64, limit int) ([]*domain.PasswordHistory, error) Create(ctx context.Context, history *domain.PasswordHistory) error DeleteOldRecords(ctx context.Context, userID int64, keep int) error } // UserService 用户服务 type UserService struct { userRepo userRepository userRoleRepo userRoleRepository roleRepo roleRepository passwordHistoryRepo passwordHistoryRepository } const passwordHistoryLimit = 5 // 保留最近5条密码历史 // NewUserService 创建用户服务实例 func NewUserService( userRepo userRepository, userRoleRepo userRoleRepository, roleRepo roleRepository, passwordHistoryRepo 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("密码哈希失败") } oldPasswordHash := user.Password oldPasswordChangedAt := user.PasswordChangedAt user.Password = newHashedPassword user.PasswordChangedAt = time.Now() if s.passwordHistoryRepo == nil { return s.userRepo.Update(ctx, user) } return s.userRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Model(&domain.User{}). Where("id = ?", user.ID). Updates(map[string]interface{}{"password": user.Password, "password_changed_at": user.PasswordChangedAt}).Error; err != nil { user.Password = oldPasswordHash user.PasswordChangedAt = oldPasswordChangedAt return err } if err := tx.Create(&domain.PasswordHistory{UserID: userID, PasswordHash: newHashedPassword}).Error; err != nil { user.Password = oldPasswordHash user.PasswordChangedAt = oldPasswordChangedAt return err } var ids []int64 if err := tx.Model(&domain.PasswordHistory{}). Where("user_id = ?", userID). Order("created_at DESC"). Limit(passwordHistoryLimit). Pluck("id", &ids).Error; err != nil { user.Password = oldPasswordHash user.PasswordChangedAt = oldPasswordChangedAt return err } if len(ids) > 0 { if err := tx.Where("user_id = ? AND id NOT IN ?", userID, ids).Delete(&domain.PasswordHistory{}).Error; err != nil { user.Password = oldPasswordHash user.PasswordChangedAt = oldPasswordChangedAt return err } } return nil }) } // 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 { // 验证用户名 if strings.TrimSpace(user.Username) == "" { return errors.New("用户名不能为空") } if len(user.Username) > 50 { return errors.New("用户名长度超过限制") } // 验证邮箱格式 if user.Email != nil && *user.Email != "" { if !isValidEmail(*user.Email) { return errors.New("邮箱格式不正确") } if len(*user.Email) > 100 { return errors.New("邮箱长度超过限制") } } // 验证昵称长度(按字符数计算) if utf8.RuneCountInString(user.Nickname) > 50 { return errors.New("昵称长度超过限制") } // 验证简介长度(按字符数计算) if utf8.RuneCountInString(user.Bio) > 500 { return errors.New("简介长度超过限制") } return s.userRepo.Create(ctx, user) } // isValidEmail 验证邮箱格式 func isValidEmail(email string) bool { if email == "" { return true } // 基本格式验证:必须包含@且@前后都有内容 atIndex := strings.Index(email, "@") if atIndex <= 0 || atIndex >= len(email)-1 { return false } // 检查是否包含空格 if strings.Contains(email, " ") { return false } // 检查是否只有一个@ if strings.Count(email, "@") != 1 { return false } return true } // 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) { // 处理无效的分页参数 if limit <= 0 { limit = 10 // 默认页面大小 } if offset < 0 { offset = 0 } 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"` } // UserCursorResult wraps cursor-based pagination response for users type UserCursorResult struct { Items []*domain.User `json:"items"` NextCursor string `json:"next_cursor"` HasMore bool `json:"has_more"` PageSize int `json:"page_size"` } // ListCursor 游标分页获取用户列表(推荐使用) func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (*UserCursorResult, 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 &UserCursorResult{ 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) } } // 使用 Repository 层的事务方法替换用户角色(原子操作) return s.userRoleRepo.ReplaceUserRoles(ctx, userID, roleIDs) } // 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"` }