1186 lines
33 KiB
Markdown
1186 lines
33 KiB
Markdown
# 展馆服务设计方案
|
||
|
||
## 📋 文档说明
|
||
|
||
本文档详细说明了展馆服务(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. 更新展位状态为EMPTY(booth_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. 更新展位状态为EMPTY(booth_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**:无限制(可配置)
|
||
|
||
**推荐**:选项A(10个),后续可根据需求调整
|
||
|
||
---
|
||
|
||
### 8.7 展位占用时长 ❓
|
||
|
||
**问题**:展位占用时长是多少?
|
||
|
||
**选项A**:4小时(可配置)
|
||
**选项B**:24小时(可配置)
|
||
**选项C**:永久(可配置)
|
||
|
||
**推荐**:选项A(4小时),后续可根据需求调整
|
||
|
||
---
|
||
|
||
## 九、实现计划
|
||
|
||
### 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
|