Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
@@ -0,0 +1,441 @@
# 短视频全局推荐环形队列——后端技术方案
## 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作为短期降级路径。