773 lines
21 KiB
Go
773 lines
21 KiB
Go
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
|
||
}
|