From 6f5b0ecb77472dc67a032041c7998eab81429862 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 3 Nov 2026 11:36:29 +0800 Subject: [PATCH] =?UTF-8?q?[AI]=20feat:=20dramacmt=20=E5=8A=A0=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E4=B8=8E=E9=87=8D=E7=AE=97=E4=B8=A4=E4=B8=AA=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --top-up:--min~--max 当成目标总条数按差额补,已经落在区间内的不动。 拿每次新摇的目标值比对会让重复跑把条数一路往上限顶,所以按 min 判定。 --recount:把 countComment 重算成 App 列表口径的实际可见条数,铺完自动跑一次。 这个字段只会飘不会自己回来,两个来源:发评论时无条件 +1 而审核不通过和删除都 不回减(后端唯一的重算函数明确跳过 drama);contentsync 用 $setOnInsert 赋初值 时 countComment 在清单里,源库新剧带着源站的评论数进来而本地一条评论都没有。 Co-Authored-By: Claude Opus 5 --- scripts/dramacmt/README.md | 37 +++++++++ scripts/dramacmt/main.go | 154 ++++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/scripts/dramacmt/README.md b/scripts/dramacmt/README.md index b2a3e10..be3c23b 100644 --- a/scripts/dramacmt/README.md +++ b/scripts/dramacmt/README.md @@ -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`。 + ## 撤回 机器人用户和评论都带标记,撤回是两条命令: diff --git a/scripts/dramacmt/main.go b/scripts/dramacmt/main.go index 27db9ae..b8909c1 100644 --- a/scripts/dramacmt/main.go +++ b/scripts/dramacmt/main.go @@ -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 从库里现有用户取头像路径,避免把素材路径写死在代码里。