fix: 定时任务修复+管理后台Task7支持 (Task4外键修复, Task7管理员邮箱动态查询, Task5限流防护, Task7加入SCRIPT_MAP)
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
|
||||
import "dotenv/config";
|
||||
|
||||
const base = (process.env.DATABASE_URL || "mysql://localhost:3306/zhuiguang_ai?charset=utf8mb4").replace("mysql://", "mariadb://");
|
||||
const sep = base.includes("?") ? "&" : "?";
|
||||
const connectionString = `${base}${sep}connection_limit=20&pool_timeout=30`;
|
||||
const adapter = new PrismaMariaDb(connectionString);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
function ts() {
|
||||
return `[${new Date().toISOString()}]`;
|
||||
}
|
||||
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function ensureUniqueSlug(title) {
|
||||
const baseSlug = generateSlug(title) || "topic";
|
||||
const clean = baseSlug.substring(0, 80);
|
||||
const suffix = Date.now().toString(36);
|
||||
return `${clean}-${suffix}`;
|
||||
}
|
||||
|
||||
const CATEGORY_MAP = {
|
||||
"产品发布": "ai-tools",
|
||||
"product": "ai-tools",
|
||||
"融资投资": "ai-news",
|
||||
"business": "ai-news",
|
||||
"技术突破": "tech",
|
||||
"model": "tech",
|
||||
"政策法规": "ai-news",
|
||||
"policy": "ai-news",
|
||||
"开源项目": "tech",
|
||||
"opensource": "tech",
|
||||
"行业事件": "ai-news",
|
||||
"insight": "discuss",
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!process.env.DATABASE_URL) throw new Error("缺少环境变量: DATABASE_URL");
|
||||
|
||||
const startTime = new Date();
|
||||
const now = new Date();
|
||||
const cnOffset = 8 * 60 * 60 * 1000;
|
||||
const cnTime = new Date(now.getTime() + cnOffset);
|
||||
const today = cnTime.toISOString().split("T")[0];
|
||||
|
||||
const yesterdayDate = new Date(cnTime.getTime() - 24 * 60 * 60 * 1000);
|
||||
const yesterday = yesterdayDate.toISOString().split("T")[0];
|
||||
|
||||
console.log(`${ts()} 📰 [Task7] 开始将AI日报分发到社区板块 ${today}`);
|
||||
|
||||
let report = await prisma.dailyReport.findFirst({
|
||||
where: { date: new Date(today), status: "published" },
|
||||
include: { items: { orderBy: { sortOrder: "asc" } } },
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
console.log(`${ts()} ⚠ 今日(${today})无日报,尝试昨天(${yesterday})...`);
|
||||
report = await prisma.dailyReport.findFirst({
|
||||
where: { date: new Date(yesterday), status: "published" },
|
||||
include: { items: { orderBy: { sortOrder: "asc" } } },
|
||||
});
|
||||
}
|
||||
|
||||
if (!report || report.items.length === 0) {
|
||||
console.log(`${ts()} ⚠ 没有可用的日报条目,跳过`);
|
||||
await prisma.taskLog.create({
|
||||
data: {
|
||||
taskKey: "task7-news-to-community",
|
||||
taskName: "AI日报分发到社区",
|
||||
status: "completed",
|
||||
startedAt: startTime,
|
||||
finishedAt: new Date(),
|
||||
duration: new Date().getTime() - startTime.getTime(),
|
||||
result: { created: 0, skipped: 0, message: "没有可用的日报条目" },
|
||||
},
|
||||
});
|
||||
await prisma.$disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${ts()} 📋 日报ID=${report.id}, 共${report.items.length}条新闻`);
|
||||
|
||||
const forumCategories = await prisma.forumCategory.findMany({
|
||||
where: {
|
||||
slug: { in: ["ai-tools", "ai-news", "tech", "discuss"] },
|
||||
},
|
||||
});
|
||||
const categoryBySlug = {};
|
||||
for (const c of forumCategories) {
|
||||
categoryBySlug[c.slug] = c;
|
||||
}
|
||||
console.log(`${ts()} 📂 已加载${forumCategories.length}个社区板块: ${Object.keys(categoryBySlug).join(", ")}`);
|
||||
|
||||
const requiredSlugs = ["ai-tools", "ai-news", "tech", "discuss"];
|
||||
for (const slug of requiredSlugs) {
|
||||
if (!categoryBySlug[slug]) {
|
||||
throw new Error(`社区板块不存在: ${slug},请先在管理后台创建社区分类`);
|
||||
}
|
||||
}
|
||||
|
||||
const adminUser = await prisma.user.findFirst({
|
||||
where: { role: "ADMIN" },
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
if (!adminUser) {
|
||||
throw new Error("管理员用户不存在,请先创建管理员账号");
|
||||
}
|
||||
console.log(`${ts()} 👤 管理员用户: ${adminUser.name || adminUser.email} (ID:${adminUser.id})`);
|
||||
|
||||
const topicCount = await prisma.forumTopic.count();
|
||||
console.log(`${ts()} 📊 社区现有话题数: ${topicCount}`);
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const item of report.items) {
|
||||
const forumSlug = CATEGORY_MAP[item.category];
|
||||
if (!forumSlug) {
|
||||
skipped++;
|
||||
console.log(`${ts()} ⏭ 跳过(未映射分类): [${item.category}] ${item.title}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const forumCategory = categoryBySlug[forumSlug];
|
||||
if (!forumCategory) {
|
||||
skipped++;
|
||||
console.log(`${ts()} ⏭ 跳过(板块不存在): [${item.category}] ${item.title} → ${forumSlug}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await prisma.forumTopic.findFirst({
|
||||
where: { title: item.title },
|
||||
});
|
||||
if (existing) {
|
||||
skipped++;
|
||||
console.log(`${ts()} ⏭ 跳过(已存在): ${item.title}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const slug = ensureUniqueSlug(item.title);
|
||||
const content = item.summary
|
||||
? `${item.summary}\n\n${item.detail || ""}\n\n来源: ${item.sourceUrl || "AI日报"}`
|
||||
: `${item.detail || ""}\n\n来源: ${item.sourceUrl || "AI日报"}`;
|
||||
|
||||
try {
|
||||
const topic = await prisma.forumTopic.create({
|
||||
data: {
|
||||
categoryId: forumCategory.id,
|
||||
userId: adminUser.id,
|
||||
title: item.title,
|
||||
slug,
|
||||
content: content.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.forumCategory.update({
|
||||
where: { id: forumCategory.id },
|
||||
data: { topicCount: { increment: 1 } },
|
||||
});
|
||||
|
||||
created++;
|
||||
console.log(`${ts()} ✅ 已创建话题: [${forumCategory.name}] ${item.title} (ID:${topic.id})`);
|
||||
} catch (e) {
|
||||
errors.push(`${item.title}: ${e.message}`);
|
||||
console.error(`${ts()} ❌ 创建话题失败: ${item.title} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
const result = {
|
||||
reportId: report.id,
|
||||
reportDate: report.date.toISOString().split("T")[0],
|
||||
totalItems: report.items.length,
|
||||
created,
|
||||
skipped,
|
||||
duration: `${(duration / 1000).toFixed(1)}s`,
|
||||
};
|
||||
|
||||
console.log(`\n${ts()} 🏁 [Task7] 完成: 创建${created}个话题, 跳过${skipped}个, 耗时${result.duration}`);
|
||||
|
||||
await prisma.taskLog.create({
|
||||
data: {
|
||||
taskKey: "task7-news-to-community",
|
||||
taskName: "AI日报分发到社区",
|
||||
status: errors.length > 0 && created === 0 ? "failed" : "completed",
|
||||
startedAt: startTime,
|
||||
finishedAt: endTime,
|
||||
duration,
|
||||
result,
|
||||
error: errors.length > 0 ? errors.slice(0, 10).join("\n") : null,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch(async (e) => {
|
||||
console.error(`${ts()} ❌ 任务七失败:`, e.message);
|
||||
if (prisma) {
|
||||
try {
|
||||
await prisma.taskLog.create({
|
||||
data: {
|
||||
taskKey: "task7-news-to-community",
|
||||
taskName: "AI日报分发到社区",
|
||||
status: "failed",
|
||||
startedAt: new Date(),
|
||||
finishedAt: new Date(),
|
||||
duration: 0,
|
||||
error: e.message,
|
||||
},
|
||||
});
|
||||
} catch {}
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user