Files
backend/internal/service/user_service.go

729 lines
22 KiB
Go
Raw Normal View History

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, clientIP string) error
SendPasswordResetCode(ctx context.Context, email, clientIP string) error
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email, clientIP string) error
SendChangePasswordCode(ctx context.Context, userID, clientIP 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 string, page, pageSize string) ([]*model.User, error)
GetFollowersList(ctx context.Context, userID string, 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)
// 日志服务设置
SetLogService(logService *LogService)
}
// userServiceImpl 用户服务实现
type userServiceImpl struct {
userRepo *repository.UserRepository
systemMessageService SystemMessageService
emailCodeService EmailCodeService
logService *LogService
}
// NewUserService 创建用户服务
func NewUserService(
userRepo *repository.UserRepository,
systemMessageService SystemMessageService,
emailService EmailService,
cacheBackend cache.Cache,
) UserService {
return &userServiceImpl{
userRepo: userRepo,
systemMessageService: systemMessageService,
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
logService: nil,
}
}
// SetLogService 设置日志服务
func (s *userServiceImpl) SetLogService(logService *LogService) {
s.logService = logService
}
// SendRegisterCode 发送注册验证码
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email, clientIP string) error {
user, err := s.userRepo.GetByEmail(email)
if err == nil && user != nil {
return ErrEmailExists
}
return s.emailCodeService.SendCode(ctx, CodePurposeRegister, email, clientIP)
}
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email, clientIP string) error {
user, err := s.userRepo.GetByEmail(email)
if err != nil || user == nil {
return ErrUserNotFound
}
return s.emailCodeService.SendCode(ctx, CodePurposePasswordReset, email, clientIP)
}
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email, clientIP 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, clientIP)
}
// 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, clientIP 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, clientIP)
}
// 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
}
// 记录注册日志
if s.logService != nil {
s.logService.OperationLog.RecordOperation(&model.OperationLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
Operation: string(model.OpUserRegister),
Module: "user",
TargetType: "user",
TargetID: user.ID,
IP: "",
Status: "success",
})
}
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)
}
// 登录失败处理
loginResult := string(model.LoginResultSuccess)
var failReason string
if err != nil || user == nil {
loginResult = string(model.LoginResultFail)
failReason = string(model.FailReasonUserNotFound)
// 记录失败登录日志
if s.logService != nil {
s.logService.LoginLog.RecordLogin(&model.LoginLog{
UserName: account,
Event: string(model.LoginEventFail),
IP: "",
UserAgent: "",
Result: loginResult,
FailReason: failReason,
})
}
return nil, ErrInvalidCredentials
}
if !utils.CheckPasswordHash(password, user.PasswordHash) {
loginResult = string(model.LoginResultFail)
failReason = string(model.FailReasonWrongPassword)
// 记录失败登录日志
if s.logService != nil {
s.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
Event: string(model.LoginEventFail),
IP: "",
UserAgent: "",
Result: loginResult,
FailReason: failReason,
})
}
return nil, ErrInvalidCredentials
}
if user.Status != model.UserStatusActive {
loginResult = string(model.LoginResultFail)
failReason = string(model.FailReasonAccountBanned)
// 记录失败登录日志
if s.logService != nil {
s.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
Event: string(model.LoginEventFail),
IP: "",
UserAgent: "",
Result: loginResult,
FailReason: failReason,
})
}
return nil, ErrUserBanned
}
// 记录成功登录日志
if s.logService != nil {
s.logService.LoginLog.RecordLogin(&model.LoginLog{
UserID: user.ID,
UserName: user.Username,
NickName: user.Nickname,
LoginType: string(model.LoginTypePassword),
Event: string(model.LoginEventLogin),
IP: "",
UserAgent: "",
Result: string(model.LoginResultSuccess),
})
}
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 {
err := s.userRepo.Update(user)
if err != nil {
return err
}
// 记录数据变更日志
if s.logService != nil {
s.logService.DataChangeLog.RecordDataChange(&model.DataChangeLog{
UserID: user.ID,
ChangeType: string(model.ChangeProfile),
TargetType: "user",
TargetID: user.ID,
FieldName: "profile",
OldValue: "",
NewValue: fmt.Sprintf("昵称:%s", user.Nickname),
OperatorType: string(model.OperatorTypeSelf),
Reason: "用户修改个人资料",
})
}
return nil
}
// 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
if err := s.userRepo.Update(user); err != nil {
return err
}
// 记录数据变更日志
if s.logService != nil {
s.logService.DataChangeLog.RecordDataChange(&model.DataChangeLog{
UserID: userID,
ChangeType: string(model.ChangePassword),
TargetType: "user",
TargetID: userID,
FieldName: "password",
OldValue: "***",
NewValue: "***",
OperatorType: string(model.OperatorTypeSelf),
Reason: "用户主动修改密码",
})
}
return nil
}
// 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
)