feat:修改藏品等级硬编码改为根据数据库的配置来定义

This commit is contained in:
zerosaturation 2026-05-25 13:23:30 +08:00
parent 8dce6ae11a
commit 7c5d5a7275
9 changed files with 135 additions and 13 deletions

View File

@ -119,3 +119,20 @@ var LevelOrderMap = map[string]int{
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

@ -55,6 +55,10 @@ func (r *AssetLevelRepository) CreateChangeLog(log *models.AssetLevelChangeLog)
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).

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

@ -29,6 +29,7 @@ type assetLevelService struct {
levelRepo *repository.AssetLevelRepository
seasonRepo *repository.SeasonRepository
decayConfigRepo *repository.SeasonDecayConfigRepository
assetRepo repository.AssetRepository // 用于同步等级到AssetRegistry.Grade可选
}
func NewAssetLevelService(
@ -173,6 +174,8 @@ func (s *assetLevelService) AddExhibitionHours(assetID int64, hours int) (string
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
@ -210,6 +213,8 @@ func (s *assetLevelService) AddLikes(assetID int64, count int) (string, bool, er
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
@ -245,11 +250,53 @@ func (s *assetLevelService) RemoveLikes(assetID int64, count int) (string, bool,
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 {
@ -372,6 +419,8 @@ func (s *assetLevelService) SeasonReset(seasonID string) error {
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)
}
}

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

@ -28,6 +28,7 @@ type RevenueService interface {
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实现
@ -322,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()
@ -334,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",
@ -349,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)
@ -423,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