429 lines
11 KiB
Go
429 lines
11 KiB
Go
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
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
return newLevel, downgraded, nil
|
||
}
|
||
|
||
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))
|
||
}
|
||
}
|
||
|
||
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)
|
||
} |