全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除

This commit is contained in:
ZhuiGuangAI Dev
2026-06-12 17:27:47 +08:00
parent 464924aea0
commit 4c325c8699
454 changed files with 33647 additions and 980 deletions
+224
View File
@@ -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);
});