package service import ( "context" "fmt" "strings" "carrot_bbs/internal/cache" apperrors "carrot_bbs/internal/errors" "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/utils" "carrot_bbs/internal/repository" ) // UserService 用户服务接口 type UserService interface { // 验证码相关 SendRegisterCode(ctx context.Context, email string) error SendPasswordResetCode(ctx context.Context, email string) error SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error SendChangePasswordCode(ctx context.Context, userID string) error // 邮箱验证 VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error // 用户认证 Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) Login(ctx context.Context, account, password string) (*model.User, error) // 用户查询 GetUserByID(ctx context.Context, id string) (*model.User, error) GetUserPostCount(ctx context.Context, userID string) (int64, error) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) // 用户更新 UpdateUser(ctx context.Context, user *model.User) error ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error // 关注相关 GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) FollowUser(ctx context.Context, followerID, followeeID string) error UnfollowUser(ctx context.Context, followerID, followeeID string) error GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) // 黑名单相关 BlockUser(ctx context.Context, blockerID, blockedID string) error UnblockUser(ctx context.Context, blockerID, blockedID string) error GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) } // userServiceImpl 用户服务实现 type userServiceImpl struct { userRepo *repository.UserRepository systemMessageService SystemMessageService emailCodeService EmailCodeService } // NewUserService 创建用户服务 func NewUserService( userRepo *repository.UserRepository, systemMessageService SystemMessageService, emailService EmailService, cacheBackend cache.Cache, ) UserService { return &userServiceImpl{ userRepo: userRepo, systemMessageService: systemMessageService, emailCodeService: NewEmailCodeService(emailService, cacheBackend), } } // SendRegisterCode 发送注册验证码 func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error { user, err := s.userRepo.GetByEmail(email) if err == nil && user != nil { return ErrEmailExists } return s.emailCodeService.SendCode(ctx, CodePurposeRegister, email) } // SendPasswordResetCode 发送找回密码验证码 func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email string) error { user, err := s.userRepo.GetByEmail(email) if err != nil || user == nil { return ErrUserNotFound } return s.emailCodeService.SendCode(ctx, CodePurposePasswordReset, email) } // SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码 func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error { user, err := s.userRepo.GetByID(userID) if err != nil || user == nil { return ErrUserNotFound } targetEmail := strings.TrimSpace(email) if targetEmail == "" && user.Email != nil { targetEmail = strings.TrimSpace(*user.Email) } if targetEmail == "" || !utils.ValidateEmail(targetEmail) { return ErrInvalidEmail } if user.EmailVerified && user.Email != nil && strings.EqualFold(strings.TrimSpace(*user.Email), targetEmail) { return ErrEmailAlreadyVerified } existingUser, queryErr := s.userRepo.GetByEmail(targetEmail) if queryErr == nil && existingUser != nil && existingUser.ID != userID { return ErrEmailExists } return s.emailCodeService.SendCode(ctx, CodePurposeEmailVerify, targetEmail) } // VerifyCurrentUserEmail 验证当前用户邮箱 func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error { user, err := s.userRepo.GetByID(userID) if err != nil || user == nil { return ErrUserNotFound } targetEmail := strings.TrimSpace(email) if targetEmail == "" && user.Email != nil { targetEmail = strings.TrimSpace(*user.Email) } if targetEmail == "" || !utils.ValidateEmail(targetEmail) { return ErrInvalidEmail } if err := s.emailCodeService.VerifyCode(CodePurposeEmailVerify, targetEmail, verificationCode); err != nil { return err } existingUser, queryErr := s.userRepo.GetByEmail(targetEmail) if queryErr == nil && existingUser != nil && existingUser.ID != userID { return ErrEmailExists } user.Email = &targetEmail user.EmailVerified = true return s.userRepo.Update(user) } // SendChangePasswordCode 发送修改密码验证码 func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID string) error { user, err := s.userRepo.GetByID(userID) if err != nil || user == nil { return ErrUserNotFound } if user.Email == nil || strings.TrimSpace(*user.Email) == "" { return ErrEmailNotBound } return s.emailCodeService.SendCode(ctx, CodePurposeChangePassword, *user.Email) } // Register 用户注册 func (s *userServiceImpl) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) { // 验证用户名 if !utils.ValidateUsername(username) { return nil, ErrInvalidUsername } // 注册必须提供邮箱并完成验证码校验 if email == "" || !utils.ValidateEmail(email) { return nil, ErrInvalidEmail } if err := s.emailCodeService.VerifyCode(CodePurposeRegister, email, verificationCode); err != nil { return nil, err } // 验证密码 if !utils.ValidatePassword(password) { return nil, ErrWeakPassword } // 验证手机号(如果提供) if phone != "" && !utils.ValidatePhone(phone) { return nil, ErrInvalidPhone } // 检查用户名是否已存在 existingUser, err := s.userRepo.GetByUsername(username) if err == nil && existingUser != nil { return nil, ErrUsernameExists } // 检查邮箱是否已存在(如果提供) if email != "" { existingUser, err = s.userRepo.GetByEmail(email) if err == nil && existingUser != nil { return nil, ErrEmailExists } } // 检查手机号是否已存在(如果提供) if phone != "" { existingUser, err = s.userRepo.GetByPhone(phone) if err == nil && existingUser != nil { return nil, ErrPhoneExists } } // 密码哈希 hashedPassword, err := utils.HashPassword(password) if err != nil { return nil, err } // 创建用户 user := &model.User{ Username: username, Nickname: nickname, EmailVerified: true, PasswordHash: hashedPassword, Status: model.UserStatusActive, } // 如果提供了邮箱,设置指针值 if email != "" { user.Email = &email } // 如果提供了手机号,设置指针值 if phone != "" { user.Phone = &phone } err = s.userRepo.Create(user) if err != nil { return nil, err } return user, nil } // Login 用户登录 func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) { account = strings.TrimSpace(account) var ( user *model.User err error ) if utils.ValidateEmail(account) { user, err = s.userRepo.GetByEmail(account) } else if utils.ValidatePhone(account) { user, err = s.userRepo.GetByPhone(account) } else { user, err = s.userRepo.GetByUsername(account) } if err != nil || user == nil { return nil, ErrInvalidCredentials } if !utils.CheckPasswordHash(password, user.PasswordHash) { return nil, ErrInvalidCredentials } if user.Status != model.UserStatusActive { return nil, ErrUserBanned } return user, nil } // GetUserByID 根据ID获取用户 func (s *userServiceImpl) GetUserByID(ctx context.Context, id string) (*model.User, error) { return s.userRepo.GetByID(id) } // GetUserPostCount 获取用户帖子数(实时计算) func (s *userServiceImpl) GetUserPostCount(ctx context.Context, userID string) (int64, error) { return s.userRepo.GetPostsCount(userID) } // GetUserPostCountBatch 批量获取用户帖子数(实时计算) func (s *userServiceImpl) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) { return s.userRepo.GetPostsCountBatch(userIDs) } // GetUserByIDWithFollowingStatus 根据ID获取用户(包含当前用户是否关注的状态) func (s *userServiceImpl) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) { user, err := s.userRepo.GetByID(userID) if err != nil { return nil, false, err } // 如果查询的是当前用户自己,不需要检查关注状态 if userID == currentUserID { return user, false, nil } isFollowing, err := s.userRepo.IsFollowing(currentUserID, userID) if err != nil { return user, false, err } return user, isFollowing, nil } // GetUserByIDWithMutualFollowStatus 根据ID获取用户(包含双向关注状态) func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) { user, err := s.userRepo.GetByID(userID) if err != nil { return nil, false, false, err } // 如果查询的是当前用户自己,不需要检查关注状态 if userID == currentUserID { return user, false, false, nil } // 当前用户是否关注了该用户 isFollowing, err := s.userRepo.IsFollowing(currentUserID, userID) if err != nil { return user, false, false, err } // 该用户是否关注了当前用户 isFollowingMe, err := s.userRepo.IsFollowing(userID, currentUserID) if err != nil { return user, isFollowing, false, err } return user, isFollowing, isFollowingMe, nil } // UpdateUser 更新用户 func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error { return s.userRepo.Update(user) } // GetFollowers 获取粉丝 func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) { return s.userRepo.GetFollowers(userID, page, pageSize) } // GetFollowing 获取关注 func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) { return s.userRepo.GetFollowing(userID, page, pageSize) } // FollowUser 关注用户 func (s *userServiceImpl) FollowUser(ctx context.Context, followerID, followeeID string) error { blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID) if err != nil { return err } if blocked { return ErrUserBlocked } // 检查是否已经关注 isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID) if err != nil { return err } if isFollowing { return nil // 已经关注,直接返回成功 } // 创建关注关系 follow := &model.Follow{ FollowerID: followerID, FollowingID: followeeID, } err = s.userRepo.CreateFollow(follow) if err != nil { return err } // 刷新关注者的关注数(通过实际计数,更可靠) err = s.userRepo.RefreshFollowingCount(followerID) if err != nil { // 不回滚,计数可以通过其他方式修复 } // 刷新被关注者的粉丝数(通过实际计数,更可靠) err = s.userRepo.RefreshFollowersCount(followeeID) if err != nil { // 不回滚,计数可以通过其他方式修复 } // 发送关注通知给被关注者 if s.systemMessageService != nil { // 异步发送通知,不阻塞主流程 go func() { _ = s.systemMessageService.SendFollowNotification(context.Background(), followeeID, followerID) }() } return nil } // UnfollowUser 取消关注用户 func (s *userServiceImpl) UnfollowUser(ctx context.Context, followerID, followeeID string) error { // 检查是否已经关注 isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID) if err != nil { return err } if !isFollowing { return nil // 没有关注,直接返回成功 } // 删除关注关系 err = s.userRepo.DeleteFollow(followerID, followeeID) if err != nil { return err } // 刷新关注者的关注数(通过实际计数,更可靠) err = s.userRepo.RefreshFollowingCount(followerID) if err != nil { } // 刷新被关注者的粉丝数(通过实际计数,更可靠) err = s.userRepo.RefreshFollowersCount(followeeID) if err != nil { } return nil } // BlockUser 拉黑用户,并自动清理双向关注/粉丝关系 func (s *userServiceImpl) BlockUser(ctx context.Context, blockerID, blockedID string) error { if blockerID == blockedID { return ErrInvalidOperation } return s.userRepo.BlockUserAndCleanupRelations(blockerID, blockedID) } // UnblockUser 取消拉黑 func (s *userServiceImpl) UnblockUser(ctx context.Context, blockerID, blockedID string) error { if blockerID == blockedID { return ErrInvalidOperation } return s.userRepo.UnblockUser(blockerID, blockedID) } // GetBlockedUsers 获取黑名单列表 func (s *userServiceImpl) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) { return s.userRepo.GetBlockedUsers(blockerID, page, pageSize) } // IsBlocked 检查当前用户是否已拉黑目标用户 func (s *userServiceImpl) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) { return s.userRepo.IsBlocked(blockerID, blockedID) } // GetFollowingList 获取关注列表(字符串参数版本) func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) { // 转换字符串参数为整数 pageInt := 1 pageSizeInt := 20 if page != "" { _, err := fmt.Sscanf(page, "%d", &pageInt) if err != nil { pageInt = 1 } } if pageSize != "" { _, err := fmt.Sscanf(pageSize, "%d", &pageSizeInt) if err != nil { pageSizeInt = 20 } } users, _, err := s.userRepo.GetFollowing(userID, pageInt, pageSizeInt) return users, err } // GetFollowersList 获取粉丝列表(字符串参数版本) func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) { // 转换字符串参数为整数 pageInt := 1 pageSizeInt := 20 if page != "" { _, err := fmt.Sscanf(page, "%d", &pageInt) if err != nil { pageInt = 1 } } if pageSize != "" { _, err := fmt.Sscanf(pageSize, "%d", &pageSizeInt) if err != nil { pageSizeInt = 20 } } users, _, err := s.userRepo.GetFollowers(userID, pageInt, pageSizeInt) return users, err } // GetMutualFollowStatus 批量获取双向关注状态 func (s *userServiceImpl) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) { return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs) } // CheckUsernameAvailable 检查用户名是否可用 func (s *userServiceImpl) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) { user, err := s.userRepo.GetByUsername(username) if err != nil { return true, nil // 用户不存在,可用 } return user == nil, nil } // ChangePassword 修改密码 func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error { // 获取用户 user, err := s.userRepo.GetByID(userID) if err != nil { return ErrUserNotFound } if user.Email == nil || strings.TrimSpace(*user.Email) == "" { return ErrEmailNotBound } if err := s.emailCodeService.VerifyCode(CodePurposeChangePassword, *user.Email, verificationCode); err != nil { return err } // 验证旧密码 if !utils.CheckPasswordHash(oldPassword, user.PasswordHash) { return ErrInvalidCredentials } // 哈希新密码 hashedPassword, err := utils.HashPassword(newPassword) if err != nil { return err } // 更新密码 user.PasswordHash = hashedPassword return s.userRepo.Update(user) } // ResetPasswordByEmail 通过邮箱重置密码 func (s *userServiceImpl) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error { email = strings.TrimSpace(email) if !utils.ValidateEmail(email) { return ErrInvalidEmail } if !utils.ValidatePassword(newPassword) { return ErrWeakPassword } if err := s.emailCodeService.VerifyCode(CodePurposePasswordReset, email, verificationCode); err != nil { return err } user, err := s.userRepo.GetByEmail(email) if err != nil || user == nil { return ErrUserNotFound } hashedPassword, err := utils.HashPassword(newPassword) if err != nil { return err } user.PasswordHash = hashedPassword return s.userRepo.Update(user) } // Search 搜索用户 func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) { return s.userRepo.Search(keyword, page, pageSize) } // 错误定义 - 使用统一的 AppError var ( ErrInvalidUsername = apperrors.ErrInvalidUsername ErrInvalidEmail = apperrors.ErrInvalidEmail ErrInvalidPhone = apperrors.ErrInvalidPhone ErrWeakPassword = apperrors.ErrWeakPassword ErrUsernameExists = apperrors.ErrUsernameExists ErrEmailExists = apperrors.ErrEmailExists ErrPhoneExists = apperrors.ErrPhoneExists ErrUserNotFound = apperrors.ErrUserNotFound ErrUserBanned = apperrors.ErrUserBanned ErrUserBlocked = apperrors.ErrUserBlocked ErrInvalidOperation = apperrors.ErrInvalidOperation ErrEmailServiceUnavailable = apperrors.ErrEmailServiceUnavailable ErrVerificationCodeTooFrequent = apperrors.ErrVerificationCodeTooFrequent ErrVerificationCodeInvalid = apperrors.ErrVerificationCodeInvalid ErrVerificationCodeExpired = apperrors.ErrVerificationCodeExpired ErrVerificationCodeUnavailable = apperrors.ErrVerificationCodeUnavailable ErrEmailAlreadyVerified = apperrors.ErrEmailAlreadyVerified ErrEmailNotBound = apperrors.ErrEmailNotBound ErrInvalidCredentials = apperrors.ErrInvalidCredentials )