新增 /v1/admin/auth/login 接口,支持用户名密码登录获取 JWT Token。 - 添加 admin_users 表存储管理员账号和哈希密码 - 使用 bcryptjs 进行密码哈希(cost=10) - JWT Token 认证优先,保留 ADMIN_TOKEN 作为向后兼容 - 记录登录审计日志到 admin_audit_log - 种子数据创建默认管理员(username: admin, password: admin123) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
6.1 KiB
TypeScript
230 lines
6.1 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
|
import { db } from '../../src/db/client.js';
|
|
import { categories, questions, knowledgeCards, skillTree, achievements, adminUsers } from '../../src/db/schema.js';
|
|
import { eq } from 'drizzle-orm';
|
|
import * as adminAuthService from '../../src/services/admin/admin-auth.js';
|
|
|
|
import categoriesData from '../../content/categories.json' with { type: 'json' };
|
|
import historyData from '../../content/history.json' with { type: 'json' };
|
|
import dramaData from '../../content/drama.json' with { type: 'json' };
|
|
import crosstalkData from '../../content/crosstalk.json' with { type: 'json' };
|
|
import skillTreeData from '../../content/skill-tree.json' with { type: 'json' };
|
|
import achievementsData from '../../content/achievements.json' with { type: 'json' };
|
|
|
|
interface QuestionInput {
|
|
categoryId: string;
|
|
contentType: string;
|
|
difficulty: number;
|
|
stem: { text: string };
|
|
correctAnswer: string;
|
|
distractors: string[];
|
|
knowledgeCard: {
|
|
summary: string;
|
|
deepDive?: string;
|
|
sourceRef?: string;
|
|
};
|
|
}
|
|
|
|
interface SkillTreeNodeInput {
|
|
categoryId: string;
|
|
title: string;
|
|
sortOrder: number;
|
|
questionsRequired: number;
|
|
passThreshold: number;
|
|
parentId: string | null;
|
|
}
|
|
|
|
async function seedCategories() {
|
|
let inserted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const cat of categoriesData) {
|
|
const [existing] = await db.select().from(categories).where(eq(categories.id, cat.id)).limit(1);
|
|
if (existing) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
await db.insert(categories).values({
|
|
id: cat.id,
|
|
name: cat.name,
|
|
slug: cat.slug,
|
|
sortOrder: cat.sortOrder,
|
|
status: cat.status,
|
|
});
|
|
inserted++;
|
|
}
|
|
|
|
console.log(`Categories: ${inserted} inserted, ${skipped} skipped`);
|
|
}
|
|
|
|
async function seedSkillTree() {
|
|
let inserted = 0;
|
|
let skipped = 0;
|
|
const chapterIdMap = new Map<number, string>(); // sortOrder → id (per category)
|
|
|
|
for (const node of skillTreeData as SkillTreeNodeInput[]) {
|
|
// Check if chapter with same category + title already exists
|
|
const existing = await db
|
|
.select()
|
|
.from(skillTree)
|
|
.where(eq(skillTree.title, node.title))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
chapterIdMap.set(node.sortOrder, existing[0]!.id);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const id = uuid();
|
|
let parentId: string | null = null;
|
|
|
|
if (node.parentId === 'DEPENDS_ON_PREVIOUS') {
|
|
// Find the previous chapter in the same category
|
|
const previousSortOrder = node.sortOrder - 1;
|
|
parentId = chapterIdMap.get(previousSortOrder) ?? null;
|
|
}
|
|
|
|
await db.insert(skillTree).values({
|
|
id,
|
|
categoryId: node.categoryId,
|
|
title: node.title,
|
|
sortOrder: node.sortOrder,
|
|
questionsRequired: node.questionsRequired,
|
|
passThreshold: node.passThreshold,
|
|
parentId,
|
|
});
|
|
|
|
chapterIdMap.set(node.sortOrder, id);
|
|
inserted++;
|
|
}
|
|
|
|
console.log(`Skill tree: ${inserted} inserted, ${skipped} skipped`);
|
|
}
|
|
|
|
async function seedQuestions(allQuestions: QuestionInput[]) {
|
|
let inserted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const q of allQuestions) {
|
|
// Check by unique combination: categoryId + correctAnswer + stem text
|
|
const stemText = q.stem.text;
|
|
const existing = await db
|
|
.select()
|
|
.from(questions)
|
|
.where(eq(questions.correctAnswer, q.correctAnswer))
|
|
.limit(1);
|
|
|
|
const alreadyExists = existing.some(
|
|
(row) => (row.stem as { text: string }).text === stemText && row.categoryId === q.categoryId,
|
|
);
|
|
|
|
if (alreadyExists) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const questionId = uuid();
|
|
|
|
await db.insert(questions).values({
|
|
id: questionId,
|
|
stem: q.stem,
|
|
contentType: q.contentType as 'text',
|
|
correctAnswer: q.correctAnswer,
|
|
distractors: q.distractors,
|
|
categoryId: q.categoryId,
|
|
difficulty: q.difficulty,
|
|
status: 'published',
|
|
});
|
|
|
|
if (q.knowledgeCard) {
|
|
await db.insert(knowledgeCards).values({
|
|
id: uuid(),
|
|
questionId,
|
|
summary: q.knowledgeCard.summary,
|
|
deepDive: q.knowledgeCard.deepDive ?? null,
|
|
sourceRef: q.knowledgeCard.sourceRef ?? null,
|
|
});
|
|
}
|
|
|
|
inserted++;
|
|
}
|
|
|
|
console.log(`Questions: ${inserted} inserted, ${skipped} skipped`);
|
|
}
|
|
|
|
async function seedAchievements() {
|
|
let inserted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const a of achievementsData as Array<{
|
|
type: string;
|
|
name: string;
|
|
description: string;
|
|
iconUrl: string | null;
|
|
condition: Record<string, number>;
|
|
}>) {
|
|
const existing = await db
|
|
.select()
|
|
.from(achievements)
|
|
.where(eq(achievements.name, a.name))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
await db.insert(achievements).values({
|
|
id: uuid(),
|
|
type: a.type as 'knowledge' | 'behavior',
|
|
name: a.name,
|
|
description: a.description,
|
|
iconUrl: a.iconUrl ?? null,
|
|
condition: a.condition,
|
|
});
|
|
inserted++;
|
|
}
|
|
|
|
console.log(`Achievements: ${inserted} inserted, ${skipped} skipped`);
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Starting seed data import...\n');
|
|
|
|
// Step 0: Admin users (no dependencies)
|
|
await seedAdminUsers();
|
|
|
|
// Step 1: Categories (no dependencies)
|
|
await seedCategories();
|
|
|
|
// Step 2: Skill tree (depends on categories)
|
|
await seedSkillTree();
|
|
|
|
// Step 3: Questions + Knowledge cards (depends on categories)
|
|
const allQuestions: QuestionInput[] = [
|
|
...(historyData as QuestionInput[]),
|
|
...(dramaData as QuestionInput[]),
|
|
...(crosstalkData as QuestionInput[]),
|
|
];
|
|
await seedQuestions(allQuestions);
|
|
|
|
// Step 4: Achievements
|
|
await seedAchievements();
|
|
|
|
console.log('\nSeed data import complete!');
|
|
process.exit(0);
|
|
}
|
|
|
|
async function seedAdminUsers() {
|
|
// Create default admin user (username: admin, password: admin123)
|
|
// Note: In production, change the password immediately!
|
|
await adminAuthService.createAdminUser('admin', 'admin123', 'super_admin');
|
|
console.log('Admin user seeded: username=admin, password=admin123 (CHANGE IN PRODUCTION!)');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Seed failed:', err);
|
|
process.exit(1);
|
|
});
|