13 KiB
查询藏品点赞用户列表接口设计
1. 需求概述
接口名称: GET /api/v1/social/assets/:asset_id/likers
功能描述: 查询指定藏品的所有点赞用户列表
权限控制: 仅登录用户可访问
返回内容: 点赞用户的基本信息(昵称、头像、粉丝等级、点赞时间)
2. 现有代码分析
2.1 相关数据模型
AssetLike 模型 (pkg/models/asset.go):
type AssetLike struct {
ID int64 // 点赞记录ID
AssetID int64 // 资产ID
UserID int64 // 用户ID
StarID int64 // 明星ID
ExhibitionID int64 // 展览ID
CreatedAt int64 // 点赞时间
}
关联关系:
AssetLike.Asset-> AssetAssetLike.User-> UserAssetLike.Star-> Star
2.2 现有 Repository 层
asset_like_repository.go 已有的方法:
GetByAsset(assetID, limit, offset)- 分页获取点赞记录CountByAsset(assetID)- 统计点赞数
问题: 没有 JOIN 用户表,需要新增或扩展方法
注意: GetByAsset 方法只需要 assetID,但 JOIN fan_profiles 需要 star_id。由于 AssetLike 模型包含 StarID 字段,查询时应以 AssetLike.StarID 作为 JOIN 条件传给 fan_profiles。
2.3 现有 Service 层
asset_like_service.go 已有的方法:
GetAssetLikes(ctx, assetID, page, pageSize)- 分页获取点赞列表
问题: 只返回 AssetLike 记录,没有返回用户信息
2.4 现有 Gateway 层
social_controller.go 已有点赞相关端点:
POST /api/v1/social/assets/:asset_id/like- 点赞DELETE /api/v1/social/assets/:asset_id/like- 取消点赞
缺失: 没有查询点赞用户的端点
2.5 Proto 定义 [待确认]
需要检查 pkg/proto/social/ 是否有现成的消息定义。如已有可复用消息(如 AssetLiker),直接引用;如无则按 Section 5 新增。
3. 架构设计
3.1 整体架构
HTTP Request
│
▼
Gateway (social_controller.go)
│ GET /api/v1/social/assets/:asset_id/likers
│
▼
Dubbo RPC Call
│
▼
SocialService (Dubbo Provider)
│
├──▶ Redis Cache (asset_likers:{asset_id})
│
└──▶ SocialRepository ──► asset_likes + users + fan_profiles JOIN
3.2 数据流
- Gateway 接收请求,验证登录态
- 通过 Dubbo 调用 SocialService
- SocialService 先查 Redis 缓存
- 缓存未命中 → 调用 SocialRepository 查询(直接访问 DB)
- 组装用户信息返回,写入 Redis 缓存
4. 性能优化设计
4.1 分页设计(必须)
游标分页 vs OFFSET 分页:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| OFFSET | 简单,支持跳页 | 大 OFFSET 性能差 | 数据量小(<1万) |
| 游标 | 大数据量性能稳定 | 不能跳页 | 数据量大(>1万) |
决策:采用游标分页
- 理由:藏品点赞列表通常不会很大,但游标分页更健壮
- 实现:
cursor=上一页最后一条的created_at值 - 首次请求
cursor=0时,查 DB 不带游标条件,返回第一页 cursor=0仅表示不需要created_at < ?条件,不表示要重新查 DB
4.2 索引优化
已有索引:
-- asset_likes 表
CREATE INDEX idx_asset_likes_asset ON asset_likes(asset_id); -- 仅 asset_id
CREATE INDEX idx_asset_likes_user_star ON asset_likes(user_id, star_id); -- 覆盖用户+明星查询
缺失索引(必须新增):
-- 游标分页查询:WHERE asset_id = ? AND created_at < ? ORDER BY created_at DESC
-- 注意:asset_likes 表无软删除字段,无需添加 WHERE 条件
CREATE INDEX idx_asset_likes_asset_created ON asset_likes(asset_id, created_at DESC);
-- 幂等执行:迁移脚本应检查索引是否已存在,避免重复执行报错
JOIN 查询计划:
asset_likes ──(idx_asset_likes_asset_created)──▶ 扫描 asset_id + created_at 排序
│
└──▶ users ──(主键 id)──▶ avatar_url
└──▶ fan_profiles ──(uk_fan_profiles_user_star)──▶ nickname, level
注意:昵称 nickname 来自 fan_profiles 表,不是 users 表
4.3 Redis 缓存策略(必须)
缓存 Key 设计
asset_likers:{asset_id} → 完整点赞用户列表(JSON)
为什么不按 cursor 分页存储:
- 游标分页的 cursor 值是动态的(最后一条的
liked_at) - 如果缓存每一页的 cursor,key 会很多且难以管理
- 更好的方案:缓存完整列表,按 cursor 在内存中切片
缓存数据结构
// AssetLikersCache 缓存数据结构
type AssetLikersCache struct {
Users []AssetLikerWithTotal `json:"users"` // 完整用户列表(按 liked_at DESC)
Total int64 `json:"total"` // 总数
UpdatedAt int64 `json:"updated_at"` // 缓存更新时间
}
// AssetLikerWithTotal 用户+点赞时间
type AssetLikerWithTotal struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
FanLevel int32 `json:"fan_level"`
LikedAt int64 `json:"liked_at"`
StarID int64 `json:"star_id"` // 用于 JOIN fan_profiles,缓存时保留
}
查询流程
1. GET asset_likers:{asset_id}
2. 命中 → 从 users 列表中找 liked_at < cursor 的前 page_size 条(cursor=0 时直接取前 page_size 条)
3. 未命中 → 查 DB,LIMIT 1000(最多缓存1000条),写入 Redis
4. 返回数据(users, total, has_more, next_cursor)
游标=0 的处理逻辑(缓存命中时):
- 缓存命中时,无论 cursor 是否为 0,直接从缓存的完整列表中按 cursor 切片
cursor=0时返回第一页(liked_at 最大的前 page_size 条)- 缓存未命中时,
cursor=0才查 DB,且不带游标条件
缓存失效
点赞/取消点赞时删除缓存(应在 LikeAsset 和 UnlikeAsset 方法末尾调用):
// InvalidateAssetLikersCache 删除藏品点赞用户列表缓存
func InvalidateAssetLikersCache(ctx context.Context, assetID int64) error {
if database.RedisClient == nil {
return nil // Redis 未初始化时跳过
}
key := fmt.Sprintf("asset_likers:%d", assetID)
return database.RedisClient.Del(ctx, key).Err()
}
TTL 设计
- TTL = 60 秒(1分钟)
- 理由:藏品点赞延迟 1 分钟可接受,且能有效抗住热点压力
容量规划
| 藏品热度 | 点赞数 | 缓存大小(估算) |
|---|---|---|
| 普通 | 0-500 | ~50KB |
| 热门 | 500-5000 | ~500KB |
| 顶流 | 5000+ | ~5MB(截断到1000条) |
截断策略:最多缓存 1000 条,超出部分不缓存(直接查 DB)
截断边界行为:
- 当某藏品点赞数 > 1000 时,缓存只存储前 1000 条
- 用户翻页 cursor 超出第 1000 条时 → 缓存未命中 → 直接查 DB,不写入缓存
- 第 1000 条以内的翻页请求均命中缓存,保证热点藏品翻页稳定
缓存雪崩防护:
- TTL = 60 秒,保证缓存会自然过期
- 即使热点藏品频繁更新,最坏情况延迟也只有 60 秒
需要修改的文件
backend/pkg/database/redis.go- 添加GetAssetLikersCache和SetAssetLikersCache方法backend/services/socialService/service/asset_like_service.go-GetAssetLikers方法:查缓存 → 缓存未命中查DB → 写入缓存backend/services/socialService/service/asset_like_service.go-LikeAsset/UnlikeAsset方法末尾调用InvalidateAssetLikersCache
5. Proto 定义 (pkg/proto/social/)
新增请求消息:
message GetAssetLikersRequest {
int64 asset_id = 1;
int32 page_size = 2; // 每页数量(默认20,最大100)
int64 cursor = 3; // 游标(上一页最后一条的 created_at,首次请求传0)
}
message GetAssetLikersResponse {
CommonPB base = 1;
repeated AssetLiker users = 2;
int64 total = 3;
bool has_more = 4; // 是否有更多
int64 next_cursor = 5; // 下一页游标
}
message AssetLiker {
int64 user_id = 1;
string nickname = 2;
string avatar = 3;
int32 fan_level = 4;
int64 liked_at = 5;
}
5.1 Repository 层
// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页)
// 注意:JOIN fan_profiles 需要 star_id,查询时从 AssetLike.StarID 获取
GetByAssetWithUsers(assetID int64, starID int64, cursor int64, limit int) ([]*AssetLikeWithUser, error)
// AssetLikeWithUser 点赞记录+用户信息
type AssetLikeWithUser struct {
UserID int64
Nickname string // 来自 fan_profiles.nickname
Avatar string // 来自 users.avatar_url
FanLevel int32 // 来自 fan_profiles.level
LikedAt int64
StarID int64 // 来自 al.star_id,用于 JOIN fan_profiles
}
SQL 查询(游标分页):
SELECT
al.user_id,
al.star_id, -- 用于后续 fan_profiles JOIN,需显式 select
COALESCE(fp.nickname, '匿名用户') as nickname, -- 昵称来自 fan_profiles
u.avatar_url, -- 头像来自 users
COALESCE(fp.level, 1) as fan_level, -- 粉丝等级别名,统一输出为 fan_level
al.created_at as liked_at
FROM asset_likes al
JOIN users u ON al.user_id = u.id
LEFT JOIN fan_profiles fp ON al.user_id = fp.user_id AND al.star_id = fp.star_id -- star_id 来自 al.star_id
WHERE al.asset_id = ?
AND (? = 0 OR al.created_at < ?) -- cursor=0 时跳过游标条件,返回第一页
ORDER BY al.created_at DESC
LIMIT ? -- page_size
注意:
users表无 nickname 字段,昵称来自fan_profiles.nickname- 需要
COALESCE处理用户可能没有 fan_profile 的情况
5.2 Service 层
func (s *AssetLikeService) GetAssetLikers(ctx context.Context, assetID int64, starID int64, cursor int64, pageSize int32) (*GetAssetLikersResponse, error)
starID 来源:
- 从
Asset模型中获取(Asset 与 Star 有关联) - 或从请求上下文/缓存中获取(待实现时确认)
校验逻辑:
- 验证资产是否存在(调用
assetClient.GetAssetForRPC) - 资产不存在返回错误
- 查询缓存 → 命中则直接切片返回
- 未命中 → 查 DB(传入 starID 用于 JOIN fan_profiles)→ 写入缓存 → 返回
5.3 Gateway 层
路由: GET /api/v1/social/assets/:asset_id/likers
请求参数:
asset_id: 路径参数page_size: 查询参数(默认20,最大100)cursor: 查询参数(上一页返回的 next_cursor,首次请求传0)
响应格式:
{
"code": 0,
"message": "success",
"data": {
"users": [
{
"user_id": 123,
"nickname": "用户昵称", // 来自 fan_profiles.nickname
"avatar": "https://...", // 来自 users.avatar_url
"fan_level": 5, // 来自 fan_profiles.level
"liked_at": 1716200000000
}
],
"total": 100,
"has_more": true,
"next_cursor": 1716199999000
}
}
6. 路由注册
在 router.go 中添加:
social.GET("/assets/:asset_id/likers", socialCtrl.GetAssetLikers)
7. 错误处理 [待确认具体错误码]
| 错误场景 | HTTP 状态码 | 错误码 | 错误信息 |
|---|---|---|---|
| 未登录 | 401 | UNAUTHORIZED |
未授权 |
| 资产不存在 | 404 | ASSET_NOT_FOUND |
资产不存在 |
| 服务调用失败 | 500 | INTERNAL_ERROR |
服务调用失败 |
8. 文件清单
需要修改的文件:
backend/scripts/migrate_asset_likes_cursor_pagination.sql- 新增idx_asset_likes_asset_created索引backend/proto/social.proto- 新增 GetAssetLikersRequest/GetAssetLikersResponse/AssetLiker 消息backend/pkg/proto/social/*.go- 重新生成(需执行 protoc 编译)backend/pkg/database/redis.go- 新增 GetAssetLikersCache / SetAssetLikersCache / InvalidateAssetLikersCache 方法backend/services/socialService/repository/social_repository.go- 新增GetByAssetWithUsers(assetID, starID, cursor, limit)接口方法和实现backend/services/socialService/service/asset_like_service.go- 新增 GetAssetLikers 方法(含缓存逻辑),修改 LikeAsset/UnlikeAsset 添加缓存失效backend/services/socialService/provider/social_provider.go- 在 SocialServiceHandler 接口添加 GetAssetLikers 方法注册backend/gateway/controller/social_controller.go- 新增 GetAssetLikers Handlerbackend/gateway/router/router.go- 注册路由GET /assets/:asset_id/likers
9. 实现顺序
- 新增索引(迁移脚本)
- Proto 定义(消息结构)
- Redis 缓存方法(
backend/pkg/database/redis.go) - Repository 层(数据查询)
- 点赞/取消点赞时触发缓存失效(在 Service 层修改前先加,确保无竞态)
- Service 层(业务逻辑 + 缓存读取)
- Provider 层(注册 RPC 方法)
- Controller 层(HTTP 处理)
- 路由注册
10. 数据库迁移
-- 为游标分页新增索引(幂等)
CREATE INDEX IF NOT EXISTS idx_asset_likes_asset_created ON asset_likes(asset_id, created_at DESC);