两处原因,分别处理: 1. contentsync 只按 _id 水位线插新增,源库事后打上的 isDelete 传不过来, 源站删掉的重复集在目标库一直留着。每轮同步后补扫一次 media / media_content / video_info,把源库的软删除标记同步过去,并按 web/service/mediacontentser 的同一口径重算父级 currentEpisode / freeEpisode。只做「源库删了→目标也删」一个方向,不与本地下架保护冲突。 2. 各站 skd 的媒资库同步会为同一集(mediaId+hashId 相同)另插一条自己的 _id, 与 contentsync 带过来的源库那条并存。媒资库那条在 SaleCoin=0 时 listenPermission=2,本该收费的集可以白看。插入时若发现本地已有同一集, 把本地那条 isActive:false 下架;并新增 skd 配置开关 switch.mediaSyncDisabled,非源站可直接关掉这条同步链。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.2 KiB
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…)完全一致。
所以同步必须是字段级的。本地字段分三类,都不能被源库覆盖:
- 运行时计数器 —— 各站点自己累积的播放 / 点赞 / 收藏 / 购买数
- 本地定时任务写入 ——
skd/job/hotvalue/video.go会重算并写回hot - 后台运营编排 ——
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。