Implement sensitive word filtering feature with configurable database and Redis support. Add security enhancements including WebSocket origin validation, improved password strength requirements, timing attack prevention, and production JWT secret validation. Add batch operation validation limits and optimize user blocking queries with subqueries. BREAKING CHANGE: Password validation now requires minimum 8 characters with uppercase, lowercase, and digit (was 6-50 characters without complexity requirements)
725 lines
22 KiB
Go
725 lines
22 KiB
Go
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)
|
||
}
|
||
|
||
// 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,
|
||
logService *LogService,
|
||
) UserService {
|
||
return &userServiceImpl{
|
||
userRepo: userRepo,
|
||
systemMessageService: systemMessageService,
|
||
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
||
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 {
|
||
// 时序攻击防护:执行假的密码哈希验证,保持响应时间一致
|
||
_ = utils.CheckPasswordHash(password, "$2a$10$fakehashfortimingattackprevention1234567890")
|
||
|
||
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
|
||
)
|