#!/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(); });