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 刷新Token(userID 和 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_at(updated_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 }