From 7c5d5a7275c56ab723a31be9445c084453f959ae Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Mon, 25 May 2026 13:23:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E8=97=8F=E5=93=81?= =?UTF-8?q?=E7=AD=89=E7=BA=A7=E7=A1=AC=E7=BC=96=E7=A0=81=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E6=95=B0=E6=8D=AE=E5=BA=93=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=9D=A5=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pkg/models/asset_level.go | 17 +++++++ backend/pkg/proto/task/task.pb.go | 15 +++++- backend/proto/task.proto | 1 + .../repository/asset_level_repository.go | 4 ++ .../repository/asset_repository.go | 14 ++++++ .../service/asset_level_service.go | 49 +++++++++++++++++++ .../galleryService/client/task_rpc_client.go | 2 + .../galleryService/service/cleanup_worker.go | 1 + .../taskService/service/revenue_service.go | 45 ++++++++++++----- 9 files changed, 135 insertions(+), 13 deletions(-) diff --git a/backend/pkg/models/asset_level.go b/backend/pkg/models/asset_level.go index e860e41..160e077 100644 --- a/backend/pkg/models/asset_level.go +++ b/backend/pkg/models/asset_level.go @@ -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 +} diff --git a/backend/pkg/proto/task/task.pb.go b/backend/pkg/proto/task/task.pb.go index f52f929..01de3dc 100644 --- a/backend/pkg/proto/task/task.pb.go +++ b/backend/pkg/proto/task/task.pb.go @@ -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" + diff --git a/backend/proto/task.proto b/backend/proto/task.proto index ee7940b..4d1ba28 100644 --- a/backend/proto/task.proto +++ b/backend/proto/task.proto @@ -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 { diff --git a/backend/services/assetService/repository/asset_level_repository.go b/backend/services/assetService/repository/asset_level_repository.go index 69ffa7e..2830d43 100644 --- a/backend/services/assetService/repository/asset_level_repository.go +++ b/backend/services/assetService/repository/asset_level_repository.go @@ -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). diff --git a/backend/services/assetService/repository/asset_repository.go b/backend/services/assetService/repository/asset_repository.go index 1cd4c62..690596f 100644 --- a/backend/services/assetService/repository/asset_repository.go +++ b/backend/services/assetService/repository/asset_repository.go @@ -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 { diff --git a/backend/services/assetService/service/asset_level_service.go b/backend/services/assetService/service/asset_level_service.go index 194e9ee..e153c70 100644 --- a/backend/services/assetService/service/asset_level_service.go +++ b/backend/services/assetService/service/asset_level_service.go @@ -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) } } diff --git a/backend/services/galleryService/client/task_rpc_client.go b/backend/services/galleryService/client/task_rpc_client.go index 0c4c20e..16475d4 100644 --- a/backend/services/galleryService/client/task_rpc_client.go +++ b/backend/services/galleryService/client/task_rpc_client.go @@ -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) diff --git a/backend/services/galleryService/service/cleanup_worker.go b/backend/services/galleryService/service/cleanup_worker.go index e778a93..d423f0e 100644 --- a/backend/services/galleryService/service/cleanup_worker.go +++ b/backend/services/galleryService/service/cleanup_worker.go @@ -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记录收益失败,跳过标记已处理以便重试", diff --git a/backend/services/taskService/service/revenue_service.go b/backend/services/taskService/service/revenue_service.go index d33200a..290f933 100644 --- a/backend/services/taskService/service/revenue_service.go +++ b/backend/services/taskService/service/revenue_service.go @@ -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