# 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%)。 ## 用法 ```bash # 增量:只取新增。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 静态二进制: ```bash 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。