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