# 热门推荐模块设计方案 ## 一、需求概述 在广场页面顶部新增 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