# 灵感瀑布流(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 游标分页设计 **游标结构(JSON,base64 编码):** ```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 缓存