topfans/docs/superpowers/specs/2026-04-27-my-assets-design.md
zerosaturation dcd8cd4527 feat: 实现我的作品统计接口(点赞/展出)
- 新增 GET /api/v1/me/liked-assets 接口
- 新增 GET /api/v1/me/exhibited-assets 接口
- 新增 GetMyLikedAssets 和 GetMyExhibitedAssets RPC 方法
- 新增 ExhibitedAssetItemDTO 和 GetMyExhibitedAssetsResponseDTO
- 前端新增 getUserLikedAssetsApi 和 getUserExhibitedAssetsApi(暂不实现)
- 更新设计文档,标记他人作品统计接口为暂不实现

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 13:35:21 +08:00

781 lines
19 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-27
> **项目:** TopFans 我的作品统计
> **服务:** socialService / galleryService
> **状态:** 设计中
---
## 一、设计目标
提供用户查看自己点赞过的作品和展出过的作品的统计接口,返回实时点赞数。
---
## 二、数据来源
### 2.1 我点赞的作品
**主表:** asset_likes点赞记录表
**关联表:**
- assets资产表- 用于获取藏品信息
- exhibitions展品展示表- 用于过滤展出中且未过期的藏品
- exhibition_revenue_records收益记录表- 用于获取当前可领取收益
**筛选条件:**
- `user_id = ?` (当前用户)
- `star_id = ?` (当前 star_id)
- `assets.deleted_at IS NULL` (藏品未删除)
- `assets.is_active = true` (藏品已激活)
- `exhibitions.deleted_at IS NULL` (展出记录未删除)
- `exhibitions.expire_at > now` (展出未过期)
### 2.2 我展出的作品
**主表:** exhibitions展品展示表
**关联表:**
- assets资产表- 用于获取藏品信息
- exhibition_revenue_records收益记录表- 用于获取当前可领取收益
**筛选条件:**
- `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 | 用户点赞该作品的时间(毫秒时间戳) |
| earnings | int64 | 当前可领取收益status='claimable' 的 crystal_amount 汇总) |
---
### 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,
"earnings": 500
}
],
"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 | 展出过期时间(毫秒时间戳) |
| earnings | int64 | 当前可领取收益status='claimable' 的 crystal_amount 汇总) |
---
### 3.3 获取我今日点赞的作品(暂不实现)
```
GET /api/v1/me/today-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.4 获取我本周点赞的作品(暂不实现)
```
GET /api/v1/me/week-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.5 获取他人点赞的作品列表(暂不实现)
```
GET /api/v1/users/{user_id}/liked-assets
```
**Path 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| user_id | int64 | 是 | 他人用户ID |
**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.6 获取他人展出的作品列表(暂不实现)
```
GET /api/v1/users/{user_id}/exhibited-assets
```
**Path 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| user_id | int64 | 是 | 他人用户ID |
**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.7 错误码
| 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; // 点赞时间(毫秒时间戳)
int64 earnings = 6; // 当前可领取收益
}
```
---
### 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; // 展出过期时间(毫秒时间戳)
int64 earnings = 7; // 当前可领取收益
}
```
---
### 4.3 我今日/本周点赞的作品(暂不实现)
```protobuf
// 获取我今日点赞的作品列表请求
message GetMyTodayLikedAssetsRequest {
int32 page = 1; // 页码默认1
int32 page_size = 2; // 每页数量默认20最大100
}
// 获取我今日点赞的作品列表响应
message GetMyTodayLikedAssetsResponse {
topfans.common.BaseResponse base = 1;
LikedAssetsData data = 2;
}
// 获取我本周点赞的作品列表请求
message GetMyWeekLikedAssetsRequest {
int32 page = 1; // 页码默认1
int32 page_size = 2; // 每页数量默认20最大100
}
// 获取我本周点赞的作品列表响应
message GetMyWeekLikedAssetsResponse {
topfans.common.BaseResponse base = 1;
LikedAssetsData data = 2;
}
```
> **状态:暂不实现**
---
### 4.4 他人点赞/展出的作品(暂不实现)
```protobuf
// 获取他人点赞的作品列表请求
message GetUserLikedAssetsRequest {
int64 user_id = 1; // 他人用户ID
int32 page = 2; // 页码默认1
int32 page_size = 3; // 每页数量默认20最大100
}
// 获取他人点赞的作品列表响应
message GetUserLikedAssetsResponse {
topfans.common.BaseResponse base = 1;
LikedAssetsData data = 2;
}
// 获取他人展出的作品列表请求
message GetUserExhibitedAssetsRequest {
int64 user_id = 1; // 他人用户ID
int32 page = 2; // 页码默认1
int32 page_size = 3; // 每页数量默认20最大100
}
// 获取他人展出的作品列表响应
message GetUserExhibitedAssetsResponse {
topfans.common.BaseResponse base = 1;
ExhibitedAssetsData data = 2;
}
```
> **状态:暂不实现**
---
### 4.5 Service 方法
在 SocialService 中新增方法:
```protobuf
// 社交服务
service SocialService {
// ... 现有方法 ...
// 获取我点赞的作品列表
rpc GetMyLikedAssets(GetMyLikedAssetsRequest) returns (GetMyLikedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/me/liked-assets"
};
}
// 获取我今日点赞的作品列表(暂不实现)
rpc GetMyTodayLikedAssets(GetMyTodayLikedAssetsRequest) returns (GetMyTodayLikedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/me/today-liked-assets"
};
}
// 获取我本周点赞的作品列表(暂不实现)
rpc GetMyWeekLikedAssets(GetMyWeekLikedAssetsRequest) returns (GetMyWeekLikedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/me/week-liked-assets"
};
}
// 获取他人点赞的作品列表(暂不实现)
rpc GetUserLikedAssets(GetUserLikedAssetsRequest) returns (GetUserLikedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/users/{user_id}/liked-assets"
};
}
}
```
在 GalleryService 中新增方法:
```protobuf
// 展馆服务
service GalleryService {
// ... 现有方法 ...
// 获取我展出的作品列表
rpc GetMyExhibitedAssets(GetMyExhibitedAssetsRequest) returns (GetMyExhibitedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/me/exhibited-assets"
};
}
// 获取他人展出的作品列表(暂不实现)
rpc GetUserExhibitedAssets(GetUserExhibitedAssetsRequest) returns (GetUserExhibitedAssetsResponse) {
option (google.api.http) = {
get: "/api/v1/users/{user_id}/exhibited-assets"
};
}
}
```
---
## 五、核心逻辑
### 5.1 查询我点赞的作品(只返回展出中且未过期的)
```sql
SELECT
al.asset_id,
a.name,
a.cover_url,
a.like_count,
al.created_at as liked_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings
FROM asset_likes al
JOIN assets a ON al.asset_id = a.id
JOIN exhibitions e ON e.asset_id = a.id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE al.user_id = ?
AND al.star_id = ?
AND a.deleted_at IS NULL
AND a.is_active = true
AND e.deleted_at IS NULL
AND e.expire_at > ?
GROUP BY al.asset_id, a.name, a.cover_url, a.like_count, al.created_at
ORDER BY al.created_at DESC
LIMIT ? OFFSET ?;
-- 计数
SELECT COUNT(DISTINCT al.asset_id)
FROM asset_likes al
JOIN assets a ON al.asset_id = a.id
JOIN exhibitions e ON e.asset_id = a.id
WHERE al.user_id = ?
AND al.star_id = ?
AND a.deleted_at IS NULL
AND a.is_active = true
AND e.deleted_at IS NULL
AND e.expire_at > ?;
```
**参数说明:**
- `? = user_id` (当前用户)
- `? = star_id` (当前 star_id)
- `? = now` (当前时间戳,只显示展出中且未过期的)
- `? = 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,
COALESCE(SUM(err.crystal_amount), 0) as earnings
FROM exhibitions e
JOIN assets a ON e.asset_id = a.id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE e.occupier_uid = ?
AND e.occupier_star_id = ?
AND e.deleted_at IS NULL
AND e.expire_at > ? -- 只返回未过期的
GROUP BY e.asset_id, a.name, a.cover_url, a.like_count, e.start_time, 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
4. **点赞作品也只显示展出中且未过期的** — 通过 JOIN exhibitions 并过滤 `expire_at > now`
5. **今日/本周点赞暂不实现** — API 和 Proto 已定义,但代码实现待后续
6. **每个藏品返回当前可领取收益** — 关联 exhibition_revenue_records 表,汇总 `status='claimable'``crystal_amount`
7. **他人点赞/展出的作品列表暂不实现** — API 和 Proto 已定义,但代码实现待后续