33 KiB
展馆服务设计方案
📋 文档说明
本文档详细说明了展馆服务(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)
响应数据:
{
"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:展位IDstatus:展位状态(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
请求参数:
{
"asset_id": "asset_888",
"gallery_owner_id": 10000001,
"slot_id": 1
}
说明:
- 参考 HTTP REST API 接口文档,参数中
slot_id在文档中写为slot_it(可能是笔误),实际实现使用slot_id
响应数据:
{
"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
业务逻辑流程:
- 验证资产是否存在且属于当前用户(调用Asset Service)
- 验证资产是否已在其他展位展示(查询exhibitions表)
- 验证展位是否存在且为空(查询booth_slots表)
- 验证展位是否已解锁(is_enabled = true)
- 如果放置在他人的展馆,验证同一明星(star_id)
- 创建展品展示记录(exhibitions表)
- 更新展位状态(booth_slots表)
- 更新资产的is_exhibited字段(调用Asset Service)
- 发布事件:gallery.exhibit(通知Task Service)
2.2.2 下架展位藏品
功能描述:主动下架展位中的藏品(只有放置者可以下架)。
业务规则:
- 只有放置者(occupier_uid)可以下架
- 下架后,展位变为空(EMPTY)
- 更新资产的is_exhibited字段
API设计:
POST /api/galleries/remove
请求参数:
{
"asset_id": "asset_888",
"gallery_owner_uid": 10000001,
"slot_id": 1
}
说明:
- 参考 HTTP REST API 接口文档,参数使用
gallery_owner_uid(不是gallery_owner_id) - 参数为可选(Body,可选)
响应数据:
{
"code": 200,
"message": "ok",
"data": {}
}
说明:
- 响应为空 data,参考 HTTP REST API 接口文档
业务逻辑流程:
- 验证展品展示记录是否存在
- 验证当前用户是否为放置者(occupier_uid)
- 删除展品展示记录(exhibitions表)
- 更新展位状态为EMPTY(booth_slots表)
- 更新资产的is_exhibited字段(调用Asset Service)
- 发布事件: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): 展位IDconfirm(body, optional): 确认操作
响应数据:
{
"code": 200,
"message": "ok",
"data": {
"slot_id": 2,
"status": "EMPTY"
}
}
业务逻辑流程:
- 验证展位是否存在且属于当前用户
- 验证展位是否被占用
- 删除展品展示记录(exhibitions表)
- 更新展位状态为EMPTY(booth_slots表)
- 更新资产的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)
响应数据:
{
"code": 200,
"message": "ok",
"data": {
"slot_total": 3,
"crystal_balance": 120
}
}
说明:
- 参考 HTTP REST API 接口文档,响应包含:
slot_total:展位总数crystal_balance:水晶余额(如果使用水晶购买,显示扣除后的余额)
- 解锁后的展位信息可以通过"获取我的展馆"接口查看
业务逻辑流程:
- 查询当前用户的粉丝档案(获取等级、水晶余额)
- 查询当前展位数和已解锁展位数
- 查询下一个展位的解锁规则(从config中获取)
- 优先检查等级解锁:
- 如果用户等级达到要求,直接解锁(不消耗水晶)
- 创建新展位(booth_slots表),is_enabled = true
- 如果等级不够,检查水晶购买:
- 验证水晶余额是否足够
- 如果足够,扣除水晶余额(调用User Service)
- 创建新展位(booth_slots表),is_enabled = true
- 如果等级不够且水晶不足,返回错误提示
三、数据库设计
3.1 展位表(booth_slots)
表名:booth_slots
字段定义:
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,后续展位为falseunlock_type:解锁方式,level(等级)、crystal(水晶)、free(免费)unlock_value:解锁条件值,如等级5或水晶100
3.2 展品展示表(exhibitions)
表名:exhibitions
字段定义:
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:占位者用户IDexpire_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中,后续版本会引入规则表)
解锁方式:
- 优先等级解锁:优先检查是否达到指定等级,如果达到则免费解锁,无需消耗水晶
- 水晶购买:如果等级不够,可以使用水晶购买,消耗指定数量的水晶
实现逻辑:
- 查询下一个展位的解锁规则(从config中获取)
- 检查用户等级是否达到要求
- 如果等级达到,直接解锁(不消耗水晶)
- 如果等级不够,检查水晶余额是否足够
- 如果水晶足够,扣除水晶并解锁
- 如果都不满足,返回错误提示
4.3 展位占用规则
规则:
- 占用时长:4小时(可配置)
- 占用期间,展位所有者可以踢走占位
- 占用到期后,自动下架(需要定时任务)
- 一个资产同时只能在一个展位展示
需要确认:❓ 占用到期后的处理?
- 选项A:自动下架,展位变为空(需要定时任务)
- 选项B:保持占用,但标记为过期(需要前端显示过期状态)
4.4 展位放置规则 ✅ 已确认:可以放置到他人展馆
规则:
- 只能放置自己拥有的资产
- 展位必须为空(EMPTY)且已解锁(is_enabled = true)
- 可以放置在他人的展馆,但需要验证同一明星(star_id)
- 一个资产同时只能在一个展位展示
- 放置后,展位被占用,占用时长为4小时(硬编码在config中)
- 占用期间,展位所有者可以踢走占位
实现逻辑:
- 验证资产是否存在且属于当前用户(调用Asset Service)
- 验证资产是否已在其他展位展示(查询exhibitions表)
- 验证展位是否存在且为空(查询booth_slots表)
- 验证展位是否已解锁(is_enabled = true)
- 如果放置在他人的展馆,验证同一明星(star_id)
- 创建展品展示记录(exhibitions表)
- 更新展位状态(booth_slots表)
- 更新资产的is_exhibited字段(调用Asset Service)
- 发布事件:gallery.exhibit(通知Task Service)
五、服务间交互
5.1 与 Asset Service 的交互
5.1.1 验证资产是否存在且属于当前用户
RPC接口:
rpc GetAssetForRPC(GetAssetForRPCRequest) returns (GetAssetForRPCResponse);
使用场景:
- 放置资产前验证资产是否存在
- 验证资产是否属于当前用户
- 验证资产是否已在其他展位展示
请求字段:
asset_id:资产IDuser_id:用户IDstar_id:明星ID
响应字段:
asset_id:资产IDowner_uid:所有者用户IDis_exhibited:是否已在展位展示
5.1.2 更新资产展馆状态
RPC接口:
rpc UpdateAssetExhibitionStatus(UpdateAssetExhibitionStatusRequest) returns (UpdateAssetExhibitionStatusResponse);
使用场景:
- 放置资产时,更新
is_exhibited = true - 下架资产时,更新
is_exhibited = false
请求字段:
asset_id:资产IDis_exhibited:是否在展位展示
5.2 与 User Service 的交互
5.2.1 获取粉丝档案信息
RPC接口:
rpc GetFanProfile(GetFanProfileRequest) returns (GetFanProfileResponse);
使用场景:
- 解锁展位时,获取用户等级和水晶余额
- 验证用户权限
请求字段:
user_id:用户IDstar_id:明星ID
响应字段:
user_id:用户IDstar_id:明星IDlevel:等级crystal_balance:水晶余额
5.2.2 扣除水晶余额
RPC接口:
rpc UpdateCrystalBalance(UpdateCrystalBalanceRequest) returns (UpdateCrystalBalanceResponse);
使用场景:
- 使用水晶购买展位时,扣除水晶余额
请求字段:
user_id:用户IDstar_id:明星IDdelta:变化量(负数表示扣除)
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
配置项:
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的实现方式):
// 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操作)
- 展位数据访问
- 展品展示数据访问
主要方法:
// 展馆相关
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层
职责:
- 展馆业务逻辑
- 展位业务逻辑
- 展品业务逻辑
- 规则管理
主要方法:
// 展馆服务
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层处理
主要方法:
// 实现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
定义:
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 解锁方式优先级 ✅ 已确认:优先等级解锁
决定:优先使用等级解锁,如果等级不够,可以使用水晶购买
实现逻辑:
- 查询下一个展位的解锁规则(从config中获取)
- 检查用户等级是否达到要求
- 如果等级达到,直接解锁(不消耗水晶)
- 如果等级不够,检查水晶余额是否足够
- 如果水晶足够,扣除水晶并解锁
- 如果都不满足,返回错误提示
优势:
- 鼓励用户提升等级
- 提供灵活性,等级不够时可以使用水晶
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接口定义和数据库设计
任务:
- 创建
proto/gallery.proto文件 - 定义数据库表结构(booth_slots、exhibitions)
- 创建数据库模型(
pkg/models/gallery.go) - 编译Proto文件
注意:
- 暂不创建规则表(gallery_rules),规则硬编码在config文件中
- 后续版本会引入规则表
预计工作量:2-3小时
9.2 阶段二:Repository层实现
任务:
- 实现
gallery_repository.go - 实现展馆数据访问方法
- 实现展位数据访问方法(包括懒加载创建初始展位)
- 实现展品数据访问方法
- 编写单元测试
注意:
- 暂不实现规则数据加载方法,规则硬编码在config文件中
- 懒加载逻辑:首次查询时自动创建初始展位
预计工作量:4-6小时
9.3 阶段三:Service层实现
任务:
- 实现
gallery_service.go - 实现
slot_service.go - 实现
exhibition_service.go - 实现业务逻辑和规则验证
- 编写单元测试
预计工作量:6-8小时
9.4 阶段四:RPC客户端实现
任务:
- 实现
asset_rpc_client.go - 实现
user_rpc_client.go - 实现
task_rpc_client.go(可选) - 实现错误处理和日志记录
预计工作量:2-3小时
9.5 阶段五:Provider层实现
任务:
- 实现
gallery_provider.go - 实现所有RPC接口
- 从Dubbo attachments提取用户信息
- 委托给Service层处理
预计工作量:3-4小时
9.6 阶段六:主程序和配置
任务:
- 实现
main.go - 实现
config/gallery_config.go(硬编码规则配置) - 配置
configs/dubbo.yaml - 实现优雅关闭
注意:
- 规则配置硬编码在
config/gallery_config.go中,参考其他service的实现方式 - 暂不实现规则加载机制(从数据库加载),后续版本会引入
预计工作量:2-3小时
9.7 阶段七:Gateway层集成
任务:
- 实现
gateway/controller/gallery_controller.go - 实现
gateway/dto/gallery_dto.go - 实现
gateway/dto/gallery_converter.go - 配置路由
- 测试接口
预计工作量:3-4小时
9.8 阶段八:测试和优化
任务:
- 单元测试
- 集成测试
- 性能测试
- 优化和修复
预计工作量:4-6小时
总预计工作量:26-37小时(约3-5个工作日)
十、总结
10.1 设计要点
- 服务职责清晰:展馆服务专注于展馆、展位、展品的管理
- 数据隔离:使用
user_id + star_id进行数据隔离 - 规则硬编码:展位规则硬编码在config文件中,与其他service实现方式一致
- 服务间解耦:通过RPC调用其他服务,不直接访问其他服务的数据库
- 扩展性强:支持后续功能扩展(如展位装饰、展品排序、私有/公有展位等)
- 展位可见性:展位支持私有/公有标志位,便于后续功能扩展
10.2 关键设计决策
- 展位初始化:✅ 懒加载方式,首次查询时自动创建
- 解锁方式:✅ 优先使用等级解锁,不够可以使用水晶购买
- 他人展馆:✅ 可以放置在他人的展馆,增加社交互动
- 规则管理:✅ 规则硬编码在config文件中,后续版本会引入规则表
- 展位可见性:✅ 展位支持私有/公有标志位(visibility字段)
- 事件通知:直接RPC调用Task Service,简单高效
10.3 下一步行动
- 确认问题:与产品/业务确认上述需要确认的问题
- 细化设计:根据确认结果,细化数据库设计和业务逻辑
- 开始实现:按照实现计划,逐步实现各个阶段
- 测试验证:完成实现后,进行充分的测试验证
文档版本: v1.0
最后更新: 2026-01-12
维护者: AI Assistant