topfans/backend/services/assetService/service/asset_level_service.go

478 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"fmt"
"time"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
"github.com/topfans/backend/services/assetService/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
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) (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) (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.SeasonExhibitionHours += hours
record.LifetimeExhibitionHours += hours
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,
"exhibition_complete", record.SeasonExhibitionHours, record.SeasonLikes,
fmt.Sprintf("展出完成,时长+%d小时", hours))
// 同步等级到AssetRegistry.Grade
s.syncGradeToAssetRegistry(record.AssetID, newLevel)
}
return newLevel, upgraded, 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))
}
}
func (s *assetLevelService) GetChangeLogs(assetID int64, page, pageSize int) ([]*models.AssetLevelChangeLog, error) {
offset := (page - 1) * pageSize
return s.levelRepo.GetChangeLogs(assetID, pageSize, offset)
}