其余四站的连接串只在同步服务器上,照 contentsync 的办法交叉编译 scp 过去, 用服务器自己的 env.sh,凭据不出那台机器。每次 contentsync 带进新短剧之后 要跑一遍 run_cmt.sh。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
9.1 KiB
Markdown
192 lines
9.1 KiB
Markdown
# dramacmt
|
||
|
||
给在线短剧铺机器人评论。一次性运营工具,不进 cron。
|
||
|
||
## 为什么不用后台已有的评论导入
|
||
|
||
`web/api/commentctrl.AddContentExcel` 有三个限制:
|
||
|
||
1. 只认视频(按 `vidmod.GetVideoInfo` 查对象),短剧的 `objID` 是 `media._id`,走不通
|
||
2. 要上传 Excel
|
||
3. 它的 `userID` 取自 `static.json` 的 `sysUser`——**那些 uid 在 `user` 表里并不存在**
|
||
|
||
第 3 点是实际踩到的坑:评论列表填充用户信息时(`app/service/commentser/cmtcoverter.go`)
|
||
查不到用户**不会丢弃评论,只是把昵称和头像留空**。线上已经有 5962 条这样的
|
||
「无名无头像」评论(抽查 400 个引用 uid,0 命中)。
|
||
|
||
所以本工具自己建一批机器人用户,评论挂在它们身上。
|
||
|
||
## 机器人用户
|
||
|
||
- uid 从 **1001** 开始往上发,`--users` 控制池子大小。该区间原本完全空闲
|
||
(`uid 1~17` 是库里已有的 17 个机器人,1000 以下整段不碰)
|
||
- **池子要够大**:同一部剧内用户不重复,所以 `--users` 必须大于 `--max`;
|
||
而且池子太小的话同一个昵称头像会在整个评论区刷屏。实测取值:
|
||
tangguo 5,000(和 `scripts/vidcmt` 共用),其余四站 3,000
|
||
- uid 必须 `<= 111999`(`common/constant.RobotUIDLimit`)——
|
||
`commentser.getTrackInfo` 靠这个阈值判定 `isRobot`,后台可按机器人筛选和撤回
|
||
- uid 池远低于 `auto_id` 的 uid 计数器(当时是 3111989,只增不减),
|
||
不会和后续真实注册撞号
|
||
- 昵称按「姓 + 名」随机拼,风格对齐库里现有那 17 个机器人(郏哲、溪焱这种)
|
||
- 头像**从库里现有用户采样**,不写死路径(各站 `static.json` 里是同一批 90 张)
|
||
|
||
`user` 表的唯一索引要当心:
|
||
|
||
| 索引 | 类型 | 处理 |
|
||
|---|---|---|
|
||
| `devID` | **非 sparse 唯一** | 必须给唯一非空值,用 `ROBOT%07d` |
|
||
| `token` / `promotionCode` / `mobile` | 部分唯一(`{$gt: ""}`) | 留空串即豁免 |
|
||
|
||
已存在的 uid **不覆盖**(昵称头像可能被运营改过),所以可以重复跑。
|
||
|
||
## 评论
|
||
|
||
要能在 App 里显示,必须满足 `models/v/cmtmod.GetParentCmtList` 的全部条件:
|
||
|
||
```
|
||
level = 1 status = 1 isGodComment = false
|
||
isDelete = false isAdvertiser 非 true
|
||
createdAt <= now
|
||
```
|
||
|
||
注意 `status`:App 发评论走的 `getTrackInfo` 写的是 `status: 2`(未审核),
|
||
而列表只认 `1`。所以铺数据必须直接写 `1`,否则一条都不显示。
|
||
|
||
其余字段整体对齐线上真实短剧评论的形状,两个容易写错的地方:
|
||
|
||
- `type` 是 `"VIDEO"`,不是 `"drama"`——`objType` 才是 `"drama"`。
|
||
`getTrackInfo` 对所有 objType 都传 `cmtmod.CmtTypeVideo`
|
||
- `tousername` 落库键名是**小写**的:结构体那个字段只有 `form`/`json` tag,
|
||
没写 `bson` tag,driver 默认把字段名小写
|
||
- `city` 必须写 `"-"`:发评论路径里 `GetLocationByIP` 的调用是注释掉的
|
||
(`app/service/commentser/cmtsend.go:114-116`),线上真实评论**没有一条**带
|
||
归属地。`City` 会原样出参给 App(`cmtcoverter.go:46`),给真城市等于在评论区
|
||
挂个「我是机器人」的标。第一版写了随机城市,已改掉,线上那 19,697 条也已回刷
|
||
|
||
**`media.countComment` 必须同步 `$inc`**。App 的「总评论条数」在第一页直接读这个
|
||
字段(`app/api/commentctrl`),不补的话前端显示 0 条、但列表里有评论。
|
||
|
||
### 点赞数与时间
|
||
|
||
- `createdAt` 随机铺在该剧 `latestPublishedAt`(没有就退 `createdAt`)到现在之间,
|
||
再按时间排序写入
|
||
- `likeCount` 走长尾:55% 落 0~5、30% 落 6~30、12% 落 31~120、3% 落 121~400。
|
||
热门评论区要求 `likeCount > 0` 且按它倒序,所以不能全给 0
|
||
- 再按文案长短压上限:≤4 字的短评最多 30 赞,≤8 字最多 120。
|
||
不压的话会出现「可以」两个字拿 164 赞这种明显假的数据
|
||
- **不写 `like` 集合的记录**。`likeCount` 是展示值,`isLike` 只反映当前请求用户,
|
||
不建 like 文档不影响功能,也省掉几万条垃圾数据
|
||
|
||
文案池在 `content.go`,**637 条**互不重复。单剧内文案和用户都不重复,
|
||
所以 `--max` 不能超过池子容量(`main.go` 里有硬校验)。
|
||
|
||
容量还要留足余量,不能刚好够:铺 200 条时池子只有 220 条的话,每部剧都会抽到
|
||
九成文案,几千部剧的评论区长得一模一样。现在抽 200 条约占池子三成。
|
||
|
||
## 用法
|
||
|
||
```bash
|
||
go build -o /tmp/dramacmt ./scripts/dramacmt/
|
||
|
||
# 干跑,看会写多少
|
||
/tmp/dramacmt --dst "$DST_TANGGUO" --db tangguo --dry-run
|
||
|
||
# 只给评论数为 0 的剧铺,每部 20~50 条(默认)
|
||
/tmp/dramacmt --dst "$DST_TANGGUO" --db tangguo
|
||
|
||
# 所有在线短剧都加铺 50~200 条(其余四站 2026-10-30 实际用的参数)
|
||
/tmp/dramacmt --dst "$DST_HUANGGUO" --db huangguo --users 3000 \
|
||
--min 50 --max 200 --only-empty=false
|
||
|
||
# 补齐模式:把每部剧补到 50~200 条总量(tangguo 2026-10-30 实际用的参数)
|
||
/tmp/dramacmt --dst "$DST_TANGGUO" --db tangguo --users 5000 \
|
||
--min 50 --max 200 --only-empty=false --top-up
|
||
|
||
# 只重算 countComment,不铺评论(每次 contentsync 带进新剧之后都要跑)
|
||
/tmp/dramacmt --dst "$DST_TANGGUO" --db tangguo --recount
|
||
|
||
# 调试:只跑 3 部,固定种子便于复现
|
||
/tmp/dramacmt --dst "$DST_TANGGUO" --db tangguo --limit 3 --seed 42
|
||
```
|
||
|
||
`--db` 必须显式传:URI 里的 `authSource` 通常是 `admin`,推不出业务库名。
|
||
|
||
### `--top-up`
|
||
|
||
默认模式下 `--min~--max` 是**新增**条数,`--top-up` 把它当成**目标总条数**,
|
||
按差额补,已经够的跳过。
|
||
|
||
区别在于底子不空的站:tangguo 的短剧先前已经铺过 20~50 条,照新增算会冲到
|
||
250 条,和其余四站的 50~200 对不上。代价是每部剧多一次 `CountDocuments`
|
||
往返(773 部约两分钟,远端副本集的网络延迟占大头)。
|
||
|
||
现有条数按 App 列表第 1 页的口径数,不读 `countComment`——那个字段可能是飘的。
|
||
|
||
### `--recount`
|
||
|
||
把每部短剧的 `countComment` 重算成 App 列表口径的实际可见条数。铺完会自动跑一次,
|
||
`--no-recount` 可以跳过,`--recount` 则是只重算不铺。
|
||
|
||
**这个字段只会飘,不会自己回来**,有两个来源:
|
||
|
||
1. 发评论时 `incCommentCount` 无条件 `+1`(`app/service/commentser/cmtsend.go:224`),
|
||
审核不通过和删除都不回减。后端唯一的重算函数
|
||
`web/service/commentser.rebuildSingleObjCommentCount` **明确跳过 drama**
|
||
(`commentAgent.go:268` 只认 video 和 cartoon),所以线上没有任何一条路会修它
|
||
2. `contentsync` 用 `$setOnInsert` 给新文档的本地字段赋初值,`countComment` 在
|
||
那份清单里(`scripts/contentsync/spec.go`),于是**源库新剧带着源站的评论数
|
||
进来,本地却一条评论都没有**
|
||
|
||
第 2 条意味着每次 contentsync 带进新短剧之后都要重算一次,否则新剧在 App 里显示
|
||
「N 条评论」但列表是空的。2026-11-03 实测:一次增量同步带进 16 部新短剧,
|
||
全都带着源站的 `countComment`。
|
||
|
||
## 部署
|
||
|
||
tangguo 的连接串在本仓库 `app/config/app.json` 的 `mongo.videoDbUrl`,本地直接跑。
|
||
**其余四站的连接串只在同步服务器上**(`/server/sync/env.sh`、`hgmj_env.sh`,权限 600),
|
||
所以照 `scripts/contentsync` 的办法交叉编译静态二进制 scp 过去,让它用服务器自己的
|
||
连接串,凭据不出那台机器:
|
||
|
||
```bash
|
||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dramacmt ./scripts/dramacmt/
|
||
scp dramacmt root@172.104.39.32:/server/sync/
|
||
```
|
||
|
||
服务器上的 `/server/sync/run_cmt.sh` 负责 source 连接串并按站点循环,日志
|
||
`/var/log/dramacmt.log`。**不进 cron**,手动跑:
|
||
|
||
```bash
|
||
/server/sync/run_cmt.sh # 默认 huangguo 91fans hgmj 51dj
|
||
SITES="hgmj 51dj" /server/sync/run_cmt.sh # 指定站点
|
||
DRY=--dry-run /server/sync/run_cmt.sh # 干跑
|
||
```
|
||
|
||
`--top-up` 把 50~200 当成目标总量、已达标的不动,所以可以反复跑。**每次
|
||
contentsync 带进新短剧之后跑一遍**:新剧既没有评论,又带着源站的 `countComment`,
|
||
不补的话在 App 里显示「有 N 条评论但列表是空的」。
|
||
|
||
2026-11-03 实测一次增量同步之后:四站各有 43 / 43 / 39 / 39 部新剧待补,
|
||
共写入 19,300 条,重算修正 229 部,耗时 9 秒。
|
||
|
||
## 撤回
|
||
|
||
机器人用户和评论都带标记,撤回是两条命令:
|
||
|
||
```js
|
||
// 撤回评论(软删,保守做法)。uid 上限按当站实际的 --users 取
|
||
db.comment.updateMany({isRobot: true, objType: "drama", userID: {$gte: 1001, $lte: 5000}},
|
||
{$set: {isDelete: true}})
|
||
```
|
||
|
||
硬删同理,把 `$set` 换成 `deleteMany`。两种都做完后 `countComment` 要重算,
|
||
口径和 App 列表第 1 页保持一致(`app/service/commentser.GetTotalComments`):
|
||
|
||
```js
|
||
countDocuments({objID, objType: "drama", level: 1, status: 1,
|
||
isDelete: false, isAdvertiser: {$in: [null, false]}})
|
||
```
|
||
|
||
短剧和影视不一样:**第 1 页的总条数直接读 `media.countComment`**,第 2 页之后才
|
||
实时查,所以这个字段不重算前端就会出现「显示 N 条但列表是空的」。
|