Compare commits

...

10 Commits

Author SHA1 Message Date
zerosaturation
057f9bce1c feat:修改藏品等级硬编码改为根据数据库的配置来定义 2026-05-25 13:23:30 +08:00
zerosaturation
99a53d522c fix: add new models to autoMigrate and start season reset worker 2026-05-25 12:35:19 +08:00
zerosaturation
8a71b6ff75 test: add asset level service unit tests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:26:54 +08:00
zerosaturation
8a4ddb86bf fix: wire AssetLevelService into existing service constructors 2026-05-25 12:25:45 +08:00
zerosaturation
5868e55fa2 feat: integrate asset level system with existing services
- mintService: Add AssetLevelService field and call GetOrCreateRecord after asset creation
- assetLikeService: Add AssetLevelService and call AddLikes/RemoveLikes in LikeAsset/UnlikeAsset
- revenueService: Add AssetLevelService and call AddExhibitionHours in OnExhibitionCompleted

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:23:23 +08:00
zerosaturation
037b7cc3f9 feat: add asset level provider for dependency injection
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:17:29 +08:00
zerosaturation
391ef8c43d feat: add season reset worker
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:16:05 +08:00
zerosaturation
fac8fea3b9 feat: add asset level service with upgrade/downgrade logic
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:08:29 +08:00
zerosaturation
0c556182fc feat: add asset level repositories
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:34:42 +08:00
zerosaturation
7271da0e1e feat: add asset level models
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:33:23 +08:00
19 changed files with 2337 additions and 28 deletions

View File

@ -0,0 +1,138 @@
package models
import (
"time"
"gorm.io/gorm"
)
// AssetLevel 藏品等级配置
type AssetLevel struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Level string `gorm:"type:varchar(10);unique;not null"`
LevelOrder int `gorm:"not null"`
HourlyRevenue int `gorm:"not null"`
RequireHours int `gorm:"not null"`
RequireLikes int `gorm:"not null"`
IsInitial bool `gorm:"default:false"`
CreatedAt int64 `gorm:"not null"`
UpdatedAt int64 `gorm:"not null"`
}
func (AssetLevel) TableName() string { return "asset_levels" }
// AssetLevelRecord 藏品等级记录
type AssetLevelRecord struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
AssetID int64 `gorm:"unique;not null"`
CurrentLevel string `gorm:"type:varchar(10);not null;default:'N'"`
SeasonExhibitionHours int `gorm:"default:0;not null"`
SeasonLikes int `gorm:"default:0;not null"`
LifetimeExhibitionHours int `gorm:"default:0;not null"`
LifetimeLikes int `gorm:"default:0;not null"`
SeasonID string `gorm:"type:varchar(50)"`
UpdatedAt int64 `gorm:"not null"`
}
func (AssetLevelRecord) TableName() string { return "asset_level_records" }
// AssetLevelChangeLog 等级变化日志
type AssetLevelChangeLog struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
AssetID int64 `gorm:"not null;index"`
FromLevel string `gorm:"type:varchar(10)"`
ToLevel string `gorm:"type:varchar(10);not null"`
TriggerType string `gorm:"type:varchar(20);not null"`
TriggerHours int `gorm:"default:0"`
TriggerLikes int `gorm:"default:0"`
ChangeReason string `gorm:"type:varchar(255)"`
CreatedAt int64 `gorm:"not null;index"`
}
func (AssetLevelChangeLog) TableName() string { return "asset_level_change_logs" }
// Season 赛季配置
type Season struct {
ID string `gorm:"primaryKey;type:varchar(50)"`
Name string `gorm:"type:varchar(100);not null"`
DurationDays int `gorm:"not null;default:84"`
StartTime int64 `gorm:"not null"`
EndTime int64 `gorm:"not null"`
ResetStrategy string `gorm:"type:varchar(20);default:'percentage_decay'"`
ResetLevel bool `gorm:"default:true"`
Status string `gorm:"type:varchar(20);default:'active'"`
CreatedAt int64 `gorm:"not null"`
UpdatedAt int64 `gorm:"not null"`
}
func (Season) TableName() string { return "seasons" }
// CalculateEndTime 计算赛季结束时间
func (s *Season) CalculateEndTime() int64 {
return s.StartTime + int64(s.DurationDays)*86400000
}
// BeforeCreate 创建前钩子
func (s *Season) BeforeCreate(tx *gorm.DB) error {
now := time.Now().UnixMilli()
s.CreatedAt = now
s.UpdatedAt = now
s.EndTime = s.CalculateEndTime()
if s.Status == "" {
s.Status = "active"
}
return nil
}
// BeforeUpdate 更新前钩子
func (s *Season) BeforeUpdate(tx *gorm.DB) error {
s.UpdatedAt = time.Now().UnixMilli()
s.EndTime = s.CalculateEndTime()
return nil
}
// SeasonDecayConfig 赛季降序百分比配置
type SeasonDecayConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
SeasonID string `gorm:"type:varchar(50);not null;uniqueIndex:uk_season_level"`
Level string `gorm:"type:varchar(10);not null;uniqueIndex:uk_season_level"`
PreservePercent int `gorm:"not null;default:100"`
UpdatedAt int64 `gorm:"not null"`
}
func (SeasonDecayConfig) TableName() string { return "season_decay_config" }
// 等级常量
const (
LevelN = "N"
LevelR = "R"
LevelSR = "SR"
LevelSSR = "SSR"
LevelUR = "UR"
)
// LevelOrderMap 等级顺序映射
var LevelOrderMap = map[string]int{
LevelN: 1,
LevelR: 2,
LevelSR: 3,
LevelSSR: 4,
LevelUR: 5,
}
// LevelToGradeMap 等级字符串到前端Grade的映射1-5
var LevelToGradeMap = map[string]int{
LevelN: 1,
LevelR: 2,
LevelSR: 3,
LevelSSR: 4,
LevelUR: 5,
}
// LevelToGrade 将等级字符串转换为前端Grade
func LevelToGrade(level string) int {
if grade, ok := LevelToGradeMap[level]; ok {
return grade
}
return 1 // 默认返回1
}

