topfans/docs/superpowers/specs/2026-04-13-collection-asset-starbook-refactor.md
zheng020 11bcfcd68b docs: update starbook refactor spec with slot table and dynamic total_slots
- Add starbook_slots table (new, separate from booth_slots)
- Change total_slots from hardcoded 15 to dynamic fan_profiles.starbook_limit
- Clarify same asset_id cannot be registered as multiple types
- Add placed_at to SlotAsset proto and TypeScript
- Fix grade=0 vs NULL explanation in DB/API contract
- Update affected scope (4 new tables, new StarbookSlotRepository)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 14:32:56 +08:00

754 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 典藏/活动藏品体系 + 星册重构设计
> 日期2026-04-13
> 状态:草稿,待评审
> 负责人zheng020
---
## 一、背景与目标
### 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` | 普通藏品(铸爱流程),结构不变 |
### 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 按 gradecollection 按 categoryactivity 按 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`(数据库存 NULLAPI 转换层统一返回 0`category` 为典藏子分类
- `type='activity'``items[]` 列表(按 activity_type 分组),`grade=0`(数据库存 NULLAPI 转换层统一返回 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_registrytype='regular', grade=1|