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>
This commit is contained in:
parent
a5e3008819
commit
11bcfcd68b
@ -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)|
|
||||
Loading…
Reference in New Issue
Block a user