Files
huangguo_server/SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

442 lines
10 KiB
Markdown
Raw Permalink 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.
# 短视频全局推荐环形队列——后端技术方案
## 1. 文档范围
本文仅覆盖《91porn迭代需求》第一项“短视频推荐逻辑修改”,以当前代码、测试H5和测试管理后台为准。
已确认的产品口径:
- 只推荐审核通过的短视频;
- VIP、金币、广告和特殊视频均可参与推荐,其他状态和内容类型不参与;
- 互动分为 `点赞×1 + 收藏×2 + 评论×3 + 转发×5`
- 使用全部累计真实互动,不使用运营假数据;
- 取消点赞、取消收藏、删除评论等行为不扣推荐累计分;
- 互动分和全局队列每日更新一次;
- 每批固定20条,严格按17条高分视频加3条新视频组织;
- 新视频按审核通过时间24小时计算,并从高分池排除;
- 新视频不足3条时用高分视频补齐;
- 用户偏移存Redis Hash
- 每日新队列切换后所有用户从新队列起点重新开始;
- 队尾通过取模回到队首。
## 2. 现状核对
### 2.1 H5实际调用
测试H5短视频Swiper实际调用:
```http
GET /api/app/recommend/vid/list?pageNumber={n}&pageSize={size}
```
当前响应:
```json
{
"vInfos": [],
"totalPages": 10
}
```
H5还单独调用:
```http
GET /api/app/recommend/vid/ad
GET /api/app/vid/user/count?vid={videoId}
```
广告插入和免费观看次数不并入本次推荐队列算法。
### 2.2 后端现状
主接口位于:
- `app/api/recommctrl/recommctrl.go`
- `app/service/recommser/recommser.go`
当前算法:
```text
Redis Set(short-videos-all-ids-list)
-> SRANDMEMBER N
-> 查询视频详情
-> 返回
```
另有遗留接口:
```http
GET /api/app/vid/module/short/all
```
该接口使用另一个Redis Set `shortVideosRecoCache`,并存在5分钟全局响应缓存。两条接口目前算法和Redis Key均不统一。
### 2.3 管理后台现状
后台已有“视频热度配置”:
```http
GET /api/web/admin/vid/popularity/config
POST /api/web/admin/vid/popularity/config
```
该配置计算播放量、有效播放量、点赞量和审核时间衰减,是通用视频热度,不符合本次固定的互动分算法。因此:
- 本次推荐不能复用现有 `VideoPopularityConfig`
- 不修改现有热度配置页面;
- 一期不新增运营配置页面;
- 推荐权重按产品确认值作为后端常量;
- 灰度开关和任务时间使用服务配置。
## 3. 总体架构
```text
真实互动事件
视频推荐累计字段(只增不减)
每日SKD任务计算recommendScore
生成17+3全局有序队列
完整写入Redis版本Key
原子切换current版本
推荐接口按用户Hash偏移环形读取
```
不在请求时实时计算分数,不为每个用户生成独立队列。
## 4. 数据模型
在视频模型增加:
```go
RecommendLikeCount int64 `json:"-" bson:"recommendLikeCount"`
RecommendCollectCount int64 `json:"-" bson:"recommendCollectCount"`
RecommendCommentCount int64 `json:"-" bson:"recommendCommentCount"`
RecommendShareCount int64 `json:"-" bson:"recommendShareCount"`
RecommendScore int64 `json:"-" bson:"recommendScore"`
RecommendScoreAt time.Time `json:"-" bson:"recommendScoreAt"`
```
计算公式:
```text
recommendScore =
recommendLikeCount
+ recommendCollectCount × 2
+ recommendCommentCount × 3
+ recommendShareCount × 5
```
### 4.1 为什么不能直接使用现有字段
现有 `likeCount/collectCount/commentCount/shareCount` 会在取消或删除时下降,而产品已确认取消行为不扣推荐分。
现有 `fakeLikeCount/fakeCommentCount/fakeShareCount` 包含运营假数据,禁止参与推荐分。
因此需要独立的单调递增推荐累计字段。
### 4.2 互动写入规则
仅在真实行为首次成功时递增:
- 点赞成功:`recommendLikeCount + 1`
- 收藏成功:`recommendCollectCount + 1`
- 评论发布成功:`recommendCommentCount + 1`
- 转发首次有效记录:`recommendShareCount + 1`
现有 `POST /api/app/share/output` 增加可选参数 `videoID`。新客户端分享短视频时必须传入,服务端仅在ID合法且视频当前为审核通过的短视频时累计;旧客户端不传保持原分享能力,但无法归属到具体视频,因此不累计推荐分享分。
以下行为不修改推荐累计字段:
- 取消点赞;
- 取消收藏;
- 删除评论;
- 运营修改假互动;
- 重复请求和重复事件。
每个互动入口必须先利用现有行为唯一性或幂等逻辑确认“首次成功”,再累计推荐字段。
## 5. 历史数据初始化
上线前执行一次幂等脚本:
```text
recommendLikeCount = max(likeCount, 0)
recommendCollectCount = max(collectCount, 0)
recommendCommentCount = max(commentCount, 0)
recommendShareCount = max(shareCount, 0)
```
不读取任何 `fake*` 字段。
脚本增加初始化版本标记,重复执行不得覆盖已经由线上事件增长的新累计值。
## 6. 每日队列生成
任务放在 `skd/job`,使用现有Cron框架。默认按北京时间凌晨低峰运行,具体时间由部署配置确定。
### 6.1 候选条件
```text
status = 审核通过
newsType = 短视频
未删除
```
VIP、金币、广告、特殊视频不作为排除条件。
### 6.2 新视频池
```text
reviewAt >= generatedAt - 24h
reviewAt <= generatedAt
```
排序:
```text
reviewAt desc, _id desc
```
进入新视频池的视频必须从高分池排除,当天队列中只使用一次。
### 6.3 高分池
排序:
```text
recommendScore desc, reviewAt desc, _id desc
```
排除当日新视频池全部ID。
### 6.4 组装算法
每个推荐块:
```text
high = 最多17条
new = 最多3条
缺少的新视频名额由high补齐
块内确定性洗牌
```
要求:
- 同一视频在当日全局队列只出现一次;
- 新视频消费完后,后续块全部由高分视频组成;
- 不复制视频凑足队列长度;
- 尾部不足20条时保留实际数量;
- 洗牌使用日期版本作为种子,便于问题复现。
## 7. Redis设计
```text
recommend:short:current
recommend:short:queue:{version}
recommend:short:offset:{version}
recommend:short:meta:{version}
recommend:short:build-lock:{version}
```
示例:
```text
recommend:short:current -> 20260724
recommend:short:queue:20260724 -> LIST(videoId...)
recommend:short:offset:20260724 -> HASH(uid => nextOffset)
recommend:short:meta:20260724 -> HASH(length, generatedAt, startOffset)
```
规则:
- `queue` 使用Redis List,保持顺序;
- `offset` 使用一个Hash存放全部用户偏移;
- `startOffset` 第一期固定为0
- 新版本先完整写入,再原子切换 `current`
- 新版本Key设置72小时TTL
- 版本切换天然实现每日偏移重置,不扫描删除旧Hash;
- 构建锁避免多个SKD实例同时生成同一版本。
## 8. 环形读取
`offset` 表示下一次读取位置:
```text
newOffset = (offset + scannedCount) % queueLength
```
使用Lua脚本原子完成:
1. 读取当前版本;
2. 读取队列长度;
3. `HGET`用户偏移;
4. 无偏移时使用0
5. 计算本次候选索引;
6. 推进并保存新偏移;
7. 返回版本、旧偏移和候选视频ID。
接口层查询Mongo并再次校验视频状态。发现失效视频时继续向后扫描补位,最多扫描一整圈。
约束:
- 同一响应内同一视频最多出现一次;
- 队列长度小于20时只返回一圈;
- Mongo返回结果必须按Redis ID顺序重新排列;
- 多设备和并发请求共享同一用户偏移。
## 9. App接口改造
### 9.1 H5主接口
保留现有地址:
```http
GET /api/app/recommend/vid/list
```
修改 `recommser.GetVidList`,由随机Set切换到环形队列服务。
### 9.2 兼容接口
保留:
```http
GET /api/app/vid/module/short/all
```
该接口删除5分钟全局响应缓存,并委托同一个环形队列服务读取,避免两个客户端获得不同推荐逻辑。
### 9.3 广告接口
保持不变:
```http
GET /api/app/recommend/vid/ad
```
广告仍由H5在推荐内容之外按现有规则插入,不占17+3名额。
### 9.4 免费观看次数接口关联改动
现有接口:
```http
GET /api/app/vid/user/count
```
传或不传 `vid` 的两个响应分支都增加:
```json
{
"totalWatchCount": 3
}
```
该字段读取现有 `appg.Conf.Base.TotalWatch`,表示系统配置的免费观看总次数;现有 `watchCount` 继续表示用户剩余次数。不新增数据库字段,不修改扣减逻辑。此项已纳入接口设计,但代码尚未修改。
## 10. 响应兼容
H5主接口保留:
```json
{
"vInfos": [],
"totalPages": 100
}
```
新增:
```json
{
"hasNext": true,
"queueVersion": "20260724"
}
```
说明:
- `vInfos`结构不变;
- `totalPages`继续返回 `ceil(queueLength / 20)`,仅兼容旧客户端;
- 环形队列非空时 `hasNext=true`
- 前端不得用 `pageNumber`计算服务端offset
- `queueVersion`仅用于日志排查。
## 11. 灰度、降级和监控
服务配置:
```text
shortRecommendV2Enabled
shortRecommendCron
shortRecommendKeyTTLHours = 72
```
降级顺序:
1. 当日队列不可用时使用上一有效版本;
2. 没有任何版本或Redis异常时,回退现有 `ShortVideosKey` 随机逻辑;
3. 降级必须记录日志和指标。
监控:
- 生成耗时和任务状态;
- 候选、高分、新视频和最终队列数量;
- 每块17+3比例;
- 重复率;
- 接口耗时和空结果率;
- 失效视频跳过数;
- Redis/Lua失败数;
- 版本切换时间;
- 降级次数。
## 12. 索引
建议Mongo索引:
```text
status + newsType + reviewAt
status + newsType + recommendScore + reviewAt
```
根据生产数据量通过 `explain` 确认最终字段顺序。
## 13. 测试
必须覆盖:
- 正常17+3
- 新视频0/1/2/3条;
- 新视频池耗尽;
- 高分池不足;
- 队列不足20条;
- 尾部跨界读取;
- 用户首次进入;
- 不同用户偏移隔离;
- 同用户并发;
- 每日版本重置;
- 队列中视频下架后的补位;
- 假数据不计分;
- 取消互动不扣分;
- 历史初始化幂等;
- SKD重复执行;
- 生成失败继续使用旧队列;
- H5主接口和兼容接口响应结构。
## 14. 发布步骤
1. 发布新增字段和索引;
2. 执行历史累计初始化;
3. 发布互动累计逻辑;
4. 发布SKD任务,影子生成队列但不切流;
5. 核对数量、分数、17+3比例和重复率;
6. 发布App接口兼容字段;
7. 小流量开启 `shortRecommendV2Enabled`
8. 全量切换;
9. 保留旧随机Set作为短期降级路径。