Files
huangguo_server/91迭代后端技术方案.md
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

232 lines
13 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.
# 91迭代后端技术方案
适用分支:`feat/91-iteration`
本文覆盖本次迭代的后端整体改造。短视频环形队列的底层细节另见
`SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md`,前端对接以 `短视频推荐接口.md` 为准。
## 1. 总体架构
本次不新增独立服务、Kafka Topic、MQ、Redis实例、端口或操作系统crontab,继续使用现有三个发布单元:
- `app`:App/H5接口、推荐读取、观看次数、付费引导、AI女友等。
- `web`:亚模块、付费引导、评论Banner、VIP A/B等管理接口。
- `skd`:短视频推荐分刷新与全局队列构建。
数据存储沿用MongoDB与Redis。观看次数消费、AI女友上下分和VIP实验发布使用MongoDB事务,生产MongoDB必须支持事务。
## 2. 短视频全局推荐
### 2.1 推荐分
视频增加单调递增的真实互动计数和每日计算分:
```text
recommendScore = recommendLikeCount
+ recommendCollectCount * 2
+ recommendCommentCount * 3
+ recommendShareCount * 5
```
真实互动成功新增时进入累计:点赞、收藏每次从未激活变为激活时增加,
评论每次成功发布时增加;取消或删除不回退,不计入 `fake*` 数据。
分享按用户+视频+北京自然日去重,可选 `eventId` 用于跨日重试幂等。
SKD使用Mongo游标固定高水位分批扫描,有界worker并发计算和BulkWrite。历史数据不依赖一次性推荐分脚本:刷新时对每项真实计数取
`max(推荐累计, 历史真实计数, 0)`,并写入 `recommendInitialized=true`。生产首次写入量受 `maxInitializationWrites` 保护。
### 2.2 队列构建
候选仅包含当前审核通过、`newsType=SHORT`、未删除、`recoWeight!=-1`、未被亚模块配置排除的视频。
- 新视频:按审核通过时间计算24小时,从高分池排除。
- 每块:严格17条高分+3条新视频,新视频不足时用高分补足。
- 去重:同一队列中视频不重复,尾块不足20条保留实际数量。
- 定时:SKD启动时及默认每10分钟做健康检查;当天队列健康时不重复生成,缺失或不健康才重建。
新队列通过临时building key分批Pipeline写入,验证完整后原子切换 `current`。版本为 `YYYYMMDD-<revision>`,切版后新的offset Hash天然使全体用户从起点开始。
### 2.3 请求侧环形消费
推荐接口不使用pageNumber计算服务端位置。每个用户的下一读取位置存在当前版本的Redis Hash中,队尾按取模回绕。
读取使用 `Reserve -> Mongo分批校验/补位 -> Commit` 协议:
- Reserve阶段不立即推进offset,同一用户并发请求由15秒租约串行化。
- 只按实际扫描前缀Commit,查询失败或取消时Abort。
- 只有客户端显式传入、Trim后非空且不超过128字节的 `X-Request-ID`
才对应commit receipt,解决“Redis已提交但HTTP响应丢失”的重试幂等;
中间件自动生成并回传的request ID不参与此业务幂等。
- Mongo按有界批次取数,扫描量受队列长度、`scanMultiplier``maxBatches` 共同限制,避免失效视频导致无界扫描。
主接口 `/api/app/recommend/vid/list` 和兼容接口 `/api/app/vid/module/short/all`
复用同一队列服务。只有尚未成功Reserve、返回的 `QueueVersion` 为空且非请求取消类故障时,
才使用原随机Set降级。Reserve成功后的Mongo校验或Commit失败会Abort并返回错误,
不再切到随机集;请求取消或超时在两个入口中保留返回空成功结果的兼容分支。
## 3. 首页随机刷新与亚模块治理
`module_conf` 增加稳定能力字段,前端不再根据可编辑标题判断:
- `haiJiaoStyle.sortRules[].refreshMode=RANDOM_TOP_N`
- `randomCandidateN=30`
- `onlineAt/offlineAt`
- `excludeLatest/excludeRecommend`
- `searchOnlyWhenInactive`
历史 `val=2` 在没有新字段时由后端兼容归一为随机刷新。新刷新接口从热门候选前30条中随机返回,`refreshToken` 保证同一次重试顺序稳定。
亚模块元数据使用15秒进程内不可变快照和singleflight。管理后台变更后会清理共享缓存和当前进程快照;
其他App进程最多延迟约15秒生效。`excludeLatest``excludeRecommend`
分别只控制“最新”和“推荐”场景。只有当亚模块当前失效且
`searchOnlyWhenInactive=true` 时,才拦截搜索以外的普通入口、分享和直接详情;
搜索结果签发10分钟、绑定UID和视频ID的 `searchAccessToken`,详情接口校验后放行。
## 4. 更新红点
`GET /api/app/content/update-markers` 聚合首页最新、当日最新及各亚模块最后内容时间。视频取最近审核时间,动漫/漫画同时考虑子集更新时间;“当日”按UTC+8自然日计算。
返回结果在Redis缓存30秒,前端保存本地已读时间并自行判断红点,后端不保存每用户红点状态。
## 5. AI女友V2与钱包
`POST /api/app/aimatev2/url` 在用户级Redis锁内:
1. 先回收上一次第三方剩余金额。
2. 读取主钱包,按 `10金币=1元` 转换并请求第三方授权URL。
3. 授权URL成功后,在Mongo事务内扣减主钱包并写入 `fund_transfer_log`
`GET /api/app/mine/wallet` 返回钱包前尝试将第三方余额下分回主钱包,保留不足1金币的换算余数供下次结算。
`fund_transfer_log` 记录上下分类型、金额、操作后余额和余数,用于审计和辅助状态排查;
它没有业务幂等唯一键,第三方上下分与本地Mongo事务也不是跨系统原子操作。
## 6. 付费引导与会员内容上新
`payment_guide` 按场景、用户分层、排序、启用状态和有效期选择配置。过期会员、已退款且权益回收的用户按未付费分层处理。注册不足24小时的未付费用户始终属于新用户分层;免费次数是否用完只控制 `HOME_NEW_USER_FREE_TRIAL``show`,不得影响其他场景的新老用户分层。
`VIP_CONTENT_UPDATE` 动态查询审核通过的最新VIP视频,排除免费区、金币视频、不可推荐和业务配置排除的亚模块,按 `reviewAt desc, _id desc` 返回。数量由 `videoLimit` 控制,默认4,最大20。
视频ID顺序生成 `contentVersion`。展示回执按“UID+配置+场景+内容版本”幂等记录到 `payment_guide_impression`;新内容产生新版本后可再次提示。
## 7. 评论区Banner
`scene_banner` 支持 `COMMENT_TOP` 场景的多张图片/GIF、内链/外链/无跳转、排序、启用和上下架时间。App端只返回当前有效数据,按 `sort desc, updatedAt desc` 轮播。历史每场景单条唯一索引需在模型初始化时删除,再创建查询索引。
## 8. VIP卡片A/B与下单归因
`vip_card_experiment` 保存A/B流量、套餐集合、默认套餐、角标文案、皮肤标识和 `uiConfig`。UID通过稳定哈希命中A或B,发布新实验时在事务内停用旧实验,唯一active-slot索引保证全局只有一个生效实验。
App套餐接口仅返回当前UID命中分组的:
- `experimentId/variant/skinKey/defaultProductId`
- `uiConfig.backgroundImage`(会员中心整页背景)
- `uiConfig.badgeStyles`(角标背景色和文字色)
- 每个套餐的 `badgeType/badgeText`
`vip_card_analytics_event` 使用唯一 `eventId` 幂等保存卡皮展示、套餐展示和无购买关闭事件,统计同时返回去重人数和次数。
下单增加 `sourcePage/sourceRef/videoId/activityId/experimentId/experimentVariant/sessionId``sourcePage` 允许任意非空值,Trim后最长128个Unicode字符,空值归一为 `UNKNOWN`。参与实验时,后端校验UID分组、套餐归属和会话ID,并在创建订单时固化归因,支付回调不重新分组。
## 9. 免费观看与试看角标
`GET /api/app/vid/user/count` 增加 `totalWatchCount`,值改为后端系统配置 `sys_conf``totalWatchCount``gpCode=common`)。新客户端通过 `POST /api/app/vid/play/consume` 消费次数:
- 在事务内写当日观看记录并扣减用户剩余次数。
- `consumeKey=UID+视频+自然日` 的唯一索引保证并发只扣1次。
- 遇到Mongo短暂事务冲突时最多有界重试5次,提交后再清理用户缓存。
当前实现边界:消费接口会校验审核、VIP/作者、免费区、金币和当日已看,
但没有限制 `newsType=SP/SHORT`。因此其他审核通过且同样非金币、非免费区的视频也可能消费次数;
若产品要求与角标资格完全一致,上线前需补充类型校验。
视频列表/详情增加 `showFreeTrialBadge/freeTrialRemaining/canUseFreeTrial`。后端在一次列表组装中复用用户上下文,不逐视频查用户或配置。只有同时满足以下条件时显示:
- `freeTrialBadgeEnabled=true`
- 已登录、非VIP、剩余次数大于0
- 普通/短视频VIP内容,非金币、非免费区、非用户自己发布
## 10. 动漫/漫画最新时间
`media` 增加 `latestPublishedAt`。媒体首次/重新上架、批量激活或成功新增子集时同步更新;
普通媒体元数据更新不改该时间。亚模块“最新”排序为:
```text
latestPublishedAt desc,
contentUpdateTime desc,
createdAt desc,
_id desc
```
查询会追加旧时间字段作为后续排序键,但不等价于针对每条记录做
`$ifNull` 回退,新旧文档混排仍可能失序。因此上线时必须单独交付并执行
`script/mongo/backfill_media_latest_published_at.js`;该脚本不依赖服务启动,本次与文档包一起迁移。
## 11. 视频置顶
现有视频编辑接口的 `liaoBaTopSort` 取消0~99限制,允许重复值;保存时仅更新当前视频,不交换其他视频。置顶列表按 `liaoBaTopSort desc, reviewAt desc, _id desc` 稳定排序。
## 12. 数据模型、索引与缓存
新增Mongo集合:
- `fund_transfer_log`
- `payment_guide`
- `payment_guide_impression`
- `scene_banner`
- `vip_card_experiment`
- `vip_card_analytics_event`
关键索引:
- 推荐扫描:`newsType, status, deleteAt, _id`。Mongo读取和写入按有界批次执行,
worker数有上限;但所有合格候选的精简快照会留在内存中再排序并组装17+3队列,
内存随候选量增长,上线前需用生产规模数据压测。
- 免费消费:`user_act.consumeKey` 部分唯一索引。
- 付费引导曝光:`uid, configId, scene, contentVersion` 唯一。
- VIP实验:`experimentId` 唯一、active-slot部分唯一、`eventId` 唯一。
- 订单归因:`sourcePage, createdAt` 复合索引,以及
`experimentId, experimentVariant, productID, status` 复合索引。
- AI资金日志:`uid, category, createdAt desc`
- 媒体最新:模块、状态、删除标记与 `latestPublishedAt/contentUpdateTime/createdAt/_id` 复合索引。
关键Redis键:
```text
recommend:short:current
recommend:short:queue:{version}
recommend:short:offset:{version}
recommend:short:meta:{version}
recommend:short:build-lock
recommend:short:reservation:{version}:{uid}
recommend:short:commit-receipt:{uid}:{receiptId}
content:update-markers:v1
redsync:ai-fund:{uid}
```
App与SKD的 `keyTTLHours` 必须一致。`queue/meta` 默认按发布时间+72小时过期,
`offset` 使用meta记录的同一绝对过期时间;`current` 不设TTL
`build-lock` 固定10分钟,`reservation` 固定15秒,`commit-receipt` 固定30秒。
## 13. 发布与数据迁移
1. 核对 `91迭代配置改动点.md`,完成App、SKD运行配置和各环境业务配置。
2. 在低峰发布 `web`,确认新集合/索引初始化成功,再发布 `app`
3. 统计未初始化推荐字段数量,根据生产量调整 `maxInitializationWrites`
4. 发布 `skd`。可先使 `skd.shortRecommend.enabled=true` 生成队列,保持 `app.shortRecommend.enabled=false` 验证健康度,再开启App全局读取。
5. 单独执行媒体历史回填脚本;执行前后统计缺失数并抽样验证。
6. 逐项配置亚模块、付费引导、评论Banner、VIP A/B和免费试看开关;测试库数据不会自动迁移到生产。
推荐开关是服务实例级全局开关,不是按UID白名单灰度。影子验证应通过“SKD开、App关”完成。
## 14. 自测与验收
- 推荐:17+3比例、新视频不足、环形回绕、每日切版、同UID并发、超时重试、失效视频补位、降级。
- 亚模块:上下架边界、各入口排除、搜索token的UID/视频/过期校验、随机刷新重试稳定。
- 付费引导:全部用户分层、仅展示一次、内容版本变化、`videoLimit` 0/4/20/越界。
- VIP A/B:UID稳定分流、UI配置下发、事件去重、人/次统计、创建/支付/退款口径、下单归因。
- 免费观看:同UID+视频并发只扣1次,VIP/发布者/免费区/金币/当日已看分支,角标开关和剩余次数实时失效。
- AI女友:重复进入、重复返回钱包、余数处理、第三方失败、Mongo事务回滚和流水一致。
- 媒体:存量回填、新上架、子集更新、相同时间稳定排序与索引explain。
上线前应在接近生产数量级的数据上验证推荐全量扫描时间、BulkWrite批次、Redis发布耗时、接口P95/P99、Mongo连接池和Redis内存占用。