From ad773ffc27418707004533c6810d18e04633c160 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Tue, 28 Apr 2026 16:53:17 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-04-27-inspiration-flow-design.md | 201 ++++++++++-------- 1 file changed, 113 insertions(+), 88 deletions(-) diff --git a/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md b/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md index d8ed87d..ddab4d2 100644 --- a/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md +++ b/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md @@ -1,6 +1,6 @@ -# 灵感瀑布(横向瀑布流藏品展示)设计文档 +# 灵感瀑布流(Inspiration Flow)设计文档 -> **创建日期:** 2026-04-27 +> **创建日期:** 2026-04-28 > **项目:** TopFans 横向瀑布流藏品展示 > **服务:** galleryService (Go Dubbo-go) > **状态:** 设计中 @@ -9,7 +9,9 @@ ## 一、设计目标 -横向瀑布流展示该 star_id 下所有用户展出的藏品,支持随机展示、分页、按类型过滤。 +横向瀑布流展示该 star_id 下所有用户展出的藏品,支持随机展示、无限滚动加载、按类型过滤。 + +**无限滚动实现方式:** 使用游标分页(Cursor-based Pagination),前端滚动到底部时携带 `cursor` 参数加载下一批数据。 --- @@ -17,20 +19,20 @@ **主表:** Exhibition(展品展示表) -**关联表:** Asset(资产表)- 用于按 material_type 过滤 +**关联表:** +- Asset(资产表)- 用于获取藏品名称、封面、点赞数 +- FanProfile(粉丝档案表)- 用于获取展出者昵称 **筛选条件:** - `occupier_star_id = ?` (当前用户 star_id) - `expire_at > now` (未过期) - `deleted_at IS NULL` (未删除) -**按 type 过滤:** JOIN Asset 表,WHERE assets.material_type = ? - --- ## 三、API 设计 -### 3.1 获取灵感瀑布(横向瀑布流)藏品列表 +### 3.1 获取灵感瀑布藏品列表 ``` GET /api/v1/inspiration-flow @@ -40,8 +42,8 @@ GET /api/v1/inspiration-flow | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| -| page | int | 否 | 1 | 页码 | -| page_size | int | 否 | 20 | 每页数量(最大100) | +| cursor | string | 否 | 空 | 游标(首次请求为空,加载更多时传上次返回的 cursor) | +| limit | int | 否 | 20 | 每页数量(最大 50) | | type | string | 否 | all | 过滤类型:badge/poster/original/all | **HTTP 响应:** @@ -60,14 +62,20 @@ GET /api/v1/inspiration-flow "owner_nickname": "粉丝昵称" } ], - "page": 1, - "page_size": 20, - "total": 100, + "cursor": "eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9", "has_more": true } } ``` +**响应字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| items | array | 藏品列表 | +| cursor | string | 下次请求的游标,base64 编码的 JSON | +| has_more | bool | 是否还有更多数据 | + **错误码:** | code | 说明 | @@ -85,9 +93,9 @@ GET /api/v1/inspiration-flow ```protobuf // 获取灵感瀑布藏品列表请求 message GetInspirationFlowRequest { - int32 page = 1; // 页码(默认1) - int32 page_size = 2; // 每页数量(默认20,最大100) - string type = 3; // 过滤类型:badge/poster/original/all(默认all) + string cursor = 1; // 游标(首次请求为空) + int32 limit = 2; // 每页数量(默认20,最大50) + string type = 3; // 过滤类型:badge/poster/original/all(默认all) } // 获取灵感瀑布藏品列表响应 @@ -99,16 +107,14 @@ message GetInspirationFlowResponse { // 灵感瀑布数据 message InspirationFlowData { repeated InspirationFlowItem items = 1; // 藏品列表 - int32 page = 2; // 当前页码 - int32 page_size = 3; // 每页数量 - int64 total = 4; // 总数量 - bool has_more = 5; // 是否有更多 + string cursor = 2; // 下次请求的游标 + bool has_more = 3; // 是否有更多 } // 灵感瀑布藏品项 message InspirationFlowItem { int64 asset_id = 1; // 资产ID - string name = 2; // 藏品名称 + string name = 2; // 藏品名称 string cover_url = 3; // 封面图URL int32 like_count = 4; // 点赞数 string owner_nickname = 5; // 展出者昵称 @@ -137,7 +143,22 @@ service GalleryService { ## 五、核心逻辑 -### 5.1 查询逻辑 +### 5.1 游标分页设计 + +**游标结构(JSON,base64 编码):** +```json +{ + "offset": 20, + "limit": 20 +} +``` + +**为什么用游标分页而非 offset 分页:** +1. **性能稳定**:offset 翻页越深性能越差,游标分页性能恒定 +2. **无限滚动友好**:用户滚动过程中数据可能变化,游标避免重复/遗漏 +3. **适合随机排序**:配合 RANDOM() 避免翻页时数据错位 + +### 5.2 查询逻辑 ```sql SELECT @@ -156,35 +177,30 @@ WHERE e.occupier_star_id = ? AND a.is_active = true AND (? = 'all' OR a.material_type = ?) ORDER BY RANDOM() -LIMIT ? OFFSET ?; +LIMIT ?; ``` **参数说明:** - `? = star_id` (当前用户 star_id) - `? = now` (当前时间戳) - `? = type` (过滤类型) -- `? = page_size` -- `? = (page - 1) * page_size` +- `? = limit` (每页数量) -### 5.2 6小时自动下架 +### 5.3 游标编解码 -已实现的 `GetExpiredExhibitions` 方法查询已过期的展览记录,调度器定期清理。 +**编码(服务端):** +```go +cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":%d,"limit":%d}`, offset, limit))) +``` -**下架时执行:** -1. 设置 `deleted_at = now` -2. 调用 `ClearAssetLikeRecords` RPC 重置点赞数为 0 -3. 更新 `asset_registry.display_status = 0` - -### 5.3 重新上架逻辑 - -当藏品重新展示时: -1. 创建新的 Exhibition 记录 -2. 更新 `asset_registry.display_status = 1` -3. 点赞数从 0 开始(因为之前下架时已清除点赞记录) - -### 5.4 随机展示 - -使用 `ORDER BY RANDOM()` 实现随机排序。 +**解码(服务端):** +```go +decoded, _ := base64.StdEncoding.DecodeString(cursor) +var cursorData map[string]int +json.Unmarshal(decoded, &cursorData) +offset := cursorData["offset"] +limit := cursorData["limit"] +``` --- @@ -219,94 +235,103 @@ type Asset struct { } ``` -**新增字段(需数据库变更):** -```sql -ALTER TABLE assets ADD COLUMN material_type VARCHAR(50) DEFAULT 'original'; -``` - -**material_type 枚举值:** -- `original` — 原创(默认值) -- `badge` — 吧唧 -- `poster` — 海报 - --- -## 七、收益记录 - -**预留:** 收益记录通过经济系统已有的 `exhibition_revenue` change_type 实现。 - -后续按需在 Exhibition 创建/删除时写入 `crystal_transaction_records`,change_type 为 `exhibition_revenue`。 - ---- - -## 八、配置项 +## 七、配置项 | 配置项 | 说明 | 默认值 | |--------|------|--------| -| exhibition_duration | 展览时长(秒) | 14400(4小时,可配置) | -| inspiration_flow_page_size | 默认每页数量 | 20 | -| inspiration_flow_max_page_size | 最大每页数量 | 100 | +| inspiration_flow_limit | 默认每页数量 | 20 | +| inspiration_flow_max_limit | 最大每页数量 | 50 | --- -## 九、项目文件结构 +## 八、项目文件结构 ``` backend/ ├── proto/ │ └── gallery.proto # 修改:新增 GetInspirationFlow 方法 -│ + ├── pkg/proto/ │ ├── gallery/ -│ │ ├── gallery.pb.go # 重新生成 +│ │ ├── gallery.pb.go # 重新生成 │ │ └── gallery.triple.go # 重新生成 -│ + ├── services/galleryService/ │ ├── repository/ -│ │ └── gallery_repository.go # 修改:新增 GetActiveExhibitions 方法 +│ │ └── gallery_repository.go # 修改:新增 GetInspirationFlow 方法 │ │ │ ├── service/ -│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法 +│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法 │ │ │ ├── provider/ -│ │ └── gallery_provider.go # 修改:新增 GetInspirationFlow Handler +│ │ └── gallery_provider.go # 修改:新增 GetInspirationFlow Handler │ │ │ └── config/ -│ └── gallery_config.go # 修改:新增灵感瀑布配置项 -│ -├── scripts/ -│ └── migrate_add_material_type.sql # 新增:material_type 字段迁移脚本 -│ +│ └── gallery_config.go # 修改:新增灵感瀑布配置项 + └── gateway/ ├── controller/ - │ └── gallery_controller.go # 修改:新增 GetInspirationFlow 路由处理 + │ └── gallery_controller.go # 修改:新增 GetInspirationFlow 路由处理 + │ + ├── dto/ + │ ├── gallery_dto.go # 修改:新增 InspirationFlow DTO + │ └── gallery_converter.go # 修改:新增转换函数 │ └── router/ - └── router.go # 修改:新增 /api/v1/inspiration-flow 路由 + └── router.go # 修改:新增 /api/v1/inspiration-flow 路由 ``` --- -## 十、待确认事项 +## 九、数据库变更(必须执行) -1. **material_type 枚举值**:badge/poster/original,后续按需扩展 -2. **随机排序策略**:ORDER BY RANDOM() 在数据量大时可能有性能问题,后续可考虑按 like_count 随机采样 -3. **展览时长**:默认 6 小时(21600 秒),可通过配置修改 +**注意:** 当前 `assets` 表**没有** `material_type` 字段,此变更**必须执行**后才能支持按类型过滤。 -## 十一、数据库变更 - -### 11.1 新增字段 +### 9.1 DDL ```sql -- assets 表新增 material_type 字段 -ALTER TABLE assets ADD COLUMN material_type VARCHAR(50) DEFAULT 'original'; +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); ``` -### 11.2 迁移脚本 +### 9.2 迁移脚本 ```sql --- 迁移脚本:backend/scripts/migrate_add_material_type.sql -``` \ No newline at end of file +-- 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 缓存