View File

@ -1726,6 +1726,7 @@ type OnExhibitionCompletedRequest struct {
CrystalAmount int64 `protobuf:"varint,7,opt,name=crystal_amount,json=crystalAmount,proto3" json:"crystal_amount,omitempty"`
StartTime int64 `protobuf:"varint,8,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"`
ExpireAt int64 `protobuf:"varint,9,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"`
LikeCount int32 `protobuf:"varint,10,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 点赞数用于在taskService中根据资产等级重新计算收益
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1823,6 +1824,13 @@ func (x *OnExhibitionCompletedRequest) GetExpireAt() int64 {
return 0
}
func (x *OnExhibitionCompletedRequest) GetLikeCount() int32 {
if x != nil {
return x.LikeCount
}
return 0
}
type OnExhibitionCompletedResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
@ -2001,7 +2009,7 @@ const file_task_proto_rawDesc = "" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"c\n" +
"\x15InitUserTasksResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\"\xcd\x02\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\"\xec\x02\n" +
"\x1cOnExhibitionCompletedRequest\x12#\n" +
"\rexhibition_id\x18\x01 \x01(\x03R\fexhibitionId\x12\x19\n" +
"\basset_id\x18\x02 \x01(\x03R\aassetId\x12\x17\n" +
@ -2012,7 +2020,10 @@ const file_task_proto_rawDesc = "" +
"\x0ecrystal_amount\x18\a \x01(\x03R\rcrystalAmount\x12\x1d\n" +
"\n" +
"start_time\x18\b \x01(\x03R\tstartTime\x12\x1b\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\"}\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\x12\x1d\n" +
"\n" +
"like_count\x18\n" +
" \x01(\x05R\tlikeCount\"}\n" +
"\x1dOnExhibitionCompletedResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12*\n" +
"\x11revenue_record_id\x18\x02 \x01(\x03R\x0frevenueRecordId2\xc6\f\n" +

View File

@ -195,6 +195,7 @@ message OnExhibitionCompletedRequest {
int64 crystal_amount = 7;
int64 start_time = 8;
int64 expire_at = 9;
int32 like_count = 10; // taskService中根据资产等级重新计算收益
}
message OnExhibitionCompletedResponse {

View File

@ -7,6 +7,7 @@ import (
"os/signal"
"strconv"
"syscall"
"time"
"dubbo.apache.org/dubbo-go/v3/client"
_ "dubbo.apache.org/dubbo-go/v3/imports"
@ -132,11 +133,16 @@ func main() {
userClient := assetClient.NewUserServiceClient(userServiceClient)
logger.Logger.Info("User Service RPC client initialized")
// 创建 Provider 层实例(用于获取 AssetLevelService
assetLevelProvider := provider.NewAssetLevelProvider(database.GetDB())
assetLevelSvc := assetLevelProvider.GetLevelService()
logger.Logger.Info("AssetLevelProvider initialized")
// 创建 Service 层实例
registryRepo := starbookRepo.NewAssetRegistryRepository(database.GetDB())
assetService := service.NewAssetService(assetRepo, mintOrderRepo, assetLikeRepo, userClient, database.GetDB(), registryRepo)
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB())
mintService := service.NewMintService(assetRepo, mintOrderRepo, userClient, database.GetDB(), config.GlobalAssetConfig, registryRepo, mintCostRepo, userMintCountRepo, assetLevelSvc)
assetLikeService := service.NewAssetLikeService(assetRepo, assetLikeRepo, database.GetDB(), assetLevelSvc)
rankingService := service.NewRankingService(rankingRepo, userClient)
materialService := service.NewMaterialService(materialRepo, relationRepo)
logger.Logger.Info("Service layer initialized")
@ -146,6 +152,18 @@ func main() {
rankingProvider := provider.NewRankingProvider(rankingService)
logger.Logger.Info("Provider layer initialized")
// 启动赛季重置 Worker每小时检查一次
seasonResetWorker := assetLevelProvider.GetSeasonResetWorker()
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
<-ticker.C
seasonResetWorker.Run()
}
}()
logger.Logger.Info("Season reset worker started")
// 创建 Dubbo 服务器
srv, err := server.NewServer(
server.WithServerProtocol(
@ -201,6 +219,10 @@ func autoMigrate() error {
&models.AssetLike{},
&models.Material{},
&models.AssetMaterialRelation{},
&models.AssetLevelRecord{},
&models.AssetLevelChangeLog{},
&models.Season{},
&models.SeasonDecayConfig{},
}
for _, table := range tables {

View File

@ -0,0 +1,35 @@
package provider
import (
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
"github.com/topfans/backend/services/assetService/worker"
"gorm.io/gorm"
)
type AssetLevelProvider struct {
LevelService service.AssetLevelService
db *gorm.DB
}
func NewAssetLevelProvider(db *gorm.DB) *AssetLevelProvider {
levelRepo := repository.NewAssetLevelRepository(db)
seasonRepo := repository.NewSeasonRepository(db)
decayConfigRepo := repository.NewSeasonDecayConfigRepository(db)
levelService := service.NewAssetLevelService(levelRepo, seasonRepo, decayConfigRepo)
return &AssetLevelProvider{
LevelService: levelService,
db: db,
}
}
func (p *AssetLevelProvider) GetLevelService() service.AssetLevelService {
return p.LevelService
}
func (p *AssetLevelProvider) GetSeasonResetWorker() *worker.SeasonResetWorker {
seasonRepo := repository.NewSeasonRepository(p.db)
return worker.NewSeasonResetWorker(seasonRepo, p.LevelService)
}

View File

@ -0,0 +1,70 @@
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type AssetLevelRepository struct {
db *gorm.DB
}
func NewAssetLevelRepository(db *gorm.DB) *AssetLevelRepository {
return &AssetLevelRepository{db: db}
}
func (r *AssetLevelRepository) Create(record *models.AssetLevelRecord) error {
return r.db.Create(record).Error
}
func (r *AssetLevelRepository) Save(record *models.AssetLevelRecord) error {
return r.db.Save(record).Error
}
func (r *AssetLevelRepository) GetByAssetID(assetID int64) (*models.AssetLevelRecord, error) {
var record models.AssetLevelRecord
err := r.db.Where("asset_id = ?", assetID).First(&record).Error
if err != nil {
return nil, err
}
return &record, nil
}
func (r *AssetLevelRepository) GetBySeason(seasonID string) ([]*models.AssetLevelRecord, error) {
var records []*models.AssetLevelRecord
err := r.db.Where("season_id = ?", seasonID).Find(&records).Error
return records, err
}
func (r *AssetLevelRepository) GetAllLevels() ([]*models.AssetLevel, error) {
var levels []*models.AssetLevel
err := r.db.Order("level_order ASC").Find(&levels).Error
return levels, err
}
func (r *AssetLevelRepository) GetLevelConfig(level string) (*models.AssetLevel, error) {
var config models.AssetLevel
err := r.db.Where("level = ?", level).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
func (r *AssetLevelRepository) CreateChangeLog(log *models.AssetLevelChangeLog) error {
return r.db.Create(log).Error
}
func (r *AssetLevelRepository) GetDB() *gorm.DB {
return r.db
}
func (r *AssetLevelRepository) GetChangeLogs(assetID int64, limit, offset int) ([]*models.AssetLevelChangeLog, error) {
var logs []*models.AssetLevelChangeLog
err := r.db.Where("asset_id = ?", assetID).
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&logs).Error
return logs, err
}

View File

@ -48,6 +48,9 @@ type AssetRepository interface {
// DecrementLikeCount 减少点赞数
DecrementLikeCount(assetID int64) error
// UpdateGradeByAssetID 根据asset_id更新藏品等级用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade
UpdateGradeByAssetID(assetID int64, grade int32) error
// UpdateMaterialTypeByLikes 根据点赞数和创建时间更新素材类型
UpdateMaterialTypeByLikes(assetID int64, likes int32, createdAt int64) error
@ -154,6 +157,17 @@ func (r *assetRepository) GetGradeByAssetID(assetID int64) (int32, error) {
return 0, nil
}
// UpdateGradeByAssetID 根据asset_id更新藏品等级用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade
func (r *assetRepository) UpdateGradeByAssetID(assetID int64, grade int32) error {
if assetID <= 0 {
return errors.New("asset_id must be greater than 0")
}
return r.db.Table("public.asset_registry").
Where("asset_id = ?", assetID).
Update("grade", grade).Error
}
// GetByIDs 批量查询资产
func (r *assetRepository) GetByIDs(assetIDs []int64) ([]*models.Asset, error) {
if len(assetIDs) == 0 {

View File

@ -0,0 +1,29 @@
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type SeasonDecayConfigRepository struct {
db *gorm.DB
}
func NewSeasonDecayConfigRepository(db *gorm.DB) *SeasonDecayConfigRepository {
return &SeasonDecayConfigRepository{db: db}
}
func (r *SeasonDecayConfigRepository) GetBySeason(seasonID string) ([]*models.SeasonDecayConfig, error) {
var configs []*models.SeasonDecayConfig
err := r.db.Where("season_id = ?", seasonID).Find(&configs).Error
return configs, err
}
func (r *SeasonDecayConfigRepository) GetBySeasonAndLevel(seasonID, level string) (*models.SeasonDecayConfig, error) {
var config models.SeasonDecayConfig
err := r.db.Where("season_id = ? AND level = ?", seasonID, level).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}

View File

@ -0,0 +1,67 @@
package repository
import (
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
type SeasonRepository struct {
db *gorm.DB
}
func NewSeasonRepository(db *gorm.DB) *SeasonRepository {
return &SeasonRepository{db: db}
}
func (r *SeasonRepository) GetByID(seasonID string) (*models.Season, error) {
var season models.Season
err := r.db.Where("id = ?", seasonID).First(&season).Error
if err != nil {
return nil, err
}
return &season, nil
}
func (r *SeasonRepository) GetActiveSeason() (*models.Season, error) {
var season models.Season
err := r.db.Where("status = ?", "active").First(&season).Error
if err != nil {
return nil, err
}
return &season, nil
}
func (r *SeasonRepository) GetEndedSeasons() ([]*models.Season, error) {
var seasons []*models.Season
err := r.db.Where("status = ? AND end_time < ?", "active", gorm.Expr("NOW()")).Find(&seasons).Error
return seasons, err
}
func (r *SeasonRepository) Save(season *models.Season) error {
return r.db.Save(season).Error
}
func (r *SeasonRepository) Create(season *models.Season) error {
return r.db.Create(season).Error
}
func (r *SeasonRepository) GetOrCreate(seasonID string, durationDays int, resetStrategy string) (*models.Season, error) {
season, err := r.GetByID(seasonID)
if err == nil {
return season, nil
}
if err == gorm.ErrRecordNotFound {
season = &models.Season{
ID: seasonID,
DurationDays: durationDays,
ResetStrategy: resetStrategy,
ResetLevel: true,
Status: "active",
}
if err := r.Create(season); err != nil {
return nil, err
}
return season, nil
}
return nil, err
}

View File

@ -0,0 +1,478 @@
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)
}

View File

@ -0,0 +1,28 @@
package service
import (
"testing"
)
func TestCalculateBuff(t *testing.T) {
tests := []struct {
likeCount int
expected int
}{
{0, 0},
{4, 0},
{5, 10},
{9, 10},
{10, 20},
{29, 20},
{30, 30},
{100, 30},
}
for _, tt := range tests {
result := CalculateBuff(tt.likeCount)
if result != tt.expected {
t.Errorf("CalculateBuff(%d) = %d, want %d", tt.likeCount, result, tt.expected)
}
}
}

View File

@ -15,9 +15,10 @@ import (
// AssetLikeService 资产点赞业务逻辑层
type AssetLikeService struct {
assetRepo repository.AssetRepository
assetLikeRepo repository.AssetLikeRepository
db *gorm.DB
assetRepo repository.AssetRepository
assetLikeRepo repository.AssetLikeRepository
db *gorm.DB
assetLevelService AssetLevelService // 资产等级服务
}
// NewAssetLikeService 创建资产点赞Service实例
@ -25,11 +26,13 @@ func NewAssetLikeService(
assetRepo repository.AssetRepository,
assetLikeRepo repository.AssetLikeRepository,
db *gorm.DB,
assetLevelService AssetLevelService,
) *AssetLikeService {
return &AssetLikeService{
assetRepo: assetRepo,
assetLikeRepo: assetLikeRepo,
db: db,
assetRepo: assetRepo,
assetLikeRepo: assetLikeRepo,
db: db,
assetLevelService: assetLevelService,
}
}
@ -223,6 +226,19 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI
zap.Int32("like_count", asset.LikeCount),
)
// 5. 更新资产等级记录(点赞数变化)
if s.assetLevelService != nil {
if newLevel, upgraded, err := s.assetLevelService.AddLikes(assetID, 1); err != nil {
logger.Logger.Warn("Failed to add likes to asset level",
zap.Int64("asset_id", assetID),
zap.Error(err))
} else if upgraded {
logger.Logger.Info("Asset leveled up due to likes",
zap.Int64("asset_id", assetID),
zap.String("new_level", newLevel))
}
}
return asset.LikeCount, nil
}
@ -338,6 +354,19 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta
zap.Int32("like_count", asset.LikeCount),
)
// 4. 更新资产等级记录(取消点赞)
if s.assetLevelService != nil {
if newLevel, downgraded, err := s.assetLevelService.RemoveLikes(assetID, 1); err != nil {
logger.Logger.Warn("Failed to remove likes from asset level",
zap.Int64("asset_id", assetID),
zap.Error(err))
} else if downgraded {
logger.Logger.Info("Asset downgraded due to like removal",
zap.Int64("asset_id", assetID),
zap.String("new_level", newLevel))
}
}
return asset.LikeCount, nil
}

View File

@ -67,6 +67,7 @@ type mintService struct {
registryRepo starbookRepo.AssetRegistryRepository // 资产索引仓库(用于星册体系)
localMintCostRepo repository.MintCostRepository // 铸造消耗配置仓库
userMintCountRepo repository.UserMintCountRepository // 用户铸爱累计仓库
assetLevelService AssetLevelService // 资产等级服务
}
// NewMintService 创建铸造服务实例
@ -79,6 +80,7 @@ func NewMintService(
registryRepo starbookRepo.AssetRegistryRepository,
localMintCostRepo repository.MintCostRepository,
userMintCountRepo repository.UserMintCountRepository,
assetLevelService AssetLevelService,
) MintService {
return &mintService{
assetRepo: assetRepo,
@ -89,6 +91,7 @@ func NewMintService(
registryRepo: registryRepo,
localMintCostRepo: localMintCostRepo,
userMintCountRepo: userMintCountRepo,
assetLevelService: assetLevelService,
}
}
@ -439,7 +442,16 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st
return nil, err
}
// 4. 无需异步 AI 处理cover_url 已在步骤 3.2 中直接设置
// 4. 初始化资产等级记录
if s.assetLevelService != nil && asset != nil {
if _, err := s.assetLevelService.GetOrCreateRecord(asset.ID); err != nil {
logger.Logger.Warn("Failed to create asset level record",
zap.Int64("asset_id", asset.ID),
zap.Error(err))
}
}
// 5. 无需异步 AI 处理cover_url 已在步骤 3.2 中直接设置
// 5. 获取所有者的昵称(创建时所有者就是当前用户)
var ownerNickname string

View File

@ -0,0 +1,52 @@
package worker
import (
"time"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/services/assetService/repository"
"github.com/topfans/backend/services/assetService/service"
"go.uber.org/zap"
)
type SeasonResetWorker struct {
seasonRepo *repository.SeasonRepository
levelService service.AssetLevelService
}
func NewSeasonResetWorker(
seasonRepo *repository.SeasonRepository,
levelService service.AssetLevelService,
) *SeasonResetWorker {
return &SeasonResetWorker{
seasonRepo: seasonRepo,
levelService: levelService,
}
}
func (w *SeasonResetWorker) Run() {
now := time.Now().UnixMilli()
// 获取已结束的赛季
seasons, err := w.seasonRepo.GetEndedSeasons()
if err != nil {
logger.Logger.Error("SeasonResetWorker: failed to get ended seasons", zap.Error(err))
return
}
for _, season := range seasons {
if season.EndTime > now {
continue // 还未真正结束
}
if err := w.levelService.SeasonReset(season.ID); err != nil {
logger.Logger.Error("SeasonResetWorker: failed to reset season",
zap.String("season_id", season.ID),
zap.Error(err))
continue
}
logger.Logger.Info("SeasonResetWorker: season reset completed",
zap.String("season_id", season.ID))
}
}

View File

@ -28,6 +28,7 @@ type OnExhibitionCompletedRequest struct {
StartTime int64
ExpireAt int64
CrystalAmount int64
LikeCount int32 // 点赞数用于taskService根据资产等级重新计算收益
}
// OnExhibitionCompletedResponse 展位完成响应
@ -72,6 +73,7 @@ func (c *taskRPCClient) OnExhibitionCompleted(ctx context.Context, req *OnExhibi
StartTime: req.StartTime,
ExpireAt: req.ExpireAt,
CrystalAmount: req.CrystalAmount,
LikeCount: req.LikeCount,
}
resp, err := c.client.OnExhibitionCompleted(ctx, pbReq)

View File

@ -132,6 +132,7 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
StartTime: e.StartTime,
ExpireAt: now,
CrystalAmount: revenue,
LikeCount: int32(likeCount),
})
if err != nil {
logger.Logger.Error("调用TaskService记录收益失败跳过标记已处理以便重试",

View File

@ -17,6 +17,8 @@ import (
pb "github.com/topfans/backend/pkg/proto/task"
pbGallery "github.com/topfans/backend/pkg/proto/gallery"
pbUser "github.com/topfans/backend/pkg/proto/user"
assetLevelRepo "github.com/topfans/backend/services/assetService/repository"
assetLevelSvc "github.com/topfans/backend/services/assetService/service"
"github.com/topfans/backend/services/taskService/client"
"github.com/topfans/backend/services/taskService/config"
"github.com/topfans/backend/services/taskService/model"
@ -105,9 +107,15 @@ func main() {
logger.Logger.Info("Gallery RPC client initialized")
// 6. Init services
// Create AssetLevelService for revenue calculations
assetLevelRepository := assetLevelRepo.NewAssetLevelRepository(db)
seasonRepository := assetLevelRepo.NewSeasonRepository(db)
seasonDecayConfigRepository := assetLevelRepo.NewSeasonDecayConfigRepository(db)
assetLevelService := assetLevelSvc.NewAssetLevelService(assetLevelRepository, seasonRepository, seasonDecayConfigRepository)
dailySvc := service.NewDailyTaskService(dailyRepo, userRPCClient)
onboardingSvc := service.NewOnboardingService(onboardingRepo, dailyRepo, userRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient, galleryRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient, galleryRPCClient, assetLevelService)
logger.Logger.Info("Services initialized")
// 7. Init workergoroutine 中启动)

View File

@ -7,6 +7,7 @@ import (
pbCommon "github.com/topfans/backend/pkg/proto/common"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/task"
"github.com/topfans/backend/services/taskService/client"
"github.com/topfans/backend/services/taskService/model"
@ -20,24 +21,39 @@ type RevenueService interface {
ClaimExhibitionRevenue(ctx context.Context, userID, starID int64, revenueID int64) (*pb.ClaimExhibitionRevenueResponse, error)
ClaimAllExhibitionRevenue(ctx context.Context, userID, starID int64) (*pb.ClaimAllExhibitionRevenueResponse, error)
OnExhibitionCompleted(ctx context.Context, req *pb.OnExhibitionCompletedRequest) (*pb.OnExhibitionCompletedResponse, error)
SetAssetLevelService(svc AssetLevelService)
}
// AssetLevelService 资产等级服务接口定义在assetService
type AssetLevelService interface {
GetOrCreateRecord(assetID int64) (*models.AssetLevelRecord, error)
AddExhibitionHours(assetID int64, hours int) (string, bool, error)
CalculateRevenue(assetID int64, likeCount int, startTime, endTime int64, revenueBoostBps int) (int64, error)
}
// revenueService 展示收益Service实现
type revenueService struct {
revenueRepo repository.RevenueRepository
userRPCClient client.UserServiceClient
revenueRepo repository.RevenueRepository
userRPCClient client.UserServiceClient
galleryRPCClient client.GalleryServiceClient
assetLevelService AssetLevelService // 资产等级服务
}
// NewRevenueService 创建收益Service实例
func NewRevenueService(revenueRepo repository.RevenueRepository, userRPCClient client.UserServiceClient, galleryRPCClient client.GalleryServiceClient) RevenueService {
func NewRevenueService(revenueRepo repository.RevenueRepository, userRPCClient client.UserServiceClient, galleryRPCClient client.GalleryServiceClient, assetLevelService AssetLevelService) RevenueService {
return &revenueService{
revenueRepo: revenueRepo,
userRPCClient: userRPCClient,
revenueRepo: revenueRepo,
userRPCClient: userRPCClient,
galleryRPCClient: galleryRPCClient,
assetLevelService: assetLevelService,
}
}
// SetAssetLevelService 设置资产等级服务
func (s *revenueService) SetAssetLevelService(svc AssetLevelService) {
s.assetLevelService = svc
}
// GetExhibitionRevenue 获取展示收益列表
func (s *revenueService) GetExhibitionRevenue(ctx context.Context, userID, starID int64, status string, page, pageSize int32) (*pb.GetExhibitionRevenueResponse, error) {
logger.Logger.Debug("GetExhibitionRevenue",
@ -307,7 +323,33 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
zap.Int64("slot_id", req.SlotId),
zap.Int64("occupier_uid", req.OccupierUid),
zap.Int64("slot_owner_uid", req.SlotOwnerUid),
zap.Int64("crystal_amount", req.CrystalAmount))
zap.Int64("crystal_amount", req.CrystalAmount),
zap.Int32("like_count", req.LikeCount))
// 计算实际上架时长(毫秒转小时)
startTime := req.StartTime
expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000
// 重新计算收益使用资产等级对应的R0值而非galleryService传来的硬编码R0=5
var finalRevenue int64
if s.assetLevelService != nil && req.AssetId > 0 {
if calculatedRevenue, err := s.assetLevelService.CalculateRevenue(req.AssetId, int(req.LikeCount), startTime, expireAt, 0); err == nil {
finalRevenue = calculatedRevenue
logger.Logger.Info("OnExhibitionCompleted: recalculated revenue using asset level",
zap.Int64("asset_id", req.AssetId),
zap.Int64("original_revenue", req.CrystalAmount),
zap.Int64("recalculated_revenue", finalRevenue))
} else {
// 计算失败,使用传来的值
finalRevenue = req.CrystalAmount
logger.Logger.Warn("OnExhibitionCompleted: failed to calculate revenue with asset level, using original",
zap.Int64("asset_id", req.AssetId),
zap.Error(err))
}
} else {
finalRevenue = req.CrystalAmount
}
// 收益归属资产主人(铸爱用户),无论展位是否为自己
now := time.Now().UnixMilli()
@ -319,7 +361,7 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
SlotID: req.SlotId,
SlotOwnerUID: req.SlotOwnerUid, // 记录展位所有者信息(仅供参考)
SlotType: "exhibition", // 上架展示收益
CrystalAmount: req.CrystalAmount,
CrystalAmount: finalRevenue, // 使用重新计算的收益
CycleStartTime: req.StartTime,
CycleEndTime: req.ExpireAt,
Status: "claimable",
@ -334,12 +376,6 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
return &pb.OnExhibitionCompletedResponse{Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_INTERNAL_ERROR}}, err
}
// 增加用户累计上架时长(展位主人获得上架时长累计)
// 计算实际上架时长(毫秒转小时)
startTime := req.StartTime
expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000
// sourceID 用于去重,避免重复累计
sourceID := fmt.Sprintf("exhibition_%d", req.ExhibitionId)
@ -367,6 +403,21 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
zap.Int64("crystal_reward", crystalReward))
}
// 增加资产累计展出时长(资产等级系统)
if s.assetLevelService != nil && req.AssetId > 0 && actualHours > 0 {
if newLevel, upgraded, err := s.assetLevelService.AddExhibitionHours(req.AssetId, int(actualHours)); err != nil {
logger.Logger.Warn("OnExhibitionCompleted: failed to add exhibition hours to asset level",
zap.Int64("asset_id", req.AssetId),
zap.Int64("hours", actualHours),
zap.Error(err))
} else if upgraded {
logger.Logger.Info("OnExhibitionCompleted: asset leveled up due to exhibition",
zap.Int64("asset_id", req.AssetId),
zap.String("new_level", newLevel),
zap.Int64("hours", actualHours))
}
}
logger.Logger.Info("OnExhibitionCompleted: success",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int64("revenue_record_id", createdRecord.ID))
@ -393,15 +444,17 @@ func CalculateBuff(likeCount int) int {
}
}
// CalculateExhibitionRevenue 计算单次上架收益
// CalculateExhibitionRevenue 计算单次上架收益(参考实现,未被调用)
// 注意:实际收益计算在 OnExhibitionCompleted 中通过 AssetLevelService.CalculateRevenue 实现
// 此函数保留用于参考和测试场景
// 设计文档公式:
// R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时
// R0 = 5 水晶/小时(默认,仅作参考)
// T = 上架时长(小时)
// Buff(n) 根据点赞数计算
// 应用永久收益提升revenueBoostBps (bps),如 500 = +5%
func CalculateExhibitionRevenue(likeCount int, startTime, endTime int64, revenueBoostBps int) int64 {
R0 := int64(5) // 水晶/小时
R0 := int64(5) // 水晶/小时(默认参考值)
// 计算上架时长(毫秒转小时)
T := (endTime - startTime) / 3600000

File diff suppressed because it is too large Load Diff