Files
rootandClaude Opus 5 acb9a34520 [AI] fix: 修复短剧剧集重复显示
两处原因,分别处理:

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>
2026-10-29 20:18:36 +08:00

135 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。