diff --git a/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md b/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md index 6bfdcc1..f9ed793 100644 --- a/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md +++ b/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md @@ -1,11 +1,137 @@ # 典藏/活动藏品体系 + 星册重构设计 > 日期: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 现状问题 @@ -38,6 +164,8 @@ |------|------| | `assets` | 普通藏品(铸爱流程),结构不变 | +**说明:** `cover_url_signed`(预签名URL)不在数据库中存储,由 Service 层在响应时动态生成(调用OSS生成预签名URL),避免URL过期问题。 + ### 2.2 新建表 #### 2.2.1 典藏藏品表 `collection_assets` @@ -144,38 +272,6 @@ CREATE INDEX idx_registry_star_activity ON asset_registry (star_id, activity_id) 5. **同一 `asset_id` 只能属于一种 type**,普通藏品、典藏、活动藏品互斥,不能重复注册 6. **`grade` 字段在数据库中为 NULL 表示非普通藏品**,在 API 响应中统一转换为 `0`(由 Gateway 转换层处理) -### 2.4 星册槽位表 `starbook_slots` - -新建独立表,与展馆(GalleryService)的 `booth_slots` 完全分离: - -```sql -CREATE TABLE starbook_slots ( - id BIGSERIAL PRIMARY KEY, - owner_uid BIGINT NOT NULL, - star_id BIGINT NOT NULL, - slot_index SMALLINT NOT NULL, -- 槽位序号(从 1 开始) - asset_id BIGINT, -- NULL 表示空槽位 - asset_type VARCHAR(20), -- 'regular' | 'collection' | 'activity',与 registry 对应 - placed_at BIGINT NOT NULL, -- 放置时间(毫秒时间戳) - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, - - CONSTRAINT uk_owner_star_slot UNIQUE (owner_uid, star_id, slot_index), - CONSTRAINT fk_starbook_asset FOREIGN KEY (asset_id, asset_type) - REFERENCES asset_registry(asset_id, asset_type) ON DELETE SET NULL -); - -CREATE INDEX idx_starbook_owner_star ON starbook_slots (owner_uid, star_id); -CREATE INDEX idx_starbook_asset ON starbook_slots (asset_id, asset_type); -``` - -**说明:** -- `slot_index` 从 1 开始,上限由 `fan_profiles.starbook_limit` 控制(动态,非写死) -- `total_slots` 从 `fan_profiles.starbook_limit` 动态读取,而非写死 15 -- 槽位数量支持动态扩展(运营可调整 starbook_limit) -- 外键约束 `asset_id + asset_type` 关联 `asset_registry`,确保只有注册过的藏品才能放入星册 -- **与 booth_slots 完全独立**,星册槽位只用于星册页面展示 - --- ## 三、藏品创建流程 @@ -241,16 +337,14 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error | 接口 | 方法 | 说明 | |------|------|------| -| `/api/v1/starbook/home` | GET | 星册首页,按 type → category → grade 分组,含普通/典藏/活动三种类型 | +| `/api/v1/starbook/home` | GET | 星册首页,按 type → category/grade 分组,含普通/典藏/活动三种类型 | | `/api/v1/starbook/items` | GET | 某分组的藏品列表(分页) | -| `/api/v1/starbook/place` | POST | 放置藏品到星册槽位 | -| `/api/v1/starbook/slots/{slot_index}` | DELETE | 从槽位移除 | ### 5.3 详细接口定义 #### GET /api/v1/starbook/home -**说明:** 获取当前用户在当前 star 下的星册首页数据。`total_slots` 从 `fan_profiles.starbook_limit` 动态读取。 +**说明:** 获取当前用户在当前 star 下的星册首页数据 **响应示例:** @@ -258,45 +352,23 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error { "code": 200, "data": { - "total_slots": 3, - "slots": [ - { - "slot_index": 1, - "asset": null, - "locked": false - }, - { - "slot_index": 2, - "asset": { - "asset_id": 101, - "asset_type": "collection", - "name": "限定典藏-1", - "cover_url_signed": "https://oss-cn-shanghai.../signed", - "like_count": 820, - "category": "limited" - }, - "locked": false - } - ], "groups": [ { "type": "regular", "category": "castlove", - "category_name": "典藏", + "category_name": "普通", "grades": [ { - "grade": 3, + "grade": 5, "items": [ - { "asset_id": 201, "name": "铸爱藏品-A", "cover_url_signed": "https://...", "like_count": 820, "category": "castlove", "grade": 3 } + { "asset_id": 201, "name": "铸爱藏品-A", "cover_url_signed": "https://...", "like_count": 820, "category": "castlove", "grade": 5 } ], "total_count": 3, "has_more": false }, { - "grade": 2, - "items": [ - { "asset_id": 202, "name": "铸爱藏品-B", "cover_url_signed": "https://...", "like_count": 450, "category": "castlove", "grade": 2 } - ], + "grade": 4, + "items": [...], "total_count": 8, "has_more": true } @@ -312,14 +384,6 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error "total_count": 5, "has_more": true }, - { - "type": "collection", - "category": "classic", - "category_name": "经典典藏", - "items": [...], - "total_count": 3, - "has_more": false - }, { "type": "activity", "category": "birthday", @@ -334,22 +398,23 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error ``` **说明:** -- `total_slots`:从 `fan_profiles.starbook_limit` 动态读取,非写死 -- `slots`:星册槽位,当前展示的藏品直接返回预签名 URL,`placed_at` 为放置时间 +- `star_id` 从认证上下文(JWT Token)获取,不在请求参数中显式传递 - `groups`:三种类型的分组逻辑不同 - - `type='regular'`(普通藏品):按 `grade` 分组(grade=1/2/3),`category='castlove'` 固定 + - `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) | +| grade | int | 否 | 等级(仅 regular 时有效,1/2/3...) | | page | int | 否 | 默认 1 | | page_size | int | 否 | 默认 20 | @@ -367,7 +432,7 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error "cover_url_signed": "https://oss-cn-shanghai.../signed", "like_count": 120, "category": "castlove", - "grade": 2, + "grade": 5, "created_at": 1743004800000 } ], @@ -380,57 +445,10 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error ``` **说明:** -- `type='regular'` 时 `grade` 取值 1/2/3,`category='castlove'` 固定 +- `type='regular'` 时 `grade` 取值 1/2/3...,`category='castlove'` 固定 - `type='collection'` 时无 grade 字段,按 `category` 筛选 - `type='activity'` 时无 grade 字段,按 `activity_type`(即 category 参数)筛选 -#### POST /api/v1/starbook/place - -**请求:** - -```json -{ - "asset_id": 101, - "asset_type": "collection", - "slot_index": 1 -} -``` - -**说明:** 增加 `asset_type` 字段,用于后端定位 registry 中的具体记录。 - -**响应:** - -```json -{ - "code": 200, - "data": { - "slot_index": 1, - "asset": { - "asset_id": 101, - "asset_type": "collection", - "name": "限定典藏-1", - "cover_url_signed": "https://oss-cn-shanghai.../signed", - "like_count": 820 - } - } -} -``` - -#### DELETE /api/v1/starbook/slots/{slot_index} - -**说明:** 从指定槽位移除藏品 - -**响应:** - -```json -{ - "code": 200, - "data": { - "slot_index": 1 - } -} -``` - --- ## 六、后端模块划分 @@ -442,16 +460,14 @@ UpgradeGrade(assetID int64, fromGrade, toGrade int) error | `AssetService` | 现有流程改造:创建普通藏品时同步写 asset_registry (type='regular', grade=1);预留 grade 升级接口 | | `CollectionService`(新建) | 管理 `collection_assets`,典藏专属逻辑(含 category 子分类) | | `ActivityAssetService`(新建) | 管理 `activity_assets`,活动藏品专属逻辑 | -| `StarbookService`(新建) | 星册业务编排,管理 `starbook_slots` 表,调用各 Service,整合数据,统一生成预签名 URL | +| `StarbookService`(新建) | 星册业务编排,调用各 Service,整合数据,统一生成预签名 URL | ### 6.2 StarbookService 核心逻辑 -1. 从 `fan_profiles.starbook_limit` 获取当前用户的槽位上限 `total_slots` -2. 从 `starbook_slots` 读取已放置的藏品,填充 `slots` 数组 -3. 从 `asset_registry` 查询当前用户 + 当前 star 下的所有藏品(regular + collection + activity) -4. 按 type → 各自维度分组(regular 按 grade,collection 按 category,activity 按 activity_type) -5. 批量调用 OSS 生成预签名 URL(已有 `generatePresignedURL` 逻辑复用) -6. 返回结构化数据 +1. 从 `asset_registry` 查询当前用户 + 当前 star 下的所有藏品(regular + collection + activity) +2. 按 type → 各自维度分组(regular 按 grade,collection 按 category,activity 按 activity_type) +3. 批量调用 OSS 生成预签名 URL(已有 `generatePresignedURL` 逻辑复用) +4. 返回结构化数据 ### 6.3 Repository 层 @@ -491,21 +507,6 @@ service StarbookService { get: "/api/v1/starbook/items" }; } - - // 放置藏品到槽位 - rpc PlaceToSlot(PlaceToSlotRequest) returns (PlaceToSlotResponse) { - option (google.api.http) = { - post: "/api/v1/starbook/place" - body: "*" - }; - } - - // 从槽位移除 - rpc RemoveFromSlot(RemoveFromSlotRequest) returns (RemoveFromSlotResponse) { - option (google.api.http) = { - delete: "/api/v1/starbook/slots/{slot_index}" - }; - } } message GetStarbookHomeRequest {} @@ -516,26 +517,7 @@ message GetStarbookHomeResponse { } message StarbookHomeData { - int32 total_slots = 1; - repeated SlotInfo slots = 2; - repeated AssetGroup groups = 3; -} - -message SlotInfo { - int32 slot_index = 1; - bool locked = 2; - SlotAsset asset = 3; -} - -message SlotAsset { - int64 asset_id = 1; - string asset_type = 2; // 'regular' / 'collection' / 'activity' - string name = 3; - string cover_url_signed = 4; - int32 like_count = 5; - string category = 6; // regular: 'castlove' / collection: category / activity: activity_type - int32 grade = 7; // 仅 regular 时有效(1/2/3),其他类型为 0 - int64 placed_at = 8; // 放置时间(毫秒时间戳) + repeated AssetGroup groups = 1; } message AssetGroup { @@ -544,7 +526,7 @@ message AssetGroup { string category_name = 3; // regular 使用 grades 分组;collection/activity 使用 flat items 列表 repeated GradeSection grades = 4; // 仅 regular 时有效 - repeated AssetItem items = 5; // collection / activity 使用 + repeated AssetItem items = 5; // collection / activity 时有效 int32 total_count = 6; bool has_more = 7; } @@ -563,13 +545,13 @@ message AssetItem { 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 + 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 grade = 3; // 仅 regular 时有效(1/2/3...) int32 page = 4; int32 page_size = 5; } @@ -586,25 +568,6 @@ message AssetListData { int32 page_size = 4; bool has_more = 5; } - -message PlaceToSlotRequest { - int64 asset_id = 1; - string asset_type = 2; // 'regular' / 'collection' / 'activity' - int32 slot_index = 3; -} - -message PlaceToSlotResponse { - topfans.common.BaseResponse base = 1; - SlotInfo slot = 2; -} - -message RemoveFromSlotRequest { - int32 slot_index = 1; -} - -message RemoveFromSlotResponse { - topfans.common.BaseResponse base = 1; -} ``` --- @@ -646,7 +609,6 @@ function loadStarbookData() { |--------|--------| | 调用 `getMyAssetsApi`(普通藏品) | 调用 `getStarbookHomeApi`(统一,含 regular/collection/activity) | | 手动解析封面 URL(`Promise.all` 逐个调 OSS) | 后端直接返回 `cover_url_signed` | -| 前端硬编码 15 槽位 | 后端返回 `total_slots`(从 `fan_profiles.starbook_limit` 动态读取) | | 前端按 index 硬分组 | 后端按 type → category → grade 分组返回 | | 普通藏品不展示在星册 | 普通藏品(type='regular')纳入星册体系 | @@ -655,34 +617,15 @@ function loadStarbookData() { | 页面 | 路由 | 说明 | |------|------|------| | 星册主页 | `/pages/starbook/index` | 不变 | -| 查看更多 | `/pages/starbook/items?type=collection&category=limited&grade=3` | 新页面,接分页数据 | +| 查看更多 | `/pages/starbook/items?type=regular&category=castlove&grade=5` | 新页面,接分页数据 | ### 8.4 数据结构(前端 TypeScript 类型) ```ts interface StarbookHomeData { - total_slots: number // 从 fan_profiles.starbook_limit 动态读取 - slots: SlotInfo[] groups: AssetGroup[] } -interface SlotInfo { - slot_index: number - locked: boolean - asset: SlotAsset | null -} - -interface SlotAsset { - asset_id: number - asset_type: 'regular' | 'collection' | 'activity' - name: string - cover_url_signed: string - like_count: number - category: string - grade: number // regular: 1/2/3,其他类型为 0 - placed_at: number // 放置时间(毫秒时间戳) -} - interface AssetGroup { type: 'regular' | 'collection' | 'activity' category: string // regular: 'castlove' / collection: 子分类 / activity: activity_type @@ -695,7 +638,7 @@ interface AssetGroup { } interface GradeSection { - grade: number // regular: 1/2/3 + grade: number // regular: 1/2/3... items: AssetItem[] total_count: number has_more: boolean @@ -708,12 +651,18 @@ interface AssetItem { like_count: number created_at: number category: string // regular: 'castlove' / collection: 子分类 / activity: activity_type - grade: number // regular: 1/2/3,其他类型为 0 + grade: number // regular: 1/2/3...,其他类型为 0 +} + +// grade 中文转换函数(前端使用) +function toChineseGrade(grade: number): string { + const map = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' }; + return `等级${map[grade] || grade}`; } ``` **说明:** -- `type='regular'`:`grades[]` 分组(按 grade),`grade` 取值 1/2/3,`category='castlove'` 固定 +- `type='regular'`:`grades[]` 分组(按 grade),`grade` 取值 1/2/3...,`category='castlove'` 固定 - `type='collection'`:`items[]` 列表(按 category 分组),`grade=0`(数据库存 NULL,API 转换层统一返回 0),`category` 为典藏子分类 - `type='activity'`:`items[]` 列表(按 activity_type 分组),`grade=0`(数据库存 NULL,API 转换层统一返回 0),`category` 为活动类型 @@ -744,10 +693,10 @@ interface AssetItem { | 范围 | 变更内容 | |------|----------| -| 数据库 | 新建 4 张表(collection_assets, activity_assets, asset_registry, starbook_slots) | +| 数据库 | 新建 3 张表(collection_assets, activity_assets, asset_registry) | | Proto | 新建 starbook.proto | -| Gateway | 新建 StarbookController,新增 4 个路由 | +| Gateway | 新建 StarbookController,新增 2 个路由 | | Service 层 | 新建 CollectionService, ActivityAssetService, StarbookService | -| Repository 层 | 新建 CollectionRepository, ActivityAssetRepository, AssetRegistryRepository, StarbookSlotRepository | +| Repository 层 | 新建 CollectionRepository, ActivityAssetRepository, AssetRegistryRepository | | 前端 | StarbookContent.vue 重构,新增 /pages/starbook/items.vue | | 铸爱流程 | **受影响**,普通藏品创建时需同步写入 asset_registry(type='regular', grade=1)|