diff --git a/backend/services/galleryService/config/gallery_config.go b/backend/services/galleryService/config/gallery_config.go index 5e5c91e..a2a2781 100644 --- a/backend/services/galleryService/config/gallery_config.go +++ b/backend/services/galleryService/config/gallery_config.go @@ -50,32 +50,16 @@ type ServiceURLs struct { var ( // GalleryRules 展馆规则配置(硬编码) GalleryRules = &GalleryRulesConfig{ - InitialSlotCount: 6, // 3 个公开 + 3 个私有 + InitialSlotCount: 2, // 2 个展位 GrabSlotDuration: 14400, // 4小时 - // 按等级解锁:第4个展位需要等级5,第5个展位需要等级6,以此类推 - UnlockLevelBySlot: map[int]int{ - 4: 5, // 第4个展位需要等级5 - 5: 6, // 第5个展位需要等级6 - 6: 7, // 第6个展位需要等级7 - 7: 8, // 第7个展位需要等级8 - 8: 9, // 第8个展位需要等级9 - 9: 10, // 第9个展位需要等级10 - 10: 11, // 第10个展位需要等级11 - }, + // 按等级解锁:清空(不再解锁额外展位) + UnlockLevelBySlot: map[int]int{}, - // 按水晶解锁:第4个展位需要水晶100,第5个展位需要水晶200,以此类推 - UnlockCrystalBySlot: map[int]int{ - 4: 100, // 第4个展位需要水晶100 - 5: 200, // 第5个展位需要水晶200 - 6: 300, // 第6个展位需要水晶300 - 7: 400, // 第7个展位需要水晶400 - 8: 500, // 第8个展位需要水晶500 - 9: 600, // 第9个展位需要水晶600 - 10: 700, // 第10个展位需要水晶700 - }, + // 按水晶解锁:清空 + UnlockCrystalBySlot: map[int]int{}, - MaxSlotCount: 10, + MaxSlotCount: 2, // 最多 2 个展位 } // DBConfig 数据库配置(由 main.go 的 flag 或环境变量注入) diff --git a/backend/services/galleryService/repository/gallery_repository.go b/backend/services/galleryService/repository/gallery_repository.go index 98db545..9c0b87c 100644 --- a/backend/services/galleryService/repository/gallery_repository.go +++ b/backend/services/galleryService/repository/gallery_repository.go @@ -123,6 +123,7 @@ func (r *galleryRepository) GetSlotsByUser(userID, starID int64) ([]*models.Boot var slots []*models.BoothSlot err := r.db.Where("user_id = ? AND star_id = ?", userID, starID). Order("slot_index ASC"). + Limit(2). Find(&slots).Error return slots, err } @@ -436,7 +437,7 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ? AND exhibitions.deleted_at IS NULL - ORDER BY exhibitions.expire_at DESC, bs.slot_index ASC + ORDER BY bs.slot_index ASC, exhibitions.expire_at DESC LIMIT ? OFFSET ? `, userID, starID, pageSize, offset).Scan(&items).Error diff --git a/backend/services/galleryService/service/exhibition_service.go b/backend/services/galleryService/service/exhibition_service.go index c258f50..d1c5b38 100644 --- a/backend/services/galleryService/service/exhibition_service.go +++ b/backend/services/galleryService/service/exhibition_service.go @@ -84,16 +84,9 @@ func (s *exhibitionService) PlaceAsset(userID, starID int64, req *pb.PlaceAssetR return nil, errors.New("展位已被占用") } - // 4. 校验权限与可见性(支持放置到他人展馆) - isOwner := slot.UserID == userID - if !isOwner { - // 如果不是自己的展位,必须是 public 且在同一个明星下 - if slot.Visibility != "public" { - return nil, errors.New("该展位是私有的,无法放置展品") - } - if slot.StarID != starID { - return nil, errors.New("只能在同一明星的展馆中放置展品") - } + // 4. 校验权限:只能放到自己的展位 + if slot.UserID != userID { + return nil, errors.New("只能在自己的展位放置藏品") } // 5. 执行放置逻辑 diff --git a/backend/services/galleryService/service/gallery_service.go b/backend/services/galleryService/service/gallery_service.go index 0e35a0f..5f39939 100644 --- a/backend/services/galleryService/service/gallery_service.go +++ b/backend/services/galleryService/service/gallery_service.go @@ -222,60 +222,20 @@ func (s *galleryService) buildSlotInfos(slots []*models.BoothSlot, viewerUID, vi // 返回 (canOperate bool, operation string) // operation 取值: "place" | "remove" | "none" func (s *galleryService) calculateOperation(slot *models.BoothSlot, exhibition *models.Exhibition, viewerUID int64, isOwner bool) (bool, string) { - logger.Logger.Info("=== calculateOperation DEBUG ===", - zap.Int64("slot_id", slot.SlotID), - zap.String("visibility", slot.Visibility), - zap.Bool("is_owner", isOwner), - zap.Int64("viewer_uid", viewerUID), - ) - if exhibition != nil { - logger.Logger.Info("=== exhibition info ===", - zap.Int64("exhibition_id", exhibition.ID), - zap.Int64("occupier_uid", exhibition.OccupierUID), - ) - } - // 未解锁的展位不能操作 if !slot.IsEnabled { return false, "none" } - // 私有展位 (我的/他的展位) - if slot.Visibility == "private" { - // 所有者访问自己展馆 - if isOwner { - if exhibition == nil { - return true, "place" // 空展位,可以放置 - } - // 有藏品时,可以主动结束展览 - return true, "remove" - } - // 访问别人展馆 - 不能操作 - return false, "none" - } - - // 公共展位 (共享展位) - if slot.Visibility == "public" { + // 自己是展位所有者 + if isOwner { if exhibition == nil { - // 空展位 - 只有访问别人展馆时可以放置 - if !isOwner { - return true, "place" - } - return false, "none" // 自己的展馆空展位不能操作 + return true, "place" // 自己的空展位,可以放置 } - - // 有藏品时 - if exhibition.OccupierUID == viewerUID { - return true, "remove" // 有自己的藏品,可以主动结束展览 - } - - // 有别人的藏品 - 只有所有者可以踢出 - if isOwner { - return true, "remove" - } - return false, "none" + return true, "remove" // 有藏品,可以移除 } + // 不是展位所有者,不能操作 return false, "none" } diff --git a/docs/migrations/002_reduce_slots_to_2.sql b/docs/migrations/002_reduce_slots_to_2.sql new file mode 100644 index 0000000..74916b8 --- /dev/null +++ b/docs/migrations/002_reduce_slots_to_2.sql @@ -0,0 +1,43 @@ +-- 展位重构迁移:从 6 槽位缩减为 2 槽位 +-- 执行时间:2026-05-20 +-- 执行人:待填写 +-- 风险等级:高(删除数据) + +-- ========== 备份(建议执行前手动备份)========== +-- 备份 booth_slots 表 +-- CREATE TABLE booth_slots_backup_20260520 AS SELECT * FROM booth_slots; + +-- 备份 exhibitions 表 +-- CREATE TABLE exhibitions_backup_20260520 AS SELECT * FROM exhibitions; + +-- ========== 迁移开始 ========== + +-- 1. 先删除 slot_index > 2 的展览记录(引用完整性) +DELETE FROM exhibitions +WHERE slot_id IN ( + SELECT slot_id FROM booth_slots + WHERE slot_index > 2 +); + +-- 2. 删除 slot_index > 2 的槽位记录 +DELETE FROM booth_slots +WHERE slot_index > 2; + +-- 3. 验证清理结果 +SELECT user_id, star_id, COUNT(*) as slot_count +FROM booth_slots +GROUP BY user_id, star_id +HAVING COUNT(*) > 2; + +-- 应该返回空结果,表示每个用户的每个明星展馆最多只有 2 个槽位 + +-- 4. 验证 exhibitions 没有残留(检查是否有关联到已删除槽位的展览) +SELECT COUNT(*) as orphaned_exhibitions +FROM exhibitions e +WHERE NOT EXISTS ( + SELECT 1 FROM booth_slots bs WHERE bs.slot_id = e.slot_id +); + +-- 应该返回 0 + +-- ========== 迁移完成 ========== \ No newline at end of file diff --git a/frontend/pages/profile/myWorks.vue b/frontend/pages/profile/myWorks.vue index 943fc73..687980a 100644 --- a/frontend/pages/profile/myWorks.vue +++ b/frontend/pages/profile/myWorks.vue @@ -25,76 +25,59 @@ - - - - - - - - 领取收益 - - - - + + + + - {{ item.like_count || 0 }} + {{ exhibitionAtSlot[0].like_count || 0 }} - - - - - {{ formatCountdown(item.id) }} - + + {{ formatCountdown(exhibitionAtSlot[0].id) }} - - + - {{ item.hourly_earnings || 0 }}/时 + {{ exhibitionAtSlot[0].hourly_earnings || 0 }}/时 + + + + + + - - - - - - - - + + + + + + + + + {{ exhibitionAtSlot[1].like_count || 0 }} - - - - - - + + + {{ formatCountdown(exhibitionAtSlot[1].id) }} + + + + + {{ exhibitionAtSlot[1].hourly_earnings || 0 }}/时 + + + + + + @@ -227,9 +210,47 @@ const goToCastlove = () => { // 藏品选择器相关 const showAssetSelector = ref(false); const assetToReplace = ref(null); -const currentSlotIndex = ref(0); +const currentSlotIndex = ref(0); // 现在存储的是 slot_index (1 或 2) + +// 我的展馆槽位信息(用于确定空位) +const mySlots = ref([]); + +const loadGalleryInfo = async () => { + try { + const galleriesRes = await getMyGalleriesApi(); + console.log('[DEBUG] 展馆API返回:', galleriesRes); + // 只取前2个可操作槽位,按 slot_index 排序 + mySlots.value = galleriesRes.data?.slots + .filter(s => s.can_operate) + .sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0)) + .slice(0, 2) || []; + console.log('[DEBUG] 加载展馆槽位 mySlots:', mySlots.value); + } catch (err) { + console.error('加载展馆信息失败:', err); + } +}; + +// 计算空位:哪些 slot_index 没有展出中 +const emptySlotIndices = computed(() => { + const occupiedSlots = exhibitionWorks.value.map(w => w.slot_index).filter(idx => idx > 0); + return [1, 2].filter(idx => !occupiedSlots.includes(idx)); +}); + +// 将作品映射到正确的左右位置(slot 1=左边, slot 2=右边) +const exhibitionAtSlot = computed(() => { + // 创建一个长度为 2 的数组,index 0=左边, index 1=右边 + const slots = [null, null]; + for (const item of exhibitionWorks.value) { + const pos = (item.slot_index ?? 0) - 1; // slot_index 1→0, 2→1 + if (pos >= 0 && pos < 2) { + slots[pos] = item; + } + } + return slots; +}); const openAssetSelector = (slotIndex = 0) => { + // slotIndex 现在是 slot_index 值(1 或 2) currentSlotIndex.value = slotIndex; showAssetSelector.value = true; }; @@ -252,11 +273,7 @@ const handleAssetSelect = async ({ asset, isReplace, oldAsset }) => { const ownerId = galleriesRes.data?.gallery_owner_id; console.log('槽位列表:', slots, 'ownerId:', ownerId); - // 过滤出可操作的槽位(can_operate: true) - const operatableSlots = slots.filter(s => s.can_operate); - console.log('可操作槽位:', operatableSlots); - - if (operatableSlots.length === 0 || !ownerId) { + if (slots.length === 0 || !ownerId) { uni.showToast({ title: '暂无可用展馆', icon: 'none' }); return; } @@ -264,11 +281,12 @@ const handleAssetSelect = async ({ asset, isReplace, oldAsset }) => { let targetSlotId = null; if (isReplace && oldAsset) { + // 替换模式:根据旧藏品找到 slot_id const slot = slots.find(s => s.asset_id === oldAsset.asset_id); targetSlotId = slot?.slot_id; } else { - // 使用 currentSlotIndex 对应可操作槽位列表中的槽位 - const targetSlot = operatableSlots[currentSlotIndex.value]; + // 新放置模式:用 currentSlotIndex(就是 slot_index)直接找 + const targetSlot = slots.find(s => s.slot_index === currentSlotIndex.value); targetSlotId = targetSlot?.slot_id; } @@ -769,6 +787,7 @@ const switchLikedTab = async (tab) => { const loadExhibitedAssets = async () => { try { const res = await getMyExhibitedAssetsApi(1, 20); + console.log('[DEBUG] 展出作品API返回:', res); if (res.data && res.data.items) { exhibitionWorks.value = res.data.items .map(item => ({ @@ -784,6 +803,7 @@ const loadExhibitedAssets = async () => { is_lenticular: item.is_lenticular ?? false, })) .sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0)); + console.log('[DEBUG] 整理后的 exhibitionWorks:', exhibitionWorks.value); // 为每个光栅卡加载层级数据 for (const item of exhibitionWorks.value) { @@ -843,6 +863,7 @@ const loadLikedAssets = async () => { onMounted(() => { initLenticularEngine(); startLenticularRenderLoop(); + loadGalleryInfo(); loadExhibitedAssets(); loadLikedAssets(); @@ -1035,7 +1056,7 @@ onShow(() => { .card-tilt-left { transform: rotate(-4deg) translateY(10rpx); - margin-right: 32rpx; + margin-right: 64rpx; border-radius: 32rpx; box-shadow: -16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9); }