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 new file mode 100644 index 0000000..6bfdcc1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md @@ -0,0 +1,753 @@ +# 典藏/活动藏品体系 + 星册重构设计 + +> 日期:2026-04-13 +> 状态:草稿,待评审 +> 负责人:zheng020 + +--- + +## 一、背景与目标 + +### 1.1 现状问题 + +- `StarbookContent.vue` 混用通用 `getMyAssetsApi`,无法区分藏品类型(普通/典藏/活动) +- `assets` 表只有一套,没有类型区分字段 +- 典藏藏品有子分类(category)需求,但目前表结构不支持 +- 普通藏品有等级(grade)需求,但目前表结构不支持 +- 活动藏品需要独立的生命周期管理 +- 前端封面 URL 逐个解析(`Promise.all` + 多次 OSS 预签名调用),性能差 +- 前端多重生命周期触发(`onMounted` + `onActivated` + `onWatch` + `onShow`),存在重复请求 + +### 1.2 重构目标 + +1. 建立普通藏品、典藏藏品、活动藏品三套并行的数据表结构 +2. 普通藏品按 grade(1/2/3)分组展示,典藏按 category 子分类分组,活动按 activity_type 分组 +3. 通过 `asset_registry` 统一索引表实现跨类型统一查询 +4. 后端批量返回预签名封面 URL,前端零额外 OSS 请求 +5. 星册页面按类型 + 子分类/等级分组展示,每组最多展示 6 条 +6. "查看更多" 跳转独立页面,支持分页 +7. 普通藏品(铸爱流程)、典藏藏品、活动藏品均可放入星册 + +--- + +## 二、数据模型 + +### 2.1 现有表(不变动) + +| 表名 | 说明 | +|------|------| +| `assets` | 普通藏品(铸爱流程),结构不变 | + +### 2.2 新建表 + +#### 2.2.1 典藏藏品表 `collection_assets` + +```sql +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` + +```sql +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` + +```sql +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/3),`collection_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 转换层处理) + +### 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 完全独立**,星册槽位只用于星册页面展示 + +--- + +## 三、藏品创建流程 + +### 3.1 普通藏品(铸爱流程) + +``` +用户上传素材 → 铸造订单创建 → assets 表写入 +→ 同时写入 asset_registry (type='regular', grade=1) -- grade 初始为1,升级规则预留 +``` + +说明:普通藏品(铸爱流程)纳入星册体系,创建时同步写入 registry,`grade` 初始为 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,升级规则待定(由团队讨论确认后实现)。设计上预留扩展接口: + +```go +// 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 | 某分组的藏品列表(分页) | +| `/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` 动态读取。 + +**响应示例:** + +```json +{ + "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": "典藏", + "grades": [ + { + "grade": 3, + "items": [ + { "asset_id": 201, "name": "铸爱藏品-A", "cover_url_signed": "https://...", "like_count": 820, "category": "castlove", "grade": 3 } + ], + "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 } + ], + "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": "collection", + "category": "classic", + "category_name": "经典典藏", + "items": [...], + "total_count": 3, + "has_more": false + }, + { + "type": "activity", + "category": "birthday", + "category_name": "生日会", + "items": [...], + "total_count": 2, + "has_more": false + } + ] + } +} +``` + +**说明:** +- `total_slots`:从 `fan_profiles.starbook_limit` 动态读取,非写死 +- `slots`:星册槽位,当前展示的藏品直接返回预签名 URL,`placed_at` 为放置时间 +- `groups`:三种类型的分组逻辑不同 + - `type='regular'`(普通藏品):按 `grade` 分组(grade=1/2/3),`category='castlove'` 固定 + - `type='collection'`(典藏):按 `category` 子分类分组,无 grade + - `type='activity'`(活动藏品):按 `activity_type` 分组,无 grade +- 每组最多返回 6 条,`has_more=true` 表示还有更多(点击"查看更多"跳转分页页) + +#### 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 | + +**响应示例:** + +```json +{ + "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": 2, + "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 参数)筛选 + +#### 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 + } +} +``` + +--- + +## 六、后端模块划分 + +### 6.1 Service 层 + +| Service | 职责 | +|---------|------| +| `AssetService` | 现有流程改造:创建普通藏品时同步写 asset_registry (type='regular', grade=1);预留 grade 升级接口 | +| `CollectionService`(新建) | 管理 `collection_assets`,典藏专属逻辑(含 category 子分类) | +| `ActivityAssetService`(新建) | 管理 `activity_assets`,活动藏品专属逻辑 | +| `StarbookService`(新建) | 星册业务编排,管理 `starbook_slots` 表,调用各 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. 返回结构化数据 + +### 6.3 Repository 层 + +| Repository | 表 | +|------------|-----| +| `CollectionRepository` | `collection_assets` | +| `ActivityAssetRepository` | `activity_assets` | +| `AssetRegistryRepository` | `asset_registry` | + +--- + +## 七、Proto 文件变更 + +### 7.1 新建 starbook.proto + +```protobuf +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" + }; + } + + // 放置藏品到槽位 + 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 {} + +message GetStarbookHomeResponse { + topfans.common.BaseResponse base = 1; + StarbookHomeData data = 2; +} + +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; // 放置时间(毫秒时间戳) +} + +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; +} + +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; +} +``` + +--- + +## 八、前端重构 + +### 8.1 生命周期优化 + +**问题:** 当前 `onMounted` + `onActivated` + `onShow` + `watch(isActive)` 四处触发 `loadAssetsList()` + +**解决:** + +```js +// 只保留 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) | +| 手动解析封面 URL(`Promise.all` 逐个调 OSS) | 后端直接返回 `cover_url_signed` | +| 前端硬编码 15 槽位 | 后端返回 `total_slots`(从 `fan_profiles.starbook_limit` 动态读取) | +| 前端按 index 硬分组 | 后端按 type → category → grade 分组返回 | +| 普通藏品不展示在星册 | 普通藏品(type='regular')纳入星册体系 | + +### 8.3 页面路由 + +| 页面 | 路由 | 说明 | +|------|------|------| +| 星册主页 | `/pages/starbook/index` | 不变 | +| 查看更多 | `/pages/starbook/items?type=collection&category=limited&grade=3` | 新页面,接分页数据 | + +### 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 + 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 +} +``` + +**说明:** +- `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` 为活动类型 + +--- + +## 九、性能保障 + +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'` | + +--- + +## 十一、影响范围 + +| 范围 | 变更内容 | +|------|----------| +| 数据库 | 新建 4 张表(collection_assets, activity_assets, asset_registry, starbook_slots) | +| Proto | 新建 starbook.proto | +| Gateway | 新建 StarbookController,新增 4 个路由 | +| Service 层 | 新建 CollectionService, ActivityAssetService, StarbookService | +| Repository 层 | 新建 CollectionRepository, ActivityAssetRepository, AssetRegistryRepository, StarbookSlotRepository | +| 前端 | StarbookContent.vue 重构,新增 /pages/starbook/items.vue | +| 铸爱流程 | **受影响**,普通藏品创建时需同步写入 asset_registry(type='regular', grade=1)|