feat: add asset level service with upgrade/downgrade logic
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0753f0b168
commit
11a59b0632
429
backend/services/assetService/service/asset_level_service.go
Normal file
429
backend/services/assetService/service/asset_level_service.go
Normal file
@ -0,0 +1,429 @@
|
||||
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)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user