package repository import ( "errors" "fmt" "math" "strings" "time" "github.com/topfans/backend/pkg/database" appErrors "github.com/topfans/backend/pkg/errors" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/models" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) // contains 检查字符串是否包含子串(不区分大小写) func contains(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } // CalculateLevel 根据经验值计算等级 // 公式: 升级到等级L需要的累计经验 = (L-1) * L * 50 // Level 1: 0经验, Level 2: 100经验, Level 3: 300经验, Level 4: 600经验... func CalculateLevel(experience int64) int32 { if experience < 0 { return 1 } // 使用公式: (L-1) * L * 50 <= experience // 解方程: L^2 - L - experience/50 <= 0 // L = (1 + sqrt(1 + 4*experience/50)) / 2 level := int32((1 + math.Sqrt(1+4*float64(experience)/50)) / 2) if level < 1 { level = 1 } return level } // GetExperienceForLevel 获取指定等级需要的经验值 func GetExperienceForLevel(level int32) int64 { if level <= 1 { return 0 } return int64((level-1) * level * 50) } // FanProfileRepository 粉丝档案Repository接口 type FanProfileRepository interface { // Create 创建粉丝档案 Create(profile *models.FanProfile) error // GetByUserAndStar 根据user_id + star_id查询 GetByUserAndStar(userID, starID int64) (*models.FanProfile, error) // GetByUserID 查询用户的所有粉丝身份 GetByUserID(userID int64) ([]*models.FanProfile, error) // ExistsByNickname 检查昵称是否已存在 ExistsByNickname(nickname string) (bool, error) // CountByUserID 统计用户粉丝身份数量 CountByUserID(userID int64) (int64, error) // Update 更新粉丝档案 Update(profile *models.FanProfile) error // UpdateNickname 更新昵称 UpdateNickname(userID, starID int64, nickname string) error // IncrementAssetsCount 增加资产计数 IncrementAssetsCount(userID, starID int64, delta int32) error // SyncLevelFromExperience 根据经验值同步等级(只升级不降级) SyncLevelFromExperience(userID, starID int64) (int32, error) // DecrementAssetsCount 减少资产计数 DecrementAssetsCount(userID, starID int64, delta int32) error // UpdateChainAddress 更新链地址 UpdateChainAddress(userID, starID int64, address string) error // UpdateSocial 更新好友数量(social字段) UpdateSocial(userID, starID int64, delta int32) (int32, error) // UpdateCrystalBalance 更新水晶余额(支持流水记录) // changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust // sourceID: 关联业务ID // description: 可读描述 UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) // AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) // 返回: newLevel, levelDelta, crystalReward, error AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) // UpdateExperience 更新经验值 UpdateExperience(userID, starID int64, delta int64) (int64, error) // UpdateAvatar 更新头像 UpdateAvatar(userID, starID int64, avatarURL string) error } // fanProfileRepository 粉丝档案Repository实现 type fanProfileRepository struct { db *gorm.DB } // NewFanProfileRepository 创建粉丝档案Repository实例 func NewFanProfileRepository() FanProfileRepository { return &fanProfileRepository{ db: database.GetDB(), } } // Create 创建粉丝档案 func (r *fanProfileRepository) Create(profile *models.FanProfile) error { if profile == nil { return errors.New("profile cannot be nil") } if profile.UserID <= 0 { return errors.New("user_id must be greater than 0") } if profile.StarID <= 0 { return errors.New("star_id must be greater than 0") } if err := r.db.Create(profile).Error; err != nil { // 检查是否是唯一索引冲突 errStr := err.Error() if contains(errStr, "duplicate") || contains(errStr, "unique") || contains(errStr, "violates unique constraint") { // 区分不同的唯一约束冲突 if contains(errStr, "uk_fan_profiles_star_nickname") { // star_id + nickname 唯一约束冲突 return appErrors.ErrNicknameAlreadyExists } else if contains(errStr, "uk_fan_profiles_user_star") { // user_id + star_id 唯一约束冲突 return appErrors.ErrFanProfileAlreadyExists } // 其他唯一约束冲突 return appErrors.ErrFanProfileAlreadyExists } return err } return nil } // GetByUserAndStar 根据user_id + star_id查询 func (r *fanProfileRepository) GetByUserAndStar(userID, starID int64) (*models.FanProfile, error) { if userID <= 0 { return nil, errors.New("user_id must be greater than 0") } if starID <= 0 { return nil, errors.New("star_id must be greater than 0") } var profile models.FanProfile if err := r.db.Where("user_id = ? AND star_id = ? AND is_active = ?", userID, starID, true). First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, appErrors.ErrFanProfileNotFound } return nil, err } return &profile, nil } // GetByUserID 查询用户的所有粉丝身份 func (r *fanProfileRepository) GetByUserID(userID int64) ([]*models.FanProfile, error) { if userID <= 0 { return nil, errors.New("user_id must be greater than 0") } var profiles []*models.FanProfile if err := r.db.Where("user_id = ? AND is_active = ?", userID, true). Order("created_at ASC"). Find(&profiles).Error; err != nil { return nil, err } return profiles, nil } // CountByUserID 统计用户粉丝身份数量 func (r *fanProfileRepository) CountByUserID(userID int64) (int64, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } var count int64 if err := r.db.Model(&models.FanProfile{}). Where("user_id = ? AND is_active = ?", userID, true). Count(&count).Error; err != nil { return 0, err } return count, nil } // ExistsByNickname 检查昵称是否已存在 func (r *fanProfileRepository) ExistsByNickname(nickname string) (bool, error) { if nickname == "" { return false, errors.New("nickname cannot be empty") } var count int64 if err := r.db.Model(&models.FanProfile{}). Where("nickname = ? AND is_active = ?", nickname, true). Count(&count).Error; err != nil { return false, err } return count > 0, nil } // Update 更新粉丝档案 func (r *fanProfileRepository) Update(profile *models.FanProfile) error { if profile == nil { return errors.New("profile cannot be nil") } if profile.ID == 0 { return errors.New("profile id cannot be zero") } if err := r.db.Model(profile).Updates(profile).Error; err != nil { return err } return nil } // UpdateNickname 更新昵称 func (r *fanProfileRepository) UpdateNickname(userID, starID int64, nickname string) error { if userID <= 0 { return errors.New("user_id must be greater than 0") } if starID <= 0 { return errors.New("star_id must be greater than 0") } if nickname == "" { return errors.New("nickname cannot be empty") } result := r.db.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("nickname", nickname) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("fan profile not found") } return nil } // IncrementAssetsCount 增加资产计数 func (r *fanProfileRepository) IncrementAssetsCount(userID, starID int64, delta int32) error { if userID <= 0 { return errors.New("user_id must be greater than 0") } if starID <= 0 { return errors.New("star_id must be greater than 0") } if delta < 0 { return errors.New("delta must be greater than or equal to 0") } return r.db.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). UpdateColumn("assets_count", gorm.Expr("assets_count + ?", delta)).Error } // DecrementAssetsCount 减少资产计数 func (r *fanProfileRepository) DecrementAssetsCount(userID, starID int64, delta int32) error { if userID <= 0 { return errors.New("user_id must be greater than 0") } if starID <= 0 { return errors.New("star_id must be greater than 0") } if delta < 0 { return errors.New("delta must be greater than or equal to 0") } return r.db.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ? AND assets_count >= ?", userID, starID, delta). UpdateColumn("assets_count", gorm.Expr("assets_count - ?", delta)).Error } // UpdateChainAddress 更新链地址 func (r *fanProfileRepository) UpdateChainAddress(userID, starID int64, address string) error { if userID <= 0 { return errors.New("user_id must be greater than 0") } if starID <= 0 { return errors.New("star_id must be greater than 0") } if address == "" { return errors.New("chain address cannot be empty") } result := r.db.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("chain_address", address) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("fan profile not found") } return nil } // UpdateSocial 更新好友数量(social字段) // delta: 变化量,正数表示增加,负数表示减少 // 返回: 更新后的好友数量 func (r *fanProfileRepository) UpdateSocial(userID, starID int64, delta int32) (int32, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } if starID <= 0 { return 0, errors.New("star_id must be greater than 0") } // 使用事务确保原子性 var newSocial int32 err := r.db.Transaction(func(tx *gorm.DB) error { // 先查询当前的 social 值 var profile models.FanProfile if err := tx.Where("user_id = ? AND star_id = ?", userID, starID). First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return appErrors.ErrFanProfileNotFound } return err } // 计算新值 newSocial = profile.Social + delta // 确保不会小于 0 if newSocial < 0 { newSocial = 0 } // 更新 social 字段 if err := tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("social", newSocial).Error; err != nil { return err } return nil }) if err != nil { return 0, err } return newSocial, nil } // UpdateCrystalBalance 更新水晶余额(支持流水记录) // delta: 变化量,正数表示增加,负数表示减少 // changeType: 变化类型,如 task_reward/mint_cost/mint_reward/exhibition_revenue/level_up_bonus/manual_adjust // sourceID: 关联业务ID // description: 可读描述 // 返回: 更新后的水晶余额 func (r *fanProfileRepository) UpdateCrystalBalance(userID, starID int64, delta int64, changeType string, sourceID string, description string) (int64, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } if starID <= 0 { return 0, errors.New("star_id must be greater than 0") } // 使用事务确保原子性 var newBalance int64 err := r.db.Transaction(func(tx *gorm.DB) error { // 1. SELECT FOR UPDATE 加行锁(悲观锁策略) var profile models.FanProfile if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("user_id = ? AND star_id = ?", userID, starID). First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return appErrors.ErrFanProfileNotFound } return err } // 2. 计算余额变化 balanceBefore := profile.CrystalBalance newBalance = balanceBefore + delta // 确保不会小于 0 if newBalance < 0 { newBalance = 0 } // 3. 写入水晶流水(复式记账,包含余额快照) crystalRecord := &models.CrystalTransactionRecord{ UserID: userID, StarID: starID, ChangeType: changeType, Delta: delta, BalanceBefore: balanceBefore, BalanceAfter: newBalance, SourceID: sourceID, Description: description, CreatedAt: time.Now().UnixMilli(), } if err := tx.Create(crystalRecord).Error; err != nil { return err } // 4. 更新 crystal_balance 字段 if err := tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("crystal_balance", newBalance).Error; err != nil { return err } return nil }) if err != nil { return 0, err } return newBalance, nil } // GetOrCreateExhibitionHours 获取或创建用户累计上架时长记录 func (r *fanProfileRepository) GetOrCreateExhibitionHours(tx *gorm.DB, userID, starID int64) (*models.UserExhibitionHours, error) { var existing models.UserExhibitionHours err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(&existing).Error if err == nil { return &existing, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } // 创建新记录 now := time.Now().UnixMilli() newRecord := &models.UserExhibitionHours{ UserID: userID, StarID: starID, TotalExhibitionHours: 0, UpdatedAt: now, } if err := tx.Create(newRecord).Error; err != nil { return nil, err } return newRecord, nil } // CalculateLevelFromExhibitionHours 根据累计上架时长计算等级 func CalculateLevelFromExhibitionHours(totalHours int64) int32 { db := database.GetDB() if db == nil { return 1 } var threshold models.LevelThreshold err := db.Where("max_exhibition_hours <= ?", totalHours). Order("level DESC"). First(&threshold).Error if err != nil || threshold.Level == 0 { return 1 } return threshold.Level } // GetLevelCap 获取当前等级上限 func GetLevelCap() int32 { db := database.GetDB() if db == nil { return 20 } var config models.LevelCapConfig err := db.First(&config).Error if err != nil { return 20 // 默认20级 } return config.MaxLevel } // AddExhibitionHours 增加用户累计上架时长并同步等级(事务性) // 返回: newLevel, levelDelta, crystalReward, error func (r *fanProfileRepository) AddExhibitionHours(userID, starID int64, hours int64) (int32, int32, int64, error) { var result struct { OldLevel int32 NewLevel int32 CrystalReward int64 } err := r.db.Transaction(func(tx *gorm.DB) error { // 1. 获取或创建累计时长记录 exhibitionHours, err := r.GetOrCreateExhibitionHours(tx, userID, starID) if err != nil { return err } // 2. 原子性累加时长(避免竞态条件) now := time.Now().UnixMilli() if err := tx.Model(&models.UserExhibitionHours{}). Where("user_id = ? AND star_id = ?", userID, starID). Updates(map[string]interface{}{ "total_exhibition_hours": gorm.Expr("total_exhibition_hours + ?", hours), "updated_at": now, }).Error; err != nil { return err } // 重新查询更新后的时长 if err := tx.Where("user_id = ? AND star_id = ?", userID, starID).First(exhibitionHours).Error; err != nil { return err } // 3. 获取当前等级上限 maxLevel := GetLevelCap() // 4. 计算新等级(基于累计时长) newLevel := CalculateLevelFromExhibitionHours(exhibitionHours.TotalExhibitionHours) if newLevel > maxLevel { newLevel = maxLevel } // 5. SELECT FOR UPDATE 加行锁获取粉丝档案当前等级 var profile models.FanProfile if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil { return err } result.OldLevel = profile.Level result.NewLevel = newLevel // 6. 如有升级,发放奖励 if newLevel > profile.Level { // 查询升级奖励 rewards, err := r.getLevelUpRewards(tx, newLevel) if err != nil { logger.Logger.Warn("Failed to get level up rewards", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("level", newLevel), zap.Error(err)) } // 计算水晶奖励总额 var crystalReward int64 = 0 var likeBetCountReward int32 = 0 for _, reward := range rewards { if reward.RewardType == "crystal" && reward.IsEnabled { crystalReward += reward.RewardValue } if reward.RewardType == "like_bet_count" && reward.IsEnabled { likeBetCountReward += int32(reward.RewardValue) } } result.CrystalReward = crystalReward // 发放升级奖励(水晶 + 点赞押注次数) if crystalReward > 0 || likeBetCountReward > 0 { balanceBefore := profile.CrystalBalance balanceAfter := balanceBefore + crystalReward // 写入水晶流水(只有水晶有流水) if crystalReward > 0 { crystalRecord := &models.CrystalTransactionRecord{ UserID: userID, StarID: starID, ChangeType: "level_up_bonus", Delta: crystalReward, BalanceBefore: balanceBefore, BalanceAfter: balanceAfter, SourceID: "", Description: fmt.Sprintf("升级到%d级奖励", newLevel), CreatedAt: time.Now().UnixMilli(), } if err := tx.Create(crystalRecord).Error; err != nil { return err } } // 更新 FanProfile(等级 + 水晶余额 + 点赞押注次数) updates := map[string]interface{}{ "level": newLevel, } if crystalReward > 0 { updates["crystal_balance"] = balanceAfter } if likeBetCountReward > 0 { updates["like_bet_count"] = gorm.Expr("like_bet_count + ?", likeBetCountReward) } if err := tx.Model(&profile).Updates(updates).Error; err != nil { return err } } else { // 只更新等级 if err := tx.Model(&profile).Update("level", newLevel).Error; err != nil { return err } } logger.Logger.Info("Level up from exhibition hours", zap.Int64("user_id", userID), zap.Int64("star_id", starID), zap.Int32("old_level", profile.Level), zap.Int32("new_level", newLevel), zap.Int64("crystal_reward", crystalReward), zap.Int32("like_bet_count_reward", likeBetCountReward), zap.Int64("total_hours", exhibitionHours.TotalExhibitionHours)) } return nil }) if err != nil { return 0, 0, 0, err } levelDelta := result.NewLevel - result.OldLevel return result.NewLevel, levelDelta, result.CrystalReward, nil } // getLevelUpRewards 获取指定等级的升级奖励 func (r *fanProfileRepository) getLevelUpRewards(tx *gorm.DB, level int32) ([]*models.LevelUpRewardConfig, error) { var rewards []*models.LevelUpRewardConfig err := tx.Where("level = ? AND is_enabled = ?", level, true).Find(&rewards).Error return rewards, err } // UpdateExperience 更新经验值(同时自动更新等级) // UpdateExperience 更新经验值(同时自动更新等级) // delta: 变化量,正数表示增加,负数表示减少 // 返回: 更新后的经验值 func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int64) (int64, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } if starID <= 0 { return 0, errors.New("star_id must be greater than 0") } // 使用事务确保原子性 var newExperience int64 err := r.db.Transaction(func(tx *gorm.DB) error { // 先查询当前的 experience 值 var profile models.FanProfile if err := tx.Where("user_id = ? AND star_id = ?", userID, starID). First(&profile).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return appErrors.ErrFanProfileNotFound } return err } // 计算新经验值 newExperience = profile.Experience + delta // 确保不会小于 0 if newExperience < 0 { newExperience = 0 } // 根据新经验值计算新等级 newLevel := CalculateLevel(newExperience) // 更新 experience 和 level 字段 if err := tx.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Updates(map[string]interface{}{ "experience": newExperience, "level": newLevel, }).Error; err != nil { return err } return nil }) if err != nil { return 0, err } return newExperience, nil } // UpdateAvatar 更新头像 func (r *fanProfileRepository) UpdateAvatar(userID, starID int64, avatarURL string) error { if userID <= 0 { return errors.New("user_id must be greater than 0") } if starID <= 0 { return errors.New("star_id must be greater than 0") } if avatarURL == "" { return errors.New("avatar_url cannot be empty") } result := r.db.Model(&models.FanProfile{}). Where("user_id = ? AND star_id = ?", userID, starID). Update("avatar_url", avatarURL) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return appErrors.ErrFanProfileNotFound } return nil } // SyncLevelFromExperience 根据经验值同步等级(只升级不降级) // 在获取用户信息时调用,确保等级与经验值匹配 func (r *fanProfileRepository) SyncLevelFromExperience(userID, starID int64) (int32, error) { var profile models.FanProfile if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil { return 0, err } newLevel := CalculateLevel(profile.Experience) // 只升级,不降级 if newLevel > profile.Level { if err := r.db.Model(&profile).Update("level", newLevel).Error; err != nil { return profile.Level, err } return newLevel, nil } return profile.Level, nil }