- settlement(1.1): exhibition_revenue_records 加 UNIQUE(exhibition_id,cycle_start_time) + CreateRevenueRecord ON CONFLICT DO NOTHING; MQ 用 settled_at 替代复用 is_processed; 恢复扫描器过滤改 settled_at IS NULL; created_at 统一毫秒; 删死代码 cleanup_worker.go。 - hours(1.2): 新增 exhibition_hours_log/asset_exhibition_hours_log(source_id 唯一)幂等表; fan_profile/assetLevel 的 AddExhibitionHours 按 sourceID 幂等(事务包裹); 存量重算脚本。 - mint(1.4/1.5/1.6): crystal_transaction_records (source_id,change_type) 部分唯一索引 + UpdateCrystalBalance/CreateMintOrder 幂等; 保底改 crypto/rand; 下线伪 tx_hash; doMint Redis Lua 原子限流。 - migrations 001/002/003; 各服务单测(自包含, 缺 DB t.Skip)。 Co-Authored-By: Claude <noreply@anthropic.com>
636 lines
18 KiB
Go
636 lines
18 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/topfans/backend/pkg/logger"
|
||
"github.com/topfans/backend/pkg/models"
|
||
eventPb "github.com/topfans/backend/pkg/proto/event"
|
||
"github.com/topfans/backend/pkg/statistic"
|
||
"github.com/topfans/backend/services/assetService/repository"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
type AssetLevelService interface {
|
||
GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error)
|
||
GetRecordByAssetID(assetID int64) (*models.AssetLevelRecord, error)
|
||
GetLevelConfig(level string) (*models.AssetLevel, error)
|
||
GetAllLevels() ([]*models.AssetLevel, error)
|
||
AddExhibitionHours(assetID int64, hours int, sourceID string) (string, bool, error)
|
||
AddLikes(assetID int64, count int) (string, bool, error)
|
||
RemoveLikes(assetID int64, count int) (string, bool, error)
|
||
CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error)
|
||
SeasonReset(seasonID string) error
|
||
GetCurrentSeason() (*models.Season, error)
|
||
GetChangeLogs(assetID int64, page, pageSize int) ([]*models.AssetLevelChangeLog, error)
|
||
}
|
||
|
||
type assetLevelService struct {
|
||
levelRepo *repository.AssetLevelRepository
|
||
seasonRepo *repository.SeasonRepository
|
||
decayConfigRepo *repository.SeasonDecayConfigRepository
|
||
assetRepo repository.AssetRepository // 用于同步等级到AssetRegistry.Grade(可选)
|
||
}
|
||
|
||
func NewAssetLevelService(
|
||
levelRepo *repository.AssetLevelRepository,
|
||
seasonRepo *repository.SeasonRepository,
|
||
decayConfigRepo *repository.SeasonDecayConfigRepository,
|
||
) AssetLevelService {
|
||
return &assetLevelService{
|
||
levelRepo: levelRepo,
|
||
seasonRepo: seasonRepo,
|
||
decayConfigRepo: decayConfigRepo,
|
||
}
|
||
}
|
||
|
||
func (s *assetLevelService) GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error) {
|
||
record, err := s.levelRepo.GetByAssetID(assetID)
|
||
if err == nil {
|
||
return record, nil
|
||
}
|
||
if err == gorm.ErrRecordNotFound {
|
||
record = &models.AssetLevelRecord{
|
||
AssetID: assetID,
|
||
CurrentLevel: models.LevelN,
|
||
}
|
||
if err := s.levelRepo.Create(record); err != nil {
|
||
return nil, err
|
||
}
|
||
return record, nil
|
||
}
|
||
return nil, err
|
||
}
|
||
|
||
func (s *assetLevelService) GetRecordByAssetID(assetID int64) (*models.AssetLevelRecord, error) {
|
||
return s.levelRepo.GetByAssetID(assetID)
|
||
}
|
||
|
||
func (s *assetLevelService) GetLevelConfig(level string) (*models.AssetLevel, error) {
|
||
return s.levelRepo.GetLevelConfig(level)
|
||
}
|
||
|
||
func (s *assetLevelService) GetAllLevels() ([]*models.AssetLevel, error) {
|
||
return s.levelRepo.GetAllLevels()
|
||
}
|
||
|
||
func (s *assetLevelService) GetCurrentSeason() (*models.Season, error) {
|
||
return s.seasonRepo.GetActiveSeason()
|
||
}
|
||
|
||
func (s *assetLevelService) getDefaultSeason() *models.Season {
|
||
return &models.Season{
|
||
ID: "season_1",
|
||
Name: "第一赛季",
|
||
DurationDays: 84,
|
||
ResetStrategy: "percentage_decay",
|
||
ResetLevel: true,
|
||
Status: "active",
|
||
}
|
||
}
|
||
|
||
// CheckUpgrade 检查是否可以升级(使用赛季内累计)
|
||
func (s *assetLevelService) CheckUpgrade(record *models.AssetLevelRecord) (string, bool) {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
currentOrder := models.LevelOrderMap[record.CurrentLevel]
|
||
|
||
for i := len(levels) - 1; i >= 0; i-- {
|
||
level := levels[i]
|
||
if level.LevelOrder <= currentOrder {
|
||
continue
|
||
}
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
return level.Level, true
|
||
}
|
||
}
|
||
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
// CheckDowngrade 检查是否需要降级
|
||
func (s *assetLevelService) CheckDowngrade(record *models.AssetLevelRecord) (string, bool) {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel, false
|
||
}
|
||
|
||
currentOrder := models.LevelOrderMap[record.CurrentLevel]
|
||
|
||
for _, level := range levels {
|
||
if level.LevelOrder != currentOrder {
|
||
continue
|
||
}
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
return record.CurrentLevel, false
|
||
}
|
||
}
|
||
|
||
newLevel := models.LevelN
|
||
for _, level := range levels {
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
newLevel = level.Level
|
||
}
|
||
}
|
||
|
||
return newLevel, newLevel != record.CurrentLevel
|
||
}
|
||
|
||
func (s *assetLevelService) AddExhibitionHours(assetID int64, hours int, sourceID string) (string, bool, error) {
|
||
if sourceID == "" {
|
||
logger.Logger.Warn("AssetLevelService.AddExhibitionHours called with empty sourceID, idempotency degraded",
|
||
zap.Int64("asset_id", assetID))
|
||
}
|
||
|
||
db := s.levelRepo.GetDB()
|
||
if db == nil {
|
||
return "", false, fmt.Errorf("asset level repository db not initialized")
|
||
}
|
||
|
||
var resultLevel string
|
||
var upgraded bool
|
||
|
||
// 整个累积时长流程(log INSERT + 判 RowsAffected + 累加 Save + 升级 changelog)放在同一事务:
|
||
// - log 写入了但累加崩了 → 一起回滚(避免"log 记了但 hours 没加"孤儿态)
|
||
// - 重复路径 RowsAffected==0 直接 return,事务早结束
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
txRepo := s.levelRepo.WithTx(tx)
|
||
|
||
// 0. 幂等闸口
|
||
if sourceID != "" {
|
||
logRow := &models.AssetExhibitionHoursLog{
|
||
SourceID: sourceID,
|
||
AssetID: assetID,
|
||
Hours: hours,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
res := tx.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "source_id"}},
|
||
DoNothing: true,
|
||
}).Create(logRow)
|
||
if res.Error != nil {
|
||
return res.Error
|
||
}
|
||
if res.RowsAffected == 0 {
|
||
// 重复 sourceID:回查当前等级后 return,事务提前结束
|
||
rec, qerr := txRepo.GetByAssetID(assetID)
|
||
if qerr != nil && qerr != gorm.ErrRecordNotFound {
|
||
return qerr
|
||
}
|
||
logger.Logger.Info("AssetLevelService.AddExhibitionHours: duplicate source_id, skip",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("source_id", sourceID))
|
||
if rec != nil {
|
||
resultLevel = rec.CurrentLevel
|
||
}
|
||
upgraded = false
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// 1. 获取或创建累计时长记录
|
||
record, err := getOrCreateRecordTx(tx, assetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
if record.SeasonID == "" {
|
||
season, err := s.GetCurrentSeason()
|
||
if err != nil {
|
||
season = s.getDefaultSeason()
|
||
}
|
||
record.SeasonID = season.ID
|
||
}
|
||
|
||
// 2. 累加时长(原子更新,避免竞态)
|
||
now := time.Now().UnixMilli()
|
||
if err := tx.Model(&models.AssetLevelRecord{}).
|
||
Where("asset_id = ?", assetID).
|
||
Updates(map[string]any{
|
||
"season_exhibition_hours": gorm.Expr("season_exhibition_hours + ?", hours),
|
||
"lifetime_exhibition_hours": gorm.Expr("lifetime_exhibition_hours + ?", hours),
|
||
"updated_at": now,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 3. 重新读最新 record(供 CheckUpgrade 用最新 hours)
|
||
updated, err := txRepo.GetByAssetID(assetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
updated.CurrentLevel = record.CurrentLevel
|
||
updated.SeasonID = record.SeasonID
|
||
record = updated
|
||
|
||
// 4. 计算新等级
|
||
newLevel, isUpgraded := s.CheckUpgrade(record)
|
||
if isUpgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
// 5. 落库累计/等级
|
||
if err := txRepo.Save(record); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 6. 升级 changelog + asset_registry.grade 同步(同事务)
|
||
if isUpgraded && newLevel != oldLevel {
|
||
changeLog := &models.AssetLevelChangeLog{
|
||
AssetID: record.AssetID,
|
||
FromLevel: oldLevel,
|
||
ToLevel: newLevel,
|
||
TriggerType: "exhibition_complete",
|
||
TriggerHours: record.SeasonExhibitionHours,
|
||
TriggerLikes: record.SeasonLikes,
|
||
ChangeReason: fmt.Sprintf("展出完成,时长+%d小时", hours),
|
||
CreatedAt: now,
|
||
}
|
||
if err := txRepo.CreateChangeLog(changeLog); err != nil {
|
||
return err
|
||
}
|
||
grade := models.LevelToGrade(newLevel)
|
||
if err := tx.Table("public.asset_registry").
|
||
Where("asset_id = ?", record.AssetID).
|
||
Update("grade", grade).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
resultLevel = newLevel
|
||
upgraded = isUpgraded
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
// 升级事件埋点(statistic)走事务外:fire-and-forget,不应阻塞核心事务
|
||
if upgraded && resultLevel != "" {
|
||
// 升级事件埋点(沿用 logLevelChange 既有行为,但不在事务内)
|
||
statistic.Get().TrackEvent(context.Background(), &eventPb.Event{
|
||
EventType: "asset.level_up",
|
||
UserId: 0,
|
||
StarId: 0,
|
||
OccurredAt: time.Now().UnixMilli(),
|
||
Properties: map[string]string{
|
||
"asset_id": strconv.FormatInt(assetID, 10),
|
||
"to": resultLevel,
|
||
},
|
||
})
|
||
}
|
||
|
||
return resultLevel, upgraded, nil
|
||
}
|
||
|
||
// getOrCreateRecordTx 事务内获取或创建 asset_level_records 行。
|
||
// 走 INSERT ... ON CONFLICT(asset_id) DO NOTHING 保证并发安全。
|
||
func getOrCreateRecordTx(tx *gorm.DB, assetID int64) (*models.AssetLevelRecord, error) {
|
||
var record models.AssetLevelRecord
|
||
err := tx.Where("asset_id = ?", assetID).First(&record).Error
|
||
if err == nil {
|
||
return &record, nil
|
||
}
|
||
if err != gorm.ErrRecordNotFound {
|
||
return nil, err
|
||
}
|
||
// 不存在则插入空记录;并发场景下 ON CONFLICT DO NOTHING 防重复插入
|
||
newRec := &models.AssetLevelRecord{
|
||
AssetID: assetID,
|
||
CurrentLevel: models.LevelN,
|
||
}
|
||
if err := tx.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "asset_id"}},
|
||
DoNothing: true,
|
||
}).Create(newRec).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
// 重读一次拿稳定 record(包含数据库默认值)
|
||
if err := tx.Where("asset_id = ?", assetID).First(&record).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return &record, nil
|
||
}
|
||
|
||
func (s *assetLevelService) AddLikes(assetID int64, count int) (string, bool, error) {
|
||
record, err := s.GetOrCreateRecord(assetID)
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
if record.SeasonID == "" {
|
||
season, err := s.GetCurrentSeason()
|
||
if err != nil {
|
||
season = s.getDefaultSeason()
|
||
}
|
||
record.SeasonID = season.ID
|
||
}
|
||
|
||
record.SeasonLikes += count
|
||
record.LifetimeLikes += count
|
||
|
||
newLevel, upgraded := s.CheckUpgrade(record)
|
||
if upgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
if err := s.levelRepo.Save(record); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
if upgraded && newLevel != oldLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"like_update", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("点赞数达到%d触发升级", record.SeasonLikes))
|
||
// 同步等级到AssetRegistry.Grade
|
||
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
|
||
}
|
||
|
||
return newLevel, upgraded, nil
|
||
}
|
||
|
||
func (s *assetLevelService) RemoveLikes(assetID int64, count int) (string, bool, error) {
|
||
record, err := s.GetOrCreateRecord(assetID)
|
||
if err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
oldLevel := record.CurrentLevel
|
||
|
||
record.SeasonLikes -= count
|
||
if record.SeasonLikes < 0 {
|
||
record.SeasonLikes = 0
|
||
}
|
||
record.LifetimeLikes -= count
|
||
if record.LifetimeLikes < 0 {
|
||
record.LifetimeLikes = 0
|
||
}
|
||
|
||
newLevel, downgraded := s.CheckDowngrade(record)
|
||
if downgraded {
|
||
record.CurrentLevel = newLevel
|
||
}
|
||
|
||
if err := s.levelRepo.Save(record); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
if downgraded && newLevel != oldLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"like_remove", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("取消点赞,点赞数降至%d触发降级", record.SeasonLikes))
|
||
// 同步等级到AssetRegistry.Grade
|
||
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
|
||
}
|
||
|
||
return newLevel, downgraded, nil
|
||
}
|
||
|
||
// syncGradeToAssetRegistry 将等级同步到AssetRegistry.Grade
|
||
// 直接使用gorm.DB更新,不依赖repository层
|
||
func (s *assetLevelService) syncGradeToAssetRegistry(assetID int64, level string) {
|
||
if s.assetRepo == nil {
|
||
// 直接使用levelRepo的db进行更新
|
||
grade := models.LevelToGrade(level)
|
||
db := s.levelRepo.GetDB()
|
||
if db != nil {
|
||
if err := db.Table("public.asset_registry").
|
||
Where("asset_id = ?", assetID).
|
||
Update("grade", grade).Error; err != nil {
|
||
logger.Logger.Warn("syncGradeToAssetRegistry failed",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("level", level),
|
||
zap.Int("grade", grade),
|
||
zap.Error(err))
|
||
} else {
|
||
logger.Logger.Info("syncGradeToAssetRegistry success",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("level", level),
|
||
zap.Int("grade", grade))
|
||
}
|
||
}
|
||
return
|
||
}
|
||
grade := models.LevelToGrade(level)
|
||
if err := s.assetRepo.UpdateGradeByAssetID(assetID, int32(grade)); err != nil {
|
||
logger.Logger.Warn("syncGradeToAssetRegistry failed",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("level", level),
|
||
zap.Int("grade", grade),
|
||
zap.Error(err))
|
||
} else {
|
||
logger.Logger.Info("syncGradeToAssetRegistry success",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.String("level", level),
|
||
zap.Int("grade", grade))
|
||
}
|
||
}
|
||
|
||
func (s *assetLevelService) CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error) {
|
||
record, err := s.GetRecordByAssetID(assetID)
|
||
if err != nil || record == nil {
|
||
return s.calculateDefaultRevenue(likeCount, startTime, endTime, revenueBoostBps)
|
||
}
|
||
|
||
levelConfig, err := s.GetLevelConfig(record.CurrentLevel)
|
||
if err != nil {
|
||
return s.calculateDefaultRevenue(likeCount, startTime, endTime, revenueBoostBps)
|
||
}
|
||
|
||
T := (endTime - startTime) / 3600000
|
||
if T <= 0 {
|
||
T = 1
|
||
}
|
||
|
||
R0 := int64(levelConfig.HourlyRevenue)
|
||
baseRevenue := R0 * T
|
||
|
||
buff := CalculateBuff(likeCount)
|
||
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
|
||
|
||
if revenueBoostBps > 0 {
|
||
boost := buffedRevenue * int64(revenueBoostBps) / 10000
|
||
buffedRevenue += boost
|
||
}
|
||
|
||
return buffedRevenue, nil
|
||
}
|
||
|
||
func (s *assetLevelService) calculateDefaultRevenue(likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error) {
|
||
R0 := int64(5)
|
||
T := (endTime - startTime) / 3600000
|
||
if T <= 0 {
|
||
T = 1
|
||
}
|
||
|
||
baseRevenue := R0 * T
|
||
buff := CalculateBuff(likeCount)
|
||
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
|
||
|
||
if revenueBoostBps > 0 {
|
||
boost := buffedRevenue * int64(revenueBoostBps) / 10000
|
||
buffedRevenue += boost
|
||
}
|
||
|
||
return buffedRevenue, nil
|
||
}
|
||
|
||
// CalculateBuff 根据点赞数计算Buff百分比
|
||
func CalculateBuff(likeCount int) int {
|
||
switch {
|
||
case likeCount >= 30:
|
||
return 30
|
||
case likeCount >= 10:
|
||
return 20
|
||
case likeCount >= 5:
|
||
return 10
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
func (s *assetLevelService) SeasonReset(seasonID string) error {
|
||
season, err := s.seasonRepo.GetByID(seasonID)
|
||
if err != nil || season == nil {
|
||
return fmt.Errorf("season not found: %s", seasonID)
|
||
}
|
||
|
||
if !season.ResetLevel {
|
||
return nil
|
||
}
|
||
|
||
decayConfigs, err := s.decayConfigRepo.GetBySeason(seasonID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
decayPercentMap := make(map[string]int)
|
||
for _, cfg := range decayConfigs {
|
||
decayPercentMap[cfg.Level] = cfg.PreservePercent
|
||
}
|
||
|
||
records, err := s.levelRepo.GetBySeason(seasonID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
nextSeasonID := s.calculateNextSeasonID(seasonID)
|
||
_, err = s.seasonRepo.GetOrCreate(nextSeasonID, season.DurationDays, season.ResetStrategy)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, record := range records {
|
||
oldLevel := record.CurrentLevel
|
||
|
||
preservePercent := decayPercentMap[record.CurrentLevel]
|
||
if preservePercent <= 0 {
|
||
preservePercent = 0
|
||
} else if preservePercent >= 100 {
|
||
preservePercent = 100
|
||
}
|
||
|
||
record.SeasonExhibitionHours = record.LifetimeExhibitionHours * preservePercent / 100
|
||
record.SeasonLikes = record.LifetimeLikes * preservePercent / 100
|
||
|
||
newLevel := s.recalculateLevelAfterDecay(record)
|
||
record.CurrentLevel = newLevel
|
||
record.SeasonID = nextSeasonID
|
||
|
||
if err := s.levelRepo.Save(record); err != nil {
|
||
logger.Logger.Error("SeasonReset: failed to update record",
|
||
zap.Int64("asset_id", record.AssetID),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
if oldLevel != newLevel {
|
||
s.logLevelChange(record.AssetID, oldLevel, newLevel,
|
||
"season_decay", record.SeasonExhibitionHours, record.SeasonLikes,
|
||
fmt.Sprintf("赛季%s降序,保留%d%%", seasonID, preservePercent))
|
||
// 同步等级到AssetRegistry.Grade
|
||
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
|
||
}
|
||
}
|
||
|
||
season.Status = "ended"
|
||
return s.seasonRepo.Save(season)
|
||
}
|
||
|
||
func (s *assetLevelService) calculateNextSeasonID(currentSeasonID string) string {
|
||
if len(currentSeasonID) > 0 && currentSeasonID[:7] == "season_" {
|
||
num := 0
|
||
fmt.Sscanf(currentSeasonID[7:], "%d", &num)
|
||
return fmt.Sprintf("season_%d", num+1)
|
||
}
|
||
return currentSeasonID + "_next"
|
||
}
|
||
|
||
func (s *assetLevelService) recalculateLevelAfterDecay(record *models.AssetLevelRecord) string {
|
||
levels, err := s.GetAllLevels()
|
||
if err != nil {
|
||
return record.CurrentLevel
|
||
}
|
||
|
||
newLevel := models.LevelN
|
||
for _, level := range levels {
|
||
if record.SeasonExhibitionHours >= level.RequireHours &&
|
||
record.SeasonLikes >= level.RequireLikes {
|
||
newLevel = level.Level
|
||
}
|
||
}
|
||
|
||
return newLevel
|
||
}
|
||
|
||
func (s *assetLevelService) logLevelChange(assetID int64, fromLevel, toLevel, triggerType string, triggerHours, triggerLikes int, reason string) {
|
||
log := &models.AssetLevelChangeLog{
|
||
AssetID: assetID,
|
||
FromLevel: fromLevel,
|
||
ToLevel: toLevel,
|
||
TriggerType: triggerType,
|
||
TriggerHours: triggerHours,
|
||
TriggerLikes: triggerLikes,
|
||
ChangeReason: reason,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
if err := s.levelRepo.CreateChangeLog(log); err != nil {
|
||
logger.Logger.Error("Failed to create level change log",
|
||
zap.Int64("asset_id", assetID),
|
||
zap.Error(err))
|
||
}
|
||
|
||
// 事件埋点:asset.level_up(fire-and-forget)
|
||
// 简化:直接提交,不查 user_id/star_id(metric_recent_level_ups 表后续可补)
|
||
statistic.Get().TrackEvent(context.Background(), &eventPb.Event{
|
||
EventType: "asset.level_up",
|
||
UserId: 0, // 占位,TODO 后续从 public.assets 查
|
||
StarId: 0, // 占位
|
||
OccurredAt: time.Now().UnixMilli(),
|
||
Properties: map[string]string{
|
||
"asset_id": strconv.FormatInt(assetID, 10),
|
||
"from": fromLevel,
|
||
"to": toLevel,
|
||
},
|
||
})
|
||
}
|
||
|
||
func (s *assetLevelService) GetChangeLogs(assetID int64, page, pageSize int) ([]*models.AssetLevelChangeLog, error) {
|
||
offset := (page - 1) * pageSize
|
||
return s.levelRepo.GetChangeLogs(assetID, pageSize, offset)
|
||
} |