topfans/backend/docs/展馆服务设计方案.md
2026-04-07 22:29:48 +08:00

1186 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 展馆服务设计方案
## 📋 文档说明
本文档详细说明了展馆服务Gallery Service的设计方案包括服务职责、API设计、数据库设计、业务规则、服务交互等。
**创建日期**: 2026-01-12
**维护者**: AI Assistant
**状态**: 🟡 设计方案
**重要说明**
- 本文档中的 API 设计完全参考 `docs/HTTP REST API接口文档.md`
- 所有 API 路径、请求参数、响应格式均与 HTTP REST API 接口文档保持一致
- 如有不一致之处,以 HTTP REST API 接口文档为准
---
## 一、服务概述
### 1.1 服务定位
**Gallery Service展馆服务** 是负责用户展馆管理的核心微服务,包括:
- 展馆管理(我的展馆、他人展馆)
- 展位管理(槽位列表、解锁/购买展位)
- 展品展示(放置、下架、踢走占位)
- 展位规则管理从PostgreSQL加载
### 1.2 服务职责边界
**Gallery Service 负责:**
- ✅ 展馆的创建、查询、管理
- ✅ 展位的创建、解锁、管理
- ✅ 展品的放置、下架、踢走
- ✅ 展位规则的加载和管理
- ✅ 展位占用时长的计算和管理
**Gallery Service 不负责:**
- ❌ 资产信息管理Asset Service
- ❌ 用户信息管理User Service
- ❌ 粉丝档案管理User Service
- ❌ 水晶余额扣除User Service通过RPC调用
- ❌ 任务进度更新Task Service通过事件触发
---
## 二、核心功能设计
### 2.1 展馆管理
#### 2.1.1 获取我的展馆
**功能描述**:获取当前用户的展馆信息,包括所有展位及其状态。
**业务规则**
- 每个用户user_id + star_id拥有一个展馆
- 展馆初始有3个展位可配置
- 展位可以解锁/购买(通过等级或水晶)
- 展位状态EMPTY、OCCUPIED被占用、LOCKED未解锁
**API设计**
```
GET /api/mygalleries
```
**请求参数**从JWT Token中获取user_id和star_id
**响应数据**
```json
{
"code": 200,
"message": "ok",
"data": {
"gallery_owner_id": 10000001,
"slot_total": 3,
"slots": [
{
"slot_id": 1,
"status": "OCCUPIED",
"asset": {
"asset_id": "asset_888",
"name": "藏品名称",
"cover_url": "https://...",
"like_count": 12,
"remain_time": 5000
}
}
]
}
}
```
**说明**
- 响应格式参考 HTTP REST API 接口文档
- `slot_id`展位ID
- `status`展位状态EMPTY、OCCUPIED、LOCKED
- `asset`展品信息仅当status为OCCUPIED时存在
- `remain_time`:剩余时间(秒)
#### 2.1.2 获取他人展馆
**功能描述**:获取指定用户的展馆信息(用于广场/好友访问)。
**业务规则**
- 只能查看同一明星star_id的展馆
- 需要验证粉丝身份一致性
- 返回数据与"我的展馆"相同,但某些操作不可用(如下架、踢走)
**API设计**
```
GET /api/galleries/{target_uid}
```
**请求参数**
- `target_uid` (path): 目标用户ID
**响应数据**:同"获取我的展馆"
---
### 2.2 展品展示
#### 2.2.1 在展位展示藏品
**功能描述**:在指定展位放置藏品(可以是自己的展馆或他人的展馆)。
**业务规则**
- 只能放置自己拥有的资产
- 展位必须为空EMPTY且已解锁is_enabled = true
- 如果放置在他人的展馆需要验证同一明星star_id
- 放置后展位被占用占用时长为4小时可配置
- 占用期间,展位所有者可以踢走占位
- 一个资产同时只能在一个展位展示
**API设计**
```
POST /api/galleries/place
```
**请求参数**
```json
{
"asset_id": "asset_888",
"gallery_owner_id": 10000001,
"slot_id": 1
}
```
**说明**
- 参考 HTTP REST API 接口文档,参数中 `slot_id` 在文档中写为 `slot_it`(可能是笔误),实际实现使用 `slot_id`
**响应数据**
```json
{
"code": 200,
"message": "ok",
"data": {
"status": "OCCUPIED",
"occupied_until": "2025-12-30T10:00:00Z",
"occupier_uid": 18995036422
}
}
```
**说明**
- 响应格式参考 HTTP REST API 接口文档
- `occupied_until`占用到期时间ISO 8601格式
- `occupier_uid`占位者用户ID
**业务逻辑流程**
1. 验证资产是否存在且属于当前用户调用Asset Service
2. 验证资产是否已在其他展位展示查询exhibitions表
3. 验证展位是否存在且为空查询booth_slots表
4. 验证展位是否已解锁is_enabled = true
5. 如果放置在他人的展馆验证同一明星star_id
6. 创建展品展示记录exhibitions表
7. 更新展位状态booth_slots表
8. 更新资产的is_exhibited字段调用Asset Service
9. 发布事件gallery.exhibit通知Task Service
#### 2.2.2 下架展位藏品
**功能描述**:主动下架展位中的藏品(只有放置者可以下架)。
**业务规则**
- 只有放置者occupier_uid可以下架
- 下架后展位变为空EMPTY
- 更新资产的is_exhibited字段
**API设计**
```
POST /api/galleries/remove
```
**请求参数**
```json
{
"asset_id": "asset_888",
"gallery_owner_uid": 10000001,
"slot_id": 1
}
```
**说明**
- 参考 HTTP REST API 接口文档,参数使用 `gallery_owner_uid`(不是 `gallery_owner_id`
- 参数为可选Body可选
**响应数据**
```json
{
"code": 200,
"message": "ok",
"data": {}
}
```
**说明**
- 响应为空 data参考 HTTP REST API 接口文档
**业务逻辑流程**
1. 验证展品展示记录是否存在
2. 验证当前用户是否为放置者occupier_uid
3. 删除展品展示记录exhibitions表
4. 更新展位状态为EMPTYbooth_slots表
5. 更新资产的is_exhibited字段调用Asset Service
6. 发布事件gallery.remove通知Task Service
#### 2.2.3 踢走占位
**功能描述**:展位所有者踢走占位者(只有展位所有者可以操作)。
**业务规则**
- 只有展位所有者host_profile_id对应的user_id可以踢走
- 踢走后展位变为空EMPTY
- 更新资产的is_exhibited字段
**API设计**
```
POST /api/galleries/me/slots/{slot_id}/kick
```
**请求参数**
- `slot_id` (path): 展位ID
- `confirm` (body, optional): 确认操作
**响应数据**
```json
{
"code": 200,
"message": "ok",
"data": {
"slot_id": 2,
"status": "EMPTY"
}
}
```
**业务逻辑流程**
1. 验证展位是否存在且属于当前用户
2. 验证展位是否被占用
3. 删除展品展示记录exhibitions表
4. 更新展位状态为EMPTYbooth_slots表
5. 更新资产的is_exhibited字段调用Asset Service
---
### 2.3 展位管理
#### 2.3.1 解锁/购买新展位
**功能描述**:解锁或购买新的展位(通过等级或水晶)。
**业务规则**
- 初始展位数3个可配置
- 解锁方式:
- 等级解锁:达到指定等级即可解锁
- 水晶购买:消耗指定数量的水晶
- 解锁后展位变为可用is_enabled = true
- 如果使用水晶购买需要扣除水晶余额调用User Service
**API设计**
```
POST /api/galleries/slots_unlock
```
**请求参数**从JWT Token中获取user_id和star_id
**响应数据**
```json
{
"code": 200,
"message": "ok",
"data": {
"slot_total": 3,
"crystal_balance": 120
}
}
```
**说明**
- 参考 HTTP REST API 接口文档,响应包含:
- `slot_total`:展位总数
- `crystal_balance`:水晶余额(如果使用水晶购买,显示扣除后的余额)
- 解锁后的展位信息可以通过"获取我的展馆"接口查看
**业务逻辑流程**
1. 查询当前用户的粉丝档案(获取等级、水晶余额)
2. 查询当前展位数和已解锁展位数
3. 查询下一个展位的解锁规则从config中获取
4. **优先检查等级解锁**
- 如果用户等级达到要求,直接解锁(不消耗水晶)
- 创建新展位booth_slots表is_enabled = true
5. **如果等级不够,检查水晶购买**
- 验证水晶余额是否足够
- 如果足够扣除水晶余额调用User Service
- 创建新展位booth_slots表is_enabled = true
6. 如果等级不够且水晶不足,返回错误提示
---
## 三、数据库设计
### 3.1 展位表booth_slots
**表名**`booth_slots`
**字段定义**
```sql
CREATE TABLE booth_slots (
slot_id BIGSERIAL PRIMARY KEY,
host_profile_id BIGINT NOT NULL, -- 展馆所有者fan_profiles表的id联合主键
user_id BIGINT NOT NULL, -- 用户ID冗余字段便于查询
star_id BIGINT NOT NULL, -- 明星ID冗余字段便于查询
slot_index INT NOT NULL, -- 展位序号1, 2, 3, ...
visibility VARCHAR(20) DEFAULT 'public', -- 可见性public, private
is_enabled BOOLEAN DEFAULT false, -- 是否已解锁
unlock_type VARCHAR(20), -- 解锁方式level, crystal, free
unlock_value INT, -- 解锁条件值(等级或水晶数量)
created_at BIGINT NOT NULL, -- 创建时间(毫秒时间戳)
updated_at BIGINT NOT NULL, -- 更新时间(毫秒时间戳)
-- 索引
UNIQUE KEY uk_host_slot (host_profile_id, slot_index),
INDEX idx_user_star (user_id, star_id),
INDEX idx_star_enabled (star_id, is_enabled)
);
```
**说明**
- `host_profile_id`:展馆所有者,对应`fan_profiles`表的联合主键user_id + star_id
- `slot_index`展位序号从1开始递增
- `visibility`:可见性标志位,`public`(公有)或 `private`(私有),用于后续功能扩展
- `is_enabled`是否已解锁初始展位为true后续展位为false
- `unlock_type`解锁方式level等级、crystal水晶、free免费
- `unlock_value`解锁条件值如等级5或水晶100
### 3.2 展品展示表exhibitions
**表名**`exhibitions`
**字段定义**
```sql
CREATE TABLE exhibitions (
id BIGSERIAL PRIMARY KEY,
asset_id BIGINT NOT NULL, -- 资产ID
slot_id BIGINT NOT NULL, -- 展位ID外键booth_slots.slot_id
host_profile_id BIGINT NOT NULL, -- 展馆所有者(冗余字段)
occupier_uid BIGINT NOT NULL, -- 占位者用户ID
occupier_star_id BIGINT NOT NULL, -- 占位者明星ID冗余字段
start_time BIGINT NOT NULL, -- 开始时间(毫秒时间戳)
expire_at BIGINT NOT NULL, -- 过期时间(毫秒时间戳)
created_at BIGINT NOT NULL, -- 创建时间(毫秒时间戳)
updated_at BIGINT NOT NULL, -- 更新时间(毫秒时间戳)
-- 外键约束
FOREIGN KEY (slot_id) REFERENCES booth_slots(slot_id) ON DELETE CASCADE,
-- 索引
UNIQUE KEY uk_asset (asset_id), -- 一个资产同时只能在一个展位
INDEX idx_slot (slot_id),
INDEX idx_host (host_profile_id),
INDEX idx_occupier (occupier_uid, occupier_star_id),
INDEX idx_expire (expire_at) -- 用于定时清理过期展品
);
```
**说明**
- `asset_id`资产ID唯一约束确保一个资产同时只能在一个展位
- `slot_id`展位ID外键关联`booth_slots`表
- `host_profile_id`:展馆所有者,冗余字段,便于查询
- `occupier_uid`占位者用户ID
- `expire_at`:过期时间,用于定时清理过期展品
**注意**
- 规则表gallery_rules暂不实现规则直接硬编码在config文件中
- 后续版本会引入规则表进行统一规则配置
---
## 四、业务规则设计
### 4.1 展位初始化规则 ✅ **已确认:懒加载**
**规则**
- 首次查询展馆时,自动创建初始展位(懒加载)
- 初始展位数量3个硬编码在config中
- 初始展位全部解锁is_enabled = true
- 初始展位全部为公有visibility = 'public'
- 展位序号从1开始递增
**实现方式**
- 在Gallery Service首次查询"获取我的展馆"时,检查是否存在展位
- 如果不存在自动创建初始展位3个
- 使用事务确保原子性
**优势**
- 解耦不需要User Service调用Gallery Service
- 减少服务间依赖
- 按需创建,节省资源
### 4.2 展位解锁规则 ✅ **已确认:优先等级解锁**
**规则**
- 第1-3个展位免费解锁初始展位
- 第4个展位等级5或水晶100
- 第5个展位等级6或水晶200
- 第6个展位等级7或水晶300
- ...硬编码在config中后续版本会引入规则表
**解锁方式**
- **优先等级解锁**:优先检查是否达到指定等级,如果达到则免费解锁,无需消耗水晶
- **水晶购买**:如果等级不够,可以使用水晶购买,消耗指定数量的水晶
**实现逻辑**
1. 查询下一个展位的解锁规则从config中获取
2. 检查用户等级是否达到要求
3. 如果等级达到,直接解锁(不消耗水晶)
4. 如果等级不够,检查水晶余额是否足够
5. 如果水晶足够,扣除水晶并解锁
6. 如果都不满足,返回错误提示
### 4.3 展位占用规则
**规则**
- 占用时长4小时可配置
- 占用期间,展位所有者可以踢走占位
- 占用到期后,自动下架(需要定时任务)
- 一个资产同时只能在一个展位展示
**需要确认**:❓ 占用到期后的处理?
- 选项A自动下架展位变为空需要定时任务
- 选项B保持占用但标记为过期需要前端显示过期状态
### 4.4 展位放置规则 ✅ **已确认:可以放置到他人展馆**
**规则**
- 只能放置自己拥有的资产
- 展位必须为空EMPTY且已解锁is_enabled = true
- **可以放置在他人的展馆**但需要验证同一明星star_id
- 一个资产同时只能在一个展位展示
- 放置后展位被占用占用时长为4小时硬编码在config中
- 占用期间,展位所有者可以踢走占位
**实现逻辑**
1. 验证资产是否存在且属于当前用户调用Asset Service
2. 验证资产是否已在其他展位展示查询exhibitions表
3. 验证展位是否存在且为空查询booth_slots表
4. 验证展位是否已解锁is_enabled = true
5. 如果放置在他人的展馆验证同一明星star_id
6. 创建展品展示记录exhibitions表
7. 更新展位状态booth_slots表
8. 更新资产的is_exhibited字段调用Asset Service
9. 发布事件gallery.exhibit通知Task Service
---
## 五、服务间交互
### 5.1 与 Asset Service 的交互
#### 5.1.1 验证资产是否存在且属于当前用户
**RPC接口**
```protobuf
rpc GetAssetForRPC(GetAssetForRPCRequest) returns (GetAssetForRPCResponse);
```
**使用场景**
- 放置资产前验证资产是否存在
- 验证资产是否属于当前用户
- 验证资产是否已在其他展位展示
**请求字段**
- `asset_id`资产ID
- `user_id`用户ID
- `star_id`明星ID
**响应字段**
- `asset_id`资产ID
- `owner_uid`所有者用户ID
- `is_exhibited`:是否已在展位展示
#### 5.1.2 更新资产展馆状态
**RPC接口**
```protobuf
rpc UpdateAssetExhibitionStatus(UpdateAssetExhibitionStatusRequest) returns (UpdateAssetExhibitionStatusResponse);
```
**使用场景**
- 放置资产时,更新`is_exhibited = true`
- 下架资产时,更新`is_exhibited = false`
**请求字段**
- `asset_id`资产ID
- `is_exhibited`:是否在展位展示
### 5.2 与 User Service 的交互
#### 5.2.1 获取粉丝档案信息
**RPC接口**
```protobuf
rpc GetFanProfile(GetFanProfileRequest) returns (GetFanProfileResponse);
```
**使用场景**
- 解锁展位时,获取用户等级和水晶余额
- 验证用户权限
**请求字段**
- `user_id`用户ID
- `star_id`明星ID
**响应字段**
- `user_id`用户ID
- `star_id`明星ID
- `level`:等级
- `crystal_balance`:水晶余额
#### 5.2.2 扣除水晶余额
**RPC接口**
```protobuf
rpc UpdateCrystalBalance(UpdateCrystalBalanceRequest) returns (UpdateCrystalBalanceResponse);
```
**使用场景**
- 使用水晶购买展位时,扣除水晶余额
**请求字段**
- `user_id`用户ID
- `star_id`明星ID
- `delta`:变化量(负数表示扣除)
### 5.3 与 Task Service 的交互
#### 5.3.1 发布事件(可选)
**事件类型**
- `gallery.exhibit`:展品上架事件
- `gallery.remove`:展品下架事件
- `gallery.unlock`:展位解锁事件
**实现方式**
- 直接RPC调用Task Service推荐
- 或使用消息队列RocketMQ/Kafka/RabbitMQ
**需要确认**:❓ 事件通知方式?
- 选项A直接RPC调用简单推荐
- 选项B消息队列解耦但需要额外基础设施
---
## 六、实现架构
### 6.1 服务结构
```
services/galleryService/
├── main.go # 主程序入口
├── go.mod # 依赖管理
├── go.sum # 依赖锁定
├── configs/
│ └── dubbo.yaml # Dubbo配置
├── config/
│ └── gallery_config.go # 展馆服务配置
├── repository/
│ ├── gallery_repository.go # 展馆数据访问层
│ ├── gallery_repository_test.go # Repository测试
│ └── README.md # Repository文档
├── service/
│ ├── gallery_service.go # 展馆业务逻辑
│ ├── slot_service.go # 展位业务逻辑
│ └── exhibition_service.go # 展品业务逻辑
├── client/
│ ├── asset_rpc_client.go # Asset Service RPC客户端
│ ├── user_rpc_client.go # User Service RPC客户端
│ └── task_rpc_client.go # Task Service RPC客户端可选
├── provider/
│ └── gallery_provider.go # RPC接口实现
└── IMPLEMENTATION_COMPLETE.md # 实现完成总结
```
### 6.2 配置层Config
**文件**`config/gallery_config.go`
**配置项**
```go
type GalleryConfig struct {
// 规则配置(硬编码,后续版本会引入规则表)
Rules *GalleryRules
// RPC客户端配置
AssetServiceURL string
UserServiceURL string
TaskServiceURL string
// 数据库配置
Database *DatabaseConfig
}
type GalleryRules struct {
// 初始展位数默认3
InitialSlotCount int
// 抢展位时长默认14400即4小时
GrabSlotDuration int64
// 按等级解锁成本slot_index -> level
// 例如第4个展位需要等级5第5个展位需要等级6
UnlockLevelBySlot map[int]int
// 按水晶解锁成本slot_index -> crystal_cost
// 例如第4个展位需要水晶100第5个展位需要水晶200
UnlockCrystalBySlot map[int]int
// 最大展位数默认10
MaxSlotCount int
}
```
**配置示例**参考其他service的实现方式
```go
// config/gallery_config.go
package config
var (
// GalleryRules 展馆规则配置(硬编码)
GalleryRules = &GalleryRulesConfig{
InitialSlotCount: 3,
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
},
// 按水晶解锁第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
},
MaxSlotCount: 10,
}
)
```
### 6.3 Repository层
**职责**
- 展馆数据访问CRUD操作
- 展位数据访问
- 展品展示数据访问
**主要方法**
```go
// 展馆相关
GetGalleryByUser(userID, starID int64) (*Gallery, error)
CreateInitialSlots(userID, starID int64) error // 懒加载时创建初始展位
// 展位相关
GetSlotsByGallery(hostProfileID int64) ([]*BoothSlot, error)
GetSlotByID(slotID int64) (*BoothSlot, error)
CreateSlot(slot *BoothSlot) error
UpdateSlotStatus(slotID int64, status string) error
UnlockSlot(slotID int64) error
// 展品相关
GetExhibitionByAsset(assetID int64) (*Exhibition, error)
GetExhibitionBySlot(slotID int64) (*Exhibition, error)
CreateExhibition(exhibition *Exhibition) error
DeleteExhibition(exhibitionID int64) error
GetExpiredExhibitions(beforeTime int64) ([]*Exhibition, error)
```
**注意**
- 规则配置硬编码在config文件中不需要从数据库加载
- 后续版本会引入规则表,届时会添加规则加载方法
### 6.4 Service层
**职责**
- 展馆业务逻辑
- 展位业务逻辑
- 展品业务逻辑
- 规则管理
**主要方法**
```go
// 展馆服务
GetMyGallery(userID, starID int64) (*GalleryResponse, error)
GetUserGallery(targetUID, starID int64) (*GalleryResponse, error)
// 展位服务
UnlockSlot(userID, starID int64) (*UnlockSlotResponse, error)
// 展品服务
PlaceAsset(userID, starID int64, req *PlaceAssetRequest) (*PlaceAssetResponse, error)
RemoveAsset(userID, starID int64, req *RemoveAssetRequest) error
KickOccupier(userID, starID int64, slotID int64) error
```
### 6.5 Provider层
**职责**
- 实现Proto定义的RPC接口
- 从Dubbo attachments提取用户信息
- 委托给Service层处理
**主要方法**
```go
// 实现GalleryServiceHandler接口
GetMyGallery(ctx context.Context, req *pb.GetMyGalleryRequest) (*pb.GetMyGalleryResponse, error)
GetUserGallery(ctx context.Context, req *pb.GetUserGalleryRequest) (*pb.GetUserGalleryResponse, error)
PlaceAsset(ctx context.Context, req *pb.PlaceAssetRequest) (*pb.PlaceAssetResponse, error)
RemoveAsset(ctx context.Context, req *pb.RemoveAssetRequest) (*pb.RemoveAssetResponse, error)
KickOccupier(ctx context.Context, req *pb.KickOccupierRequest) (*pb.KickOccupierResponse, error)
UnlockSlot(ctx context.Context, req *pb.UnlockSlotRequest) (*pb.UnlockSlotResponse, error)
```
---
## 七、Proto接口定义
### 7.1 创建Proto文件
**文件**`proto/gallery.proto`
**定义**
```protobuf
syntax = "proto3";
package topfans.gallery;
import "google/api/annotations.proto";
import "common/common.proto";
option go_package = "github.com/topfans/backend/pkg/proto/gallery";
// 展馆服务
service GalleryService {
// 获取我的展馆
rpc GetMyGallery(GetMyGalleryRequest) returns (GetMyGalleryResponse) {
option (google.api.http) = {
get: "/api/mygalleries"
};
}
// 获取他人展馆
rpc GetUserGallery(GetUserGalleryRequest) returns (GetUserGalleryResponse) {
option (google.api.http) = {
get: "/api/galleries/{target_uid}"
};
}
// 在展位展示藏品
rpc PlaceAsset(PlaceAssetRequest) returns (PlaceAssetResponse) {
option (google.api.http) = {
post: "/api/galleries/place"
body: "*"
};
}
// 下架展位藏品
rpc RemoveAsset(RemoveAssetRequest) returns (RemoveAssetResponse) {
option (google.api.http) = {
post: "/api/galleries/remove"
body: "*"
};
}
// 踢走占位
rpc KickOccupier(KickOccupierRequest) returns (KickOccupierResponse) {
option (google.api.http) = {
post: "/api/galleries/me/slots/{slot_id}/kick"
body: "*"
};
}
// 解锁/购买新展位
rpc UnlockSlot(UnlockSlotRequest) returns (UnlockSlotResponse) {
option (google.api.http) = {
post: "/api/galleries/slots_unlock"
body: "*"
};
}
}
// 请求和响应消息定义
message GetMyGalleryRequest {}
message GetMyGalleryResponse {
topfans.common.BaseResponse base = 1;
GalleryData data = 2;
}
message GetUserGalleryRequest {
int64 target_uid = 1;
}
message GetUserGalleryResponse {
topfans.common.BaseResponse base = 1;
GalleryData data = 2;
}
message PlaceAssetRequest {
int64 asset_id = 1;
int64 gallery_owner_id = 2; // 注意HTTP文档中使用 gallery_owner_id
int64 slot_id = 3; // 注意HTTP文档中写为 slot_it可能是笔误实际使用 slot_id
}
message PlaceAssetResponse {
topfans.common.BaseResponse base = 1;
PlaceAssetData data = 2;
}
message RemoveAssetRequest {
int64 asset_id = 1;
int64 gallery_owner_uid = 2; // 注意:使用 gallery_owner_uid不是 gallery_owner_id
int64 slot_id = 3;
}
message RemoveAssetResponse {
topfans.common.BaseResponse base = 1;
}
message KickOccupierRequest {
int64 slot_id = 1;
bool confirm = 2;
}
message KickOccupierResponse {
topfans.common.BaseResponse base = 1;
KickOccupierData data = 2;
}
message UnlockSlotRequest {}
message UnlockSlotResponse {
topfans.common.BaseResponse base = 1;
UnlockSlotData data = 2;
}
// 数据模型
message GalleryData {
int64 gallery_owner_id = 1;
int32 slot_total = 2;
repeated SlotInfo slots = 3;
}
message SlotInfo {
int64 slot_id = 1;
int32 slot_index = 2;
string status = 3; // EMPTY, OCCUPIED, LOCKED
bool is_enabled = 4;
AssetInfo asset = 5;
int64 occupier_uid = 6;
int64 occupied_at = 7;
int64 expire_at = 8;
UnlockCondition unlock_condition = 9;
}
message AssetInfo {
int64 asset_id = 1;
string name = 2;
string cover_url = 3;
int32 like_count = 4;
int64 remain_time = 5; // 剩余时间(秒)
}
message UnlockCondition {
string type = 1; // level, crystal
int32 value = 2;
}
message PlaceAssetData {
string status = 1; // OCCUPIED
string occupied_until = 2; // ISO 8601格式
int64 occupier_uid = 3; // 占位者用户ID
}
message KickOccupierData {
string slot_id = 1; // 注意:返回字符串格式(如 "slot_2"
string status = 2; // EMPTY
}
message UnlockSlotData {
int32 slot_total = 1; // 展位总数
int64 crystal_balance = 2; // 水晶余额(如果使用水晶购买,显示扣除后的余额)
// 注意:解锁后的展位信息可以通过"获取我的展馆"接口查看,此处不返回
}
```
---
## 八、需要确认的问题
### 8.1 展位初始化时机 ✅ **已确认:懒加载**
**决定**:首次查询时自动创建(懒加载)
**实现方式**
- 在Gallery Service首次查询"获取我的展馆"时,检查是否存在展位
- 如果不存在自动创建初始展位3个
- 使用事务确保原子性
**优势**
- 解耦不需要User Service调用Gallery Service
- 减少服务间依赖
- 按需创建,节省资源
---
### 8.2 解锁方式优先级 ✅ **已确认:优先等级解锁**
**决定**:优先使用等级解锁,如果等级不够,可以使用水晶购买
**实现逻辑**
1. 查询下一个展位的解锁规则从config中获取
2. 检查用户等级是否达到要求
3. 如果等级达到,直接解锁(不消耗水晶)
4. 如果等级不够,检查水晶余额是否足够
5. 如果水晶足够,扣除水晶并解锁
6. 如果都不满足,返回错误提示
**优势**
- 鼓励用户提升等级
- 提供灵活性,等级不够时可以使用水晶
---
### 8.3 占用到期后的处理 ❓
**问题**:展位占用到期后如何处理?
**选项A**:自动下架,展位变为空(需要定时任务)
- 优点:自动清理,展位可以重新使用
- 缺点:需要定时任务
**选项B**:保持占用,但标记为过期(需要前端显示过期状态)
- 优点:不需要定时任务
- 缺点:需要前端处理过期状态
**推荐**选项A自动下架+ 选项B前端显示过期状态定时任务异步清理
---
### 8.4 是否可以放置在他人的展馆 ✅ **已确认:可以放置**
**决定**:可以放置在他人的展馆(需要验证同一明星)
**实现逻辑**
- 验证资产是否存在且属于当前用户
- 验证展位是否存在且为空
- 如果放置在他人的展馆验证同一明星star_id
- 创建展品展示记录
**优势**
- 增加社交互动
- 提升用户参与度
---
### 8.5 事件通知方式 ❓
**问题**如何通知Task Service更新任务进度
**选项A**直接RPC调用推荐
- 优点:简单,不需要额外基础设施
- 缺点:服务间耦合
**选项B**消息队列RocketMQ/Kafka/RabbitMQ
- 优点:解耦,异步处理
- 缺点:需要额外基础设施
**推荐**选项A直接RPC调用参考Asset Service的实现
---
### 8.6 展位最大数量 ❓
**问题**:展位最大数量是多少?
**选项A**10个可配置
**选项B**20个可配置
**选项C**:无限制(可配置)
**推荐**选项A10个后续可根据需求调整
---
### 8.7 展位占用时长 ❓
**问题**:展位占用时长是多少?
**选项A**4小时可配置
**选项B**24小时可配置
**选项C**:永久(可配置)
**推荐**选项A4小时后续可根据需求调整
---
## 九、实现计划
### 9.1 阶段一Proto接口定义和数据库设计
**任务**
1. 创建`proto/gallery.proto`文件
2. 定义数据库表结构booth_slots、exhibitions
3. 创建数据库模型(`pkg/models/gallery.go`
4. 编译Proto文件
**注意**
- 暂不创建规则表gallery_rules规则硬编码在config文件中
- 后续版本会引入规则表
**预计工作量**2-3小时
---
### 9.2 阶段二Repository层实现
**任务**
1. 实现`gallery_repository.go`
2. 实现展馆数据访问方法
3. 实现展位数据访问方法(包括懒加载创建初始展位)
4. 实现展品数据访问方法
5. 编写单元测试
**注意**
- 暂不实现规则数据加载方法规则硬编码在config文件中
- 懒加载逻辑:首次查询时自动创建初始展位
**预计工作量**4-6小时
---
### 9.3 阶段三Service层实现
**任务**
1. 实现`gallery_service.go`
2. 实现`slot_service.go`
3. 实现`exhibition_service.go`
4. 实现业务逻辑和规则验证
5. 编写单元测试
**预计工作量**6-8小时
---
### 9.4 阶段四RPC客户端实现
**任务**
1. 实现`asset_rpc_client.go`
2. 实现`user_rpc_client.go`
3. 实现`task_rpc_client.go`(可选)
4. 实现错误处理和日志记录
**预计工作量**2-3小时
---
### 9.5 阶段五Provider层实现
**任务**
1. 实现`gallery_provider.go`
2. 实现所有RPC接口
3. 从Dubbo attachments提取用户信息
4. 委托给Service层处理
**预计工作量**3-4小时
---
### 9.6 阶段六:主程序和配置
**任务**
1. 实现`main.go`
2. 实现`config/gallery_config.go`(硬编码规则配置)
3. 配置`configs/dubbo.yaml`
4. 实现优雅关闭
**注意**
- 规则配置硬编码在`config/gallery_config.go`中参考其他service的实现方式
- 暂不实现规则加载机制(从数据库加载),后续版本会引入
**预计工作量**2-3小时
---
### 9.7 阶段七Gateway层集成
**任务**
1. 实现`gateway/controller/gallery_controller.go`
2. 实现`gateway/dto/gallery_dto.go`
3. 实现`gateway/dto/gallery_converter.go`
4. 配置路由
5. 测试接口
**预计工作量**3-4小时
---
### 9.8 阶段八:测试和优化
**任务**
1. 单元测试
2. 集成测试
3. 性能测试
4. 优化和修复
**预计工作量**4-6小时
---
**总预计工作量**26-37小时约3-5个工作日
---
## 十、总结
### 10.1 设计要点
1. **服务职责清晰**:展馆服务专注于展馆、展位、展品的管理
2. **数据隔离**:使用`user_id + star_id`进行数据隔离
3. **规则硬编码**展位规则硬编码在config文件中与其他service实现方式一致
4. **服务间解耦**通过RPC调用其他服务不直接访问其他服务的数据库
5. **扩展性强**:支持后续功能扩展(如展位装饰、展品排序、私有/公有展位等)
6. **展位可见性**:展位支持私有/公有标志位,便于后续功能扩展
### 10.2 关键设计决策
1. **展位初始化**:✅ 懒加载方式,首次查询时自动创建
2. **解锁方式**:✅ 优先使用等级解锁,不够可以使用水晶购买
3. **他人展馆**:✅ 可以放置在他人的展馆,增加社交互动
4. **规则管理**:✅ 规则硬编码在config文件中后续版本会引入规则表
5. **展位可见性**:✅ 展位支持私有/公有标志位visibility字段
6. **事件通知**直接RPC调用Task Service简单高效
### 10.3 下一步行动
1. **确认问题**:与产品/业务确认上述需要确认的问题
2. **细化设计**:根据确认结果,细化数据库设计和业务逻辑
3. **开始实现**:按照实现计划,逐步实现各个阶段
4. **测试验证**:完成实现后,进行充分的测试验证
---
**文档版本**: v1.0
**最后更新**: 2026-01-12
**维护者**: AI Assistant