Files
backend/internal/service/user_service.go
lan 4d8f2ec997 Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts.

Made-with: Cursor
2026-03-09 21:28:58 +08:00

593 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
fmt.Printf("[DEBUG] FollowUser called: followerID=%s, followeeID=%s\n", followerID, followeeID)
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 {
fmt.Printf("[DEBUG] Error checking existing follow: %v\n", err)
return err
}
if isFollowing {
fmt.Printf("[DEBUG] Already following, skip creation\n")
return nil // 已经关注,直接返回成功
}
// 创建关注关系
follow := &model.Follow{
FollowerID: followerID,
FollowingID: followeeID,
}
err = s.userRepo.CreateFollow(follow)
if err != nil {
fmt.Printf("[DEBUG] CreateFollow error: %v\n", err)
return err
}
fmt.Printf("[DEBUG] Follow record created successfully\n")
// 刷新关注者的关注数(通过实际计数,更可靠)
err = s.userRepo.RefreshFollowingCount(followerID)
if err != nil {
fmt.Printf("[DEBUG] Error refreshing following count: %v\n", err)
// 不回滚,计数可以通过其他方式修复
}
// 刷新被关注者的粉丝数(通过实际计数,更可靠)
err = s.userRepo.RefreshFollowersCount(followeeID)
if err != nil {
fmt.Printf("[DEBUG] Error refreshing followers count: %v\n", err)
// 不回滚,计数可以通过其他方式修复
}
// 发送关注通知给被关注者
if s.systemMessageService != nil {
// 异步发送通知,不阻塞主流程
go func() {
notifyErr := s.systemMessageService.SendFollowNotification(context.Background(), followeeID, followerID)
if notifyErr != nil {
fmt.Printf("[DEBUG] Error sending follow notification: %v\n", notifyErr)
} else {
fmt.Printf("[DEBUG] Follow notification sent successfully to %s\n", followeeID)
}
}()
}
fmt.Printf("[DEBUG] FollowUser completed: followerID=%s, followeeID=%s\n", followerID, followeeID)
return nil
}
// UnfollowUser 取消关注用户
func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
fmt.Printf("[DEBUG] UnfollowUser called: followerID=%s, followeeID=%s\n", followerID, followeeID)
// 检查是否已经关注
isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID)
if err != nil {
fmt.Printf("[DEBUG] Error checking existing follow: %v\n", err)
return err
}
if !isFollowing {
fmt.Printf("[DEBUG] Not following, skip deletion\n")
return nil // 没有关注,直接返回成功
}
// 删除关注关系
err = s.userRepo.DeleteFollow(followerID, followeeID)
if err != nil {
fmt.Printf("[DEBUG] DeleteFollow error: %v\n", err)
return err
}
fmt.Printf("[DEBUG] Follow record deleted successfully\n")
// 刷新关注者的关注数(通过实际计数,更可靠)
err = s.userRepo.RefreshFollowingCount(followerID)
if err != nil {
fmt.Printf("[DEBUG] Error refreshing following count: %v\n", err)
}
// 刷新被关注者的粉丝数(通过实际计数,更可靠)
err = s.userRepo.RefreshFollowersCount(followeeID)
if err != nil {
fmt.Printf("[DEBUG] Error refreshing followers count: %v\n", err)
}
fmt.Printf("[DEBUG] UnfollowUser completed: followerID=%s, followeeID=%s\n", followerID, followeeID)
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"}