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:*)", "Skill(superpowers:subagent-driven-development:*)",
"Bash(go build:*)", "Bash(go build:*)",
"Bash(go vet:*)", "Bash(go vet:*)",
"mcp__code-review-graph__semantic_search_nodes_tool" "mcp__code-review-graph__semantic_search_nodes_tool",
"Skill(superpowers:brainstorming)"
] ]
}, },
"enableAllProjectMcpServers": true, "enableAllProjectMcpServers": true,

View File

@ -21,6 +21,9 @@ type AssetLikeRepository interface {
// ExistsByAsset 检查用户是否对资产有点赞记录(不管 exhibition_id // ExistsByAsset 检查用户是否对资产有点赞记录(不管 exhibition_id
ExistsByAsset(assetID, userID, starID int64) (bool, error) ExistsByAsset(assetID, userID, starID int64) (bool, error)
// ExistsByAssetAndExhibition 检查用户是否对资产在指定展览中有点赞记录
ExistsByAssetAndExhibition(assetID, userID, starID, exhibitionID int64) (bool, error)
// GetByAsset 获取资产的点赞记录列表(分页) // GetByAsset 获取资产的点赞记录列表(分页)
GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error) 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 return count > 0, nil
} }
// ExistsByAsset 检查用户是否对资产有点赞记录(不管 exhibition_id // ExistsByAsset 检查用户是否对资产在当前展览中有点赞记录
// 用于排行榜等场景,判断用户是否对某个藏品点赞过 // 用于排行榜等场景,判断用户是否在当前展览中对某个藏品点赞过
func (r *assetLikeRepository) ExistsByAsset(assetID, userID, starID int64) (bool, error) { func (r *assetLikeRepository) ExistsByAsset(assetID, userID, starID int64) (bool, error) {
if assetID <= 0 { if assetID <= 0 {
return false, errors.New("asset_id must be greater than 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 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 获取资产的点赞记录列表(分页) // GetByAsset 获取资产的点赞记录列表(分页)
func (r *assetLikeRepository) GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error) { func (r *assetLikeRepository) GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error) {
if assetID <= 0 { if assetID <= 0 {

View File

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

View File

@ -315,14 +315,54 @@ func (r *galleryRepository) DeleteExhibition(exhibitionID int64) error {
// DeleteExhibitionByAsset 软删除展品展示记录根据资产ID // DeleteExhibitionByAsset 软删除展品展示记录根据资产ID
func (r *galleryRepository) DeleteExhibitionByAsset(assetID int64) error { func (r *galleryRepository) DeleteExhibitionByAsset(assetID int64) error {
now := time.Now().UnixMilli() 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). Where("asset_id = ?", assetID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"deleted_at": now, "deleted_at": now,
"updated_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 获取过期的展品展示记录(不含已删除且未处理) // GetExpiredExhibitions 获取过期的展品展示记录(不含已删除且未处理)
func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) { func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition var exhibitions []*models.Exhibition

View File

@ -40,7 +40,7 @@ func NewCleanupWorker(repo repository.GalleryRepository, assetClient client.Asse
func (w *CleanupWorker) Start() { func (w *CleanupWorker) Start() {
log.Println("清理Worker已启动每小时扫描一次过期展品") log.Println("清理Worker已启动每小时扫描一次过期展品")
ticker := time.NewTicker(1 * time.Hour) ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
// 立即执行一次清理 // 立即执行一次清理
@ -73,26 +73,30 @@ func (w *CleanupWorker) cleanup() {
w.cleanupInvalidDisplayStatus() w.cleanupInvalidDisplayStatus()
} }
// cleanupExpiredExhibitions 清理过期的展品展示记录(使用 ZSET 驱动 // cleanupExpiredExhibitions 清理过期的展品展示记录(使用 ZSET 驱动 + 数据库兜底
func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) { func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
ctx := context.Background() ctx := context.Background()
// 使用 ZSET 获取已过期的 asset_id 列表 // 1. 先尝试从 ZSET 获取过期展品
expiredAssetIDs, err := database.GetExpiredAssets(ctx, now) expiredAssetIDs, err := database.GetExpiredAssets(ctx, now)
if err != nil { if err != nil {
log.Printf("从 ZSET 获取过期展品失败: %v", err) log.Printf("从 ZSET 获取过期展品失败: %v降级到数据库查询", err)
// 降级:从数据库查询(兼容未初始化 Redis 的情况)
w.cleanupExpiredExhibitionsFromDB(now) w.cleanupExpiredExhibitionsFromDB(now)
return return
} }
if len(expiredAssetIDs) == 0 { // 2. ZSET 有数据时处理 ZSET 中的过期展品
log.Println("没有过期的展品需要清理") if len(expiredAssetIDs) > 0 {
return 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 successCount := 0
failedCount := 0 failedCount := 0
@ -168,10 +172,10 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
e.ID, e.AssetID, e.SlotID, e.OccupierUID, revenue) 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) { func (w *CleanupWorker) cleanupExpiredExhibitionsFromDB(now int64) {
expired, err := w.repo.GetExpiredExhibitions(now) expired, err := w.repo.GetExpiredExhibitions(now)
if err != nil { 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 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 下架展品 // 调用 Gallery Service 下架展品
if s.galleryRPCClient != nil && record.AssetID > 0 { if s.galleryRPCClient != nil && record.AssetID > 0 {
if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil { if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil {
@ -267,41 +243,12 @@ func (s *revenueService) ClaimAllExhibitionRevenue(ctx context.Context, userID,
if claimed { if claimed {
claimedCount++ 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 下架展品 // 调用 Gallery Service 下架展品
if s.galleryRPCClient != nil && record.AssetID > 0 { if s.galleryRPCClient != nil && record.AssetID > 0 {
if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil { if err := s.galleryRPCClient.RemoveExhibitionByAsset(ctx, record.AssetID); err != nil {
logger.Logger.Error("ClaimAllExhibitionRevenue: failed to remove exhibition", logger.Logger.Error("ClaimAllExhibitionRevenue: failed to remove exhibition",
zap.Int64("asset_id", record.AssetID), zap.Int64("asset_id", record.AssetID),
zap.Error(err)) 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)) zap.Int32("like_count", req.LikeCount))
// 计算实际上架时长(毫秒转小时) // 计算实际上架时长(毫秒转小时)
// 确保时间戳是毫秒级(防护:秒级时间戳通常 < 10000000000
startTime := req.StartTime 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 expireAt := req.ExpireAt
actualHours := (expireAt - startTime) / 3600000 actualHours := (expireAt - startTime) / 3600000
@ -353,6 +308,7 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
// 收益归属资产主人(铸爱用户),无论展位是否为自己 // 收益归属资产主人(铸爱用户),无论展位是否为自己
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
record := &model.ExhibitionRevenueRecord{ record := &model.ExhibitionRevenueRecord{
UserID: req.OccupierUid, // 收益归资产主人(铸爱用户) UserID: req.OccupierUid, // 收益归资产主人(铸爱用户)
StarID: req.OccupierStarId, StarID: req.OccupierStarId,
@ -362,7 +318,7 @@ func (s *revenueService) OnExhibitionCompleted(ctx context.Context, req *pb.OnEx
SlotOwnerUID: req.SlotOwnerUid, // 记录展位所有者信息(仅供参考) SlotOwnerUID: req.SlotOwnerUid, // 记录展位所有者信息(仅供参考)
SlotType: "exhibition", // 上架展示收益 SlotType: "exhibition", // 上架展示收益
CrystalAmount: finalRevenue, // 使用重新计算的收益 CrystalAmount: finalRevenue, // 使用重新计算的收益
CycleStartTime: req.StartTime, CycleStartTime: startTime,
CycleEndTime: req.ExpireAt, CycleEndTime: req.ExpireAt,
Status: "claimable", Status: "claimable",
CreatedAt: now, CreatedAt: now,

View File

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

View File

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