feat: 修改用户经验bug和收益记录bug

This commit is contained in:
zerosaturation 2026-05-31 20:38:58 +08:00
parent b930ad59e1
commit 5e33bbb031
8 changed files with 109 additions and 75 deletions

View File

@ -5,7 +5,8 @@
"Skill(superpowers:subagent-driven-development:*)",
"Bash(go build:*)",
"Bash(go vet:*)",
"mcp__code-review-graph__semantic_search_nodes_tool"
"mcp__code-review-graph__semantic_search_nodes_tool",
"Skill(superpowers:brainstorming)"
]
},
"enableAllProjectMcpServers": true,

View File

@ -21,6 +21,9 @@ type AssetLikeRepository interface {
// ExistsByAsset 检查用户是否对资产有点赞记录(不管 exhibition_id
ExistsByAsset(assetID, userID, starID int64) (bool, error)
// ExistsByAssetAndExhibition 检查用户是否对资产在指定展览中有点赞记录
ExistsByAssetAndExhibition(assetID, userID, starID, exhibitionID int64) (bool, error)
// GetByAsset 获取资产的点赞记录列表(分页)
GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error)
@ -142,8 +145,8 @@ func (r *assetLikeRepository) Exists(assetID, userID, starID, exhibitionID int64
return count > 0, nil
}
// ExistsByAsset 检查用户是否对资产有点赞记录(不管 exhibition_id
// 用于排行榜等场景,判断用户是否对某个藏品点赞过
// ExistsByAsset 检查用户是否对资产在当前展览中有点赞记录
// 用于排行榜等场景,判断用户是否在当前展览中对某个藏品点赞过
func (r *assetLikeRepository) ExistsByAsset(assetID, userID, starID int64) (bool, error) {
if assetID <= 0 {
return false, errors.New("asset_id must be greater than 0")
@ -169,6 +172,36 @@ func (r *assetLikeRepository) ExistsByAsset(assetID, userID, starID int64) (bool
return count > 0, nil
}
// ExistsByAssetAndExhibition 检查用户是否对资产在指定展览中有点赞记录
func (r *assetLikeRepository) ExistsByAssetAndExhibition(assetID, userID, starID, exhibitionID int64) (bool, error) {
if assetID <= 0 {
return false, errors.New("asset_id must be greater than 0")
}
if userID <= 0 {
return false, errors.New("user_id must be greater than 0")
}
if starID <= 0 {
return false, errors.New("star_id must be greater than 0")
}
if exhibitionID <= 0 {
return false, errors.New("exhibition_id must be greater than 0")
}
var count int64
err := r.db.Model(&models.AssetLike{}).
Where("asset_id = ? AND user_id = ? AND star_id = ? AND exhibition_id = ?", assetID, userID, starID, exhibitionID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// GetByAsset 获取资产的点赞记录列表(分页)
func (r *assetLikeRepository) GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error) {
if assetID <= 0 {

View File

@ -86,7 +86,7 @@ func (s *rankingService) GetHotRanking(ctx context.Context, req *ranking.GetRank
// 判断当前用户是否已点赞(只有登录用户才检查,且仅当藏品在展示中时)
isLiked := false
if req.UserId > 0 && item.ExhibitionID > 0 {
exists, err := s.assetLikeRepo.ExistsByAsset(item.AssetID, req.UserId, starID)
exists, err := s.assetLikeRepo.ExistsByAssetAndExhibition(item.AssetID, req.UserId, starID, item.ExhibitionID)
if err == nil {
isLiked = exists
}
@ -211,7 +211,7 @@ func (s *rankingService) GetOriginalRanking(ctx context.Context, req *ranking.Ge
// 判断当前用户是否已点赞(只有登录用户才检查,且仅当藏品在展示中时)
isLiked := false
if req.UserId > 0 && item.ExhibitionID > 0 {
exists, err := s.assetLikeRepo.ExistsByAsset(item.AssetID, req.UserId, starID)
exists, err := s.assetLikeRepo.ExistsByAssetAndExhibition(item.AssetID, req.UserId, starID, item.ExhibitionID)
if err == nil {
isLiked = exists
}

View File

@ -315,14 +315,54 @@ func (r *galleryRepository) DeleteExhibition(exhibitionID int64) error {
// DeleteExhibitionByAsset 软删除展品展示记录根据资产ID
func (r *galleryRepository) DeleteExhibitionByAsset(assetID int64) error {
now := time.Now().UnixMilli()
return r.db.Model(&models.Exhibition{}).
// 获取将被删除的展览ID列表
var exhibitionIDs []int64
if err := r.db.Model(&models.Exhibition{}).
Where("asset_id = ? AND deleted_at IS NULL", assetID).
Pluck("id", &exhibitionIDs).Error; err != nil {
return err
}
// 软删除展览
if err := r.db.Model(&models.Exhibition{}).
Where("asset_id = ?", assetID).
Updates(map[string]interface{}{
"deleted_at": now,
"updated_at": now,
}).Error
}).Error; err != nil {
return err
}
// 清理关联的待领取收益记录(防止孤儿记录)
if len(exhibitionIDs) > 0 {
r.db.Where("exhibition_id IN ? AND status = ?", exhibitionIDs, "claimable").
Delete(&ExhibitionRevenueRecord{})
}
return nil
}
// ExhibitionRevenueRecord 展览收益记录(本地定义,避免循环依赖)
type ExhibitionRevenueRecord struct {
ID int64 `gorm:"primaryKey;column:id;autoIncrement"`
UserID int64 `gorm:"column:user_id;not null"`
StarID int64 `gorm:"column:star_id;not null"`
ExhibitionID int64 `gorm:"column:exhibition_id;not null"`
AssetID int64 `gorm:"column:asset_id;not null"`
SlotID int64 `gorm:"column:slot_id;not null"`
SlotOwnerUID int64 `gorm:"column:slot_owner_uid;not null"`
SlotType string `gorm:"column:slot_type;size:20;not null"`
CrystalAmount int64 `gorm:"column:crystal_amount;not null"`
CycleStartTime int64 `gorm:"column:cycle_start_time;not null"`
CycleEndTime int64 `gorm:"column:cycle_end_time;not null"`
Status string `gorm:"column:status;size:20;default:claimable"`
ClaimedAt *int64 `gorm:"column:claimed_at"`
CreatedAt int64 `gorm:"column:created_at"`
}
func (ExhibitionRevenueRecord) TableName() string { return "exhibition_revenue_records" }
// GetExpiredExhibitions 获取过期的展品展示记录(不含已删除且未处理)
func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition

View File

@ -40,7 +40,7 @@ func NewCleanupWorker(repo repository.GalleryRepository, assetClient client.Asse
func (w *CleanupWorker) Start() {
log.Println("清理Worker已启动每小时扫描一次过期展品")
ticker := time.NewTicker(1 * time.Hour)
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
// 立即执行一次清理
@ -73,26 +73,30 @@ func (w *CleanupWorker) cleanup() {
w.cleanupInvalidDisplayStatus()
}
// cleanupExpiredExhibitions 清理过期的展品展示记录(使用 ZSET 驱动
// cleanupExpiredExhibitions 清理过期的展品展示记录(使用 ZSET 驱动 + 数据库兜底
func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
ctx := context.Background()
// 使用 ZSET 获取已过期的 asset_id 列表
// 1. 先尝试从 ZSET 获取过期展品
expiredAssetIDs, err := database.GetExpiredAssets(ctx, now)
if err != nil {
log.Printf("从 ZSET 获取过期展品失败: %v", err)
// 降级:从数据库查询(兼容未初始化 Redis 的情况)
log.Printf("从 ZSET 获取过期展品失败: %v降级到数据库查询", err)
w.cleanupExpiredExhibitionsFromDB(now)
return
}
if len(expiredAssetIDs) == 0 {
log.Println("没有过期的展品需要清理")
return
// 2. ZSET 有数据时处理 ZSET 中的过期展品
if len(expiredAssetIDs) > 0 {
log.Printf("ZSET 发现 %d 个过期展品,开始清理", len(expiredAssetIDs))
w.cleanupAssetsFromZSET(ctx, expiredAssetIDs, now)
}
log.Printf("ZSET 发现 %d 个过期展品,开始清理", len(expiredAssetIDs))
// 3. 兜底检查数据库中可能遗漏的过期展品ZSET 可能在 Redis 重启或数据丢失时漏掉)
w.cleanupExpiredExhibitionsFromDB(now)
}
// cleanupAssetsFromZSET 从 ZSET 处理过期展品
func (w *CleanupWorker) cleanupAssetsFromZSET(ctx context.Context, expiredAssetIDs []int64, now int64) {
// 批量删除过期记录
successCount := 0
failedCount := 0
@ -168,10 +172,10 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
e.ID, e.AssetID, e.SlotID, e.OccupierUID, revenue)
}
log.Printf("过期展品清理完成: 成功 %d 个, 失败 %d 个", successCount, failedCount)
log.Printf("ZSET 过期展品清理完成: 成功 %d 个, 失败 %d 个", successCount, failedCount)
}
// cleanupExpiredExhibitionsFromDB 降级方案:从数据库查询过期展览
// cleanupExpiredExhibitionsFromDB 兜底方案:从数据库查询过期展览
func (w *CleanupWorker) cleanupExpiredExhibitionsFromDB(now int64) {
expired, err := w.repo.GetExpiredExhibitions(now)
if err != nil {

View File

@ -177,30 +177,6 @@ func (s *revenueService) ClaimExhibitionRevenue(ctx context.Context, userID, sta
return &pb.ClaimExhibitionRevenueResponse{Base: &pbCommon.BaseResponse{Code: pbCommon.StatusCode_STATUS_BAD_REQUEST}, Success: false}, nil
}
// 增加用户累计上架时长(触发升级检查)
// 计算上架时长(毫秒转小时)
exhibitionHours := (record.CycleEndTime - record.CycleStartTime) / 3600000
if s.userRPCClient != nil {
newLevel, levelDelta, crystalReward, err := s.userRPCClient.AddExhibitionHours(ctx, userID, starID, exhibitionHours,
fmt.Sprintf("%d", record.ID))
if err != nil {
logger.Logger.Error("ClaimExhibitionRevenue: failed to add exhibition hours",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("exhibition_hours", exhibitionHours),
zap.Error(err))
} else if levelDelta > 0 {
logger.Logger.Info("领取收益触发升级",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("revenue_record_id", record.ID),
zap.Int32("old_level", newLevel-levelDelta),
zap.Int32("new_level", newLevel),
zap.Int32("level_delta", levelDelta),
zap.Int64("crystal_reward", crystalReward))
}
}
// 调用 Gallery Service 下架展品
if s.galleryRPCClient != nil && record.AssetID > 0 {
if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil {
@ -267,41 +243,12 @@ func (s *revenueService) ClaimAllExhibitionRevenue(ctx context.Context, userID,
if claimed {
claimedCount++
// 增加用户累计上架时长(触发升级检查)
exhibitionHours := (record.CycleEndTime - record.CycleStartTime) / 3600000
if exhibitionHours < 1 {
exhibitionHours = 1
}
if s.userRPCClient != nil {
newLevel, levelDelta, crystalReward, err := s.userRPCClient.AddExhibitionHours(ctx, userID, starID, exhibitionHours,
fmt.Sprintf("%d", record.ID))
if err != nil {
logger.Logger.Error("ClaimAllExhibitionRevenue: failed to add exhibition hours",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("exhibition_hours", exhibitionHours),
zap.Error(err))
} else if levelDelta > 0 {
logger.Logger.Info("一键领取触发升级",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("revenue_record_id", record.ID),
zap.Int32("old_level", newLevel-levelDelta),
zap.Int32("new_level", newLevel),
zap.Int32("level_delta", levelDelta),
zap.Int64("crystal_reward", crystalReward))
}
}
// 调用 Gallery Service 下架展品
if s.galleryRPCClient != nil && record.AssetID > 0 {
if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil {
logger.Logger.Error("ClaimAllExhibitionRevenue: failed to remove exhibition",
zap.Int64("asset_id", record.AssetID),
zap.Error(err))
} else {
logger.Logger.Info("ClaimAllExhibitionRevenue: exhibition removed",
zap.Int64("asset_id", record.AssetID))
}
}
}
@ -327,7 +274,15 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
zap.Int32("like_count", req.LikeCount))
// 计算实际上架时长(毫秒转小时)
// 确保时间戳是毫秒级(防护:秒级时间戳通常 < 10000000000
startTime := req.StartTime
if startTime > 0 && startTime < 10000000000 {
startTime = startTime * 1000
logger.Logger.Warn("OnExhibitionCompleted: converted start_time from seconds to milliseconds",
zap.Int64("exhibition_id", req.ExhibitionId),
zap.Int64("original_start_time", req.StartTime),
zap.Int64("converted_start_time", startTime))
}
expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000
@ -353,6 +308,7 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
// 收益归属资产主人(铸爱用户),无论展位是否为自己
now := time.Now().UnixMilli()
record := &model.ExhibitionRevenueRecord{
UserID: req.OccupierUid, // 收益归资产主人(铸爱用户)
StarID: req.OccupierStarId,
@ -361,8 +317,8 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
SlotID: req.SlotId,
SlotOwnerUID: req.SlotOwnerUid, // 记录展位所有者信息(仅供参考)
SlotType: "exhibition", // 上架展示收益
CrystalAmount: finalRevenue, // 使用重新计算的收益
CycleStartTime: req.StartTime,
CrystalAmount: finalRevenue, // 使用重新计算的收益
CycleStartTime: startTime,
CycleEndTime: req.ExpireAt,
Status: "claimable",
CreatedAt: now,

View File

@ -396,7 +396,7 @@ const handleClaimReward = async (item, _index) => {
// ID
const revenueRecord = records.find(r => r.asset_id === item.id);
console.log(item.id,records[1])
if (!revenueRecord) {
uni.showToast({ title: '一分钟延迟领取', icon: 'none' });
return;

View File

@ -278,7 +278,7 @@ onUnmounted(() => {
.badge-rank {
width: 80rpx;
height: 32rpx;
color: #FFFABD33;
color: #FFFABD;
font-size: 18rpx;
font-weight: 600;
border-radius: 16rpx;