From 6b26ef26dba8acdf4c57d19228dd055a00df3364 Mon Sep 17 00:00:00 2001 From: zheng020 Date: Mon, 27 Apr 2026 20:57:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-04-27-inspiration-flow-design.md | 312 +++++++++++++ .../specs/2026-04-27-my-assets-design.md | 440 ++++++++++++++++++ 2 files changed, 752 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-27-inspiration-flow-design.md create mode 100644 docs/superpowers/specs/2026-04-27-my-assets-design.md diff --git a/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md b/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md new file mode 100644 index 0000000..d8ed87d --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-inspiration-flow-design.md @@ -0,0 +1,312 @@ +# 灵感瀑布(横向瀑布流藏品展示)设计文档 + +> **创建日期:** 2026-04-27 +> **项目:** TopFans 横向瀑布流藏品展示 +> **服务:** galleryService (Go Dubbo-go) +> **状态:** 设计中 + +--- + +## 一、设计目标 + +横向瀑布流展示该 star_id 下所有用户展出的藏品,支持随机展示、分页、按类型过滤。 + +--- + +## 二、数据来源 + +**主表:** Exhibition(展品展示表) + +**关联表:** Asset(资产表)- 用于按 material_type 过滤 + +**筛选条件:** +- `occupier_star_id = ?` (当前用户 star_id) +- `expire_at > now` (未过期) +- `deleted_at IS NULL` (未删除) + +**按 type 过滤:** JOIN Asset 表,WHERE assets.material_type = ? + +--- + +## 三、API 设计 + +### 3.1 获取灵感瀑布(横向瀑布流)藏品列表 + +``` +GET /api/v1/inspiration-flow +``` + +**Query 参数:** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| page | int | 否 | 1 | 页码 | +| page_size | int | 否 | 20 | 每页数量(最大100) | +| type | string | 否 | all | 过滤类型:badge/poster/original/all | + +**HTTP 响应:** + +```json +{ + "code": 200, + "message": "ok", + "data": { + "items": [ + { + "asset_id": 123, + "name": "藏品名称", + "cover_url": "https://xxx.com/cover.png", + "like_count": 100, + "owner_nickname": "粉丝昵称" + } + ], + "page": 1, + "page_size": 20, + "total": 100, + "has_more": true + } +} +``` + +**错误码:** + +| code | 说明 | +|------|------| +| 200 | 成功 | +| 401 | 用户认证失败 | +| 500 | 服务器内部错误 | + +--- + +## 四、Proto 定义 + +### 4.1 Request / Response + +```protobuf +// 获取灵感瀑布藏品列表请求 +message GetInspirationFlowRequest { + int32 page = 1; // 页码(默认1) + int32 page_size = 2; // 每页数量(默认20,最大100) + string type = 3; // 过滤类型:badge/poster/original/all(默认all) +} + +// 获取灵感瀑布藏品列表响应 +message GetInspirationFlowResponse { + topfans.common.BaseResponse base = 1; + InspirationFlowData data = 2; +} + +// 灵感瀑布数据 +message InspirationFlowData { + repeated InspirationFlowItem items = 1; // 藏品列表 + int32 page = 2; // 当前页码 + int32 page_size = 3; // 每页数量 + int64 total = 4; // 总数量 + bool has_more = 5; // 是否有更多 +} + +// 灵感瀑布藏品项 +message InspirationFlowItem { + int64 asset_id = 1; // 资产ID + string name = 2; // 藏品名称 + string cover_url = 3; // 封面图URL + int32 like_count = 4; // 点赞数 + string owner_nickname = 5; // 展出者昵称 +} +``` + +### 4.2 Service 方法 + +在 GalleryService 中新增方法: + +```protobuf +// 展馆服务 +service GalleryService { + // ... 现有方法 ... + + // 获取灵感瀑布藏品列表 + rpc GetInspirationFlow(GetInspirationFlowRequest) returns (GetInspirationFlowResponse) { + option (google.api.http) = { + get: "/api/v1/inspiration-flow" + }; + } +} +``` + +--- + +## 五、核心逻辑 + +### 5.1 查询逻辑 + +```sql +SELECT + e.asset_id, + a.name, + a.cover_url, + a.like_count, + fp.nickname as owner_nickname +FROM exhibitions e +JOIN assets a ON e.asset_id = a.id +JOIN fan_profiles fp ON e.occupier_uid = fp.user_id AND e.occupier_star_id = fp.star_id +WHERE e.occupier_star_id = ? + AND e.expire_at > ? + AND e.deleted_at IS NULL + AND a.status = 1 + AND a.is_active = true + AND (? = 'all' OR a.material_type = ?) +ORDER BY RANDOM() +LIMIT ? OFFSET ?; +``` + +**参数说明:** +- `? = star_id` (当前用户 star_id) +- `? = now` (当前时间戳) +- `? = type` (过滤类型) +- `? = page_size` +- `? = (page - 1) * page_size` + +### 5.2 6小时自动下架 + +已实现的 `GetExpiredExhibitions` 方法查询已过期的展览记录,调度器定期清理。 + +**下架时执行:** +1. 设置 `deleted_at = now` +2. 调用 `ClearAssetLikeRecords` RPC 重置点赞数为 0 +3. 更新 `asset_registry.display_status = 0` + +### 5.3 重新上架逻辑 + +当藏品重新展示时: +1. 创建新的 Exhibition 记录 +2. 更新 `asset_registry.display_status = 1` +3. 点赞数从 0 开始(因为之前下架时已清除点赞记录) + +### 5.4 随机展示 + +使用 `ORDER BY RANDOM()` 实现随机排序。 + +--- + +## 六、数据模型 + +### 6.1 Exhibition 表(已有字段) + +```go +type Exhibition struct { + ID int64 `gorm:"primaryKey"` + AssetID int64 `gorm:"not null"` + SlotID int64 `gorm:"not null"` + HostProfileID int64 `gorm:"not null"` + OccupierUID int64 `gorm:"not null"` + OccupierStarID int64 `gorm:"not null;index"` + StartTime int64 `gorm:"not null"` + ExpireAt int64 `gorm:"not null;index"` + CreatedAt int64 `gorm:"not null"` + UpdatedAt int64 `gorm:"not null"` + DeletedAt *int64 `gorm:"index"` +} +``` + +### 6.2 Asset 表关联字段 + +```go +type Asset struct { + // ... 现有字段 ... + MaterialType string `gorm:"column:material_type"` // 素材类型:badge/poster/original + IsOriginal bool `gorm:"column:is_original"` + LikeCount int32 `gorm:"not null;default:0"` +} +``` + +**新增字段(需数据库变更):** +```sql +ALTER TABLE assets ADD COLUMN material_type VARCHAR(50) DEFAULT 'original'; +``` + +**material_type 枚举值:** +- `original` — 原创(默认值) +- `badge` — 吧唧 +- `poster` — 海报 + +--- + +## 七、收益记录 + +**预留:** 收益记录通过经济系统已有的 `exhibition_revenue` change_type 实现。 + +后续按需在 Exhibition 创建/删除时写入 `crystal_transaction_records`,change_type 为 `exhibition_revenue`。 + +--- + +## 八、配置项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| exhibition_duration | 展览时长(秒) | 14400(4小时,可配置) | +| inspiration_flow_page_size | 默认每页数量 | 20 | +| inspiration_flow_max_page_size | 最大每页数量 | 100 | + +--- + +## 九、项目文件结构 + +``` +backend/ +├── proto/ +│ └── gallery.proto # 修改:新增 GetInspirationFlow 方法 +│ +├── pkg/proto/ +│ ├── gallery/ +│ │ ├── gallery.pb.go # 重新生成 +│ │ └── gallery.triple.go # 重新生成 +│ +├── services/galleryService/ +│ ├── repository/ +│ │ └── gallery_repository.go # 修改:新增 GetActiveExhibitions 方法 +│ │ +│ ├── service/ +│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法 +│ │ +│ ├── provider/ +│ │ └── gallery_provider.go # 修改:新增 GetInspirationFlow Handler +│ │ +│ └── config/ +│ └── gallery_config.go # 修改:新增灵感瀑布配置项 +│ +├── scripts/ +│ └── migrate_add_material_type.sql # 新增:material_type 字段迁移脚本 +│ +└── gateway/ + ├── controller/ + │ └── gallery_controller.go # 修改:新增 GetInspirationFlow 路由处理 + │ + └── router/ + └── router.go # 修改:新增 /api/v1/inspiration-flow 路由 +``` + +--- + +## 十、待确认事项 + +1. **material_type 枚举值**:badge/poster/original,后续按需扩展 +2. **随机排序策略**:ORDER BY RANDOM() 在数据量大时可能有性能问题,后续可考虑按 like_count 随机采样 +3. **展览时长**:默认 6 小时(21600 秒),可通过配置修改 + +## 十一、数据库变更 + +### 11.1 新增字段 + +```sql +-- assets 表新增 material_type 字段 +ALTER TABLE assets ADD COLUMN material_type VARCHAR(50) DEFAULT 'original'; + +-- 创建索引优化查询 +CREATE INDEX IF NOT EXISTS idx_assets_material_type ON assets(material_type); +``` + +### 11.2 迁移脚本 + +```sql +-- 迁移脚本:backend/scripts/migrate_add_material_type.sql +``` \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-27-my-assets-design.md b/docs/superpowers/specs/2026-04-27-my-assets-design.md new file mode 100644 index 0000000..578cbb8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-my-assets-design.md @@ -0,0 +1,440 @@ +# 我的作品统计(点赞/展出)设计文档 + +> **创建日期:** 2026-04-27 +> **项目:** TopFans 我的作品统计 +> **服务:** socialService / galleryService +> **状态:** 设计中 + +--- + +## 一、设计目标 + +提供用户查看自己点赞过的作品和展出过的作品的统计接口,返回实时点赞数。 + +--- + +## 二、数据来源 + +### 2.1 我点赞的作品 + +**主表:** asset_likes(点赞记录表) + +**关联表:** assets(资产表)- 用于获取藏品信息 + +**筛选条件:** +- `user_id = ?` (当前用户) +- `star_id = ?` (当前 star_id) +- `assets.deleted_at IS NULL` (藏品未删除) +- `assets.is_active = true` (藏品已激活) + +### 2.2 我展出的作品 + +**主表:** exhibitions(展品展示表) + +**关联表:** assets(资产表)- 用于获取藏品信息 + +**筛选条件:** +- `occupier_uid = ?` (当前用户) +- `occupier_star_id = ?` (当前 star_id) +- `deleted_at IS NULL` (未删除) + +--- + +## 三、API 设计 + +### 3.1 获取我点赞的作品列表 + +``` +GET /api/v1/me/liked-assets +``` + +**Query 参数:** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| page | int | 否 | 1 | 页码 | +| page_size | int | 否 | 20 | 每页数量(最大100) | + +**HTTP 响应:** + +```json +{ + "code": 200, + "message": "ok", + "data": { + "items": [ + { + "asset_id": 123, + "name": "藏品名称", + "cover_url": "https://xxx.com/cover.png", + "like_count": 100, + "liked_at": 1714214400000 + } + ], + "page": 1, + "page_size": 20, + "total": 50, + "has_more": true + } +} +``` + +**字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| asset_id | int64 | 资产ID | +| name | string | 藏品名称 | +| cover_url | string | 封面图URL | +| like_count | int32 | 实时点赞数(来自 assets 表) | +| liked_at | int64 | 用户点赞该作品的时间(毫秒时间戳) | + +--- + +### 3.2 获取我展出的作品列表 + +``` +GET /api/v1/me/exhibited-assets +``` + +**Query 参数:** + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| page | int | 否 | 1 | 页码 | +| page_size | int | 否 | 20 | 每页数量(最大100) | + +**HTTP 响应:** + +```json +{ + "code": 200, + "message": "ok", + "data": { + "items": [ + { + "asset_id": 123, + "name": "藏品名称", + "cover_url": "https://xxx.com/cover.png", + "like_count": 100, + "exhibited_at": 1714214400000, + "expire_at": 1714278400000 + } + ], + "page": 1, + "page_size": 20, + "total": 10, + "has_more": false + } +} +``` + +**字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| asset_id | int64 | 资产ID | +| name | string | 藏品名称 | +| cover_url | string | 封面图URL | +| like_count | int32 | 实时点赞数(来自 assets 表) | +| exhibited_at | int64 | 展出开始时间(毫秒时间戳) | +| expire_at | int64 | 展出过期时间(毫秒时间戳) | + +--- + +### 3.3 错误码 + +| code | 说明 | +|------|------| +| 200 | 成功 | +| 401 | 用户认证失败 | +| 500 | 服务器内部错误 | + +--- + +## 四、Proto 定义 + +### 4.1 我点赞的作品 + +```protobuf +// 获取我点赞的作品列表请求 +message GetMyLikedAssetsRequest { + int32 page = 1; // 页码(默认1) + int32 page_size = 2; // 每页数量(默认20,最大100) +} + +// 获取我点赞的作品列表响应 +message GetMyLikedAssetsResponse { + topfans.common.BaseResponse base = 1; + LikedAssetsData data = 2; +} + +// 点赞作品数据 +message LikedAssetsData { + repeated LikedAssetItem items = 1; // 作品列表 + int32 page = 2; // 当前页码 + int32 page_size = 3; // 每页数量 + int64 total = 4; // 总数量 + bool has_more = 5; // 是否有更多 +} + +// 点赞作品项 +message LikedAssetItem { + int64 asset_id = 1; // 资产ID + string name = 2; // 藏品名称 + string cover_url = 3; // 封面图URL + int32 like_count = 4; // 实时点赞数 + int64 liked_at = 5; // 点赞时间(毫秒时间戳) +} +``` + +--- + +### 4.2 我展出的作品 + +```protobuf +// 获取我展出的作品列表请求 +message GetMyExhibitedAssetsRequest { + int32 page = 1; // 页码(默认1) + int32 page_size = 2; // 每页数量(默认20,最大100) +} + +// 获取我展出的作品列表响应 +message GetMyExhibitedAssetsResponse { + topfans.common.BaseResponse base = 1; + ExhibitedAssetsData data = 2; +} + +// 展出作品数据 +message ExhibitedAssetsData { + repeated ExhibitedAssetItem items = 1; // 作品列表 + int32 page = 2; // 当前页码 + int32 page_size = 3; // 每页数量 + int64 total = 4; // 总数量 + bool has_more = 5; // 是否有更多 +} + +// 展出作品项 +message ExhibitedAssetItem { + int64 asset_id = 1; // 资产ID + string name = 2; // 藏品名称 + string cover_url = 3; // 封面图URL + int32 like_count = 4; // 实时点赞数 + int64 exhibited_at = 5; // 展出开始时间(毫秒时间戳) + int64 expire_at = 6; // 展出过期时间(毫秒时间戳) +} +``` + +--- + +### 4.3 Service 方法 + +在 SocialService 中新增方法: + +```protobuf +// 社交服务 +service SocialService { + // ... 现有方法 ... + + // 获取我点赞的作品列表 + rpc GetMyLikedAssets(GetMyLikedAssetsRequest) returns (GetMyLikedAssetsResponse) { + option (google.api.http) = { + get: "/api/v1/me/liked-assets" + }; + } +} +``` + +在 GalleryService 中新增方法: + +```protobuf +// 展馆服务 +service GalleryService { + // ... 现有方法 ... + + // 获取我展出的作品列表 + rpc GetMyExhibitedAssets(GetMyExhibitedAssetsRequest) returns (GetMyExhibitedAssetsResponse) { + option (google.api.http) = { + get: "/api/v1/me/exhibited-assets" + }; + } +} +``` + +--- + +## 五、核心逻辑 + +### 5.1 查询我点赞的作品 + +```sql +SELECT + al.asset_id, + a.name, + a.cover_url, + a.like_count, + al.created_at as liked_at +FROM asset_likes al +JOIN assets a ON al.asset_id = a.id +WHERE al.user_id = ? + AND al.star_id = ? + AND a.deleted_at IS NULL + AND a.is_active = true +ORDER BY al.created_at DESC +LIMIT ? OFFSET ?; + +-- 计数 +SELECT COUNT(*) +FROM asset_likes al +JOIN assets a ON al.asset_id = a.id +WHERE al.user_id = ? + AND al.star_id = ? + AND a.deleted_at IS NULL + AND a.is_active = true; +``` + +**参数说明:** +- `? = user_id` (当前用户) +- `? = star_id` (当前 star_id) +- `? = page_size` +- `? = (page - 1) * page_size` + +--- + +### 5.2 查询我展出的作品(只返回展出中的) + +```sql +SELECT + e.asset_id, + a.name, + a.cover_url, + a.like_count, + e.start_time as exhibited_at, + e.expire_at +FROM exhibitions e +JOIN assets a ON e.asset_id = a.id +WHERE e.occupier_uid = ? + AND e.occupier_star_id = ? + AND e.deleted_at IS NULL + AND e.expire_at > ? -- 只返回未过期的 +ORDER BY e.start_time DESC +LIMIT ? OFFSET ?; + +-- 计数 +SELECT COUNT(*) +FROM exhibitions e +WHERE e.occupier_uid = ? + AND e.occupier_star_id = ? + AND e.deleted_at IS NULL + AND e.expire_at > ?; -- 只返回未过期的 +``` + +**参数说明:** +- `? = user_id` (当前用户) +- `? = star_id` (当前 star_id) +- `? = now` (当前时间戳,只显示未过期的) +- `? = page_size` +- `? = (page - 1) * page_size` + +--- + +## 六、数据模型 + +### 6.1 asset_likes 表(已有字段) + +```go +type AssetLike struct { + ID int64 `gorm:"primaryKey"` + AssetID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_asset"` + UserID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_asset"` + StarID int64 `gorm:"not null;index"` + CreatedAt int64 `gorm:"not null;index"` +} +``` + +### 6.2 exhibitions 表(已有字段) + +```go +type Exhibition struct { + ID int64 `gorm:"primaryKey"` + AssetID int64 `gorm:"not null"` + SlotID int64 `gorm:"not null"` + HostProfileID int64 `gorm:"not null"` + OccupierUID int64 `gorm:"not null;index"` + OccupierStarID int64 `gorm:"not null;index"` + StartTime int64 `gorm:"not null"` + ExpireAt int64 `gorm:"not null;index"` + CreatedAt int64 `gorm:"not null"` + UpdatedAt int64 `gorm:"not null"` + DeletedAt *int64 `gorm:"index"` +} +``` + +### 6.3 assets 表(已有字段) + +```go +type Asset struct { + ID int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(100);not null"` + CoverURL string `gorm:"type:varchar(500);not null"` + LikeCount int32 `gorm:"not null;default:0"` + Status int32 `gorm:"not null;default:0"` + DeletedAt *int64 `gorm:"index"` + IsActive bool `gorm:"default:true;not null"` +} +``` + +--- + +## 七、项目文件结构 + +``` +backend/ +├── proto/ +│ ├── social.proto # 修改:新增 GetMyLikedAssets 方法 +│ └── gallery.proto # 修改:新增 GetMyExhibitedAssets 方法 +│ +├── pkg/proto/ +│ ├── social/ +│ │ ├── social.pb.go # 重新生成 +│ │ └── social.triple.go # 重新生成 +│ └── gallery/ +│ ├── gallery.pb.go # 重新生成 +│ └── gallery.triple.go # 重新生成 +│ +├── services/socialService/ +│ ├── repository/ +│ │ └── asset_like_repository.go # 修改:新增查询方法 +│ │ +│ ├── service/ +│ │ └── asset_like_service.go # 修改:新增 GetMyLikedAssets 方法 +│ │ +│ └── provider/ +│ └── social_provider.go # 修改:新增 GetMyLikedAssets Handler +│ +├── services/galleryService/ +│ ├── repository/ +│ │ └── gallery_repository.go # 修改:新增 GetExhibitionsByOccupier 方法 +│ │ +│ ├── service/ +│ │ └── exhibition_service.go # 修改:新增 GetMyExhibitedAssets 方法 +│ │ +│ └── provider/ +│ └── gallery_provider.go # 修改:新增 GetMyExhibitedAssets Handler +│ +└── gateway/ + ├── controller/ + │ ├── social_controller.go # 修改:新增 /api/v1/me/liked-assets 路由 + │ └── gallery_controller.go # 修改:新增 /api/v1/me/exhibited-assets 路由 + │ + └── router/ + └── router.go # 修改:新增路由配置 +``` + +--- + +## 八、已确认事项 + +1. **只显示展出中的作品** — 通过 `expire_at > now` 过滤 +2. **排序方式** — 按展出时间倒序(start_time DESC) +3. **分页大小** — 默认 20,最大 100 \ No newline at end of file