2026-03-09 21:28:58 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"carrot_bbs/internal/cache"
|
2026-03-13 09:38:18 +08:00
|
|
|
|
apperrors "carrot_bbs/internal/errors"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
"carrot_bbs/internal/model"
|
|
|
|
|
|
"carrot_bbs/internal/pkg/utils"
|
|
|
|
|
|
"carrot_bbs/internal/repository"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// UserService 用户服务接口
|
|
|
|
|
|
type UserService interface {
|
2026-03-19 13:49:51 +08:00
|
|
|
|
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
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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)
|
2026-03-15 02:25:10 +08:00
|
|
|
|
GetFollowingList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
|
|
|
|
|
GetFollowersList(ctx context.Context, userID string, page, pageSize string) ([]*model.User, error)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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 {
|
2026-03-26 18:14:16 +08:00
|
|
|
|
userRepo repository.UserRepository
|
2026-03-09 21:28:58 +08:00
|
|
|
|
systemMessageService SystemMessageService
|
|
|
|
|
|
emailCodeService EmailCodeService
|
2026-03-15 02:25:10 +08:00
|
|
|
|
logService *LogService
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewUserService 创建用户服务
|
|
|
|
|
|
func NewUserService(
|
2026-03-26 18:14:16 +08:00
|
|
|
|
userRepo repository.UserRepository,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
systemMessageService SystemMessageService,
|
|
|
|
|
|
emailService EmailService,
|
|
|
|
|
|
cacheBackend cache.Cache,
|
2026-03-28 07:03:21 +08:00
|
|
|
|
logService *LogService,
|
2026-03-13 09:38:18 +08:00
|
|
|
|
) UserService {
|
|
|
|
|
|
return &userServiceImpl{
|
2026-03-09 21:28:58 +08:00
|
|
|
|
userRepo: userRepo,
|
|
|
|
|
|
systemMessageService: systemMessageService,
|
|
|
|
|
|
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
2026-03-28 07:03:21 +08:00
|
|
|
|
logService: logService,
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SendRegisterCode 发送注册验证码
|
2026-03-19 13:49:51 +08:00
|
|
|
|
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email, clientIP string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
user, err := s.userRepo.GetByEmail(email)
|
|
|
|
|
|
if err == nil && user != nil {
|
|
|
|
|
|
return ErrEmailExists
|
|
|
|
|
|
}
|
2026-03-19 13:49:51 +08:00
|
|
|
|
return s.emailCodeService.SendCode(ctx, CodePurposeRegister, email, clientIP)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 13:49:51 +08:00
|
|
|
|
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email, clientIP string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
user, err := s.userRepo.GetByEmail(email)
|
|
|
|
|
|
if err != nil || user == nil {
|
|
|
|
|
|
return ErrUserNotFound
|
|
|
|
|
|
}
|
2026-03-19 13:49:51 +08:00
|
|
|
|
return s.emailCodeService.SendCode(ctx, CodePurposePasswordReset, email, clientIP)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 13:49:51 +08:00
|
|
|
|
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email, clientIP string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 13:49:51 +08:00
|
|
|
|
return s.emailCodeService.SendCode(ctx, CodePurposeEmailVerify, targetEmail, clientIP)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// VerifyCurrentUserEmail 验证当前用户邮箱
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 发送修改密码验证码
|
2026-03-19 13:49:51 +08:00
|
|
|
|
func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID, clientIP string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
user, err := s.userRepo.GetByID(userID)
|
|
|
|
|
|
if err != nil || user == nil {
|
|
|
|
|
|
return ErrUserNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
|
|
|
|
|
|
return ErrEmailNotBound
|
|
|
|
|
|
}
|
2026-03-19 13:49:51 +08:00
|
|
|
|
return s.emailCodeService.SendCode(ctx, CodePurposeChangePassword, *user.Email, clientIP)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Register 用户注册
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 验证用户名
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 02:25:10 +08:00
|
|
|
|
// 记录注册日志
|
|
|
|
|
|
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",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Login 用户登录
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
account = strings.TrimSpace(account)
|
2026-03-15 02:25:10 +08:00
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-03-15 02:25:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 登录失败处理
|
|
|
|
|
|
loginResult := string(model.LoginResultSuccess)
|
|
|
|
|
|
var failReason string
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if err != nil || user == nil {
|
2026-04-07 00:07:40 +08:00
|
|
|
|
// 时序攻击防护:执行假的密码哈希验证,保持响应时间一致
|
|
|
|
|
|
_ = utils.CheckPasswordHash(password, "$2a$10$fakehashfortimingattackprevention1234567890")
|
|
|
|
|
|
|
2026-03-15 02:25:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return nil, ErrInvalidCredentials
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !utils.CheckPasswordHash(password, user.PasswordHash) {
|
2026-03-15 02:25:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return nil, ErrInvalidCredentials
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if user.Status != model.UserStatusActive {
|
2026-03-15 02:25:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return nil, ErrUserBanned
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 02:25:10 +08:00
|
|
|
|
// 记录成功登录日志
|
|
|
|
|
|
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),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return user, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserByID 根据ID获取用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetUserByID(ctx context.Context, id string) (*model.User, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetByID(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserPostCount 获取用户帖子数(实时计算)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetPostsCount(userID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserPostCountBatch 批量获取用户帖子数(实时计算)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetPostsCountBatch(userIDs)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserByIDWithFollowingStatus 根据ID获取用户(包含当前用户是否关注的状态)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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获取用户(包含双向关注状态)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 更新用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error {
|
2026-03-15 02:25:10 +08:00
|
|
|
|
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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFollowers 获取粉丝
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetFollowers(userID, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFollowing 获取关注
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetFollowing(userID, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FollowUser 关注用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) FollowUser(ctx context.Context, followerID, followeeID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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() {
|
2026-03-09 22:18:53 +08:00
|
|
|
|
_ = s.systemMessageService.SendFollowNotification(context.Background(), followeeID, followerID)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UnfollowUser 取消关注用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 检查是否已经关注
|
|
|
|
|
|
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 拉黑用户,并自动清理双向关注/粉丝关系
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) BlockUser(ctx context.Context, blockerID, blockedID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if blockerID == blockedID {
|
|
|
|
|
|
return ErrInvalidOperation
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.userRepo.BlockUserAndCleanupRelations(blockerID, blockedID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UnblockUser 取消拉黑
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
if blockerID == blockedID {
|
|
|
|
|
|
return ErrInvalidOperation
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.userRepo.UnblockUser(blockerID, blockedID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetBlockedUsers 获取黑名单列表
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsBlocked 检查当前用户是否已拉黑目标用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.IsBlocked(blockerID, blockedID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFollowingList 获取关注列表(字符串参数版本)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 转换字符串参数为整数
|
|
|
|
|
|
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 获取粉丝列表(字符串参数版本)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 转换字符串参数为整数
|
|
|
|
|
|
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 批量获取双向关注状态
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CheckUsernameAvailable 检查用户名是否可用
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
user, err := s.userRepo.GetByUsername(username)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return true, nil // 用户不存在,可用
|
|
|
|
|
|
}
|
|
|
|
|
|
return user == nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ChangePassword 修改密码
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 获取用户
|
|
|
|
|
|
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
|
2026-03-15 02:25:10 +08:00
|
|
|
|
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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ResetPasswordByEmail 通过邮箱重置密码
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 搜索用户
|
2026-03-13 09:38:18 +08:00
|
|
|
|
func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return s.userRepo.Search(keyword, page, pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// 错误定义 - 使用统一的 AppError
|
2026-03-09 21:28:58 +08:00
|
|
|
|
var (
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
)
|