// 对齐 forumTopic.likeCount / forumPost.likeCount 与真实 Like 表的计数 // 用法: // node scripts/reconcile-like-counts.mjs # 仅巡检 // node scripts/reconcile-like-counts.mjs --fix # 巡检 + 自动修正 // node scripts/reconcile-like-counts.mjs --scope=topic|post|both // node scripts/reconcile-like-counts.mjs --bot-only # 只检查 bot 的话题/回复 import { PrismaClient } from "@prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import "dotenv/config"; const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://"); const sep = base.includes("?") ? "&" : "?"; const connectionString = `${base}${sep}connection_limit=5&pool_timeout=15`; const adapter = new PrismaMariaDb(connectionString); const prisma = new PrismaClient({ adapter }); const args = process.argv.slice(2); const fixMode = args.includes("--fix"); const botOnly = args.includes("--bot-only"); const scopeArg = args.find((a) => a.startsWith("--scope=")); const scope = scopeArg ? scopeArg.split("=")[1] : "both"; const limitArg = args.find((a) => a.startsWith("--limit=")); const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : 5000; function ts() { return `[${new Date().toISOString()}]`; } async function reconcileTopics() { console.log(`\n${ts()} 巡检 forumTopic.likeCount ...`); const where = {}; if (botOnly) where.user = { isBot: true }; const all = await prisma.forumTopic.findMany({ where, select: { id: true, likeCount: true, userId: true }, take: limit, }); console.log(` 范围: ${all.length} 个 topic${botOnly ? " (bot 限定)" : ""}`); // 批量统计真实点赞数(按 topicId groupBy) const topicIds = all.map((t) => t.id); if (topicIds.length === 0) return { drift: 0, fixed: 0 }; const realCounts = await prisma.forumTopicLike.groupBy({ by: ["topicId"], where: { topicId: { in: topicIds } }, _count: { _all: true }, }); const realMap = new Map(realCounts.map((r) => [r.topicId, r._count._all])); let drift = 0; let fixed = 0; const samples = []; for (const t of all) { const real = realMap.get(t.id) || 0; if (real !== t.likeCount) { drift++; if (samples.length < 8) { samples.push({ id: t.id, stored: t.likeCount, real }); } if (fixMode) { await prisma.forumTopic.update({ where: { id: t.id }, data: { likeCount: real }, }); fixed++; } } } console.log(` 偏差: ${drift}${fixMode ? `,已修正 ${fixed}` : ""}`); if (samples.length > 0) { console.log(` 示例(最多 8 条):`); for (const s of samples) { console.log(` topic ${s.id}: stored=${s.stored} 实际=${s.real}`); } } return { drift, fixed }; } async function reconcilePosts() { console.log(`\n${ts()} 巡检 forumPost.likeCount ...`); const where = {}; if (botOnly) where.user = { isBot: true }; const all = await prisma.forumPost.findMany({ where, select: { id: true, likeCount: true, userId: true }, take: limit, }); console.log(` 范围: ${all.length} 个 post${botOnly ? " (bot 限定)" : ""}`); const postIds = all.map((p) => p.id); if (postIds.length === 0) return { drift: 0, fixed: 0 }; const realCounts = await prisma.forumPostLike.groupBy({ by: ["postId"], where: { postId: { in: postIds } }, _count: { _all: true }, }); const realMap = new Map(realCounts.map((r) => [r.postId, r._count._all])); let drift = 0; let fixed = 0; const samples = []; for (const p of all) { const real = realMap.get(p.id) || 0; if (real !== p.likeCount) { drift++; if (samples.length < 8) { samples.push({ id: p.id, stored: p.likeCount, real }); } if (fixMode) { await prisma.forumPost.update({ where: { id: p.id }, data: { likeCount: real }, }); fixed++; } } } console.log(` 偏差: ${drift}${fixMode ? `,已修正 ${fixed}` : ""}`); if (samples.length > 0) { console.log(` 示例(最多 8 条):`); for (const s of samples) { console.log(` post ${s.id}: stored=${s.stored} 实际=${s.real}`); } } return { drift, fixed }; } async function main() { console.log(`========== 点赞数对齐巡检 ==========`); console.log(`模式: ${fixMode ? "FIX(自动修正)" : "CHECK(仅巡检)"}`); console.log(`范围: ${scope}${botOnly ? " (仅 bot)" : ""}`); console.log(`扫描上限: ${limit}`); const summary = { topic: { drift: 0, fixed: 0 }, post: { drift: 0, fixed: 0 } }; if (scope === "topic" || scope === "both") { summary.topic = await reconcileTopics(); } if (scope === "post" || scope === "both") { summary.post = await reconcilePosts(); } console.log(`\n========== 总结 ==========`); console.log(`话题偏差: ${summary.topic.drift}${fixMode ? `(已修正 ${summary.topic.fixed})` : ""}`); console.log(`回复偏差: ${summary.post.drift}${fixMode ? `(已修正 ${summary.post.fixed})` : ""}`); const totalDrift = summary.topic.drift + summary.post.drift; if (totalDrift === 0) { console.log(`\n✅ 全部对齐,无需修正`); } else if (fixMode) { console.log(`\n🔧 已完成修正`); } else { console.log(`\n⚠️ 发现偏差,加 --fix 自动修正`); } await prisma.$disconnect(); } main().catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });