topfans/backend/services/userService/service/auth_service.go
2026-05-16 02:42:32 +08:00

688 lines
19 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 (
"errors"
"fmt"
"time"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/jwt"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/user"
"github.com/topfans/backend/pkg/validator"
"github.com/topfans/backend/services/userService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
// AuthService 认证Service接口
type AuthService interface {
// Register 注册
Register(req *pb.RegisterRequest) (*pb.RegisterResponse, error)
// Login 登录
Login(req *pb.LoginRequest) (*pb.LoginResponse, error)
// Logout 登出userID 从网关的 attachments 中获取)
Logout(userID int64) (*pb.LogoutResponse, error)
// RefreshToken 刷新TokenuserID 和 starID 从网关的 attachments 中获取)
RefreshToken(userID, starID int64) (*pb.RefreshTokenResponse, error)
// ValidateToken 验证Token用于中间件
ValidateToken(req *pb.ValidateTokenRequest) (*pb.ValidateTokenResponse, error)
}
// authService 认证Service实现
type authService struct {
userRepo repository.UserRepository
fanProfileRepo repository.FanProfileRepository
starRepo repository.StarRepository
db *gorm.DB
}
// NewAuthService 创建认证Service实例
func NewAuthService(
userRepo repository.UserRepository,
fanProfileRepo repository.FanProfileRepository,
starRepo repository.StarRepository,
db *gorm.DB,
) AuthService {
return &authService{
userRepo: userRepo,
fanProfileRepo: fanProfileRepo,
starRepo: starRepo,
db: db,
}
}
// Register 用户注册
func (s *authService) Register(req *pb.RegisterRequest) (*pb.RegisterResponse, error) {
// 1. 参数验证
if !validator.ValidateMobile(req.Mobile) {
logger.Logger.Warn("Invalid mobile format",
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrInvalidMobile
}
if valid, msg := validator.ValidatePassword(req.Password); !valid {
logger.Logger.Warn("Invalid password",
zap.String("mobile", req.Mobile),
zap.String("error", msg),
)
if msg == "password too short" {
return nil, appErrors.ErrPasswordTooShort
}
return nil, fmt.Errorf("invalid password: %s", msg)
}
if valid, msg := validator.ValidateNickname(req.Nickname); !valid {
logger.Logger.Warn("Invalid nickname",
zap.String("mobile", req.Mobile),
zap.String("error", msg),
)
return nil, fmt.Errorf("invalid nickname: %s", msg)
}
if !validator.ValidateStarID(req.StarId) {
logger.Logger.Warn("Invalid star_id",
zap.String("mobile", req.Mobile),
zap.Int64("star_id", req.StarId),
)
return nil, appErrors.ErrInvalidStarID
}
// 2. 验证手机号是否已存在
existingUser, err := s.userRepo.GetByMobile(req.Mobile)
if err != nil && !errors.Is(err, appErrors.ErrUserNotFound) {
logger.Logger.Error("Failed to check mobile existence",
zap.String("mobile", req.Mobile),
zap.Error(err),
)
return nil, fmt.Errorf("failed to check mobile: %w", err)
}
if existingUser != nil {
logger.Logger.Warn("Mobile already exists",
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrUserAlreadyExists
}
// 3. 验证明星是否存在
_, err = s.starRepo.GetByID(req.StarId)
if err != nil {
logger.Logger.Error("Failed to get star",
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
if errors.Is(err, appErrors.ErrStarNotFound) {
return nil, appErrors.ErrStarNotFound
}
return nil, fmt.Errorf("failed to get star: %w", err)
}
// 4. 使用事务创建用户和粉丝档案
var user *models.User
var fanProfile *models.FanProfile
err = s.db.Transaction(func(tx *gorm.DB) error {
// 4.1 创建用户
now := time.Now().UnixMilli()
user = &models.User{
Mobile: req.Mobile,
PasswordHash: "", // 将在Repository中加密
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
// 手动加密密码(因为需要在事务中使用)
hashedPassword, err := repository.HashPassword(req.Password)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
user.PasswordHash = hashedPassword
// 在事务中创建用户
if err := tx.Create(user).Error; err != nil {
logger.Logger.Error("Failed to create user in transaction",
zap.String("mobile", req.Mobile),
zap.Error(err),
)
return fmt.Errorf("failed to create user: %w", err)
}
// 4.2 创建第一个粉丝档案
fanProfile = &models.FanProfile{
UserID: user.ID,
StarID: req.StarId,
Nickname: req.Nickname,
Level: 1,
Times: 1,
Social: 0,
CoinBalance: 0,
CrystalBalance: 0,
Tags: models.StringArray{},
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := tx.Create(fanProfile).Error; err != nil {
logger.Logger.Error("Failed to create fan profile in transaction",
zap.Int64("user_id", user.ID),
zap.Int64("star_id", req.StarId),
zap.Error(err),
)
return fmt.Errorf("failed to create fan profile: %w", err)
}
// 4.3 生成JWT Token
token, err := jwt.GenerateToken(user.ID, req.StarId, user.UpdatedAt)
if err != nil {
logger.Logger.Error("Failed to generate token",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return fmt.Errorf("failed to generate token: %w", err)
}
// 4.4 更新用户Token
// 注意:使用 UpdateColumns 而不是 Updates避免触发 BeforeUpdate 钩子
// 因为更新 Token 不应该改变 updated_atupdated_at 用于验证 Token 有效性)
tokenExpiresAt := jwt.GetExpiresAt()
if err := tx.Model(user).UpdateColumns(map[string]interface{}{
"access_token": token,
"token_expires_at": tokenExpiresAt,
}).Error; err != nil {
logger.Logger.Error("Failed to update token in transaction",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return fmt.Errorf("failed to update token: %w", err)
}
// 更新user对象以便返回
user.AccessToken = &token
expiresAt := tokenExpiresAt
user.TokenExpiresAt = &expiresAt
return nil
})
if err != nil {
// 检查是否是唯一约束错误
errStr := err.Error()
if contains(errStr, "uk_fan_profiles_star_nickname") || contains(errStr, "该昵称已被注册") {
return nil, appErrors.ErrNicknameAlreadyExists
}
return nil, err
}
// 5. 构建响应
response := &pb.RegisterResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
AccessToken: *user.AccessToken,
ExpiresIn: jwt.GetExpiresIn(),
User: ModelToProtoUser(user),
FanProfile: ModelToProtoFanProfile(fanProfile),
}
logger.Logger.Info("User registered successfully",
zap.Int64("user_id", user.ID),
zap.String("mobile", user.Mobile),
zap.Int64("star_id", req.StarId),
)
return response, nil
}
// Login 用户登录
func (s *authService) Login(req *pb.LoginRequest) (*pb.LoginResponse, error) {
// 1. 参数验证
if !validator.ValidateMobile(req.Mobile) {
logger.Logger.Warn("Invalid mobile format",
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrInvalidMobile
}
if req.Password == "" {
logger.Logger.Warn("Password is empty",
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrInvalidPassword
}
// 2. 根据手机号查询用户
user, err := s.userRepo.GetByMobile(req.Mobile)
if err != nil {
if errors.Is(err, appErrors.ErrUserNotFound) {
logger.Logger.Warn("User not found",
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrUserNotFound
}
logger.Logger.Error("Failed to get user by mobile",
zap.String("mobile", req.Mobile),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get user: %w", err)
}
// 3. 验证密码
if !s.userRepo.VerifyPassword(user, req.Password) {
logger.Logger.Warn("Invalid password",
zap.String("mobile", req.Mobile),
zap.Int64("user_id", user.ID),
)
return nil, appErrors.ErrInvalidPassword
}
// 4. 验证用户是否激活
if !user.IsActive {
logger.Logger.Warn("User is inactive",
zap.Int64("user_id", user.ID),
zap.String("mobile", req.Mobile),
)
return nil, appErrors.ErrUserInactive
}
// 4.1 检查账号状态(冻结/封号)
accountStatus, err := s.userRepo.GetAccountStatus(user.ID)
if err != nil {
logger.Logger.Error("Failed to get account status",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get account status: %w", err)
}
// 如果有账号状态记录,需要检查具体状态
if accountStatus != nil {
if accountStatus.IsBanned() {
// 封号状态
reason := ""
if accountStatus.Reason != nil {
reason = *accountStatus.Reason
}
logger.Logger.Warn("User account is banned",
zap.Int64("user_id", user.ID),
zap.String("reason", reason),
)
return nil, fmt.Errorf("账号已被封禁%s%s", map[bool]func() string{
true: func() string {
if reason != "" {
return ",原因:" + reason
}
return ""
},
}[true](), "")
}
if accountStatus.IsFrozen() {
// 冻结状态,检查是否已过解冻时间
if accountStatus.FrozenUntil != nil && time.Now().UnixMilli() > *accountStatus.FrozenUntil {
// 冻结已过期,理论上应该更新状态,但这里先放行让用户登录
logger.Logger.Info("User account frozen but expired",
zap.Int64("user_id", user.ID),
)
} else {
reason := ""
if accountStatus.Reason != nil {
reason = *accountStatus.Reason
}
frozenUntilStr := ""
if accountStatus.FrozenUntil != nil {
frozenTime := time.UnixMilli(*accountStatus.FrozenUntil)
frozenUntilStr = frozenTime.Format("2006-01-02 15:04:05")
}
logger.Logger.Warn("User account is frozen",
zap.Int64("user_id", user.ID),
zap.String("reason", reason),
zap.Int64("frozen_until", *accountStatus.FrozenUntil),
)
errMsg := "账号已被冻结"
if reason != "" {
errMsg += ",原因:" + reason
}
if frozenUntilStr != "" {
errMsg += ",解封时间:" + frozenUntilStr
}
return nil, errors.New(errMsg)
}
}
}
// 5. 获取用户的粉丝档案列表
fanProfiles, err := s.fanProfileRepo.GetByUserID(user.ID)
if err != nil {
logger.Logger.Error("Failed to get fan profiles",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get fan profiles: %w", err)
}
if len(fanProfiles) == 0 {
logger.Logger.Error("User has no fan profiles",
zap.Int64("user_id", user.ID),
)
return nil, fmt.Errorf("user has no fan profiles")
}
// 选择当前身份
var currentProfile *models.FanProfile
if req.StarId > 0 {
// 如果指定了 star_id查找对应的粉丝档案
specifiedStarID := req.StarId
found := false
for _, profile := range fanProfiles {
if profile.StarID == specifiedStarID {
currentProfile = profile
found = true
break
}
}
if !found {
logger.Logger.Warn("Specified star_id not found in user's fan profiles",
zap.Int64("user_id", user.ID),
zap.Int64("specified_star_id", specifiedStarID),
)
return nil, fmt.Errorf("你还不是该明星的粉丝,无法切换到该身份")
}
logger.Logger.Info("Using specified star_id for login",
zap.Int64("user_id", user.ID),
zap.Int64("star_id", specifiedStarID),
)
} else {
// 没有指定 star_id使用第一个最早创建的粉丝档案
currentProfile = fanProfiles[0]
logger.Logger.Info("Using first fan profile for login",
zap.Int64("user_id", user.ID),
zap.Int64("star_id", currentProfile.StarID),
)
}
// 6. 生成JWT Token包含user_id和当前star_id
token, err := jwt.GenerateToken(user.ID, currentProfile.StarID, user.UpdatedAt)
if err != nil {
logger.Logger.Error("Failed to generate token",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to generate token: %w", err)
}
// 7. 更新用户Token
tokenExpiresAt := jwt.GetExpiresAt()
if err := s.userRepo.UpdateToken(user.ID, token, tokenExpiresAt); err != nil {
logger.Logger.Error("Failed to update token",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to update token: %w", err)
}
// 8. 构建响应
pbFanProfiles := make([]*pb.FanProfile, 0, len(fanProfiles))
for _, profile := range fanProfiles {
pbFanProfiles = append(pbFanProfiles, &pb.FanProfile{
Id: profile.ID,
UserId: profile.UserID,
StarId: profile.StarID,
Nickname: profile.Nickname,
Level: int32(profile.Level),
Times: int32(profile.Times),
Social: int32(profile.Social),
CoinBalance: profile.CoinBalance,
CrystalBalance: profile.CrystalBalance,
Tags: []string(profile.Tags),
CreatedAt: profile.CreatedAt,
})
}
response := &pb.LoginResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
AccessToken: token,
ExpiresIn: jwt.GetExpiresIn(),
User: ModelToProtoUser(user),
FanProfile: ModelToProtoFanProfile(currentProfile),
FanProfiles: pbFanProfiles,
}
logger.Logger.Info("User login successful",
zap.Int64("user_id", user.ID),
zap.String("mobile", user.Mobile),
zap.Int64("star_id", currentProfile.StarID),
)
return response, nil
}
// Logout 用户登出
func (s *authService) Logout(userID int64) (*pb.LogoutResponse, error) {
// 清除用户Token
if err := s.userRepo.ClearToken(userID); err != nil {
logger.Logger.Error("Failed to clear token",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to clear token: %w", err)
}
logger.Logger.Info("User logout successful",
zap.Int64("user_id", userID),
)
return &pb.LogoutResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// RefreshToken 刷新Token
func (s *authService) RefreshToken(userID, starID int64) (*pb.RefreshTokenResponse, error) {
// 1. 查询用户
user, err := s.userRepo.GetByID(userID)
if err != nil {
if errors.Is(err, appErrors.ErrUserNotFound) {
logger.Logger.Warn("User not found during token refresh",
zap.Int64("user_id", userID),
)
return nil, appErrors.ErrUserNotFound
}
logger.Logger.Error("Failed to get user during token refresh",
zap.Int64("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get user: %w", err)
}
// 2. 验证用户是否激活
if !user.IsActive {
logger.Logger.Warn("User is inactive during token refresh",
zap.Int64("user_id", user.ID),
)
return nil, appErrors.ErrUserInactive
}
// 3. 生成新Token
newToken, err := jwt.GenerateToken(user.ID, starID, user.UpdatedAt)
if err != nil {
logger.Logger.Error("Failed to generate new token",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to generate token: %w", err)
}
// 4. 更新数据库中的Token
tokenExpiresAt := jwt.GetExpiresAt()
if err := s.userRepo.UpdateToken(user.ID, newToken, tokenExpiresAt); err != nil {
logger.Logger.Error("Failed to update token",
zap.Int64("user_id", user.ID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to update token: %w", err)
}
logger.Logger.Info("Token refreshed successfully",
zap.Int64("user_id", user.ID),
zap.Int64("star_id", starID),
)
return &pb.RefreshTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
AccessToken: newToken,
ExpiresIn: jwt.GetExpiresIn(),
}, nil
}
// ValidateToken 验证Token用于中间件
func (s *authService) ValidateToken(req *pb.ValidateTokenRequest) (*pb.ValidateTokenResponse, error) {
// 1. 解析和验证Token检查签名和过期时间
claims, err := jwt.ValidateToken(req.AccessToken)
if err != nil {
logger.Logger.Warn("Token validation failed",
zap.Error(err),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
UserId: 0,
StarId: 0,
IsValid: false,
ExpiresAt: 0,
}, nil
}
// 2. 查询用户验证Token是否匹配
user, err := s.userRepo.GetByID(claims.UserID)
if err != nil {
logger.Logger.Warn("User not found during token validation",
zap.Int64("user_id", claims.UserID),
zap.Error(err),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "user not found",
Timestamp: time.Now().UnixMilli(),
},
UserId: 0,
StarId: 0,
IsValid: false,
ExpiresAt: 0,
}, nil
}
// 3. 验证用户是否激活
if !user.IsActive {
logger.Logger.Warn("User is inactive during token validation",
zap.Int64("user_id", user.ID),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_FORBIDDEN,
Message: "user is inactive",
Timestamp: time.Now().UnixMilli(),
},
UserId: claims.UserID,
StarId: claims.StarID,
IsValid: false,
ExpiresAt: 0,
}, nil
}
// 4. 验证Token是否匹配数据库中的Token
if user.AccessToken == nil || *user.AccessToken != req.AccessToken {
logger.Logger.Warn("Token mismatch during validation",
zap.Int64("user_id", user.ID),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "token mismatch",
Timestamp: time.Now().UnixMilli(),
},
UserId: claims.UserID,
StarId: claims.StarID,
IsValid: false,
ExpiresAt: 0,
}, nil
}
// 5. 验证updated_at是否匹配
if user.UpdatedAt != claims.UpdatedAt {
logger.Logger.Warn("Token invalidated due to user info update",
zap.Int64("user_id", user.ID),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "token expired due to user info update",
Timestamp: time.Now().UnixMilli(),
},
UserId: claims.UserID,
StarId: claims.StarID,
IsValid: false,
ExpiresAt: 0,
}, nil
}
// 6. 获取过期时间
var expiresAt int64
if claims.RegisteredClaims.ExpiresAt != nil {
expiresAt = claims.RegisteredClaims.ExpiresAt.Time.UnixMilli()
}
logger.Logger.Debug("Token validated successfully",
zap.Int64("user_id", claims.UserID),
zap.Int64("star_id", claims.StarID),
)
return &pb.ValidateTokenResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "",
Timestamp: time.Now().UnixMilli(),
},
UserId: claims.UserID,
StarId: claims.StarID,
IsValid: true,
ExpiresAt: expiresAt,
}, nil
}
// min 返回两个整数的最小值
func min(a, b int) int {
if a < b {
return a
}
return b
}