package service import ( "errors" "fmt" "strings" "time" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/database" "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" ) // GetMaxIdentities 获取最大身份数量限制(从数据库配置) func GetMaxIdentities() int { db := database.GetDB() if db == nil { logger.Logger.Warn("Database not initialized, using default MaxIdentities=2") return 2 } var config models.SystemConfig err := db.Where("config_key = ? AND is_active = ?", "max_star_identities", true).First(&config).Error if err != nil { logger.Logger.Warn("Failed to get max_star_identities config, using default=2", zap.Error(err)) return 2 } return int(config.ConfigValue) } // IdentityService 粉丝身份Service接口 type IdentityService interface { // GetFanIdentities 获取可选粉丝身份列表(所有可用明星) GetFanIdentities(req *pb.GetFanIdentitiesRequest) (*pb.GetFanIdentitiesResponse, error) // GetMyFanIdentities 获取我的粉丝身份列表(当前用户拥有的粉丝身份) GetMyFanIdentities(userID int64, currentStarID int64) (*pb.GetMyFanIdentitiesResponse, error) // AddIdentity 新增粉丝身份 AddIdentity(req *pb.AddIdentityRequest, userID int64) (*pb.AddIdentityResponse, error) // SwitchIdentity 切换粉丝身份 SwitchIdentity(req *pb.SwitchIdentityRequest, userID int64, currentStarID int64) (*pb.SwitchIdentityResponse, error) } // identityService 粉丝身份Service实现 type identityService struct { fanProfileRepo repository.FanProfileRepository starRepo repository.StarRepository userRepo repository.UserRepository db *gorm.DB } // NewIdentityService 创建粉丝身份Service实例 func NewIdentityService( fanProfileRepo repository.FanProfileRepository, starRepo repository.StarRepository, userRepo repository.UserRepository, db *gorm.DB, ) IdentityService { return &identityService{ fanProfileRepo: fanProfileRepo, starRepo: starRepo, userRepo: userRepo, db: db, } } // GetFanIdentities 获取可选粉丝身份列表(支持搜索) func (s *identityService) GetFanIdentities(req *pb.GetFanIdentitiesRequest) (*pb.GetFanIdentitiesResponse, error) { var stars []*models.Star var err error // 如果有关键词,进行搜索;否则返回所有可用明星 if req.Keyword != "" { stars, err = s.starRepo.Search(req.Keyword) } else { stars, err = s.starRepo.GetAllActive() } if err != nil { logger.Logger.Error("Failed to get stars", zap.String("keyword", req.Keyword), zap.Error(err), ) return nil, fmt.Errorf("failed to get stars: %w", err) } // 转换为proto类型(使用统一转换函数) pbStars := make([]*pb.Star, 0, len(stars)) for _, star := range stars { pbStars = append(pbStars, ModelToProtoStar(star)) } logger.Logger.Debug("Get fan identities successful", zap.String("keyword", req.Keyword), zap.Int("count", len(pbStars)), ) return &pb.GetFanIdentitiesResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Stars: pbStars, }, nil } // GetMyFanIdentities 获取我的粉丝身份列表 func (s *identityService) GetMyFanIdentities(userID int64, currentStarID int64) (*pb.GetMyFanIdentitiesResponse, error) { // 1. 参数验证 if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return nil, fmt.Errorf("invalid user_id: %d", userID) } // 2. 查询用户的所有粉丝身份 fanProfiles, err := s.fanProfileRepo.GetByUserID(userID) if err != nil { logger.Logger.Error("Failed to get fan profiles", zap.Int64("user_id", userID), zap.Error(err), ) return nil, fmt.Errorf("failed to get fan profiles: %w", err) } // 3. 查询对应的明星信息并组合 items := make([]*pb.MyFanIdentityItem, 0, len(fanProfiles)) for _, profile := range fanProfiles { // 查询明星信息 star, err := s.starRepo.GetByID(profile.StarID) if err != nil { logger.Logger.Error("Failed to get star info", zap.Int64("star_id", profile.StarID), zap.Error(err), ) // 如果查询明星失败,使用空的明星信息 star = &models.Star{ StarID: profile.StarID, Name: "", NameEn: nil, Tag: nil, IdentityID: "", } } // 组合粉丝档案和明星信息 items = append(items, &pb.MyFanIdentityItem{ FanProfile: ModelToProtoFanProfile(profile), Star: ModelToProtoStar(star), }) } logger.Logger.Debug("Get my fan identities successful", zap.Int64("user_id", userID), zap.Int64("current_star_id", currentStarID), zap.Int("count", len(items)), ) return &pb.GetMyFanIdentitiesResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, Items: items, CurrentStarId: currentStarID, }, nil } // AddIdentity 新增粉丝身份(最多2个) func (s *identityService) AddIdentity(req *pb.AddIdentityRequest, userID int64) (*pb.AddIdentityResponse, error) { // 1. 参数验证 if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return nil, fmt.Errorf("invalid user_id: %d", userID) } if !validator.ValidateStarID(req.StarId) { logger.Logger.Warn("Invalid star_id", zap.Int64("star_id", req.StarId), ) return nil, appErrors.ErrInvalidStarID } valid, msg := validator.ValidateNickname(req.Nickname) if !valid { logger.Logger.Warn("Invalid nickname", zap.Int64("user_id", userID), zap.Int64("star_id", req.StarId), zap.String("error", msg), ) return nil, fmt.Errorf("invalid nickname: %s", msg) } // 2. 检查用户已有的粉丝身份数量(最多2个) count, err := s.fanProfileRepo.CountByUserID(userID) if err != nil { logger.Logger.Error("Failed to count fan profiles", zap.Int64("user_id", userID), zap.Error(err), ) return nil, fmt.Errorf("failed to count fan profiles: %w", err) } if count >= int64(GetMaxIdentities()) { logger.Logger.Warn("Maximum identities reached", zap.Int64("user_id", userID), zap.Int64("current_count", count), zap.Int("max_identities", GetMaxIdentities()), ) return nil, appErrors.ErrMaxIdentitiesReached } // 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. 检查该用户是否已有该明星的身份 existingProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, req.StarId) if err == nil && existingProfile != nil { logger.Logger.Warn("Fan profile already exists", zap.Int64("user_id", userID), zap.Int64("star_id", req.StarId), ) return nil, fmt.Errorf("fan profile already exists for this user and star") } // 如果返回ErrFanProfileNotFound,说明不存在,可以继续创建 if err != nil && !errors.Is(err, appErrors.ErrFanProfileNotFound) { logger.Logger.Error("Failed to check existing fan profile", zap.Int64("user_id", userID), zap.Int64("star_id", req.StarId), zap.Error(err), ) return nil, fmt.Errorf("failed to check existing fan profile: %w", err) } // 5. 创建新的粉丝档案 now := time.Now().UnixMilli() fanProfile := &models.FanProfile{ UserID: userID, 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 := s.fanProfileRepo.Create(fanProfile); err != nil { logger.Logger.Error("Failed to create fan profile", zap.Int64("user_id", userID), zap.Int64("star_id", req.StarId), zap.Error(err), ) // 直接返回 Repository 层定义的业务错误 if errors.Is(err, appErrors.ErrNicknameAlreadyExists) { return nil, appErrors.ErrNicknameAlreadyExists } if errors.Is(err, appErrors.ErrFanProfileAlreadyExists) { return nil, appErrors.ErrFanProfileAlreadyExists } return nil, fmt.Errorf("failed to create fan profile: %w", err) } logger.Logger.Info("Add identity successful", zap.Int64("user_id", userID), zap.Int64("star_id", req.StarId), zap.String("nickname", req.Nickname), ) return &pb.AddIdentityResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, FanProfile: ModelToProtoFanProfile(fanProfile), }, nil } // contains 检查字符串是否包含子串(不区分大小写) func contains(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } // SwitchIdentity 切换粉丝身份(生成新Token) func (s *identityService) SwitchIdentity(req *pb.SwitchIdentityRequest, userID int64, currentStarID int64) (*pb.SwitchIdentityResponse, error) { // 1. 参数验证 if !validator.ValidateUserID(userID) { logger.Logger.Warn("Invalid user_id", zap.Int64("user_id", userID), ) return nil, fmt.Errorf("invalid user_id: %d", userID) } if !validator.ValidateStarID(req.NewStarId) { logger.Logger.Warn("Invalid new_star_id", zap.Int64("new_star_id", req.NewStarId), ) return nil, appErrors.ErrInvalidStarID } // 检查是否切换到相同的身份 if req.NewStarId == currentStarID { logger.Logger.Warn("Switching to same identity", zap.Int64("user_id", userID), zap.Int64("star_id", currentStarID), ) return nil, fmt.Errorf("already using this identity") } // 2. 查询用户 user, err := s.userRepo.GetByID(userID) if err != nil { if errors.Is(err, appErrors.ErrUserNotFound) { logger.Logger.Warn("User not found", zap.Int64("user_id", userID), ) return nil, appErrors.ErrUserNotFound } logger.Logger.Error("Failed to get user", zap.Int64("user_id", userID), zap.Error(err), ) return nil, fmt.Errorf("failed to get user: %w", err) } // 3. 验证新身份是否存在 fanProfile, err := s.fanProfileRepo.GetByUserAndStar(userID, req.NewStarId) if err != nil { if errors.Is(err, appErrors.ErrFanProfileNotFound) { logger.Logger.Warn("Fan profile not found for new star", zap.Int64("user_id", userID), zap.Int64("new_star_id", req.NewStarId), ) return nil, appErrors.ErrFanProfileNotFound } logger.Logger.Error("Failed to get fan profile", zap.Int64("user_id", userID), zap.Int64("new_star_id", req.NewStarId), zap.Error(err), ) return nil, fmt.Errorf("failed to get fan profile: %w", err) } // 4. 生成新Token(包含新的star_id) newToken, err := jwt.GenerateToken(user.ID, req.NewStarId, user.UpdatedAt) if err != nil { logger.Logger.Error("Failed to generate token", zap.Int64("user_id", userID), zap.Int64("new_star_id", req.NewStarId), zap.Error(err), ) return nil, fmt.Errorf("failed to generate token: %w", err) } // 5. 更新用户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", userID), zap.Error(err), ) return nil, fmt.Errorf("failed to update token: %w", err) } logger.Logger.Info("Switch identity successful", zap.Int64("user_id", userID), zap.Int64("old_star_id", currentStarID), zap.Int64("new_star_id", req.NewStarId), ) return &pb.SwitchIdentityResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", Timestamp: time.Now().UnixMilli(), }, AccessToken: newToken, ExpiresIn: jwt.GetExpiresIn(), FanProfile: ModelToProtoFanProfile(fanProfile), }, nil }