全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
// 回填历史 BotDailyStat 数据
|
||||
// 从 forumTopic / forumPost / 点赞表中,按本地日期(UTC+8)聚合每个 bot 的每日指标
|
||||
// 用法:node scripts/backfill-bot-daily-stats.mjs [--days=14] [--dry-run]
|
||||
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 });
|
||||
|
||||
// 把任意 Date 转成"本地日"的 key(YYYY-MM-DD)
|
||||
// 因为 Prisma @db.Date 在中国时区会被 UTC 截到前一天,所以聚合也按本地日
|
||||
function toLocalDateKey(d) {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function toUTCDateOfLocalDay(key) {
|
||||
// "YYYY-MM-DD" -> UTC 午夜 Date 对象
|
||||
const [y, m, d] = key.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const daysArg = args.find((a) => a.startsWith("--days="));
|
||||
const days = daysArg ? parseInt(daysArg.split("=")[1], 10) : 30;
|
||||
|
||||
console.log(`========== 回填 BotDailyStat(${days} 天, ${dryRun ? "DRY-RUN" : "实际写入"})==========\n`);
|
||||
|
||||
// 1. 拿到所有 bot 用户
|
||||
const botUsers = await prisma.user.findMany({
|
||||
where: { isBot: true },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (botUsers.length === 0) {
|
||||
console.log("没有 bot 用户,退出。");
|
||||
await prisma.$disconnect();
|
||||
return;
|
||||
}
|
||||
console.log(`共 ${botUsers.length} 个 bot`);
|
||||
|
||||
// 2. BotConfig 映射(userId -> configId)
|
||||
const configs = await prisma.botConfig.findMany({
|
||||
where: { userId: { in: botUsers.map((u) => u.id) } },
|
||||
select: { id: true, userId: true },
|
||||
});
|
||||
const configIdByUserId = new Map(configs.map((c) => [c.userId, c.id]));
|
||||
const botUserIds = botUsers.map((u) => u.id).filter((uid) => configIdByUserId.has(uid));
|
||||
console.log(`共 ${configs.length} 个 BotConfig(${botUsers.length - configs.length} 个 bot 还没创建 BotConfig)`);
|
||||
|
||||
// 3. 时间窗
|
||||
const since = new Date(Date.now() - days * 24 * 3600 * 1000);
|
||||
console.log(`时间窗: ${toLocalDateKey(since)} ~ 至今`);
|
||||
|
||||
// 4. 拉 bot 的所有话题(只取 createdAt)
|
||||
const topics = await prisma.forumTopic.findMany({
|
||||
where: { userId: { in: botUserIds }, createdAt: { gte: since } },
|
||||
select: { id: true, userId: true, createdAt: true, categoryId: true, likeCount: true },
|
||||
});
|
||||
console.log(`拉取到 ${topics.length} 个 bot 话题`);
|
||||
|
||||
// 5. 拉 bot 的所有回复
|
||||
const posts = await prisma.forumPost.findMany({
|
||||
where: { userId: { in: botUserIds }, createdAt: { gte: since } },
|
||||
select: { id: true, userId: true, createdAt: true, topicId: true, likeCount: true },
|
||||
});
|
||||
console.log(`拉取到 ${posts.length} 个 bot 回复`);
|
||||
|
||||
// 6. 拉 bot 话题的点赞(区分是不是 bot 自己)
|
||||
const topicIds = topics.map((t) => t.id);
|
||||
const topicLikes = topicIds.length > 0 ? await prisma.forumTopicLike.findMany({
|
||||
where: { topicId: { in: topicIds } },
|
||||
select: { topicId: true, userId: true, createdAt: true },
|
||||
}) : [];
|
||||
console.log(`拉取到 ${topicLikes.length} 条话题点赞`);
|
||||
|
||||
// 7. 拉 bot 回复的点赞
|
||||
const postIds = posts.map((p) => p.id);
|
||||
const postLikes = postIds.length > 0 ? await prisma.forumPostLike.findMany({
|
||||
where: { postId: { in: postIds } },
|
||||
select: { postId: true, userId: true, createdAt: true },
|
||||
}) : [];
|
||||
console.log(`拉取到 ${postLikes.length} 条回复点赞`);
|
||||
|
||||
// 8. 拉 bot 话题/回复下的所有回复(用于 human interactions:bot 收到真人回复)
|
||||
const topicsSet = new Set(topics.map((t) => t.id));
|
||||
const postsForBotTopics = topicsSet.size > 0 ? await prisma.forumPost.findMany({
|
||||
where: { topicId: { in: [...topicsSet] }, createdAt: { gte: since } },
|
||||
select: { id: true, userId: true, createdAt: true, topicId: true },
|
||||
}) : [];
|
||||
const botSet = new Set(botUserIds);
|
||||
const allUserIds = Array.from(new Set([
|
||||
...topicLikes.map((l) => l.userId),
|
||||
...postLikes.map((l) => l.userId),
|
||||
...postsForBotTopics.map((p) => p.userId),
|
||||
]));
|
||||
const humanIds = new Set();
|
||||
if (allUserIds.length > 0) {
|
||||
const humans = await prisma.user.findMany({
|
||||
where: { id: { in: allUserIds }, isBot: false },
|
||||
select: { id: true },
|
||||
});
|
||||
humans.forEach((h) => humanIds.add(h.id));
|
||||
}
|
||||
console.log(`识别出 ${humanIds.size} 个真人用户`);
|
||||
|
||||
// 9. 按 botConfigId + 本地日期 key 聚合
|
||||
// row: { topicCreated, replySent, repliesReceived, likesReceived, humanInteractions }
|
||||
const agg = new Map(); // key: `${cfgId}|${dateKey}` -> stats
|
||||
function bump(cfgId, dateKey, field, by = 1) {
|
||||
const key = `${cfgId}|${dateKey}`;
|
||||
if (!agg.has(key)) {
|
||||
agg.set(key, { botId: cfgId, dateKey, topicCreated: 0, replySent: 0, repliesReceived: 0, likesReceived: 0, humanInteractions: 0, followUpSent: 0, feedbackProcessed: 0 });
|
||||
}
|
||||
agg.get(key)[field] += by;
|
||||
}
|
||||
|
||||
// 9a. 话题创建
|
||||
for (const t of topics) {
|
||||
const cfgId = configIdByUserId.get(t.userId);
|
||||
if (!cfgId) continue;
|
||||
bump(cfgId, toLocalDateKey(t.createdAt), "topicCreated", 1);
|
||||
}
|
||||
|
||||
// 9b. 回复
|
||||
for (const p of posts) {
|
||||
const cfgId = configIdByUserId.get(p.userId);
|
||||
if (!cfgId) continue;
|
||||
bump(cfgId, toLocalDateKey(p.createdAt), "replySent", 1);
|
||||
}
|
||||
|
||||
// 9c. bot 话题收到的回复(repliesReceived = 该 bot 的所有话题下其他人的回复数)
|
||||
for (const p of postsForBotTopics) {
|
||||
// 找这个 topic 是哪个 bot 的
|
||||
const topic = topics.find((t) => t.id === p.topicId);
|
||||
if (!topic) continue;
|
||||
const ownerCfgId = configIdByUserId.get(topic.userId);
|
||||
if (!ownerCfgId) continue;
|
||||
// 跳过 bot 自己的回复
|
||||
if (botSet.has(p.userId)) continue;
|
||||
bump(ownerCfgId, toLocalDateKey(p.createdAt), "repliesReceived", 1);
|
||||
// 真人互动:bot 收到真人回复
|
||||
if (humanIds.has(p.userId)) {
|
||||
bump(ownerCfgId, toLocalDateKey(p.createdAt), "humanInteractions", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 9d. 话题点赞
|
||||
for (const l of topicLikes) {
|
||||
const topic = topics.find((t) => t.id === l.topicId);
|
||||
if (!topic) continue;
|
||||
const ownerCfgId = configIdByUserId.get(topic.userId);
|
||||
if (!ownerCfgId) continue;
|
||||
bump(ownerCfgId, toLocalDateKey(l.createdAt), "likesReceived", 1);
|
||||
if (humanIds.has(l.userId)) {
|
||||
bump(ownerCfgId, toLocalDateKey(l.createdAt), "humanInteractions", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 9e. 回复点赞
|
||||
for (const l of postLikes) {
|
||||
const post = posts.find((p) => p.id === l.postId);
|
||||
if (!post) continue;
|
||||
const ownerCfgId = configIdByUserId.get(post.userId);
|
||||
if (!ownerCfgId) continue;
|
||||
bump(ownerCfgId, toLocalDateKey(l.createdAt), "likesReceived", 1);
|
||||
if (humanIds.has(l.userId)) {
|
||||
bump(ownerCfgId, toLocalDateKey(l.createdAt), "humanInteractions", 1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n聚合出 ${agg.size} 条记录\n`);
|
||||
|
||||
// 10. 写入
|
||||
let writeCount = 0;
|
||||
for (const row of agg.values()) {
|
||||
const dateObj = toUTCDateOfLocalDay(row.dateKey);
|
||||
if (dryRun) {
|
||||
if (writeCount < 5) {
|
||||
console.log(` [DRY] ${row.botId} ${row.dateKey} T=${row.topicCreated} R=${row.replySent} RR=${row.repliesReceived} L=${row.likesReceived} H=${row.humanInteractions}`);
|
||||
}
|
||||
writeCount++;
|
||||
continue;
|
||||
}
|
||||
await prisma.botDailyStat.upsert({
|
||||
where: { botId_date: { botId: row.botId, date: dateObj } },
|
||||
create: {
|
||||
botId: row.botId,
|
||||
date: dateObj,
|
||||
topicCreated: row.topicCreated,
|
||||
replySent: row.replySent,
|
||||
followUpSent: row.followUpSent,
|
||||
repliesReceived: row.repliesReceived,
|
||||
likesReceived: row.likesReceived,
|
||||
humanInteractions: row.humanInteractions,
|
||||
feedbackProcessed: 0,
|
||||
},
|
||||
update: {
|
||||
topicCreated: { increment: row.topicCreated },
|
||||
replySent: { increment: row.replySent },
|
||||
repliesReceived: { increment: row.repliesReceived },
|
||||
likesReceived: { increment: row.likesReceived },
|
||||
humanInteractions: { increment: row.humanInteractions },
|
||||
},
|
||||
});
|
||||
writeCount++;
|
||||
}
|
||||
|
||||
console.log(`${dryRun ? "[DRY-RUN] 即将写入" : "已写入"} ${writeCount} 条 BotDailyStat 记录`);
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user