8.6 KiB
8.6 KiB
灵感瀑布流(Inspiration Flow)设计文档
创建日期: 2026-04-28 项目: TopFans 横向瀑布流藏品展示 服务: galleryService (Go Dubbo-go) 状态: 设计中
一、设计目标
横向瀑布流展示该 star_id 下所有用户展出的藏品,支持随机展示、无限滚动加载、按类型过滤。
无限滚动实现方式: 使用游标分页(Cursor-based Pagination),前端滚动到底部时携带 cursor 参数加载下一批数据。
二、数据来源
主表: Exhibition(展品展示表)
关联表:
- Asset(资产表)- 用于获取藏品名称、封面、点赞数
- FanProfile(粉丝档案表)- 用于获取展出者昵称
筛选条件:
occupier_star_id = ?(当前用户 star_id)expire_at > now(未过期)deleted_at IS NULL(未删除)
三、API 设计
3.1 获取灵感瀑布藏品列表
GET /api/v1/inspiration-flow
Query 参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| cursor | string | 否 | 空 | 游标(首次请求为空,加载更多时传上次返回的 cursor) |
| limit | int | 否 | 20 | 每页数量(最大 50) |
| type | string | 否 | all | 过滤类型:badge/poster/original/all |
HTTP 响应:
{
"code": 200,
"message": "ok",
"data": {
"items": [
{
"asset_id": 123,
"name": "藏品名称",
"cover_url": "https://xxx.com/cover.png",
"like_count": 100,
"owner_nickname": "粉丝昵称"
}
],
"cursor": "eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9",
"has_more": true
}
}
响应字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| items | array | 藏品列表 |
| cursor | string | 下次请求的游标,base64 编码的 JSON |
| has_more | bool | 是否还有更多数据 |
错误码:
| code | 说明 |
|---|---|
| 200 | 成功 |
| 401 | 用户认证失败 |
| 500 | 服务器内部错误 |
四、Proto 定义
4.1 Request / Response
// 获取灵感瀑布藏品列表请求
message GetInspirationFlowRequest {
string cursor = 1; // 游标(首次请求为空)
int32 limit = 2; // 每页数量(默认20,最大50)
string type = 3; // 过滤类型:badge/poster/original/all(默认all)
}
// 获取灵感瀑布藏品列表响应
message GetInspirationFlowResponse {
topfans.common.BaseResponse base = 1;
InspirationFlowData data = 2;
}
// 灵感瀑布数据
message InspirationFlowData {
repeated InspirationFlowItem items = 1; // 藏品列表
string cursor = 2; // 下次请求的游标
bool has_more = 3; // 是否有更多
}
// 灵感瀑布藏品项
message InspirationFlowItem {
int64 asset_id = 1; // 资产ID
string name = 2; // 藏品名称
string cover_url = 3; // 封面图URL
int32 like_count = 4; // 点赞数
string owner_nickname = 5; // 展出者昵称
}
4.2 Service 方法
在 GalleryService 中新增方法:
// 展馆服务
service GalleryService {
// ... 现有方法 ...
// 获取灵感瀑布藏品列表
rpc GetInspirationFlow(GetInspirationFlowRequest) returns (GetInspirationFlowResponse) {
option (google.api.http) = {
get: "/api/v1/inspiration-flow"
};
}
}
五、核心逻辑
5.1 游标分页设计
游标结构(JSON,base64 编码):
{
"offset": 20,
"limit": 20
}
为什么用游标分页而非 offset 分页:
- 性能稳定:offset 翻页越深性能越差,游标分页性能恒定
- 无限滚动友好:用户滚动过程中数据可能变化,游标避免重复/遗漏
- 适合随机排序:配合 RANDOM() 避免翻页时数据错位
5.2 查询逻辑
SELECT
e.asset_id,
a.name,
a.cover_url,
a.like_count,
fp.nickname as owner_nickname
FROM exhibitions e
JOIN assets a ON e.asset_id = a.id
JOIN fan_profiles fp ON e.occupier_uid = fp.user_id AND e.occupier_star_id = fp.star_id
WHERE e.occupier_star_id = ?
AND e.expire_at > ?
AND e.deleted_at IS NULL
AND a.status = 1
AND a.is_active = true
AND (? = 'all' OR a.material_type = ?)
ORDER BY RANDOM()
LIMIT ?;
参数说明:
? = star_id(当前用户 star_id)? = now(当前时间戳)? = type(过滤类型)? = limit(每页数量)
5.3 游标编解码
编码(服务端):
cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":%d,"limit":%d}`, offset, limit)))
解码(服务端):
decoded, _ := base64.StdEncoding.DecodeString(cursor)
var cursorData map[string]int
json.Unmarshal(decoded, &cursorData)
offset := cursorData["offset"]
limit := cursorData["limit"]
六、数据模型
6.1 Exhibition 表(已有字段)
type Exhibition struct {
ID int64 `gorm:"primaryKey"`
AssetID int64 `gorm:"not null"`
SlotID int64 `gorm:"not null"`
HostProfileID int64 `gorm:"not null"`
OccupierUID int64 `gorm:"not null"`
OccupierStarID int64 `gorm:"not null;index"`
StartTime int64 `gorm:"not null"`
ExpireAt int64 `gorm:"not null;index"`
CreatedAt int64 `gorm:"not null"`
UpdatedAt int64 `gorm:"not null"`
DeletedAt *int64 `gorm:"index"`
}
6.2 Asset 表关联字段
type Asset struct {
// ... 现有字段 ...
MaterialType string `gorm:"column:material_type"` // 素材类型:badge/poster/original
IsOriginal bool `gorm:"column:is_original"`
LikeCount int32 `gorm:"not null;default:0"`
}
七、配置项
| 配置项 | 说明 | 默认值 |
|---|---|---|
| inspiration_flow_limit | 默认每页数量 | 20 |
| inspiration_flow_max_limit | 最大每页数量 | 50 |
八、项目文件结构
backend/
├── proto/
│ └── gallery.proto # 修改:新增 GetInspirationFlow 方法
├── pkg/proto/
│ ├── gallery/
│ │ ├── gallery.pb.go # 重新生成
│ │ └── gallery.triple.go # 重新生成
├── services/galleryService/
│ ├── repository/
│ │ └── gallery_repository.go # 修改:新增 GetInspirationFlow 方法
│ │
│ ├── service/
│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法
│ │
│ ├── provider/
│ │ └── gallery_provider.go # 修改:新增 GetInspirationFlow Handler
│ │
│ └── config/
│ └── gallery_config.go # 修改:新增灵感瀑布配置项
└── gateway/
├── controller/
│ └── gallery_controller.go # 修改:新增 GetInspirationFlow 路由处理
│
├── dto/
│ ├── gallery_dto.go # 修改:新增 InspirationFlow DTO
│ └── gallery_converter.go # 修改:新增转换函数
│
└── router/
└── router.go # 修改:新增 /api/v1/inspiration-flow 路由
九、数据库变更(必须执行)
注意: 当前 assets 表没有 material_type 字段,此变更必须执行后才能支持按类型过滤。
9.1 DDL
-- assets 表新增 material_type 字段
ALTER TABLE assets ADD COLUMN IF NOT EXISTS material_type VARCHAR(50) DEFAULT 'original';
-- 创建索引优化查询
CREATE INDEX IF NOT EXISTS idx_assets_material_type ON assets(material_type);
9.2 迁移脚本
-- backend/scripts/migrate_add_material_type.sql
ALTER TABLE assets ADD COLUMN IF NOT EXISTS material_type VARCHAR(50) DEFAULT 'original';
CREATE INDEX IF NOT EXISTS idx_assets_material_type ON assets(material_type);
十、无限滚动前端对接说明
10.1 首次请求
GET /api/v1/inspiration-flow?limit=20&type=all
10.2 加载更多
GET /api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9&limit=20&type=all
10.3 前端逻辑
- 首次请求 cursor 为空
- 解析响应中的 cursor 和 has_more
- 滚动到底部时,若 has_more=true,携带 cursor 发起下一页请求
- 数据变化时重置 cursor 重新加载
十一、待确认事项
- material_type 枚举值:badge/poster/original,后续按需扩展
- 随机排序策略:ORDER BY RANDOM() 在数据量大时可能有性能问题,后续可考虑按 like_count 随机采样
- 每页数量上限:当前设为 50,是否合适?
- 是否需要缓存:热门 star_id 的数据可以考虑 Redis 缓存