Files
backend/internal/service/user_service.go
lan 90c57f1a1c
All checks were successful
Build Backend / build (push) Successful in 2m7s
Build Backend / build-docker (push) Successful in 1m51s
feat(core): optimize performance and reliability through batching and redis-backed unread counts
This commit introduces several significant architectural improvements to enhance system performance, scalability, and reliability:

- **Performance Optimization (N+1 Problem Resolution)**: Implemented batching mechanisms across `MessageHandler`, `ChatService`, `GroupService`, `MessageService`, and `UserService`. This replaces multiple individual database/cache queries with single batch operations for fetching unread counts, last messages, participants, and user information.
- **Enhanced Unread Count Management**: Migrated unread count tracking to a Redis Hash-based approach (`unread#️⃣{userID}`). This allows for atomic increments/decrements and efficient retrieval of both individual conversation unread counts and total unread counts for a user.
- **Improved Message Sequencing**: Integrated Redis-based sequence (`seq`) pre-allocation for messages to ensure strict ordering and reduce database contention during high-concurrency message creation.
- **Push Service Reliability**: Refactored the push notification system to include persistent `PushRecord` tracking. Added a recovery mechanism (`recoverPendingPushes`) to reload pending notifications from the database upon service startup, ensuring better delivery guarantees.
- **WebSocket Reliability**: Updated the WebSocket hub to move away from in-memory history replay in favor of a client-driven synchronization model (`sync_required` event and `ack` handling), reducing memory overhead and improving connection stability.
- **Cache Layer Enhancements**: Added `HIncrBy` and `IncrBySeq` to the `Cache` interface and its implementations (`RedisCache`, `LayeredCache`) to support the new unread and sequence management logic.
2026-05-04 18:31:03 +08:00

