topfans/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md
zheng020 8284999b3a docs: change TypeScript to JavaScript in frontend types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 14:32:56 +08:00

28 KiB
Raw Permalink Blame History

典藏/活动藏品体系 + 星册重构设计

日期2026-04-13 状态:已评审通过 负责人zheng020


零、重构后页面效果

星册主页StarbookContent.vue

┌─────────────────────────────────────────────┐
│  ✕ 返回                          星册      │  ← Header
├─────────────────────────────────────────────┤
│  背景图starbook.jpg满屏              │
│                                             │
│  ┌─────────────────────────────────────┐   │
│  │                                     │   │
│  │  [ 普通 ] [ 典藏 ] [ 活动 ]       │   │  ← 类型Tab横向滚动
│  │                                     │   │
│  ├─────────────────────────────────────┤   │
│  │                                     │   │
│  │  普通 · 等级五                     │   │  ← 普通grade 大在上
│  │  ┌────────┐ ┌────────┐ ┌────────┐│   │
│  │  │  封面  │ │  封面  │ │  封面  ││   │
│  │  └────────┘ └────────┘ └────────┘│   │
│  │   铸爱-A    铸爱-B    铸爱-C ★820 │   │
│  │                                     │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐│   │
│  │  │  封面  │ │  封面  │ │  更多> ││   │
│  │  └────────┘ └────────┘ └────────┘│   │
│  │   铸爱-D    铸爱-E   [点击更多]    │   │  ← 超出6个显示"更多>"
│  │                                     │   │
│  │  ─────────────────────────────────  │   │
│  │                                     │   │
│  │  普通 · 等级四                     │   │
│  │  ┌────────┐ ┌────────┐            │   │
│  │  │  封面  │ │  封面  │            │   │
│  │  └────────┘ └────────┘            │   │
│  │   铸爱-F    铸爱-G                │   │
│  │                                     │   │
│  │  ...(等级三→等级一)              │   │
│  │                                     │   │
│  └─────────────────────────────────────┘   │
│                                             │
├─────────────────────────────────────────────┤
│  [广场] [星册] [铸爱] [星际] [好友]         │  ← BottomNav
└─────────────────────────────────────────────┘

切换到"典藏"时:

│  [ 普通 ] [ 典藏 ] [ 活动 ]               │  ← 典藏Tab高亮
├─────────────────────────────────────────────┤
│  典藏 · 限定典藏                          │  ← 按category分组无grade
│  ┌────────┐ ┌────────┐ ┌────────┐        │
│  │  封面  │ │  封面  │ │  更多> │        │
│  └────────┘ └────────┘ └────────┘        │
│   典藏-A    典藏-B   [点击更多]           │
│                                             │
│  典藏 · 经典典藏                          │
│  ┌────────┐ ┌────────┐                   │
│  │  封面  │ │  封面  │                   │
│  └────────┘ └────────┘                   │
│   典藏-C    典藏-D                        │
│                                             │
│  ...                                       │

切换到"活动"时:

│  [ 普通 ] [ 典藏 ] [ 活动 ]               │  ← 活动Tab高亮
├─────────────────────────────────────────────┤
│  活动 · 生日会                            │  ← 按activity_type分组
│  ┌────────┐ ┌────────┐                    │
│  │  封面  │ │  封面  │                    │
│  └────────┘ └────────┘                    │
│   活动-A    活动-B                         │
│                                             │
│  活动 · 演唱会                            │
│  ┌────────┐ ┌────────┐                    │
│  │  ...   │ │  ...   │                    │
│  └────────┘ └────────┘                    │

