Files
backend/internal/service/user_service.go

566 lines
17 KiB
Go
Raw Normal View History

package service
import (
"context"
"fmt"
"strings"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/utils"
"carrot_bbs/internal/repository"
)
// UserService 用户服务
type UserService struct {
userRepo *repository.UserRepository
systemMessageService SystemMessageService
emailCodeService EmailCodeService
}
// NewUserService 创建用户服务
func NewUserService(
userRepo *repository.UserRepository,
systemMessageService SystemMessageService,
emailService EmailService,
cacheBackend cache.Cache,
) *UserService {
return &UserService{
userRepo: userRepo,
systemMessageService: systemMessageService,
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
}
}
// SendRegisterCode 发送注册验证码
func (s *UserService) 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 *UserService) 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 *UserService) 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 *UserService) 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 *UserService) 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 *UserService) 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 *UserService) 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 *UserService) GetUserByID(ctx context.Context, id string) (*model.User, error) {
return s.userRepo.GetByID(id)
}
// GetUserPostCount 获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
return s.userRepo.GetPostsCount(userID)
}
// GetUserPostCountBatch 批量获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
return s.userRepo.GetPostsCountBatch(userIDs)
}
// GetUserByIDWithFollowingStatus 根据ID获取用户包含当前用户是否关注的状态
func (s *UserService) 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 *UserService) 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 *UserService) UpdateUser(ctx context.Context, user *model.User) error {
return s.userRepo.Update(user)
}
// GetFollowers 获取粉丝
func (s *UserService) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowers(userID, page, pageSize)
}
// GetFollowing 获取关注
func (s *UserService) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowing(userID, page, pageSize)
}
// FollowUser 关注用户
func (s *UserService) 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 *UserService) 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 *UserService) BlockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID {
return ErrInvalidOperation
}
return s.userRepo.BlockUserAndCleanupRelations(blockerID, blockedID)
}
// UnblockUser 取消拉黑
func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID {
return ErrInvalidOperation
}
return s.userRepo.UnblockUser(blockerID, blockedID)
}
// GetBlockedUsers 获取黑名单列表
func (s *UserService) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
}
// IsBlocked 检查当前用户是否已拉黑目标用户
func (s *UserService) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
return s.userRepo.IsBlocked(blockerID, blockedID)
}
// GetFollowingList 获取关注列表(字符串参数版本)
func (s *UserService) 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 *UserService) 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 *UserService) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
}
// CheckUsernameAvailable 检查用户名是否可用
func (s *UserService) 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 *UserService) 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 *UserService) 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 *UserService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.Search(keyword, page, pageSize)
}
// 错误定义
var (
ErrInvalidUsername = &ServiceError{Code: 400, Message: "invalid username"}
ErrInvalidEmail = &ServiceError{Code: 400, Message: "invalid email"}
ErrInvalidPhone = &ServiceError{Code: 400, Message: "invalid phone number"}
ErrWeakPassword = &ServiceError{Code: 400, Message: "password too weak"}
ErrUsernameExists = &ServiceError{Code: 400, Message: "username already exists"}
ErrEmailExists = &ServiceError{Code: 400, Message: "email already exists"}
ErrPhoneExists = &ServiceError{Code: 400, Message: "phone number already exists"}
ErrUserNotFound = &ServiceError{Code: 404, Message: "user not found"}
ErrUserBanned = &ServiceError{Code: 403, Message: "user is banned"}
ErrUserBlocked = &ServiceError{Code: 403, Message: "blocked relationship exists"}
ErrInvalidOperation = &ServiceError{Code: 400, Message: "invalid operation"}
ErrEmailServiceUnavailable = &ServiceError{Code: 503, Message: "email service unavailable"}
ErrVerificationCodeTooFrequent = &ServiceError{Code: 429, Message: "verification code sent too frequently"}
ErrVerificationCodeInvalid = &ServiceError{Code: 400, Message: "invalid verification code"}
ErrVerificationCodeExpired = &ServiceError{Code: 400, Message: "verification code expired"}
ErrVerificationCodeUnavailable = &ServiceError{Code: 500, Message: "verification code storage unavailable"}
ErrEmailAlreadyVerified = &ServiceError{Code: 400, Message: "email already verified"}
ErrEmailNotBound = &ServiceError{Code: 400, Message: "email not bound"}
)
// ServiceError 服务错误
type ServiceError struct {
Code int
Message string
}
func (e *ServiceError) Error() string {
return e.Message
}
var ErrInvalidCredentials = &ServiceError{Code: 401, Message: "invalid username or password"}