docs: 修改查询藏品设计文档
This commit is contained in:
parent
ee0433464f
commit
36a366e7ba
@ -41,6 +41,8 @@ type AssetLike struct {
|
|||||||
|
|
||||||
**问题**: 没有 JOIN 用户表,需要新增或扩展方法
|
**问题**: 没有 JOIN 用户表,需要新增或扩展方法
|
||||||
|
|
||||||
|
**注意**: `GetByAsset` 方法只需要 `assetID`,但 JOIN fan_profiles 需要 `star_id`。由于 `AssetLike` 模型包含 `StarID` 字段,查询时应以 `AssetLike.StarID` 作为 JOIN 条件传给 fan_profiles。
|
||||||
|
|
||||||
### 2.3 现有 Service 层
|
### 2.3 现有 Service 层
|
||||||
|
|
||||||
`asset_like_service.go` 已有的方法:
|
`asset_like_service.go` 已有的方法:
|
||||||
@ -56,9 +58,9 @@ type AssetLike struct {
|
|||||||
|
|
||||||
**缺失**: 没有查询点赞用户的端点
|
**缺失**: 没有查询点赞用户的端点
|
||||||
|
|
||||||
### 2.5 Proto 定义
|
### 2.5 Proto 定义 [待确认]
|
||||||
|
|
||||||
需要检查 `pkg/proto/social/` 是否有现成的消息定义
|
需要检查 `pkg/proto/social/` 是否有现成的消息定义。如已有可复用消息(如 `AssetLiker`),直接引用;如无则按 Section 5 新增。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -107,22 +109,30 @@ SocialService (Dubbo Provider)
|
|||||||
|
|
||||||
**决策**:**采用游标分页**
|
**决策**:**采用游标分页**
|
||||||
- 理由:藏品点赞列表通常不会很大,但游标分页更健壮
|
- 理由:藏品点赞列表通常不会很大,但游标分页更健壮
|
||||||
- 实现:`cursor` =上一页最后一条的 `liked_at` 值
|
- 实现:`cursor` =上一页最后一条的 `created_at` 值
|
||||||
|
- 首次请求 `cursor=0` 时,查 DB 不带游标条件,返回第一页
|
||||||
|
- `cursor=0` **仅表示不需要 `created_at < ?` 条件**,不表示要重新查 DB
|
||||||
|
|
||||||
### 4.2 索引优化
|
### 4.2 索引优化
|
||||||
|
|
||||||
**已有索引(无需新增)**:
|
**已有索引**:
|
||||||
```sql
|
```sql
|
||||||
-- asset_likes 表
|
-- asset_likes 表
|
||||||
INDEX idx_asset_likes_asset (asset_id, created_at DESC) -- 覆盖查询+排序
|
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); -- 覆盖用户+明星查询
|
||||||
|
```
|
||||||
|
|
||||||
-- fan_profiles 表(已存在)
|
**缺失索引(必须新增)**:
|
||||||
UNIQUE INDEX uk_fan_profiles_user_star (user_id, star_id) -- 覆盖 JOIN
|
```sql
|
||||||
|
-- 游标分页查询: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 查询计划**:
|
**JOIN 查询计划**:
|
||||||
```
|
```
|
||||||
asset_likes ──(idx_asset_likes_asset)──▶ 扫描 asset_id
|
asset_likes ──(idx_asset_likes_asset_created)──▶ 扫描 asset_id + created_at 排序
|
||||||
│
|
│
|
||||||
└──▶ users ──(主键 id)──▶ avatar_url
|
└──▶ users ──(主键 id)──▶ avatar_url
|
||||||
└──▶ fan_profiles ──(uk_fan_profiles_user_star)──▶ nickname, level
|
└──▶ fan_profiles ──(uk_fan_profiles_user_star)──▶ nickname, level
|
||||||
@ -157,17 +167,23 @@ type AssetLikerWithTotal struct {
|
|||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
FanLevel int32 `json:"fan_level"`
|
FanLevel int32 `json:"fan_level"`
|
||||||
LikedAt int64 `json:"liked_at"`
|
LikedAt int64 `json:"liked_at"`
|
||||||
|
StarID int64 `json:"star_id"` // 用于 JOIN fan_profiles,缓存时保留
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 查询流程
|
#### 查询流程
|
||||||
```
|
```
|
||||||
1. GET asset_likers:{asset_id}
|
1. GET asset_likers:{asset_id}
|
||||||
2. 命中 → 从 users 列表中找 liked_at < cursor 的前 page_size 条
|
2. 命中 → 从 users 列表中找 liked_at < cursor 的前 page_size 条(cursor=0 时直接取前 page_size 条)
|
||||||
3. 未命中 → 查 DB,LIMIT 1000(最多缓存1000条),写入 Redis
|
3. 未命中 → 查 DB,LIMIT 1000(最多缓存1000条),写入 Redis
|
||||||
4. 返回数据(users, total, has_more, next_cursor)
|
4. 返回数据(users, total, has_more, next_cursor)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**游标=0 的处理逻辑(缓存命中时)**:
|
||||||
|
- 缓存命中时,无论 cursor 是否为 0,直接从缓存的完整列表中按 cursor 切片
|
||||||
|
- `cursor=0` 时返回第一页(liked_at 最大的前 page_size 条)
|
||||||
|
- 缓存未命中时,`cursor=0` 才查 DB,且不带游标条件
|
||||||
|
|
||||||
#### 缓存失效
|
#### 缓存失效
|
||||||
点赞/取消点赞时删除缓存(应在 `LikeAsset` 和 `UnlikeAsset` 方法末尾调用):
|
点赞/取消点赞时删除缓存(应在 `LikeAsset` 和 `UnlikeAsset` 方法末尾调用):
|
||||||
```go
|
```go
|
||||||
@ -194,14 +210,19 @@ func InvalidateAssetLikersCache(ctx context.Context, assetID int64) error {
|
|||||||
|
|
||||||
**截断策略**:最多缓存 1000 条,超出部分不缓存(直接查 DB)
|
**截断策略**:最多缓存 1000 条,超出部分不缓存(直接查 DB)
|
||||||
|
|
||||||
|
**截断边界行为**:
|
||||||
|
- 当某藏品点赞数 > 1000 时,缓存只存储前 1000 条
|
||||||
|
- 用户翻页 cursor 超出第 1000 条时 → 缓存未命中 → 直接查 DB,不写入缓存
|
||||||
|
- 第 1000 条以内的翻页请求均命中缓存,保证热点藏品翻页稳定
|
||||||
|
|
||||||
**缓存雪崩防护**:
|
**缓存雪崩防护**:
|
||||||
- TTL = 60 秒,保证缓存会自然过期
|
- TTL = 60 秒,保证缓存会自然过期
|
||||||
- 即使热点藏品频繁更新,最坏情况延迟也只有 60 秒
|
- 即使热点藏品频繁更新,最坏情况延迟也只有 60 秒
|
||||||
|
|
||||||
#### 需要修改的文件
|
#### 需要修改的文件
|
||||||
1. `pkg/database/redis.go` - 添加 `GetAssetLikersCache` 和 `SetAssetLikersCache` 方法
|
1. `backend/pkg/database/redis.go` - 添加 `GetAssetLikersCache` 和 `SetAssetLikersCache` 方法
|
||||||
2. `services/socialService/service/asset_like_service.go` - `GetAssetLikers` 方法:查缓存 → 缓存未命中查DB → 写入缓存
|
2. `backend/services/socialService/service/asset_like_service.go` - `GetAssetLikers` 方法:查缓存 → 缓存未命中查DB → 写入缓存
|
||||||
3. `services/socialService/service/asset_like_service.go` - `LikeAsset` / `UnlikeAsset` 方法末尾调用 `InvalidateAssetLikersCache`
|
3. `backend/services/socialService/service/asset_like_service.go` - `LikeAsset` / `UnlikeAsset` 方法末尾调用 `InvalidateAssetLikersCache`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -212,7 +233,7 @@ func InvalidateAssetLikersCache(ctx context.Context, assetID int64) error {
|
|||||||
message GetAssetLikersRequest {
|
message GetAssetLikersRequest {
|
||||||
int64 asset_id = 1;
|
int64 asset_id = 1;
|
||||||
int32 page_size = 2; // 每页数量(默认20,最大100)
|
int32 page_size = 2; // 每页数量(默认20,最大100)
|
||||||
int64 cursor = 3; // 游标(上一页最后一条的 liked_at)
|
int64 cursor = 3; // 游标(上一页最后一条的 created_at,首次请求传0)
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetAssetLikersResponse {
|
message GetAssetLikersResponse {
|
||||||
@ -236,7 +257,8 @@ message AssetLiker {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页)
|
// GetByAssetWithUsers 获取资产的点赞用户列表(带用户信息,游标分页)
|
||||||
GetByAssetWithUsers(assetID int64, cursor int64, limit int) ([]*AssetLikeWithUser, error)
|
// 注意:JOIN fan_profiles 需要 star_id,查询时从 AssetLike.StarID 获取
|
||||||
|
GetByAssetWithUsers(assetID int64, starID int64, cursor int64, limit int) ([]*AssetLikeWithUser, error)
|
||||||
|
|
||||||
// AssetLikeWithUser 点赞记录+用户信息
|
// AssetLikeWithUser 点赞记录+用户信息
|
||||||
type AssetLikeWithUser struct {
|
type AssetLikeWithUser struct {
|
||||||
@ -245,6 +267,7 @@ type AssetLikeWithUser struct {
|
|||||||
Avatar string // 来自 users.avatar_url
|
Avatar string // 来自 users.avatar_url
|
||||||
FanLevel int32 // 来自 fan_profiles.level
|
FanLevel int32 // 来自 fan_profiles.level
|
||||||
LikedAt int64
|
LikedAt int64
|
||||||
|
StarID int64 // 来自 al.star_id,用于 JOIN fan_profiles
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -252,14 +275,16 @@ type AssetLikeWithUser struct {
|
|||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
al.user_id,
|
al.user_id,
|
||||||
|
al.star_id, -- 用于后续 fan_profiles JOIN,需显式 select
|
||||||
COALESCE(fp.nickname, '匿名用户') as nickname, -- 昵称来自 fan_profiles
|
COALESCE(fp.nickname, '匿名用户') as nickname, -- 昵称来自 fan_profiles
|
||||||
u.avatar_url, -- 头像来自 users
|
u.avatar_url, -- 头像来自 users
|
||||||
COALESCE(fp.level, 1) as fan_level, -- 粉丝等级来自 fan_profiles
|
COALESCE(fp.level, 1) as fan_level, -- 粉丝等级别名,统一输出为 fan_level
|
||||||
al.created_at as liked_at
|
al.created_at as liked_at
|
||||||
FROM asset_likes al
|
FROM asset_likes al
|
||||||
JOIN users u ON al.user_id = u.id
|
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
|
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 al.created_at < ? -- 游标条件
|
WHERE al.asset_id = ?
|
||||||
|
AND (? = 0 OR al.created_at < ?) -- cursor=0 时跳过游标条件,返回第一页
|
||||||
ORDER BY al.created_at DESC
|
ORDER BY al.created_at DESC
|
||||||
LIMIT ? -- page_size
|
LIMIT ? -- page_size
|
||||||
```
|
```
|
||||||
@ -270,14 +295,18 @@ LIMIT ? -- page_size
|
|||||||
### 5.2 Service 层
|
### 5.2 Service 层
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *AssetLikeService) GetAssetLikers(ctx context.Context, assetID int64, cursor int64, pageSize int32) (*GetAssetLikersResponse, error)
|
func (s *AssetLikeService) GetAssetLikers(ctx context.Context, assetID int64, starID int64, cursor int64, pageSize int32) (*GetAssetLikersResponse, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**starID 来源**:
|
||||||
|
- 从 `Asset` 模型中获取(Asset 与 Star 有关联)
|
||||||
|
- 或从请求上下文/缓存中获取(待实现时确认)
|
||||||
|
|
||||||
**校验逻辑**:
|
**校验逻辑**:
|
||||||
1. 验证资产是否存在(调用 `assetClient.GetAssetForRPC`)
|
1. 验证资产是否存在(调用 `assetClient.GetAssetForRPC`)
|
||||||
2. 资产不存在返回错误
|
2. 资产不存在返回错误
|
||||||
3. 查询缓存 → 命中则直接切片返回
|
3. 查询缓存 → 命中则直接切片返回
|
||||||
4. 未命中 → 查 DB → 写入缓存 → 返回
|
4. 未命中 → 查 DB(传入 starID 用于 JOIN fan_profiles)→ 写入缓存 → 返回
|
||||||
|
|
||||||
### 5.3 Gateway 层
|
### 5.3 Gateway 层
|
||||||
|
|
||||||
@ -321,37 +350,48 @@ social.GET("/assets/:asset_id/likers", socialCtrl.GetAssetLikers)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 错误处理
|
## 7. 错误处理 [待确认具体错误码]
|
||||||
|
|
||||||
| 错误场景 | HTTP 状态码 | 错误码 | 错误信息 |
|
| 错误场景 | HTTP 状态码 | 错误码 | 错误信息 |
|
||||||
|---------|------------|--------|---------|
|
|---------|------------|--------|---------|
|
||||||
| 未登录 | 401 | - | 未授权 |
|
| 未登录 | 401 | `UNAUTHORIZED` | 未授权 |
|
||||||
| 资产不存在 | 404 | - | 资产不存在 |
|
| 资产不存在 | 404 | `ASSET_NOT_FOUND` | 资产不存在 |
|
||||||
| 服务调用失败 | 500 | - | 服务调用失败 |
|
| 服务调用失败 | 500 | `INTERNAL_ERROR` | 服务调用失败 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 文件清单
|
## 8. 文件清单
|
||||||
|
|
||||||
需要修改的文件:
|
需要修改的文件:
|
||||||
1. `backend/proto/social.proto` - 新增 GetAssetLikersRequest/GetAssetLikersResponse/AssetLiker 消息
|
1. `backend/scripts/migrate_asset_likes_cursor_pagination.sql` - 新增 `idx_asset_likes_asset_created` 索引
|
||||||
2. `backend/pkg/proto/social/*.go` - 重新生成(需执行 protoc 编译)
|
2. `backend/proto/social.proto` - 新增 GetAssetLikersRequest/GetAssetLikersResponse/AssetLiker 消息
|
||||||
3. `backend/pkg/database/redis.go` - 新增 GetAssetLikersCache / SetAssetLikersCache / InvalidateAssetLikersCache 方法
|
3. `backend/pkg/proto/social/*.go` - 重新生成(需执行 protoc 编译)
|
||||||
4. `backend/services/socialService/repository/social_repository.go` - 新增 GetAssetLikersWithUsers 接口方法和实现
|
4. `backend/pkg/database/redis.go` - 新增 GetAssetLikersCache / SetAssetLikersCache / InvalidateAssetLikersCache 方法
|
||||||
5. `backend/services/socialService/service/asset_like_service.go` - 新增 GetAssetLikers 方法(含缓存逻辑),修改 LikeAsset/UnlikeAsset 添加缓存失效
|
5. `backend/services/socialService/repository/social_repository.go` - 新增 `GetByAssetWithUsers(assetID, starID, cursor, limit)` 接口方法和实现
|
||||||
6. `backend/services/socialService/provider/social_provider.go` - 在 SocialServiceHandler 接口添加 GetAssetLikers 方法注册
|
6. `backend/services/socialService/service/asset_like_service.go` - 新增 GetAssetLikers 方法(含缓存逻辑),修改 LikeAsset/UnlikeAsset 添加缓存失效
|
||||||
7. `backend/gateway/controller/social_controller.go` - 新增 GetAssetLikers Handler
|
7. `backend/services/socialService/provider/social_provider.go` - 在 SocialServiceHandler 接口添加 GetAssetLikers 方法注册
|
||||||
8. `backend/gateway/router/router.go` - 注册路由 `GET /assets/:asset_id/likers`
|
8. `backend/gateway/controller/social_controller.go` - 新增 GetAssetLikers Handler
|
||||||
|
9. `backend/gateway/router/router.go` - 注册路由 `GET /assets/:asset_id/likers`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 实现顺序
|
## 9. 实现顺序
|
||||||
|
|
||||||
1. Proto 定义(消息结构)
|
1. 新增索引(迁移脚本)
|
||||||
2. Redis 缓存方法(`pkg/database/redis.go`)
|
2. Proto 定义(消息结构)
|
||||||
3. Repository 层(数据查询)
|
3. Redis 缓存方法(`backend/pkg/database/redis.go`)
|
||||||
4. Service 层(业务逻辑 + 缓存读取)
|
4. Repository 层(数据查询)
|
||||||
5. Provider 层(注册 RPC 方法)
|
5. **点赞/取消点赞时触发缓存失效**(在 Service 层修改前先加,确保无竞态)
|
||||||
6. Controller 层(HTTP 处理)
|
6. Service 层(业务逻辑 + 缓存读取)
|
||||||
7. 路由注册
|
7. Provider 层(注册 RPC 方法)
|
||||||
8. 点赞/取消点赞时触发缓存失效
|
8. Controller 层(HTTP 处理)
|
||||||
|
9. 路由注册
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 数据库迁移
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 为游标分页新增索引(幂等)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_asset_likes_asset_created ON asset_likes(asset_id, created_at DESC);
|
||||||
|
```
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user