714 lines
19 KiB
Markdown
714 lines
19 KiB
Markdown
# 展馆服务开发流程文档
|
||
|
||
## 📋 文档说明
|
||
|
||
本文档详细说明了展馆服务(Gallery Service)的开发流程,包括各阶段的实现步骤、代码示例、测试要求等。
|
||
|
||
**创建日期**: 2026-01-12
|
||
**维护者**: AI Assistant
|
||
**状态**: 🟡 开发指南
|
||
|
||
**参考文档**:
|
||
- `docs/展馆服务设计方案.md` - 详细设计方案
|
||
- `docs/HTTP REST API接口文档.md` - API接口定义
|
||
- `services/assetService/IMPLEMENTATION_COMPLETE.md` - Asset Service实现参考
|
||
|
||
---
|
||
|
||
## 一、开发环境准备
|
||
|
||
### 1.1 前置条件
|
||
|
||
- ✅ Go 1.21+
|
||
- ✅ PostgreSQL 数据库
|
||
- ✅ Dubbo-go 框架
|
||
- ✅ 已实现 User Service 和 Asset Service
|
||
- ✅ 已配置 Gateway 层
|
||
|
||
### 1.2 目录结构
|
||
|
||
```
|
||
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 # 实现完成总结
|
||
```
|
||
|
||
---
|
||
|
||
## 二、阶段一:项目初始化与基础配置
|
||
|
||
### 2.1 创建项目结构
|
||
在 `services/` 目录下创建 `galleryService` 文件夹,并初始化 `go.mod`:
|
||
```bash
|
||
mkdir -p services/galleryService/{config,configs,repository,service,client,provider}
|
||
cd services/galleryService
|
||
go mod init github.com/topfans/backend/services/galleryService
|
||
```
|
||
|
||
### 2.2 定义基础配置 (Config)
|
||
**文件**:`services/galleryService/config/gallery_config.go`
|
||
|
||
**职责**:定义服务启动所需的硬编码规则、DB 连接参数和 RPC 依赖地址。
|
||
|
||
**代码示例**:
|
||
```go
|
||
package config
|
||
|
||
var (
|
||
// 业务规则(硬编码)
|
||
GalleryRules = &GalleryRulesConfig{
|
||
InitialSlotCount: 3,
|
||
GrabSlotDuration: 14400, // 4小时
|
||
UnlockLevelBySlot: map[int]int{4: 5, 5: 6},
|
||
UnlockCrystalBySlot: map[int]int{4: 100, 5: 200},
|
||
}
|
||
|
||
// 基础设施配置(由 main.go 的 flag 或环境变量注入)
|
||
DBConfig = &DatabaseConfig{}
|
||
ServiceURLs = struct {
|
||
AssetService string
|
||
UserService string
|
||
}{
|
||
AssetService: "tri://localhost:20003",
|
||
UserService: "tri://localhost:20000",
|
||
}
|
||
)
|
||
```
|
||
|
||
### 2.3 Proto 编译
|
||
**文件**:`proto/gallery.proto`
|
||
1. 按照设计方案编写 Proto。
|
||
2. 运行 `./scripts/compile-proto.sh` 生成代码。
|
||
|
||
### 2.4 数据库模型与迁移
|
||
**文件**:`pkg/models/gallery.go`
|
||
1. 定义 `BoothSlot` 和 `Exhibition` 模型。
|
||
2. 在数据库中执行 SQL 脚本创建表和 `UNIQUE` 索引。
|
||
|
||
---
|
||
|
||
## 三、阶段二:Repository 层实现
|
||
|
||
### 3.1 创建Repository接口
|
||
|
||
**文件**:`services/galleryService/repository/gallery_repository.go`
|
||
|
||
**主要方法**:
|
||
```go
|
||
type GalleryRepository interface {
|
||
// 展馆相关
|
||
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)
|
||
}
|
||
```
|
||
|
||
### 3.2 实现懒加载逻辑
|
||
|
||
**关键实现**:`CreateInitialSlots`
|
||
|
||
**步骤**:
|
||
1. 检查是否已存在展位(避免重复创建)
|
||
2. 使用事务创建初始展位(3个)
|
||
3. 设置初始展位为已解锁(is_enabled = true)
|
||
4. 设置初始展位为公有(visibility = 'public')
|
||
|
||
**示例**:
|
||
```go
|
||
func (r *galleryRepository) CreateInitialSlots(userID, starID int64) error {
|
||
// 使用 PostgreSQL 的 ON CONFLICT 保证并发安全性
|
||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
now := time.Now().UnixMilli()
|
||
initialSlotCount := config.GalleryRules.InitialSlotCount
|
||
|
||
for i := 1; i <= initialSlotCount; i++ {
|
||
slot := &models.BoothSlot{
|
||
HostProfileID: getHostProfileID(userID, starID),
|
||
UserID: userID,
|
||
StarID: starID,
|
||
SlotIndex: i,
|
||
Visibility: "public",
|
||
IsEnabled: true,
|
||
UnlockType: "free",
|
||
UnlockValue: 0,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
|
||
// 使用 Clause 处理冲突,确保幂等
|
||
err := tx.Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "host_profile_id"}, {Name: "slot_index"}},
|
||
DoNothing: true,
|
||
}).Create(slot).Error
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
```
|
||
|
||
### 3.3 编写单元测试
|
||
|
||
**文件**:`services/galleryService/repository/gallery_repository_test.go`
|
||
|
||
**测试覆盖**:
|
||
- ✅ 创建初始展位
|
||
- ✅ 查询展位列表
|
||
- ✅ 创建展品展示记录
|
||
- ✅ 查询展品展示记录
|
||
- ✅ 删除展品展示记录
|
||
- ✅ 懒加载逻辑(避免重复创建)
|
||
|
||
**验收标准**:
|
||
- ✅ 所有Repository方法实现完成
|
||
- ✅ 单元测试通过
|
||
- ✅ 测试覆盖率 > 80%
|
||
- ✅ 懒加载逻辑正确
|
||
|
||
---
|
||
|
||
## 四、阶段三:Service 层实现
|
||
|
||
### 4.1 完善业务逻辑配置
|
||
在 `config/gallery_config.go` 中补充详细的业务映射(如解锁等级的具体数值),这些配置将在 Service 层被引用。
|
||
|
||
### 4.2 实现展馆服务
|
||
|
||
**文件**:`services/galleryService/service/gallery_service.go`
|
||
|
||
**关键方法**:
|
||
- `GetMyGallery`:获取我的展馆(包含懒加载逻辑)
|
||
- `GetUserGallery`:获取他人展馆
|
||
|
||
**懒加载实现**:
|
||
```go
|
||
func (s *galleryService) GetMyGallery(userID, starID int64) (*GalleryResponse, error) {
|
||
// 1. 查询展位列表
|
||
slots, err := s.repo.GetSlotsByGallery(hostProfileID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 如果不存在展位,懒加载创建初始展位
|
||
if len(slots) == 0 {
|
||
if err := s.repo.CreateInitialSlots(userID, starID); err != nil {
|
||
return nil, err
|
||
}
|
||
// 重新查询
|
||
slots, err = s.repo.GetSlotsByGallery(hostProfileID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// 3. 填充展品信息
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 4.3 实现展位服务
|
||
|
||
**文件**:`services/galleryService/service/slot_service.go`
|
||
|
||
**关键方法**:
|
||
- `UnlockSlot`:解锁/购买新展位
|
||
|
||
**解锁逻辑**(优先等级解锁):
|
||
```go
|
||
func (s *slotService) UnlockSlot(userID, starID int64) (*UnlockSlotResponse, error) {
|
||
// 1. 获取粉丝档案(等级、水晶余额)
|
||
profile, err := s.userClient.GetFanProfile(userID, starID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 2. 查询当前展位数
|
||
currentSlotCount := s.repo.GetSlotCount(userID, starID)
|
||
nextSlotIndex := currentSlotCount + 1
|
||
|
||
// 3. 获取解锁规则
|
||
requiredLevel := config.GalleryRules.UnlockLevelBySlot[nextSlotIndex]
|
||
requiredCrystal := config.GalleryRules.UnlockCrystalBySlot[nextSlotIndex]
|
||
|
||
// 4. 优先检查等级解锁
|
||
if profile.Level >= requiredLevel {
|
||
// 等级足够,直接解锁(不消耗水晶)
|
||
slot := &models.BoothSlot{
|
||
// ... 设置字段
|
||
IsEnabled: true,
|
||
UnlockType: "level",
|
||
}
|
||
if err := s.repo.CreateSlot(slot); err != nil {
|
||
return nil, err
|
||
}
|
||
return &UnlockSlotResponse{...}, nil
|
||
}
|
||
|
||
// 5. 等级不够,检查水晶购买
|
||
if profile.CrystalBalance >= requiredCrystal {
|
||
// 扣除水晶
|
||
if err := s.userClient.UpdateCrystalBalance(userID, starID, -requiredCrystal); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 创建展位
|
||
slot := &models.BoothSlot{
|
||
// ... 设置字段
|
||
IsEnabled: true,
|
||
UnlockType: "crystal",
|
||
}
|
||
if err := s.repo.CreateSlot(slot); err != nil {
|
||
return nil, err
|
||
}
|
||
return &UnlockSlotResponse{...}, nil
|
||
}
|
||
|
||
// 6. 都不满足,返回错误
|
||
return nil, errors.New("等级和水晶余额都不足")
|
||
}
|
||
```
|
||
|
||
### 4.4 实现展品服务
|
||
|
||
**文件**:`services/galleryService/service/exhibition_service.go`
|
||
|
||
**关键方法**:
|
||
- `PlaceAsset`:在展位展示藏品(支持放置到他人展馆)
|
||
- `RemoveAsset`:下架展位藏品
|
||
- `KickOccupier`:踢走占位
|
||
|
||
**放置逻辑**(包含权限校验与方案A实现):
|
||
```go
|
||
func (s *exhibitionService) PlaceAsset(userID, starID int64, req *PlaceAssetRequest) error {
|
||
// 1. 验证资产是否存在且属于当前用户 (RPC Asset Service)
|
||
asset, err := s.assetClient.GetAssetForRPC(req.AssetID, userID, starID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 2. 获取目标展位信息
|
||
slot, err := s.repo.GetSlotByID(req.SlotID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !slot.IsEnabled {
|
||
return errors.New("展位未解锁")
|
||
}
|
||
|
||
// 3. 校验权限与可见性 (新细节)
|
||
isOwner := slot.UserID == userID
|
||
if !isOwner {
|
||
// 如果不是自己的展位,必须是 public 且在同一个明星下
|
||
if slot.Visibility != "public" {
|
||
return errors.New("该展位是私有的,无法放置展品")
|
||
}
|
||
if slot.StarID != starID {
|
||
return errors.New("只能在同一明星的展馆中放置展品")
|
||
}
|
||
}
|
||
|
||
// 4. 执行放置逻辑
|
||
now := time.Now().UnixMilli()
|
||
expireAt := now + config.GalleryRules.GrabSlotDuration*1000
|
||
|
||
exhibition := &models.Exhibition{
|
||
AssetID: req.AssetID,
|
||
SlotID: req.SlotID,
|
||
HostProfileID: slot.HostProfileID,
|
||
OccupierUID: userID,
|
||
OccupierStarID: starID,
|
||
StartTime: now,
|
||
ExpireAt: expireAt,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
|
||
// 5. 使用数据库唯一索引保证原子性 (方案A)
|
||
// 如果该 asset_id 已在其他地方展示,此处会报 DB Error
|
||
if err := s.repo.CreateExhibition(exhibition); err != nil {
|
||
if strings.Contains(err.Error(), "duplicate key") {
|
||
return errors.New("资产已在其他展位展示中")
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 6. 发布事件 (不再强依赖同步更新 Asset Service 状态)
|
||
go s.publishEvent("gallery.exhibit", exhibition)
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 4.5 实现清理 Worker (新细节)
|
||
|
||
**职责**:定时扫描并删除过期的展示记录。
|
||
|
||
**实现方式**:
|
||
1. 在 `service/` 目录下创建 `cleanup_worker.go`。
|
||
2. 使用 `time.Ticker` 或 `cron` 库,每分钟运行一次。
|
||
3. 调用 Repository 层 `GetExpiredExhibitions`。
|
||
4. 批量删除过期记录。
|
||
|
||
**示例**:
|
||
```go
|
||
func (s *CleanupWorker) Start() {
|
||
ticker := time.NewTicker(1 * time.Minute)
|
||
for range ticker.C {
|
||
expired, _ := s.repo.GetExpiredExhibitions(time.Now().UnixMilli())
|
||
for _, e := range expired {
|
||
s.repo.DeleteExhibition(e.ID)
|
||
// 发送下架事件
|
||
s.publishEvent("gallery.remove.auto", e)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 所有Service方法实现完成
|
||
- ✅ 懒加载逻辑正确
|
||
- ✅ 优先等级解锁逻辑正确
|
||
- ✅ 支持放置到他人展馆
|
||
- ✅ 单元测试通过
|
||
|
||
---
|
||
|
||
## 五、阶段四:RPC客户端实现
|
||
|
||
### 5.1 Asset Service RPC客户端
|
||
|
||
**文件**:`services/galleryService/client/asset_rpc_client.go`
|
||
|
||
**需要的方法**:
|
||
- `GetAssetForRPC`:验证资产是否存在且属于当前用户。
|
||
|
||
**注意**:根据方案A,不再需要调用 `UpdateAssetExhibitionStatus`。展位状态完全由 Gallery Service 维护。
|
||
|
||
---
|
||
|
||
### 5.2 User Service RPC客户端
|
||
|
||
**文件**:`services/galleryService/client/user_rpc_client.go`
|
||
|
||
**需要的方法**:
|
||
- `GetFanProfile`:获取粉丝档案(等级、水晶余额)。
|
||
- `UpdateCrystalBalance`:扣除水晶余额。
|
||
|
||
---
|
||
|
||
### 5.3 Task Service RPC客户端(可选)
|
||
|
||
**文件**:`services/galleryService/client/task_rpc_client.go`
|
||
|
||
**需要的方法**:
|
||
- `UpdateTaskProgress`:更新任务进度。建议在 `PlaceAsset` 和 `CleanupWorker` 中触发。
|
||
|
||
**验收标准**:
|
||
- ✅ 所有RPC客户端实现完成
|
||
- ✅ 错误处理完善
|
||
- ✅ 日志记录完整
|
||
|
||
---
|
||
|
||
## 六、阶段五:Provider层实现
|
||
|
||
### 6.1 实现RPC接口
|
||
|
||
**文件**:`services/galleryService/provider/gallery_provider.go`
|
||
|
||
**关键点**:
|
||
1. 从Dubbo attachments提取用户信息
|
||
2. 委托给Service层处理
|
||
3. 统一的错误处理和日志记录
|
||
|
||
**示例**:
|
||
```go
|
||
func (p *GalleryProvider) GetMyGallery(ctx context.Context, req *pb.GetMyGalleryRequest) (*pb.GetMyGalleryResponse, error) {
|
||
// 从Dubbo attachments提取用户信息
|
||
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
|
||
if err != nil {
|
||
return &pb.GetMyGalleryResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
|
||
Message: "user authentication required",
|
||
},
|
||
}, err
|
||
}
|
||
|
||
// 调用Service层
|
||
result, err := p.galleryService.GetMyGallery(userID, starID)
|
||
if err != nil {
|
||
return &pb.GetMyGalleryResponse{
|
||
Base: &pbCommon.BaseResponse{
|
||
Code: appErrors.ToStatusCode(err),
|
||
Message: err.Error(),
|
||
},
|
||
}, err
|
||
}
|
||
|
||
// 转换响应
|
||
return convertToGetMyGalleryResponse(result), nil
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 所有RPC接口实现完成
|
||
- ✅ 用户信息提取正确
|
||
- ✅ 错误处理完善
|
||
- ✅ 日志记录完整
|
||
|
||
---
|
||
|
||
## 七、阶段六:主程序和配置
|
||
|
||
### 7.1 实现main.go
|
||
|
||
**文件**:`services/galleryService/main.go`
|
||
|
||
**关键步骤**:
|
||
1. 初始化日志
|
||
2. 连接数据库
|
||
3. 注册Dubbo服务
|
||
4. 实现优雅关闭
|
||
|
||
**参考**:`services/assetService/main.go`
|
||
|
||
### 7.2 配置Dubbo
|
||
|
||
**文件**:`services/galleryService/configs/dubbo.yaml`
|
||
|
||
**关键配置**:
|
||
- 服务端口:20004(参考其他服务)
|
||
- 注册中心配置
|
||
- 协议配置
|
||
|
||
**验收标准**:
|
||
- ✅ 服务可以正常启动
|
||
- ✅ 可以注册到Dubbo
|
||
- ✅ 优雅关闭正常
|
||
|
||
---
|
||
|
||
## 八、阶段七:Gateway层集成
|
||
|
||
### 8.1 实现Controller
|
||
|
||
**文件**:`gateway/controller/gallery_controller.go`
|
||
|
||
**需要的方法**:
|
||
- `GetMyGallery`
|
||
- `GetUserGallery`
|
||
- `PlaceAsset`
|
||
- `RemoveAsset`
|
||
- `KickOccupier`
|
||
- `UnlockSlot`
|
||
|
||
**参考**:`gateway/controller/asset_controller.go`
|
||
|
||
### 8.2 实现DTO
|
||
|
||
**文件**:
|
||
- `gateway/dto/gallery_dto.go`:DTO定义
|
||
- `gateway/dto/gallery_converter.go`:转换器
|
||
|
||
### 8.3 配置路由
|
||
|
||
**文件**:`gateway/router/router.go`
|
||
|
||
**路由配置**:
|
||
```go
|
||
galleries := v1.Group("/galleries")
|
||
galleries.Use(middleware.AuthMiddleware())
|
||
{
|
||
galleries.GET("/me", galleryCtrl.GetMyGallery)
|
||
galleries.GET("/:target_uid", galleryCtrl.GetUserGallery)
|
||
galleries.POST("/place", galleryCtrl.PlaceAsset)
|
||
galleries.POST("/remove", galleryCtrl.RemoveAsset)
|
||
galleries.POST("/me/slots/:slot_id/kick", galleryCtrl.KickOccupier)
|
||
}
|
||
|
||
mygalleries := v1.Group("/mygalleries")
|
||
mygalleries.Use(middleware.AuthMiddleware())
|
||
{
|
||
mygalleries.GET("", galleryCtrl.GetMyGallery)
|
||
}
|
||
|
||
slots := v1.Group("/galleries")
|
||
slots.Use(middleware.AuthMiddleware())
|
||
{
|
||
slots.POST("/slots_unlock", galleryCtrl.UnlockSlot)
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 所有接口实现完成
|
||
- ✅ 路由配置正确
|
||
- ✅ DTO转换正确
|
||
- ✅ 接口测试通过
|
||
|
||
---
|
||
|
||
## 九、阶段八:测试和优化
|
||
|
||
### 9.1 单元测试
|
||
|
||
**测试覆盖**:
|
||
- ✅ Repository层:所有数据访问方法
|
||
- ✅ Service层:所有业务逻辑方法
|
||
- ✅ 懒加载逻辑
|
||
- ✅ 优先等级解锁逻辑
|
||
- ✅ 放置到他人展馆逻辑
|
||
|
||
### 9.2 集成测试
|
||
|
||
**测试场景**:
|
||
1. 获取我的展馆(懒加载创建初始展位)
|
||
2. 获取他人展馆
|
||
3. 放置资产到自己的展馆
|
||
4. 放置资产到他人的展馆
|
||
5. 下架资产
|
||
6. 踢走占位
|
||
7. 解锁展位(等级解锁)
|
||
8. 解锁展位(水晶购买)
|
||
|
||
### 9.3 性能测试
|
||
|
||
**测试项**:
|
||
- 并发查询展馆
|
||
- 并发放置资产
|
||
- 懒加载性能
|
||
|
||
### 9.4 优化
|
||
|
||
**优化项**:
|
||
- 数据库查询优化
|
||
- 缓存热点数据
|
||
- 批量操作优化
|
||
|
||
**验收标准**:
|
||
- ✅ 单元测试覆盖率 > 80%
|
||
- ✅ 集成测试全部通过
|
||
- ✅ 性能测试满足要求
|
||
- ✅ 代码无linter错误
|
||
|
||
---
|
||
|
||
## 十、开发检查清单
|
||
|
||
### 10.1 代码质量
|
||
|
||
- [ ] 代码遵循Go语言规范
|
||
- [ ] 无linter错误
|
||
- [ ] 注释完整
|
||
- [ ] 错误处理完善
|
||
- [ ] 日志记录完整
|
||
|
||
### 10.2 功能完整性
|
||
|
||
- [ ] 获取我的展馆(懒加载)
|
||
- [ ] 获取他人展馆
|
||
- [ ] 放置资产(支持他人展馆)
|
||
- [ ] 下架资产
|
||
- [ ] 踢走占位
|
||
- [ ] 解锁展位(优先等级解锁)
|
||
- [ ] 规则硬编码在config中
|
||
|
||
### 10.3 测试覆盖
|
||
|
||
- [ ] Repository层单元测试
|
||
- [ ] Service层单元测试
|
||
- [ ] 集成测试
|
||
- [ ] 性能测试
|
||
|
||
### 10.4 文档
|
||
|
||
- [ ] 代码注释完整
|
||
- [ ] README文档
|
||
- [ ] 实现完成总结文档
|
||
|
||
---
|
||
|
||
## 十一、常见问题
|
||
|
||
### 11.1 懒加载并发冲突
|
||
|
||
**问题**:多个并发请求可能导致创建重复记录或报 DB Error。
|
||
|
||
**解决方案**:
|
||
- **方案 A (推荐)**:在 Repository 使用 `ON CONFLICT DO NOTHING`。
|
||
- **方案 B**:使用 Redis 分布式锁,Key 为 `gallery_init_{user_id}_{star_id}`。
|
||
|
||
### 11.2 分布式一致性 (资产展示唯一性)
|
||
|
||
**问题**:如何保证一个资产不会同时出现在两个展位?
|
||
|
||
**解决方案**:
|
||
- 在 `exhibitions` 表对 `asset_id` 建立唯一索引。这是最强力的物理保证。不再同步更新 Asset Service 的状态字段,实现完全解耦。
|
||
|
||
### 11.3 权限边界问题
|
||
|
||
**问题**:如何防止用户在他人私有展位放东西?
|
||
|
||
**解决方案**:
|
||
- 在 Service 层强制校验 `BoothSlot.Visibility`。
|
||
- 只有 `Owner` 可以往 `private` 展位放东西。
|
||
- `Public` 展位允许同明星下的所有粉丝“占领”。
|
||
|
||
---
|
||
|
||
## 十二、参考资源
|
||
|
||
### 12.1 代码参考
|
||
|
||
- `services/assetService/` - Asset Service实现
|
||
- `services/socialService/` - Social Service实现
|
||
- `services/userService/` - User Service实现
|
||
|
||
### 12.2 文档参考
|
||
|
||
- `docs/展馆服务设计方案.md` - 详细设计方案
|
||
- `docs/HTTP REST API接口文档.md` - API接口定义
|
||
- `docs/微服务架构设计.md` - 架构设计
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2026-01-12
|
||
**维护者**: AI Assistant
|