Compare commits
10 Commits
121ea91796
...
057f9bce1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
057f9bce1c | ||
|
|
99a53d522c | ||
|
|
8a71b6ff75 | ||
|
|
8a4ddb86bf | ||
|
|
5868e55fa2 | ||
|
|
037b7cc3f9 | ||
|
|
391ef8c43d | ||
|
|
fac8fea3b9 | ||
|
|
0c556182fc | ||
|
|
7271da0e1e |
138
backend/pkg/models/asset_level.go
Normal file
138
backend/pkg/models/asset_level.go
Normal 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
|
||||
}
|
||||
@ -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" +
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
478
backend/services/assetService/service/asset_level_service.go
Normal file
478
backend/services/assetService/service/asset_level_service.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
52
backend/services/assetService/worker/season_reset_worker.go
Normal file
52
backend/services/assetService/worker/season_reset_worker.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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记录收益失败,跳过标记已处理以便重试",
|
||||
|
||||
@ -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 worker(goroutine 中启动)
|
||||
|
||||
@ -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
|
||||
|
||||
1259
docs/superpowers/plans/2026-05-25-asset-level-system.md
Normal file
1259
docs/superpowers/plans/2026-05-25-asset-level-system.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user