页面说明:

  • 无槽位展示区:新设计完全移除槽位概念,改为纯分组浏览
  • 类型Tab[普通] [典藏] [活动] 三选一,点击切换
  • 默认选中"普通":进入页面默认展示普通藏品
  • 普通:按 grade 从等级五顶部到等级一底部排列grade 大的在上
  • 典藏:按 category 子分类分组,无 grade
  • 活动:按 activity_type 分组,无 grade
  • 每组最多显示6个NftCard超出6个显示"更多>"卡片("更多>"不计入6个之内
  • "更多>"卡片:点击跳转 /pages/starbook/items?type=regular&category=castlove&grade=5

查看更多页面(/pages/starbook/items

┌─────────────────────────────────────────────┐
│  ← 返回        普通 · 等级五               │  ← Header
├─────────────────────────────────────────────┤
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐│
│  │  封面图  │  │  封面图  │  │  封面图  ││
│  └──────────┘  └──────────┘  └──────────┘│
│   铸爱藏品-A     铸爱藏品-B     铸爱藏品-C │
│   ★820         ★450         ★120         │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐│
│  │  封面图  │  │  封面图  │  │  封面图  ││
│  └──────────┘  └──────────┘  └──────────┘│
│   铸爱藏品-D     铸爱藏品-E     铸爱藏品-F │
│   ★98          ★45          ★30         │
│                                             │
│  ─────── 第 1/3 页 ───────                 │
│                                             │
├─────────────────────────────────────────────┤
│  [广场] [星册] [铸爱] [星际] [好友]         │
└─────────────────────────────────────────────┘

页面说明:

  • 路由参数?type=regular&category=castlove&grade=5&page=1
  • 布局3列网格每行3个 NftCard
  • 分页每次加载20条触底自动加载下一页

一、背景与目标

1.1 现状问题

  • StarbookContent.vue 混用通用 getMyAssetsApi,无法区分藏品类型(普通/典藏/活动)
  • assets 表只有一套,没有类型区分字段
  • 典藏藏品有子分类category需求但目前表结构不支持
  • 普通藏品有等级grade需求但目前表结构不支持
  • 活动藏品需要独立的生命周期管理
  • 前端封面 URL 逐个解析(Promise.all + 多次 OSS 预签名调用),性能差
  • 前端多重生命周期触发(onMounted + onActivated + onWatch + onShow),存在重复请求

1.2 重构目标

  1. 建立普通藏品、典藏藏品、活动藏品三套并行的数据表结构
  2. 普通藏品按 grade1/2/3分组展示典藏按 category 子分类分组,活动按 activity_type 分组
  3. 通过 asset_registry 统一索引表实现跨类型统一查询
  4. 后端批量返回预签名封面 URL前端零额外 OSS 请求
  5. 星册页面按类型 + 子分类/等级分组展示,每组最多展示 6 条
  6. "查看更多" 跳转独立页面,支持分页
  7. 普通藏品(铸爱流程)、典藏藏品、活动藏品均可放入星册

二、数据模型

2.1 现有表(不变动)

表名 说明
assets 普通藏品(铸爱流程),结构不变

说明: cover_url_signed预签名URL不在数据库中存储由 Service 层在响应时动态生成调用OSS生成预签名URL避免URL过期问题。

2.2 新建表

2.2.1 典藏藏品表 collection_assets

CREATE TABLE collection_assets (
    id                  BIGSERIAL PRIMARY KEY,
    asset_id            BIGINT UNIQUE NOT NULL,           -- 关联 assets 表主键
    owner_uid           BIGINT NOT NULL,
    star_id             BIGINT NOT NULL,
    name                VARCHAR(100) NOT NULL,
    cover_url           VARCHAR(500) NOT NULL,
    category            VARCHAR(50) NOT NULL,              -- 典藏子分类(动态值,示例:'limited', 'classic'
    like_count          INT NOT NULL DEFAULT 0,
    status              SMALLINT NOT NULL DEFAULT 0,       -- 0=Pending, 1=Active
    metadata            JSONB,                             -- 预留扩展字段
    created_at          BIGINT NOT NULL,
    updated_at          BIGINT NOT NULL,

    CONSTRAINT uk_owner_star_name UNIQUE (owner_uid, star_id, name)
);

CREATE INDEX idx_collection_owner_star  ON collection_assets (owner_uid, star_id);
CREATE INDEX idx_collection_category   ON collection_assets (category);
CREATE INDEX idx_collection_asset_id    ON collection_assets (asset_id);

2.2.2 活动藏品表 activity_assets

CREATE TABLE activity_assets (
    id                  BIGSERIAL PRIMARY KEY,
    asset_id            BIGINT UNIQUE NOT NULL,
    owner_uid           BIGINT NOT NULL,
    star_id             BIGINT NOT NULL,
    activity_id         BIGINT NOT NULL,                   -- 所属活动 ID
    activity_type       VARCHAR(50) NOT NULL,             -- 活动类型(动态值,示例:'birthday', 'anniversary', 'concert'
    name                VARCHAR(100) NOT NULL,
    cover_url           VARCHAR(500) NOT NULL,
    like_count          INT NOT NULL DEFAULT 0,
    status              SMALLINT NOT NULL DEFAULT 0,
    metadata            JSONB,
    created_at          BIGINT NOT NULL,
    updated_at          BIGINT NOT NULL,

    CONSTRAINT uk_owner_activity_name UNIQUE (owner_uid, activity_id, name)
);

CREATE INDEX idx_activity_owner        ON activity_assets (owner_uid, activity_id);
CREATE INDEX idx_activity_star        ON activity_assets (star_id, activity_id);
CREATE INDEX idx_activity_type        ON activity_assets (activity_type);
CREATE INDEX idx_activity_asset_id    ON activity_assets (asset_id);

2.2.3 统一索引表 asset_registry

CREATE TABLE asset_registry (
    id                  BIGSERIAL PRIMARY KEY,
    asset_id            BIGINT NOT NULL,
    asset_type          VARCHAR(20) NOT NULL,             -- 'regular' | 'collection' | 'activity'
    owner_uid           BIGINT NOT NULL,
    star_id             BIGINT NOT NULL,
    -- 普通藏品专属字段(其他类型时为 NULL
    grade               SMALLINT,                          -- 普通藏品等级1 / 2 / 3仅 regular 时有效)
    -- 典藏专属字段(其他类型时为 NULL
    collection_category VARCHAR(50),                        -- 典藏子分类(仅 collection 时有效)
    -- 活动专属字段(其他类型时为 NULL
    activity_id         BIGINT,
    activity_type       VARCHAR(50),
    -- 公共字段
    status              SMALLINT NOT NULL DEFAULT 0,
    like_count          INT NOT NULL DEFAULT 0,
    created_at          BIGINT NOT NULL,
    updated_at          BIGINT NOT NULL,

    CONSTRAINT uk_asset_type_id          UNIQUE (asset_type, asset_id),
    CONSTRAINT uk_owner_star_type_asset  UNIQUE (owner_uid, star_id, asset_type, asset_id)
);

CREATE INDEX idx_registry_owner_star  ON asset_registry (owner_uid, star_id);
CREATE INDEX idx_registry_type_star  ON asset_registry (asset_type, star_id);
CREATE INDEX idx_registry_star_grade ON asset_registry (star_id, grade)
                                        WHERE asset_type = 'regular';
CREATE INDEX idx_registry_star_activity ON asset_registry (star_id, activity_id)
                                        WHERE asset_type = 'activity';

说明:

  • asset_type='regular'(普通藏品):grade 有效1/2/3collection_category 为 NULL
  • asset_type='collection'(典藏):collection_category 有效,grade 为 NULL
  • asset_type='activity'(活动藏品):activity_id / activity_type 有效,grade 为 NULL
  • 同一 asset_id 只能属于一种 type,不能同时注册为 regular 和 collection

2.3 设计说明

  1. 三种类型的区分方式:
    • regular(普通藏品):grade 有效1/2/3由点赞数计算得出预留升级接口
    • collection(典藏):collection_category 有效(动态字符串),无 grade
    • activity(活动藏品):activity_id / activity_type 有效,无 grade
  2. category / activity_type 为动态字符串,不预设枚举值,应用层负责校验和展示名称映射
  3. registry 表不自增 asset_id,由各类型表创建时写入,保证主从表一致性
  4. UNIQUE 约束owner_uid + star_id + asset_type + asset_id 保证用户每种类型下无重复藏品
  5. 同一 asset_id 只能属于一种 type,普通藏品、典藏、活动藏品互斥,不能重复注册
  6. grade 字段在数据库中为 NULL 表示非普通藏品,在 API 响应中统一转换为 0(由 Gateway 转换层处理)

三、藏品创建流程

3.1 普通藏品(铸爱流程)

用户上传素材 → 铸造订单创建 → assets 表写入
→ 同时写入 asset_registry (type='regular', grade=1)  -- grade 初始为1升级规则预留

说明:普通藏品(铸爱流程)纳入星册体系,创建时同步写入 registrygrade 初始为 1后续按点赞数升级规则待确认

3.2 典藏藏品

用户选择"典藏"类型 → 创建 assets 通用记录
→ 创建 collection_assets 专属记录category
→ 同时写入 asset_registry (type='collection')

3.3 活动藏品

用户参与活动并获得奖励 → 创建 assets 通用记录
→ 创建 activity_assets 专属记录activity_id, activity_type
→ 同时写入 asset_registry (type='activity')

3.4 写入一致性保障

建议在同一个事务内完成 assets 写入和 registry 写入,确保一致性。


四、普通藏品等级升级机制(预留接口)

grade 初始值为 1升级规则待定由团队讨论确认后实现。设计上预留扩展接口

// AssetService 或独立的 GradeService 预留方法
UpgradeGrade(assetID int64, fromGrade, toGrade int) error

// 升级条件判断后期实现,支持:
// - 点赞数阈值触发(如 grade1: ≥0, grade2: ≥100, grade3: ≥500
// - 全网排名触发
// - 手动升级(运营操作)
// 升级后同步更新 asset_registry.grade

典藏藏品collection无 grade 字段,按 category 子分类区分。


五、API 设计

5.1 核心原则

  • 后端批量返回预签名封面 URL,前端不单独调用 OSS 预签名接口
  • 单次请求返回首页全量数据,避免前端多次请求

5.2 接口列表

接口 方法 说明
/api/v1/starbook/home GET 星册首页,按 type → category/grade 分组,含普通/典藏/活动三种类型
/api/v1/starbook/items GET 某分组的藏品列表(分页)

5.3 详细接口定义

GET /api/v1/starbook/home

说明: 获取当前用户在当前 star 下的星册首页数据

响应示例:

{
  "code": 200,
  "data": {
    "groups": [
      {
        "type": "regular",
        "category": "castlove",
        "category_name": "普通",
        "grades": [
          {
            "grade": 5,
            "items": [
              { "asset_id": 201, "name": "铸爱藏品-A", "cover_url_signed": "https://...", "like_count": 820, "category": "castlove", "grade": 5 }
            ],
            "total_count": 3,
            "has_more": false
          },
          {
            "grade": 4,
            "items": [...],
            "total_count": 8,
            "has_more": true
          }
        ]
      },
      {
        "type": "collection",
        "category": "limited",
        "category_name": "限定典藏",
        "items": [
          { "asset_id": 101, "name": "限定典藏-1", "cover_url_signed": "https://...", "like_count": 820, "category": "limited" }
        ],
        "total_count": 5,
        "has_more": true
      },
      {
        "type": "activity",
        "category": "birthday",
        "category_name": "生日会",
        "items": [...],
        "total_count": 2,
        "has_more": false
      }
    ]
  }
}

说明:

  • star_id 从认证上下文JWT Token获取不在请求参数中显式传递
  • groups:三种类型的分组逻辑不同
    • type='regular'(普通藏品):按 grade 分组grade=1/2/3...category='castlove' 固定
    • type='collection'(典藏):按 category 子分类分组,无 grade
    • type='activity'(活动藏品):按 activity_type 分组,无 grade
  • 每组最多返回 6 条,has_more=true 表示还有更多(点击"查看更多"跳转分页页)
  • Gateway 转换层:数据库中 grade 为 NULL 时,转换为 0 返回(非 regular 类型)

GET /api/v1/starbook/items

Query 参数:

参数 类型 必填 说明
type string regular / collection / activity
category string 子分类不传则返回该类型全部regular 时传 castlove
grade int 等级(仅 regular 时有效1/2/3...
page int 默认 1
page_size int 默认 20

响应示例:

{
  "code": 200,
  "data": {
    "items": [
      {
        "asset_id": 201,
        "asset_type": "regular",
        "name": "铸爱藏品-A",
        "cover_url_signed": "https://oss-cn-shanghai.../signed",
        "like_count": 120,
        "category": "castlove",
        "grade": 5,
        "created_at": 1743004800000
      }
    ],
    "total": 15,
    "page": 1,
    "page_size": 20,
    "has_more": false
  }
}

说明:

  • type='regular'grade 取值 1/2/3...category='castlove' 固定
  • type='collection' 时无 grade 字段,按 category 筛选
  • type='activity' 时无 grade 字段,按 activity_type(即 category 参数)筛选

六、后端模块划分

6.1 Service 层

Service 职责
AssetService 现有流程改造:创建普通藏品时同步写 asset_registry (type='regular', grade=1);预留 grade 升级接口
CollectionService(新建) 管理 collection_assets,典藏专属逻辑(含 category 子分类)
ActivityAssetService(新建) 管理 activity_assets,活动藏品专属逻辑
StarbookService(新建) 星册业务编排,调用各 Service整合数据统一生成预签名 URL

6.2 StarbookService 核心逻辑

  1. asset_registry 查询当前用户 + 当前 star 下的所有藏品regular + collection + activity
  2. 按 type → 各自维度分组regular 按 gradecollection 按 categoryactivity 按 activity_type
  3. 批量调用 OSS 生成预签名 URL已有 generatePresignedURL 逻辑复用)
  4. 返回结构化数据

6.3 Repository 层

Repository
CollectionRepository collection_assets
ActivityAssetRepository activity_assets
AssetRegistryRepository asset_registry

七、Proto 文件变更

7.1 新建 starbook.proto

syntax = "proto3";

package topfans.starbook;

option go_package = "github.com/topfans/backend/pkg/proto/starbook;starbook";

import "proto/common.proto";
import "google/api/annotations.proto";

service StarbookService {
  // 星册首页
  rpc GetStarbookHome(GetStarbookHomeRequest) returns (GetStarbookHomeResponse) {
    option (google.api.http) = {
      get: "/api/v1/starbook/home"
    };
  }

  // 藏品列表(分页)
  rpc GetStarbookItems(GetStarbookItemsRequest) returns (GetStarbookItemsResponse) {
    option (google.api.http) = {
      get: "/api/v1/starbook/items"
    };
  }
}

message GetStarbookHomeRequest {}

message GetStarbookHomeResponse {
  topfans.common.BaseResponse base = 1;
  StarbookHomeData data = 2;
}

message StarbookHomeData {
  repeated AssetGroup groups = 1;
}

message AssetGroup {
  string type = 1;                // 'regular' / 'collection' / 'activity'
  string category = 2;            // 'castlove'(regular) / collection_category / activity_type
  string category_name = 3;
  // regular 使用 grades 分组collection/activity 使用 flat items 列表
  repeated GradeSection grades = 4;   // 仅 regular 时有效
  repeated AssetItem items = 5;        // collection / activity 时有效
  int32 total_count = 6;
  bool has_more = 7;
}

message GradeSection {
  int32 grade = 1;                  // 仅 regular 时使用
  repeated AssetItem items = 2;
  int32 total_count = 3;
  bool has_more = 4;
}

message AssetItem {
  int64 asset_id = 1;
  string name = 2;
  string cover_url_signed = 3;
  int32 like_count = 4;
  int64 created_at = 5;
  string category = 6;       // regular: 'castlove' / collection: category / activity: activity_type
  int32 grade = 7;          // 仅 regular 时有效1/2/3...),其他类型为 0
}

message GetStarbookItemsRequest {
  string type = 1;                // 'regular' / 'collection' / 'activity'
  string category = 2;            // regular 时固定传 'castlove'
  int32 grade = 3;               // 仅 regular 时有效1/2/3...
  int32 page = 4;
  int32 page_size = 5;
}

message GetStarbookItemsResponse {
  topfans.common.BaseResponse base = 1;
  AssetListData data = 2;
}

message AssetListData {
  repeated AssetItem items = 1;
  int64 total = 2;
  int32 page = 3;
  int32 page_size = 4;
  bool has_more = 5;
}

八、前端重构

8.1 生命周期优化

问题: 当前 onMounted + onActivated + onShow + watch(isActive) 四处触发 loadAssetsList()

解决:

// 只保留 onShow + watch isActive合并为单一加载逻辑
let lastLoadedAt = 0;

onShow(() => {
  if (props.isActive) {
    loadStarbookData();
  }
});

watch(() => props.isActive, (newVal) => {
  if (newVal) loadStarbookData();
});

// 限制频繁刷新:距离上次加载不足 1 秒则跳过
function loadStarbookData() {
  const now = Date.now();
  if (now - lastLoadedAt < 1000) return;
  lastLoadedAt = now;
  // ... 实际加载逻辑
}

8.2 数据流变更

变更前 变更后
调用 getMyAssetsApi(普通藏品) 调用 getStarbookHomeApi(统一,含 regular/collection/activity
手动解析封面 URLPromise.all 逐个调 OSS 后端直接返回 cover_url_signed
前端按 index 硬分组 后端按 type → category → grade 分组返回
普通藏品不展示在星册 普通藏品type='regular')纳入星册体系

8.3 页面路由

页面 路由 说明
星册主页 /pages/starbook/index 不变
查看更多 /pages/starbook/items?type=regular&category=castlove&grade=5 新页面,接分页数据

8.4 数据结构(前端 JavaScript

interface StarbookHomeData {
  groups: AssetGroup[]
}

interface AssetGroup {
  type: 'regular' | 'collection' | 'activity'
  category: string       // regular: 'castlove' / collection: 子分类 / activity: activity_type
  category_name: string
  // regular 按 grade 分组collection/activity 直接用 items 列表
  grades?: GradeSection[]   // 仅 regular 时有效
  items?: AssetItem[]       // collection / activity 时有效
  total_count: number
  has_more: boolean
}

interface GradeSection {
  grade: number  // regular: 1/2/3...
  items: AssetItem[]
  total_count: number
  has_more: boolean
}

interface AssetItem {
  asset_id: number
  name: string
  cover_url_signed: string
  like_count: number
  created_at: number
  category: string  // regular: 'castlove' / collection: 子分类 / activity: activity_type
  grade: number    // regular: 1/2/3...,其他类型为 0
}

// grade 中文转换函数(前端使用)
function toChineseGrade(grade) {
  const map = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
  return `等级${map[grade] || grade}`;
}

说明:

  • type='regular'grades[] 分组(按 gradegrade 取值 1/2/3...category='castlove' 固定
  • type='collection'items[] 列表(按 category 分组),grade=0(数据库存 NULLAPI 转换层统一返回 0category 为典藏子分类
  • type='activity'items[] 列表(按 activity_type 分组),grade=0(数据库存 NULLAPI 转换层统一返回 0category 为活动类型

九、性能保障

  1. 后端批量预签名:单次请求一次性生成所有封面 URL 的预签名,前端零额外请求
  2. Registry 单表索引三条索引覆盖所有查询路径owner+star, type+star, star+grade[regular]
  3. 首页按组截断:每 grade 组最多返回 6 条,has_more 标识是否还有更多
  4. 前端防抖1 秒内重复触发只执行一次加载
  5. 分类动态扩展category / activity_type 不写死枚举,应用层通过配置或枚举接口获取展示名

十、待确认事项

事项 状态 说明
典藏子分类category枚举值 待确认 动态字符串,设计已支持
活动类型activity_type枚举值 待确认 动态字符串,设计已支持
普通藏品 grade 升级规则 待确认 设计已预留 UpgradeGrade 接口
普通藏品 category 固定值 待确认 建议 category='castlove'

十一、影响范围

范围 变更内容
数据库 新建 3 张表collection_assets, activity_assets, asset_registry
Proto 新建 starbook.proto
Gateway 新建 StarbookController新增 2 个路由
Service 层 新建 CollectionService, ActivityAssetService, StarbookService
Repository 层 新建 CollectionRepository, ActivityAssetRepository, AssetRegistryRepository
前端 StarbookContent.vue 重构,新增 /pages/starbook/items.vue
铸爱流程 受影响,普通藏品创建时需同步写入 asset_registrytype='regular', grade=1