topfans/docs/superpowers/specs/2026-05-27-热门推荐模块设计.md

476 lines
15 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.

# 热门推荐模块设计方案
## 一、需求概述
在广场页面顶部新增 4 个热门分类区块,每个分类显示 8 张高点赞作品,支持单个分类刷新和查看更多分页。
### 页面结构
```
┌─────────────────────────────────┐
│ BannerCarousel │
├─────────────────────────────────┤
│ 热门推荐 (8张图) │
│ [刷新] [查看更多 >] │
├─────────────────────────────────┤
│ 热门星卡 (8张图) │
│ [刷新] [查看更多 >] │
├─────────────────────────────────┤
│ 热门吧唧 (8张图) │
│ [刷新] [查看更多 >] │
├─────────────────────────────────┤
│ 热门海报 (8张图) │
│ [刷新] [查看更多 >] │
├─────────────────────────────────┤
│ (CreationGrid 组件 - 不变) │
│ [热门作品] [最新作品] [星卡] ... │
└─────────────────────────────────┘
```
### 分类配置
| 区块 | type值 | 说明 |
|-----|-------|------|
| 热门推荐 | `hot_recommend` | 混合所有类型,高点赞作品 |
| 热门星卡 | `hot_star_card` | 只展示星卡 |
| 热门吧唧 | `hot_badge` | 只展示吧唧 |
| 热门海报 | `hot_poster` | 只展示海报 |
---
## 二、接口设计
### 2.1 接口清单
| 接口 | 方法 | 参数 | 说明 |
|-----|------|------|------|
| `/api/v1/inspiration-flow/hot/batch` | GET | 无 | 页面加载批量获取 |
| `/api/v1/inspiration-flow/hot` | GET | `type` | 单个分类刷新 |
| `/api/v1/inspiration-flow/hot/more` | GET | `type`, `cursor`, `limit` | 查看更多分页 |
| `/api/v1/inspiration-flow` | GET | `type`, `cursor`, `limit`, `direction` | 灵感瀑布流(逻辑调整) |
### 2.2 调用时序
```
页面加载 → 批量请求1次
└── GET /api/v1/inspiration-flow/hot/batch
刷新 → 单个分类刷新(点击哪个刷新请求哪个)
└── GET /api/v1/inspiration-flow/hot?type=hot_star_card
查看更多 → 新页面分页
└── GET /api/v1/inspiration-flow/hot/more?type=hot_star_card&cursor=xxx&limit=20
```
### 2.3 批量获取热门分类
**接口**: `GET /api/v1/inspiration-flow/hot/batch`
**请求参数**: 无
**业务逻辑**:
1. 计算各分类作品的点赞平均值
2. 筛选点赞数 ≥ 平均值的作品
3. 基于时间窗口伪随机排序取 8 条
4. 后端动态返回分类数据,前端根据返回的分类动态渲染
5. **排序说明**:使用随机排序,同一分类每次请求返回的作品可能不同
**响应结构**:
```json
{
"code": 200,
"data": {
"categories": [
{
"type": "hot_recommend",
"title": "热门推荐",
"items": [
{
"asset_id": "xxx",
"cover_url": "https://xxx.jpg",
"owner_nickname": "用户名",
"owner_avatar": "https://xxx.jpg",
"likes": 1234
}
]
},
{
"type": "hot_star_card",
"title": "热门星卡",
"items": [...]
},
{
"type": "hot_badge",
"title": "热门吧唧",
"items": [...]
},
{
"type": "hot_poster",
"title": "热门海报",
"items": [...]
}
]
}
}
```
**空分类处理**: 如果某个分类没有作品或查询失败,该分类不返回。
### 2.4 单个分类刷新
**接口**: `GET /api/v1/inspiration-flow/hot`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| `type` | string | 是 | `hot_recommend` / `hot_star_card` / `hot_badge` / `hot_poster` |
**业务逻辑**:
1. 计算该分类作品的点赞平均值
2. 筛选点赞数 ≥ 平均值的作品
3. 基于时间窗口伪随机排序取 8 条
4. **排序说明**:批量和刷新接口使用随机排序,每次刷新可能看到不同作品
**响应结构**:
```json
{
"code": 200,
"data": {
"type": "hot_star_card",
"title": "热门星卡",
"items": [...]
}
}
```
### 2.5 查看更多分页
**接口**: `GET /api/v1/inspiration-flow/hot/more`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| `type` | string | 是 | 分类类型(`hot_recommend`/`hot_star_card`/`hot_badge`/`hot_poster` |
| `cursor` | string | 否 | 翻页游标Base64 编码的 JSON包含 `like_count``asset_id` |
| `limit` | int | 否 | 每页数量,默认 20 |
**业务逻辑**:
1. 计算该分类作品点赞平均值
2. 筛选点赞数 ≥ 平均值的作品
3. 按点赞数 DESC、asset_id DESC 排序
4. Cursor 分页返回(游标编码 last_like_count 和 last_asset_id
**Cursor 编码格式**:
```json
// Base64 编码 {"like_count": 1234, "asset_id": 5678}
```
**响应结构**:
```json
{
"code": 200,
"data": {
"items": [
{
"asset_id": "xxx",
"cover_url": "https://xxx.jpg",
"owner_nickname": "用户名",
"owner_avatar": "https://xxx.jpg",
"likes": 1234,
"material_type": "star_card",
"sub_type": "raster"
}
],
"cursor": "xxx",
"has_more": true
}
}
```
**注意**`material_type` 表示素材类型(`star_card`/`badge`/`poster`),不是热门分类类型。
### 2.6 灵感瀑布流(逻辑调整)
**接口**: `GET /api/v1/inspiration-flow`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| `type` | string | 否 | 素材类型过滤:`badge`/`poster`/`original`/`all`,默认 `all` |
| `cursor` | string | 否 | 翻页游标 |
| `direction` | string | 否 | 滚动方向:`right`(加载新数据)/ `left`(加载历史) |
| `limit` | int | 否 | 每页数量,默认 20 |
| `session_id` | string | 否 | 会话 ID用于去重排除已展示的作品 |
**业务逻辑(调整后)**:
1. 计算该分类作品的点赞平均值(基于展出中的作品 JOIN assets
2. **第一段**:点赞 > 平均值的作品,基于时间窗口伪随机排列,取前 20 条
3. **第二段**:如果不够 20 条,从点赞 ≥ 平均值的作品中(排除第一段已选的)补充,伪随机排列
4. 两段组合返回
5. 通过 `session_id` 排除已展示的作品(`excludeIDs`
**响应结构**:
```json
{
"code": 200,
"data": {
"items": [...],
"cursor": "xxx",
"has_more": true
}
}
```
**注意**:灵感瀑布的 `type` 参数与热门推荐接口的 `type` 参数含义完全不同:
- 灵感瀑布:`badge`/`poster`/`original`/`all`(素材类型过滤)
- 热门推荐:`hot_recommend`/`hot_star_card`/`hot_badge`/`hot_poster`(分类类型)
### 2.7 热门分类 type 与资产类型映射
| 热门分类 type | 对应 assetType | 说明 |
|-------------|---------------|------|
| `hot_recommend` | 空字符串 `""` | 混合所有类型,不过滤 |
| `hot_star_card` | `star_card` | 只展示星卡 |
| `hot_badge` | `badge` | 只展示吧唧 |
| `hot_poster` | `poster` | 只展示海报 |
---
## 三、数据模型
### 3.1 数据库表结构
参考现有 `assets` 表,无需新建表。
### 3.2 伪 SQL以 star_card 为例)
**说明**:热门作品基于**展出中的作品**筛选,只有在 `exhibitions` 表中有有效展出记录且未过期的作品才能参与热门排名。
```sql
-- 计算该分类作品的点赞平均值(基于展出中的作品)
AVG_LIKES := SELECT AVG(a.like_count)
FROM exhibitions e
JOIN assets a ON a.id = e.asset_id
WHERE e.occupier_star_id = :star_id
AND e.expire_at > :now
AND e.deleted_at IS NULL
AND a.status = 1 AND a.is_active = true
AND a.asset_type = 'star_card';
-- 随机取8条基于时间窗口的伪随机避免 ORDER BY RANDOM()
WINDOW_SEED := :now / 30000 % 10; -- 每30秒变化一次
SELECT e.id as exhibition_id, e.asset_id, a.name, a.cover_url, a.like_count,
fp.nickname as owner_nickname, fp.avatar_url as owner_avatar
FROM exhibitions e
JOIN assets a ON a.id = e.asset_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 = :star_id
AND e.expire_at > :now
AND e.deleted_at IS NULL
AND a.status = 1 AND a.is_active = true
AND a.asset_type = 'star_card'
AND a.like_count >= :AVG_LIKES
AND e.id % 10 = :WINDOW_SEED
ORDER BY e.id
LIMIT 8;
```
### 3.3 随机算法说明
避免 `ORDER BY RANDOM()`(大表性能差),改用基于时间窗口的伪随机:
- `WINDOW_SEED = now / 30000 % 10` — 每 30 秒变化一次
- 通过 `exhibition_id % 10 = WINDOW_SEED` 筛选作品
- 同一时间窗口内结果固定,不同时间窗口返回不同作品
- 如果数量不够,再补充查询(不加窗口过滤)
---
## 四、后端实现
### 4.1 文件改动
| 文件 | 改动内容 |
|-----|---------|
| `backend/proto/gallery.proto` | 新增 Proto 消息定义 |
| `backend/pkg/proto/gallery/gallery.pb.go` | 重新生成 Proto 代码 |
| `backend/gateway/controller/gallery_controller.go` | 新增 `GetHotInspirationFlowBatch`、`GetHotInspirationFlow`、`GetHotInspirationFlowMore` 方法 |
| `backend/gateway/router/router.go` | 新增 `/inspiration-flow/hot/*` 路由注册 |
| `backend/gateway/dto/gallery_dto.go` | 新增 DTO 结构体 |
| `backend/gateway/dto/gallery_converter.go` | 新增 DTO 转换函数 |
| `backend/services/galleryService/service/gallery_service.go` | 新增 `GetHotInspirationFlowBatch`、`GetHotInspirationFlow`、`GetHotInspirationFlowMore` Service 方法 |
| `backend/services/galleryService/repository/gallery_repository.go` | 新增 Repository 方法 |
| `backend/pkg/database/redis.go` | 新增热门推荐缓存辅助函数 |
### 4.2 性能优化
1. **并行查询** — 批量接口用 goroutine 并行查 4 个分类
2. **缓存平均值** — 每个分类的点赞均值独立缓存TTL: 5 分钟),避免重复计算
3. **结果缓存** — 热门列表缓存 30-60 秒,降低数据库压力
4. **随机优化** — 避免 `ORDER BY RANDOM()`,使用基于时间窗口的伪随机
### 4.3 缓存策略
| 缓存项 | TTL | 说明 |
|-------|-----|------|
| 分类点赞均值 | 5 分钟 | 每个分类单独缓存,减少重复计算平均值 |
| 热门列表(批量) | 30 秒 | `/hot/batch` 结果缓存,降低数据库压力 |
| 热门列表(单个刷新) | **不缓存** | 用户主动刷新应获取新数据,只缓存点赞均值 |
---
## 五、前端实现
### 5.1 文件结构
```
frontend/pages/square/
├── square.vue # 修改集成4个热门分类区块
├── components/
│ └── HotCategoryBlock.vue # 新增:单个热门分类区块组件
└── hot-category-more.vue # 新增:热门分类查看更多页面
```
### 5.2 新增文件
| 文件 | 说明 |
|-----|------|
| `pages/square/components/HotCategoryBlock.vue` | 单个热门分类区块组件 |
| `pages/square/hot-category-more.vue` | 热门分类查看更多页面 |
### 5.3 修改文件
| 文件 | 改动内容 |
|-----|---------|
| `frontend/pages/square/square.vue` | 顶部新增 4 个 HotCategoryBlock |
| `frontend/utils/api.js` | 新增批量获取、单个刷新、查看更多 API |
### 5.4 HotCategoryBlock 组件
```
HotCategoryBlock.vue
├── props: { categoryType } // 传入后端返回的 type如 "hot_star_card"
├── 状态: items, loading, refreshing, title
├── 方法: handleRefresh(), handleViewMore()
└── 模板:
├── 标题 (title)
├── 4x2 网格 (items) + loading 骨架屏
├── 刷新按钮 (loading 状态)
└── 查看更多按钮
```
**说明**
- `categoryType` 直接使用后端返回的 type`hot_star_card`
- 刷新时调用 `getHotInspirationFlowApi(categoryType)`
- 查看更多时调用 `getHotInspirationFlowMoreApi(categoryType, ...)`
**刷新交互**:
- 整个区块显示骨架屏 loading
- 刷新按钮显示 loading 状态
- 刷新完成后正常显示
### 5.5 hot-category-more.vue 页面
```
hot-category-more.vue
├── onLoad: 获取 type 参数
├── 状态: items, cursor, loading, hasMore, title
├── 方法: loadMore(), loadData()
└── 模板:
├── 返回按钮 + 标题
└── 分页网格列表
```
**星卡子标签**(仅星卡分类显示):
| 子标签 | value | 说明 |
|-------|-------|------|
| 全部 | `all` | 全部星卡 |
| 光栅卡 | `raster` | 光栅卡类型 |
| 镭射卡 | `holographic` | 镭射卡类型 |
| 撕拉卡 | `tear_off` | 撕拉卡类型 |
| 拍立得 | `polaroid` | 拍立得类型 |
### 5.6 API 封装
```javascript
// 批量获取热门分类(页面加载用)
export function getHotInspirationFlowBatchApi() {
return request({
url: '/api/v1/inspiration-flow/hot/batch',
method: 'GET'
})
}
// 单个分类刷新
export function getHotInspirationFlowApi(type) {
return request({
url: '/api/v1/inspiration-flow/hot',
method: 'GET',
data: { type }
})
}
// 查看更多分页
export function getHotInspirationFlowMoreApi(type, cursor = '', limit = 20) {
return request({
url: '/api/v1/inspiration-flow/hot/more',
method: 'GET',
data: { type, cursor, limit }
})
}
```
**调用示例**
```javascript
// 批量加载(页面初始化)
const res = await getHotInspirationFlowBatchApi()
res.data.categories.forEach(cat => {
// cat.type: "hot_recommend", cat.title: "热门推荐", cat.items: [...]
console.log(cat.type, cat.items.length)
})
// 单个刷新(点击刷新按钮)
const refreshRes = await getHotInspirationFlowApi('hot_star_card')
// 查看更多(跳转 hot-category-more.vue
uni.navigateTo({
url: `/pages/square/hot-category-more?type=${type}&title=${encodeURIComponent(title)}`
})
```
---
## 六、实现步骤
### 后端
- [ ] **Phase 1**: 新增 `/api/v1/inspiration-flow/hot/batch` 批量接口(并行查询 + 缓存)
- [ ] **Phase 2**: 新增 `/api/v1/inspiration-flow/hot` 单个分类接口
- [ ] **Phase 3**: 新增 `/api/v1/inspiration-flow/hot/more` 查看更多接口
- [ ] **Phase 4**: 修改 `/api/v1/inspiration-flow` 逻辑(分段填充)
### 前端
- [ ] **Phase 5**: 新增 `utils/api.js` API 方法
- [ ] **Phase 6**: 新增 `HotCategoryBlock.vue` 组件
- [ ] **Phase 7**: 新增 `hot-category-more.vue` 页面
- [ ] **Phase 8**: 修改 `square.vue` 集成 4 个热门分类区块
- [ ] **Phase 9**: CreationGrid 保持不变
### 联调
- [ ] **Phase 10**: 前后端联调测试
---
## 七、注意事项
1. **后端动态返回分类** — 前端无需硬编码分类,根据接口返回的 `categories` 动态渲染
2. **空分类不显示** — 如果某个分类没有作品,该分类不返回或前端忽略
3. **扩展性** — 后续增加新分类(如"热门贴纸"),只需后端调整,前端无需改动
4. **性能优先** — 后端必须做好并行查询和缓存优化,确保批量接口响应时间 < 200ms