全项目扫描修复: 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
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env node
/**
* 同步勋章定义到数据库
* - 把 achievements 表填充/更新为代码中维护的 ACHIEVEMENT_DEFS
* - 对存量用户批量评估一次(首屏打开即有数据)
*/
import { PrismaClient } from "@prisma/client";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import "dotenv/config";
const base = process.env.DATABASE_URL.replace("mysql://", "mariadb://");
const cs = base + (base.includes("?") ? "&" : "?") + "connection_limit=5";
const prisma = new PrismaClient({ adapter: new PrismaMariaDb(cs) });
const ACHIEVEMENT_DEFS = [
{ key: "first_post", name: "初次发声", description: "发布第 1 个话题", icon: "🌱", tier: "bronze", points: 10, category: "post", sortOrder: 1, target: 1 },
{ key: "first_reply", name: "热心回复", description: "发出第 1 条回复", icon: "💬", tier: "bronze", points: 10, category: "reply", sortOrder: 2, target: 1 },
{ key: "first_like", name: "首获认可", description: "首次被点赞", icon: "👍", tier: "bronze", points: 10, category: "social", sortOrder: 3, target: 1 },
{ key: "posts_10", name: "话题达人", description: "累计发布 10 个话题", icon: "📚", tier: "silver", points: 30, category: "post", sortOrder: 10, target: 10 },
{ key: "replies_50", name: "热心答主", description: "累计发出 50 条回复", icon: "🗣️", tier: "silver", points: 30, category: "reply", sortOrder: 11, target: 50 },
{ key: "likes_50", name: "社区明星", description: "累计被点赞 50 次", icon: "⭐", tier: "silver", points: 30, category: "social", sortOrder: 12, target: 50 },
{ key: "checkin_7", name: "周满贯", description: "连续签到 7 天", icon: "🔥", tier: "silver", points: 30, category: "checkin", sortOrder: 13, target: 7 },
{ key: "best_answer_1", name: "最佳答主", description: "首个回复被采纳为最佳答案", icon: "🏆", tier: "silver", points: 30, category: "milestone", sortOrder: 14, target: 1 },
{ key: "posts_100", name: "百帖宗师", description: "累计发布 100 个话题", icon: "📕", tier: "gold", points: 100, category: "post", sortOrder: 20, target: 100 },
{ key: "replies_500", name: "千言万语", description: "累计发出 500 条回复", icon: "📝", tier: "gold", points: 100, category: "reply", sortOrder: 21, target: 500 },
{ key: "likes_500", name: "万人迷", description: "累计被点赞 500 次", icon: "💖", tier: "gold", points: 100, category: "social", sortOrder: 22, target: 500 },
{ key: "checkin_30", name: "月之守护", description: "累计签到 30 天", icon: "🌙", tier: "gold", points: 100, category: "checkin", sortOrder: 23, target: 30 },
{ key: "best_answer_10", name: "答疑名师", description: "10 个回复被采纳为最佳答案", icon: "👑", tier: "gold", points: 100, category: "milestone", sortOrder: 24, target: 10 },
{ key: "pioneer", name: "先驱者", description: "社区前 100 位注册用户", icon: "🚀", tier: "special", points: 200, category: "milestone", sortOrder: 30, target: 1 },
{ key: "night_owl", name: "深夜猫头鹰", description: "在 0~5 点发布 5 个话题", icon: "🦉", tier: "special", points: 50, category: "milestone", sortOrder: 31, target: 5 },
];
async function evaluateForUser(userId) {
const [topicCount, postCount, likeCount, bestAnswerCount, totalCheckIns] =
await Promise.all([
prisma.forumTopic.count({ where: { userId } }),
prisma.forumPost.count({ where: { userId } }),
prisma.forumPostLike.count({ where: { post: { userId } } }),
prisma.forumPost.count({ where: { userId, isAnswer: true } }),
prisma.forumCheckIn.count({ where: { userId } }),
]);
const progressFor = (key) => {
switch (key) {
case "first_post": return Math.min(topicCount, 1);
case "first_reply": return Math.min(postCount, 1);
case "first_like": return Math.min(likeCount, 1);
case "posts_10": return Math.min(topicCount, 10);
case "replies_50": return Math.min(postCount, 50);
case "likes_50": return Math.min(likeCount, 50);
case "checkin_7": return Math.min(totalCheckIns, 7);
case "best_answer_1": return Math.min(bestAnswerCount, 1);
case "posts_100": return Math.min(topicCount, 100);
case "replies_500": return Math.min(postCount, 500);
case "likes_500": return Math.min(likeCount, 500);
case "checkin_30": return Math.min(totalCheckIns, 30);
case "best_answer_10": return Math.min(bestAnswerCount, 10);
case "pioneer": return 1;
case "night_owl": return 0;
default: return 0;
}
};
const existing = await prisma.userAchievement.findMany({ where: { userId } });
const map = new Map(existing.map((e) => [e.achievementKey, e]));
for (const def of ACHIEVEMENT_DEFS) {
const progress = progressFor(def.key);
const prev = map.get(def.key);
const shouldUnlock = progress >= def.target;
if (prev) {
const data = { progress };
if (shouldUnlock && !prev.unlockedAt) {
data.unlockedAt = new Date();
}
await prisma.userAchievement.update({ where: { id: prev.id }, data });
} else {
await prisma.userAchievement.create({
data: {
userId,
achievementKey: def.key,
progress,
unlockedAt: shouldUnlock ? new Date() : null,
},
});
}
}
}
async function main() {
console.log(`[seed-achievements] 同步 ${ACHIEVEMENT_DEFS.length} 个勋章定义...`);
for (const def of ACHIEVEMENT_DEFS) {
const data = {
key: def.key,
name: def.name,
description: def.description,
icon: def.icon,
tier: def.tier,
points: def.points,
category: def.category,
sortOrder: def.sortOrder,
isActive: true,
};
await prisma.achievement.upsert({
where: { key: def.key },
create: data,
update: data,
});
}
console.log("[seed-achievements] 勋章定义已同步");
const users = await prisma.user.findMany({ select: { id: true } });
console.log(`[seed-achievements] 评估 ${users.length} 个用户的勋章进度...`);
for (const u of users) {
await evaluateForUser(u.id);
}
console.log("[seed-achievements] 完成");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});