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

8.6 KiB
Raw Blame History

灵感瀑布流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 游标分页设计

游标结构JSONbase64 编码):

{
  "offset": 20,
  "limit": 20
}

为什么用游标分页而非 offset 分页:

  1. 性能稳定offset 翻页越深性能越差,游标分页性能恒定
  2. 无限滚动友好:用户滚动过程中数据可能变化,游标避免重复/遗漏
  3. 适合随机排序:配合 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 前端逻辑

  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 缓存