862 lines
27 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"
"strconv"
"strings"
"time"
"with_you/internal/cache"
apperrors "with_you/internal/errors"
"with_you/internal/model"
"with_you/internal/pkg/utils"
"with_you/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)
GetUsersByIDs(ctx context.Context, ids []string) (map[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)
IsBlockedBatch(ctx context.Context, blockerID string, blockedIDs []string) (map[string]bool, error)
// 隐私设置
GetPrivacySettings(ctx context.Context, userID string) (*model.PrivacySettings, error)
UpdatePrivacySettings(ctx context.Context, userID string, settings *model.PrivacySettings) error
CheckVisibility(ctx context.Context, viewerID, targetUserID string, visibility model.VisibilityLevel) (bool, error)
CanViewFollowers(ctx context.Context, viewerID, targetUserID string) (bool, error)
CanViewFollowing(ctx context.Context, viewerID, targetUserID string) (bool, error)
CanViewPosts(ctx context.Context, viewerID, targetUserID string) (bool, error)
CanViewFavorites(ctx context.Context, viewerID, targetUserID string) (bool, error)
// 账号注销
RequestAccountDeletion(ctx context.Context, userID, password string) error
CancelAccountDeletion(ctx context.Context, userID string) error
GetDeletionStatus(ctx context.Context, userID string) (isPending bool, requestedAt *time.Time, daysLeft int, err 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 && user.Status != model.UserStatusPendingDeletion {
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 user.DeletionRequestedAt != nil {
_ = s.userRepo.CancelDeletion(user.ID)
user.DeletionRequestedAt = nil
user.Status = model.UserStatusActive
}
// 记录成功登录日志
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)
}
// GetUsersByIDs 批量获取用户信息
func (s *userServiceImpl) GetUsersByIDs(ctx context.Context, ids []string) (map[string]*model.User, error) {
return s.userRepo.GetUsersByIDs(ctx, ids)
}
// 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)
}
// IsBlockedBatch 批量检查拉黑关系
func (s *userServiceImpl) IsBlockedBatch(ctx context.Context, blockerID string, blockedIDs []string) (map[string]bool, error) {
return s.userRepo.IsBlockedBatch(blockerID, blockedIDs)
}
// GetFollowingList 获取关注列表(字符串参数版本)
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
pageInt := 1
pageSizeInt := 20
if page != "" {
if v, err := strconv.Atoi(page); err != nil || v < 1 {
pageInt = 1
} else {
pageInt = v
}
}
if pageSize != "" {
if v, err := strconv.Atoi(pageSize); err != nil || v < 1 {
pageSizeInt = 20
} else {
pageSizeInt = v
}
}
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 != "" {
if v, err := strconv.Atoi(page); err != nil || v < 1 {
pageInt = 1
} else {
pageInt = v
}
}
if pageSize != "" {
if v, err := strconv.Atoi(pageSize); err != nil || v < 1 {
pageSizeInt = 20
} else {
pageSizeInt = v
}
}
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)
}
// GetPrivacySettings 获取隐私设置
func (s *userServiceImpl) GetPrivacySettings(ctx context.Context, userID string) (*model.PrivacySettings, error) {
return s.userRepo.GetPrivacySettings(userID)
}
// UpdatePrivacySettings 更新隐私设置
func (s *userServiceImpl) UpdatePrivacySettings(ctx context.Context, userID string, settings *model.PrivacySettings) error {
return s.userRepo.UpdatePrivacySettings(userID, settings)
}
// CheckVisibility 检查可见性权限
func (s *userServiceImpl) CheckVisibility(ctx context.Context, viewerID, targetUserID string, visibility model.VisibilityLevel) (bool, error) {
switch visibility {
case model.VisibilityEveryone:
return true, nil
case model.VisibilityFollowing:
if viewerID == "" {
return false, nil
}
return s.userRepo.IsFollowing(viewerID, targetUserID)
case model.VisibilitySelf:
return viewerID == targetUserID, nil
default:
return false, nil
}
}
// CanViewFollowers 检查是否可以查看粉丝列表
func (s *userServiceImpl) CanViewFollowers(ctx context.Context, viewerID, targetUserID string) (bool, error) {
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
if err != nil {
return false, err
}
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FollowersVisibility)
}
// CanViewFollowing 检查是否可以查看关注列表
func (s *userServiceImpl) CanViewFollowing(ctx context.Context, viewerID, targetUserID string) (bool, error) {
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
if err != nil {
return false, err
}
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FollowingVisibility)
}
// CanViewPosts 检查是否可以查看帖子列表
func (s *userServiceImpl) CanViewPosts(ctx context.Context, viewerID, targetUserID string) (bool, error) {
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
if err != nil {
return false, err
}
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.PostsVisibility)
}
// CanViewFavorites 检查是否可以查看收藏列表
func (s *userServiceImpl) CanViewFavorites(ctx context.Context, viewerID, targetUserID string) (bool, error) {
settings, err := s.userRepo.GetPrivacySettings(targetUserID)
if err != nil {
return false, err
}
return s.CheckVisibility(ctx, viewerID, targetUserID, settings.FavoritesVisibility)
}
// RequestAccountDeletion 申请注销账号
func (s *userServiceImpl) RequestAccountDeletion(ctx context.Context, userID, password string) error {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return ErrUserNotFound
}
if !utils.CheckPasswordHash(password, user.PasswordHash) {
return ErrInvalidCredentials
}
return s.userRepo.RequestDeletion(userID)
}
// CancelAccountDeletion 取消注销
func (s *userServiceImpl) CancelAccountDeletion(ctx context.Context, userID string) error {
return s.userRepo.CancelDeletion(userID)
}
// GetDeletionStatus 获取注销状态
func (s *userServiceImpl) GetDeletionStatus(ctx context.Context, userID string) (isPending bool, requestedAt *time.Time, daysLeft int, err error) {
user, err := s.userRepo.GetByID(userID)
if err != nil {
return false, nil, 0, err
}
if user.DeletionRequestedAt == nil {
return false, nil, 0, nil
}
daysLeft = 90 - int(time.Since(*user.DeletionRequestedAt).Hours()/24)
if daysLeft < 0 {
daysLeft = 0
}
return true, user.DeletionRequestedAt, daysLeft, nil
}
// 错误定义 - 使用统一的 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
)