# 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-`,切版后新的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内存占用。