Files
rootandClaude Opus 5 ed51664a09 [AI] fix: 剧集去重键补上 videoUrl
媒资库会给同一集重新发 hashId(「JK学姐不为人知的秘密」源库那条
hashId 6aa23652…、本地那条 6acaf74c…,videoUrl 一模一样),
只比 hashId 会漏掉这一类重复,前端仍显示「1 1 2 2」。
改成 mediaId 下 hashId 或 videoUrl 任一相同即认定是同一集。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-29 20:24:38 +08:00
..

contentsync

把源库 91porn_video 的帖子 / 视频 / 短剧增量同步到 91fans、huangguo、tangguo 三个库。

为什么不能直接整文档覆盖

对源库与目标库做过逐字段抽样比对(video_info 400 条 / 其余各 300 条), 74 个字段里只有 9 个分叉,且全部是站点本地产生的数据:

集合 分叉字段 分叉比例
video_info pageViewCount / playCount / fakePlayCount / likeCount / fakeLikeCount / collectCount / forwardCount / purchaseCount / updatedAt 最高 88%
media countBrowse / countCollect / countLike / countPurchases / hot / updateTime 最高 39%
tag tPlayCount / vPlayCount / playRating / videoCount / updatedAt 最高 42%
media_content / media_tag / media_tag_dimension 无 0%

内容字段(title、cover、tags、status、newsType、coins…)完全一致。

所以同步必须是字段级的。本地字段分三类,都不能被源库覆盖:

  1. 运行时计数器 —— 各站点自己累积的播放 / 点赞 / 收藏 / 购买数
  2. 本地定时任务写入 —— skd/job/hotvalue/video.go 会重算并写回 hot
  3. 后台运营编排 —— web/service/vidser/vid.go 写 happinessPlazaTop、liaoBaTopSort

清单见 spec.go,新增字段前先确认它属于「源库内容」还是「站点本地」。

写入语义

一条 upsert 同时处理新增和更新:

UpdateOne({_id}, {
    $set:         <内容字段>,    // 每次同步都以源库为准
    $setOnInsert: <本地字段>,    // 只在首次插入时给初值,之后永不再碰
}, upsert: true)

只更新不删除。源库下架通过 status 字段自然同步过去。

本地下架保护

每批写入前先查目标库这批 _id 里哪些是 status: 0,整条踢出——连内容字段 也不写。站点运营下架之后,源库无权再把它改回上架。

水位线仍会越过被跳过的文档,否则它会永久卡住水位线导致每次重扫。

没有 status 字段的集合(media_tag / media_tag_dimension / tag)自然查不出东西, 等于不生效。

软删除补扫

水位线只管新增,所以源库事后打上的 isDelete: true 永远传不过来:源站把重复 剧集软删掉了,目标库却还留着,App 剧集列表就出现「1 1 1 2 2 2」这种重复集。 2026-10-29 排查 琼明神女录 显示 6 集(实际 2 集)就是这个原因。

每轮同步结束后对 media / media_content / video_info 补扫一次:把源库 isDelete: true 的文档在目标库也标成已删除。--sweep-deleted 默认开,三个库 6 秒。

  • 只做「源库删了 → 目标也删」这一个方向,绝不把目标库已删的恢复回来,所以和 上面的本地下架保护不冲突
  • 只写 isDelete,不碰 updateTime(那是站点本地字段)
  • 顺带按 web/service/mediacontentser 的同一口径重算父级 currentEpisode / freeEpisode——这两个字段原本只在后台编辑子集时才重算,不补的话集数会停在 含重复集的旧值

一次性修复历史欠账时实测:三个库各 261 条 media_content + 18 条 media, 涉及 28 部短剧。freeEpisode 只减不增(减少量正好等于被去掉的重复免费集数)。

媒资库重复集去重

各站点的 skd 自己也在跑媒资库同步(common/media/syncMedia.go,每 30 分钟), 它按 (mediaId, hashId) 去重、用自己的 _id 为同一集插一条;本同步再把源库 那条插进来,同一集就成了两条。两个源库的 (mediaId, hashId) 重复组都是 0, 只有目标站有——这是本同步引入的。

更要紧的是媒资库那条在 SaleCoin = 0 时 listenPermission = 2(免费), 本该收费的集可以白看。2026-10-29 的存量:

站点 重复组 其中免费副本
tangguo / huangguo / 91fans 各 268 各 241
hgmj 97 84
51dj 102 97

购买记录全部挂在源库那条上(本地副本购买恒为 0,因为它免费、没人需要买), 所以以源库那条为准:插入时若发现本地已有同一集,把本地那条 isActive: false 下架(不删除,便于回查)。

根治办法是关掉各站 skd 的媒资库同步:skd.json 里 switch.mediaSyncDisabled: true (见 skd/job/job.go)。源站必须保持开启,否则新剧集无从进库。 关掉之后新剧集改由本同步每天 04:00 从源站带过来,延迟从 30 分钟变成最多 24 小时。

增量水位线

每个「目标库 × 集合」各记一份水位线,存在 state.json:

常规同步只取新增:_id > lastID,按 _id 升序。ObjectId 单调递增且天然有索引, 所以每次只读上次之后产生的文档,耗时与集合总量无关(实测 1~3 秒)。

刻意不做存量修改比对。 源库改了已存在的文档——改标题、下架、调价、换封面—— 不会同步过来。需要补齐时手动跑 --full 重扫全部(三个库约 17 分钟起)。

这是明确的取舍。参考量级:一次 --full 里 video_info 更新了约 7300 条 (占 273K 的 2.7%)、media 更新了约 5900 条(占 20K 的 29%)。

用法

# 增量:只取新增。cron 每天北京时间 04:00 跑的就是这个,耗时数秒
/server/sync/run.sh

# 全量:忽略水位线重扫,补齐存量修改。约 17 分钟起,手动跑
/server/sync/run.sh --full

# 只跑某几个集合 / 干跑
/server/sync/contentsync --src "$SRC_URI" --dst "tangguo=$DST_TANGGUO" \
    --only video_info,media --dry-run --state /server/sync/state.json

部署

服务器上无 Go / node / mongosh,所以本地交叉编译后 scp 静态二进制:

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o contentsync ./scripts/contentsync/
scp contentsync root@172.104.39.32:/server/sync/

/server/sync/env.sh(600,含连接串)由 run.sh source。不走 Jenkins。

cron:0 20 * * * /server/sync/run.sh

注意机器系统时区是 UTC,且 Debian 系的 cron 3.0pl1 不支持 CRON_TZ (那是 cronie 的特性),所以只能直接写 UTC 时刻:20:00 UTC = 北京时间 04:00。