128 lines
6.2 KiB
JavaScript
128 lines
6.2 KiB
JavaScript
#!/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();
|
|
});
|