topfans/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md
2026-04-28 16:53:17 +08:00

338 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 灵感瀑布流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 响应:**
```json
{
"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
```protobuf
// 获取灵感瀑布藏品列表请求
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 中新增方法:
```protobuf
// 展馆服务
service GalleryService {
// ... 现有方法 ...
// 获取灵感瀑布藏品列表
rpc GetInspirationFlow(GetInspirationFlowRequest) returns (GetInspirationFlowResponse) {
option (google.api.http) = {
get: "/api/v1/inspiration-flow"
};
}
}
```
---
## 五、核心逻辑
### 5.1 游标分页设计
**游标结构JSONbase64 编码):**
```json
{
"offset": 20,
"limit": 20
}
```
**为什么用游标分页而非 offset 分页:**
1. **性能稳定**offset 翻页越深性能越差,游标分页性能恒定
2. **无限滚动友好**:用户滚动过程中数据可能变化,游标避免重复/遗漏
3. **适合随机排序**:配合 RANDOM() 避免翻页时数据错位
### 5.2 查询逻辑
```sql
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 游标编解码
**编码(服务端):**
```go
cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":%d,"limit":%d}`, offset, limit)))
```
**解码(服务端):**
```go
decoded, _ := base64.StdEncoding.DecodeString(cursor)
var cursorData map[string]int
json.Unmarshal(decoded, &cursorData)
offset := cursorData["offset"]
limit := cursorData["limit"]
```
---
## 六、数据模型
### 6.1 Exhibition 表(已有字段)
```go
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 表关联字段
```go
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
```sql
-- 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 迁移脚本
```sql
-- 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 前端逻辑
1. 首次请求 cursor 为空
2. 解析响应中的 cursor 和 has_more
3. 滚动到底部时,若 has_more=true携带 cursor 发起下一页请求
4. 数据变化时重置 cursor 重新加载
---
## 十一、待确认事项
1. **material_type 枚举值**badge/poster/original后续按需扩展
2. **随机排序策略**ORDER BY RANDOM() 在数据量大时可能有性能问题,后续可考虑按 like_count 随机采样
3. **每页数量上限**:当前设为 50是否合适
4. **是否需要缓存**:热门 star_id 的数据可以考虑 Redis 缓存