# 我的作品统计(点赞/展出)设计文档 > **创建日期:** 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 已定义,但代码实现待后续