[AI] feat: dramacmt 加补齐与重算两个模式
--top-up:--min~--max 当成目标总条数按差额补,已经落在区间内的不动。 拿每次新摇的目标值比对会让重复跑把条数一路往上限顶,所以按 min 判定。 --recount:把 countComment 重算成 App 列表口径的实际可见条数,铺完自动跑一次。 这个字段只会飘不会自己回来,两个来源:发评论时无条件 +1 而审核不通过和删除都 不回减(后端唯一的重算函数明确跳过 drama);contentsync 用 $setOnInsert 赋初值 时 countComment 在清单里,源库新剧带着源站的评论数进来而本地一条评论都没有。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -98,12 +98,49 @@ go build -o /tmp/dramacmt ./scripts/dramacmt/
|
||||
/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`。
|
||||
|
||||
## 撤回
|
||||
|
||||
机器人用户和评论都带标记,撤回是两条命令:
|
||||
|
||||
+150
-4
@@ -43,6 +43,9 @@ func main() {
|
||||
minCmt = flag.Int("min", 20, "每部剧最少评论数")
|
||||
maxCmt = flag.Int("max", 50, "每部剧最多评论数")
|
||||
onlyEmpty = flag.Bool("only-empty", true, "只给 countComment=0 的剧铺")
|
||||
topUp = flag.Bool("top-up", false, "补齐模式:--min~--max 当成目标总条数,按差额补,已经够的跳过")
|
||||
recount = flag.Bool("recount", false, "只重算 countComment,不铺评论")
|
||||
noRecount = flag.Bool("no-recount", false, "铺完后不重算 countComment")
|
||||
limit = flag.Int("limit", 0, "最多处理多少部剧,0 为不限(调试用)")
|
||||
dryRun = flag.Bool("dry-run", false, "只统计不写入")
|
||||
seed = flag.Int64("seed", 0, "随机种子,0 用当前时间")
|
||||
@@ -64,7 +67,6 @@ func main() {
|
||||
*seed = time.Now().UnixNano()
|
||||
}
|
||||
rnd := rand.New(rand.NewSource(*seed))
|
||||
fmt.Printf("随机种子 %d(复现同一批数据可用 --seed)\n", *seed)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
@@ -81,6 +83,15 @@ func main() {
|
||||
}
|
||||
db := cli.Database(*dbName)
|
||||
|
||||
if *recount {
|
||||
if err := recountAll(ctx, db, *dryRun); err != nil {
|
||||
fatal("重算 countComment 失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("随机种子 %d(复现同一批数据可用 --seed)\n", *seed)
|
||||
|
||||
portraits, err := samplePortraits(ctx, db)
|
||||
if err != nil {
|
||||
fatal("取头像素材失败: %v", err)
|
||||
@@ -102,9 +113,26 @@ func main() {
|
||||
}
|
||||
fmt.Printf("待铺评论的在线短剧 %d 部\n\n", len(dramas))
|
||||
|
||||
total, done := 0, 0
|
||||
total, done, skipped := 0, 0, 0
|
||||
for _, d := range dramas {
|
||||
n := *minCmt + rnd.Intn(*maxCmt-*minCmt+1)
|
||||
if *topUp {
|
||||
// 补齐模式下 --min~--max 是目标总条数,不是新增条数。
|
||||
// 糖果的短剧已经有 20~50 条,照新增算会冲到 250 条,和其余站对不上。
|
||||
cur, err := visibleCount(ctx, db, d.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("[FAIL] %s 查现有条数: %v\n", d.Title, err)
|
||||
continue
|
||||
}
|
||||
// 已经落在 [min, max] 区间内的一律不动。
|
||||
// 拿每次新摇的目标值去比对会让重复跑把条数一路往 max 顶
|
||||
// (已有 60 条的剧遇到目标 180 还会再补 120),这样跑几次就全挤到上限了。
|
||||
if int(cur) >= *minCmt {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
n -= int(cur)
|
||||
}
|
||||
cmts := buildComments(d, n, uids, rnd)
|
||||
total += len(cmts)
|
||||
if *dryRun {
|
||||
@@ -121,11 +149,129 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
tail := ""
|
||||
if *topUp {
|
||||
tail = fmt.Sprintf(",另有 %d 部已经够了跳过", skipped)
|
||||
}
|
||||
if *dryRun {
|
||||
fmt.Printf("\nDRY-RUN:%d 部剧,将写入 %d 条评论(未写入)\n", done, total)
|
||||
fmt.Printf("\nDRY-RUN:%d 部剧,将写入 %d 条评论(未写入)%s\n", done, total, tail)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n完成:%d 部剧,共写入 %d 条评论\n", done, total)
|
||||
fmt.Printf("\n完成:%d 部剧,共写入 %d 条评论%s\n", done, total, tail)
|
||||
|
||||
if *noRecount {
|
||||
return
|
||||
}
|
||||
fmt.Println()
|
||||
if err := recountAll(ctx, db, false); err != nil {
|
||||
fatal("重算 countComment 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// recountAll 把每部短剧的 countComment 重算成 App 列表口径的实际可见条数。
|
||||
//
|
||||
// 两个地方会让这个字段飘,所以铺完必须重算一次:
|
||||
// - 发评论时 incCommentCount 无条件 +1(app/service/commentser/cmtsend.go),
|
||||
// 审核不通过和删除都不回减,后端也没有任何一条路会重算短剧的这个字段
|
||||
// - contentsync 用 $setOnInsert 给新文档的本地字段赋初值,countComment 在列表里,
|
||||
// 所以源库新剧带着源站的评论数进来,本地却一条评论都没有
|
||||
//
|
||||
// 口径与 app/service/commentser.GetTotalComments 一致——App 评论列表第 2 页之后
|
||||
// 就是用它实时算总数的,第 1 页读 countComment,两者必须对得上。
|
||||
func recountAll(ctx context.Context, db *mongo.Database, dryRun bool) error {
|
||||
cur, err := db.Collection("comment").Aggregate(ctx, []bson.M{
|
||||
{"$match": bson.M{"objType": "drama", "level": 1, "status": 1, "isDelete": false,
|
||||
"isAdvertiser": bson.M{"$in": []any{nil, false}}}},
|
||||
{"$group": bson.M{"_id": "$objID", "n": bson.M{"$sum": 1}}},
|
||||
}, options.Aggregate().SetAllowDiskUse(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
real := map[primitive.ObjectID]int64{}
|
||||
for cur.Next(ctx) {
|
||||
var row struct {
|
||||
ID primitive.ObjectID `bson:"_id"`
|
||||
N int64 `bson:"n"`
|
||||
}
|
||||
if err := cur.Decode(&row); err != nil {
|
||||
cur.Close(ctx)
|
||||
return err
|
||||
}
|
||||
real[row.ID] = row.N
|
||||
}
|
||||
cur.Close(ctx)
|
||||
if err := cur.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mcur, err := db.Collection("media").Find(ctx, bson.M{"mediaType": "drama"},
|
||||
options.Find().SetProjection(bson.M{"_id": 1, "countComment": 1}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mcur.Close(ctx)
|
||||
|
||||
var (
|
||||
models []mongo.WriteModel
|
||||
scanned, up, down int
|
||||
sumOld, sumNew int64
|
||||
)
|
||||
for mcur.Next(ctx) {
|
||||
var row struct {
|
||||
ID primitive.ObjectID `bson:"_id"`
|
||||
Count int64 `bson:"countComment"`
|
||||
}
|
||||
if err := mcur.Decode(&row); err != nil {
|
||||
return err
|
||||
}
|
||||
scanned++
|
||||
want := real[row.ID]
|
||||
if row.Count == want {
|
||||
continue
|
||||
}
|
||||
if want > row.Count {
|
||||
up++
|
||||
} else {
|
||||
down++
|
||||
}
|
||||
sumOld += row.Count
|
||||
sumNew += want
|
||||
models = append(models, mongo.NewUpdateOneModel().
|
||||
SetFilter(bson.M{"_id": row.ID}).
|
||||
SetUpdate(bson.M{"$set": bson.M{"countComment": want}}))
|
||||
}
|
||||
if err := mcur.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("重算 countComment:扫 %d 部短剧,不一致 %d 部(调高 %d,调低 %d),合计 %d -> %d\n",
|
||||
scanned, len(models), up, down, sumOld, sumNew)
|
||||
if dryRun {
|
||||
fmt.Println(" [dry-run] 未写入")
|
||||
return nil
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
res, err := db.Collection("media").BulkWrite(ctx, models, options.BulkWrite().SetOrdered(false))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" 已写入:matched=%d modified=%d\n", res.MatchedCount, res.ModifiedCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// visibleCount 返回一部剧当前在 App 评论列表里能看到的一级评论条数。
|
||||
// 口径与 app/service/commentser.GetTotalComments 一致。
|
||||
func visibleCount(ctx context.Context, db *mongo.Database, mediaID primitive.ObjectID) (int64, error) {
|
||||
return db.Collection("comment").CountDocuments(ctx, bson.M{
|
||||
"objID": mediaID,
|
||||
"objType": "drama",
|
||||
"level": 1,
|
||||
"status": 1,
|
||||
"isDelete": false,
|
||||
"isAdvertiser": bson.M{"$in": []any{nil, false}},
|
||||
})
|
||||
}
|
||||
|
||||
// samplePortraits 从库里现有用户取头像路径,避免把素材路径写死在代码里。
|
||||
|
||||
Reference in New Issue
Block a user