# 展馆接口数据库字段说明 ## 接口说明 ### GET /api/v1/galleries/{target_uid} - 获取他人展馆 **路径参数**: - `target_uid`: 目标用户ID(数据库 `users.id`) **功能**:查看指定用户的展馆信息(展位列表、展品展示情况) --- ## 数据库表与字段映射 ### 1. 主要数据表 #### `booth_slots` 表(展位表) | 数据库字段 | Go 模型字段 | 类型 | 说明 | |-----------|------------|------|------| | `slot_id` | `SlotID` | `int64` | 展位ID(主键,自增) | | `host_profile_id` | `HostProfileID` | `int64` | 展馆所有者ID(计算值:`user_id * 1000000 + star_id`) | | `user_id` | `UserID` | `int64` | 用户ID(外键关联 `users.id`) | | `star_id` | `StarID` | `int64` | 明星ID(用于数据隔离) | | `slot_index` | `SlotIndex` | `int` | 展位序号(1, 2, 3...,与 `host_profile_id` 联合唯一) | | `visibility` | `Visibility` | `string` | 可见性(默认 `'public'`) | | `is_enabled` | `IsEnabled` | `bool` | 是否已解锁(`false`=未解锁/LOCKED,`true`=已解锁) | | `unlock_type` | `UnlockType` | `string` | 解锁类型(`'level'` 或 `'crystal'`) | | `unlock_value` | `UnlockValue` | `int` | 解锁条件值(等级或水晶数量) | | `created_at` | `CreatedAt` | `int64` | 创建时间(毫秒时间戳) | | `updated_at` | `UpdatedAt` | `int64` | 更新时间(毫秒时间戳) | **查询逻辑**: ```sql SELECT * FROM booth_slots WHERE user_id = ? AND star_id = ? ORDER BY slot_index ASC ``` #### `exhibitions` 表(展品展示表) | 数据库字段 | Go 模型字段 | 类型 | 说明 | |-----------|------------|------|------| | `id` | `ID` | `int64` | 主键(自增) | | `asset_id` | `AssetID` | `int64` | 资产ID(外键关联 `assets.id`,唯一索引) | | `slot_id` | `SlotID` | `int64` | 展位ID(外键关联 `booth_slots.slot_id`) | | `host_profile_id` | `HostProfileID` | `int64` | 展馆所有者ID(与 `booth_slots.host_profile_id` 一致) | | `occupier_uid` | `OccupierUID` | `int64` | 占位者用户ID(实际放置藏品的用户) | | `occupier_star_id` | `OccupierStarID` | `int64` | 占位者明星ID | | `start_time` | `StartTime` | `int64` | 占用开始时间(毫秒时间戳) | | `expire_at` | `ExpireAt` | `int64` | 占用过期时间(毫秒时间戳,有索引) | | `created_at` | `CreatedAt` | `int64` | 创建时间 | | `updated_at` | `UpdatedAt` | `int64` | 更新时间 | **查询逻辑**(判断展位是否被占用): ```sql SELECT * FROM exhibitions WHERE slot_id = ? LIMIT 1 ``` --- ## 响应包字段说明 ### 响应结构 ```json { "code": 200, "message": "ok", "data": { "gallery_owner_id": 37000088, "slot_total": 3, "slots": [...] } } ``` ### 字段详解 #### `gallery_owner_id`(展馆所有者ID) - **计算方式**:`user_id * 1000000 + star_id` - **示例**:`37000088` = `37 * 1000000 + 88` - 表示:用户ID=37,明星ID=88 的展馆 - **用途**: - 用于标识展馆的唯一所有者(同一用户在不同明星下有不同的展馆) - 在放置藏品时,需要传入此 ID 作为 `gallery_owner_id` 参数 #### `slot_total`(展位总数) - **来源**:`booth_slots` 表中该用户在该明星下的展位数量 - **说明**:初始展位数量由配置决定(通常为 3 个) #### `slots[]`(展位列表) 每个展位对象包含: | 字段 | 类型 | 说明 | 数据来源 | |------|------|------|----------| | `slot_id` | `int64` | 展位ID(主键) | `booth_slots.slot_id` | | `slot_index` | `int32` | 展位序号(1, 2, 3...) | `booth_slots.slot_index` | | `status` | `string` | 展位状态 | **计算得出**(见下方) | | `is_enabled` | `bool` | 是否已解锁 | `booth_slots.is_enabled` | | `asset` | `object\|null` | 展品信息(仅 `status=OCCUPIED` 时存在) | 从 `exhibitions` + `assets` 表关联查询 | | `occupier_uid` | `int64\|null` | 占位者用户ID(仅 `status=OCCUPIED` 时存在) | `exhibitions.occupier_uid` | | `occupied_at` | `int64\|null` | 占用开始时间(仅 `status=OCCUPIED` 时存在) | `exhibitions.start_time` | | `expire_at` | `int64\|null` | 占用过期时间(仅 `status=OCCUPIED` 时存在) | `exhibitions.expire_at` | | `unlock_condition` | `object\|null` | 解锁条件(仅 `status=LOCKED` 时存在) | 从 `booth_slots.unlock_type` + `unlock_value` 计算 | ### `status` 状态计算逻辑 ```go if !slot.IsEnabled { status = "LOCKED" // 未解锁 } else { exhibition := GetExhibitionBySlot(slot.SlotID) if exhibition == nil { status = "EMPTY" // 已解锁但空展位 } else { status = "OCCUPIED" // 已解锁且有展品 } } ``` **状态值说明**: - `LOCKED`:展位未解锁(`is_enabled=false`),需要满足解锁条件才能使用 - `EMPTY`:展位已解锁(`is_enabled=true`),但当前没有展品 - `OCCUPIED`:展位已解锁且有展品展示(`exhibitions` 表中有对应记录) ### `asset` 对象(展品信息) 当 `status=OCCUPIED` 时,`asset` 字段包含: | 字段 | 类型 | 说明 | 数据来源 | |------|------|------|----------| | `asset_id` | `int64` | 资产ID | `exhibitions.asset_id` → `assets.id` | | `name` | `string` | 资产名称 | `assets.name` | | `cover_url` | `string` | 封面图URL | `assets.cover_url` | | `like_count` | `int32` | 点赞数 | `assets.like_count` | | `remain_time` | `int64` | 剩余时间(秒) | `(exhibitions.expire_at - now) / 1000` | **注意**:`asset` 信息通过 RPC 调用 `AssetService.GetAsset` 获取,需要传入 `occupier_uid` 和 `occupier_star_id`(因为资产属于占位者,不是展馆所有者)。 ### `unlock_condition` 对象(解锁条件) 当 `status=LOCKED` 时,`unlock_condition` 字段包含: | 字段 | 类型 | 说明 | 数据来源 | |------|------|------|----------| | `type` | `string` | 解锁类型:`"level"` 或 `"crystal"` | `booth_slots.unlock_type` 或配置规则 | | `value` | `int32` | 解锁条件值(等级或水晶数量) | `booth_slots.unlock_value` 或配置规则 | --- ## 与 `/api/v1/galleries/me` 的响应格式对比 ### 响应格式是否一致? **答案:是的,响应格式完全一致。** #### 代码证据 1. **DTO 定义**(`gateway/dto/gallery_dto.go`): ```go // GetMyGalleryResponseDTO 获取我的展馆响应 type GetMyGalleryResponseDTO struct { GalleryOwnerID int64 `json:"gallery_owner_id"` SlotTotal int32 `json:"slot_total"` Slots []SlotInfoDTO `json:"slots"` } // GetUserGalleryResponseDTO 获取他人展馆响应(与我的展馆响应相同) type GetUserGalleryResponseDTO struct { GalleryOwnerID int64 `json:"gallery_owner_id"` SlotTotal int32 `json:"slot_total"` Slots []SlotInfoDTO `json:"slots"` } ``` **注释明确说明:`GetUserGalleryResponseDTO` 与 `GetMyGalleryResponseDTO` 相同** 2. **转换函数**(`gateway/dto/gallery_converter.go`): ```go // ConvertGalleryData 转换展馆数据(我的展馆) func ConvertGalleryData(pbData *pbGallery.GalleryData) *GetMyGalleryResponseDTO { // ... 转换逻辑 } // ConvertGalleryDataToUser 转换展馆数据(他人展馆) func ConvertGalleryDataToUser(pbData *pbGallery.GalleryData) *GetUserGalleryResponseDTO { // ... 转换逻辑(与 ConvertGalleryData 完全相同) } ``` **两个函数的转换逻辑完全一致** 3. **Service 层返回**(`services/galleryService/service/gallery_service.go`): ```go // GetMyGallery 和 GetUserGallery 都返回 *pb.GalleryData // 结构完全相同,只是查询的目标用户不同 ``` #### 实际差异 虽然响应格式相同,但**数据内容**有区别: | 对比项 | `/api/v1/galleries/me` | `/api/v1/galleries/{target_uid}` | |--------|------------------------|----------------------------------| | **查询目标** | 当前登录用户(`user_id` 从 JWT 获取) | 指定用户(`target_uid` 从路径参数获取) | | **`gallery_owner_id`** | `user_id * 1000000 + star_id`(当前用户) | `target_uid * 1000000 + star_id`(目标用户) | | **`slots[]` 数据** | 当前用户的展位 | 目标用户的展位 | | **权限** | 可查看自己的所有展位(包括未解锁的) | 只能查看目标用户已解锁的展位(`is_enabled=true`) | **注意**:代码中 `buildSlotInfos` 的 `isOwner` 参数用于区分是否显示解锁条件等细节,但响应结构本身是一致的。 --- ## 使用示例 ### 1. 查看他人展馆 ```bash # 查看用户ID=37的展馆 curl -X GET "http://localhost:8080/api/v1/galleries/37" \ -H "Authorization: Bearer " ``` **响应示例**: ```json { "code": 200, "message": "ok", "data": { "gallery_owner_id": 37000088, // 37 * 1000000 + 88 "slot_total": 3, "slots": [ { "slot_id": 46, "slot_index": 1, "status": "EMPTY", // 已解锁但空展位 "is_enabled": true, // asset, occupier_uid 等字段不存在(因为 status=EMPTY) }, { "slot_id": 47, "slot_index": 2, "status": "OCCUPIED", // 有展品 "is_enabled": true, "asset": { "asset_id": 123, "name": "数字艺术品", "cover_url": "https://...", "like_count": 10, "remain_time": 86400 }, "occupier_uid": 20, // 占位者用户ID "occupied_at": 1705747200000, "expire_at": 1705833600000 }, { "slot_id": 48, "slot_index": 3, "status": "LOCKED", // 未解锁 "is_enabled": false, "unlock_condition": { "type": "crystal", "value": 100 } } ] } } ``` ### 2. 前端使用建议 1. **展示展位列表**: - 遍历 `data.slots[]`,根据 `status` 显示不同状态 - `EMPTY`:显示空展位占位图 - `OCCUPIED`:显示 `asset.cover_url` 和资产信息 - `LOCKED`:显示锁定图标和 `unlock_condition` 2. **放置藏品**: - 需要 `gallery_owner_id`、`slot_id`、`asset_id` - 调用 `POST /api/v1/galleries/place` 3. **权限判断**: - 只有展馆所有者可以放置/移除展品 - 他人只能查看已解锁的展位(`is_enabled=true`) --- ## 数据库查询示例 ### 查询展位列表 ```sql -- 查询用户ID=37,明星ID=88的所有展位 SELECT slot_id, slot_index, is_enabled, unlock_type, unlock_value FROM booth_slots WHERE user_id = 37 AND star_id = 88 ORDER BY slot_index ASC; ``` ### 查询展品展示情况 ```sql -- 查询展位ID=47的展品信息 SELECT e.asset_id, e.occupier_uid, e.start_time, e.expire_at, a.name AS asset_name, a.cover_url FROM exhibitions e LEFT JOIN assets a ON e.asset_id = a.id WHERE e.slot_id = 47; ``` --- ## 总结 1. **数据库表**: - `booth_slots`:展位基础信息 - `exhibitions`:展品展示关系 2. **响应格式**: - `/api/v1/galleries/me` 和 `/api/v1/galleries/{target_uid}` **响应格式完全一致** - 都返回 `{ gallery_owner_id, slot_total, slots[] }` - 差异仅在于数据内容(查询的目标用户不同) 3. **`gallery_owner_id` 计算**: - 公式:`user_id * 1000000 + star_id` - 示例:用户37、明星88 → `37000088` 4. **`status` 状态**: - `LOCKED`:未解锁(`is_enabled=false`) - `EMPTY`:已解锁但空展位(`is_enabled=true` 且 `exhibitions` 无记录) - `OCCUPIED`:有展品(`is_enabled=true` 且 `exhibitions` 有记录)