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

33 KiB
Raw Permalink Blame History

展馆服务设计方案

📋 文档说明

本文档详细说明了展馆服务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展位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

请求参数

{
  "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

业务逻辑流程

  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

请求参数

{
  "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 接口文档

业务逻辑流程

  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): 确认操作

响应数据

{
  "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

响应数据

{
  "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

字段定义

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

字段定义

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接口

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接口

rpc UpdateAssetExhibitionStatus(UpdateAssetExhibitionStatusRequest) returns (UpdateAssetExhibitionStatusResponse);

使用场景

  • 放置资产时,更新is_exhibited = true
  • 下架资产时,更新is_exhibited = false

请求字段

  • asset_id资产ID
  • is_exhibited:是否在展位展示

5.2 与 User Service 的交互

5.2.1 获取粉丝档案信息

RPC接口

rpc GetFanProfile(GetFanProfileRequest) returns (GetFanProfileResponse);

使用场景

  • 解锁展位时,获取用户等级和水晶余额
  • 验证用户权限

请求字段

  • user_id用户ID
  • star_id明星ID

响应字段

  • user_id用户ID
  • star_id明星ID
  • level:等级
  • crystal_balance:水晶余额

5.2.2 扣除水晶余额

RPC接口

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

配置项

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 解锁方式优先级 已确认:优先等级解锁

决定:优先使用等级解锁,如果等级不够,可以使用水晶购买

实现逻辑

  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 展位最大数量

问题:展位最大数量是多少?

选项A10个可配置 选项B20个可配置 选项C:无限制(可配置)

推荐选项A10个后续可根据需求调整


8.7 展位占用时长

问题:展位占用时长是多少?

选项A4小时可配置 选项B24小时可配置 选项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