两处原因,分别处理: 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>
135 lines
6.2 KiB
Markdown
135 lines
6.2 KiB
Markdown
# 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。
|