duoqi-api/db/seeds/index.ts
Wang Zhuoxuan 3991a02a8c feat: 添加管理员用户名密码登录功能
新增 /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>
2026-04-11 15:25:31 +08:00

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);
});