15 KiB
15 KiB
热门推荐模块设计方案
一、需求概述
在广场页面顶部新增 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
请求参数: 无
业务逻辑:
- 计算各分类作品的点赞平均值
- 筛选点赞数 ≥ 平均值的作品
- 基于时间窗口伪随机排序取 8 条
- 后端动态返回分类数据,前端根据返回的分类动态渲染
- 排序说明:使用随机排序,同一分类每次请求返回的作品可能不同
响应结构:
{
"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 |
业务逻辑:
- 计算该分类作品的点赞平均值
- 筛选点赞数 ≥ 平均值的作品
- 基于时间窗口伪随机排序取 8 条
- 排序说明:批量和刷新接口使用随机排序,每次刷新可能看到不同作品
响应结构:
{
"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 |
业务逻辑:
- 计算该分类作品点赞平均值
- 筛选点赞数 ≥ 平均值的作品
- 按点赞数 DESC、asset_id DESC 排序
- Cursor 分页返回(游标编码 last_like_count 和 last_asset_id)
Cursor 编码格式:
// Base64 编码 {"like_count": 1234, "asset_id": 5678}
响应结构:
{
"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(用于去重,排除已展示的作品) |
业务逻辑(调整后):
- 计算该分类作品的点赞平均值(基于展出中的作品 JOIN assets)
- 第一段:点赞 > 平均值的作品,基于时间窗口伪随机排列,取前 20 条
- 第二段:如果不够 20 条,从点赞 ≥ 平均值的作品中(排除第一段已选的)补充,伪随机排列
- 两段组合返回
- 通过
session_id排除已展示的作品(excludeIDs)
响应结构:
{
"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 表中有有效展出记录且未过期的作品才能参与热门排名。
-- 计算该分类作品的点赞平均值(基于展出中的作品)
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 性能优化
- 并行查询 — 批量接口用 goroutine 并行查 4 个分类
- 缓存平均值 — 每个分类的点赞均值独立缓存(TTL: 5 分钟),避免重复计算
- 结果缓存 — 热门列表缓存 30-60 秒,降低数据库压力
- 随机优化 — 避免
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 封装
// 批量获取热门分类(页面加载用)
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 }
})
}
调用示例:
// 批量加载(页面初始化)
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.jsAPI 方法 - Phase 6: 新增
HotCategoryBlock.vue组件 - Phase 7: 新增
hot-category-more.vue页面 - Phase 8: 修改
square.vue集成 4 个热门分类区块 - Phase 9: CreationGrid 保持不变
联调
- Phase 10: 前后端联调测试
七、注意事项
- 后端动态返回分类 — 前端无需硬编码分类,根据接口返回的
categories动态渲染 - 空分类不显示 — 如果某个分类没有作品,该分类不返回或前端忽略
- 扩展性 — 后续增加新分类(如"热门贴纸"),只需后端调整,前端无需改动
- 性能优先 — 后端必须做好并行查询和缓存优化,确保批量接口响应时间 < 200ms