- 新增 POST /auth/fusion/token 获取 SDK 鉴权 Token - 新增 POST /auth/fusion/verify 用 verifyToken 换取手机号并登录/注册 - 新增 GET /auth/providers 按平台返回可用登录方式列表 - 新增 PUT /admin/auth-providers 管理端热切换第三方登录开关 - 新增 appSettings 表存储运行时配置,支持不重启生效 - 修复 schema 中超长外键名称导致的 db:push 失败
252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
|
import { db } from '../../src/db/client.js';
|
|
import { categories, questions, knowledgeCards, skillTree, achievements, appSettings } 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 0.5: App settings (auth provider toggles)
|
|
await seedAuthProviderSettings();
|
|
|
|
// 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!)');
|
|
}
|
|
|
|
async function seedAuthProviderSettings() {
|
|
const defaults = [
|
|
{ key: 'auth_provider_fusion_enabled', value: 'true', description: '一键登录(融合认证号码认证)' },
|
|
{ key: 'auth_provider_phone_sms_enabled', value: 'true', description: '短信验证码登录(融合认证短信)' },
|
|
{ key: 'auth_provider_huawei_enabled', value: 'true', description: '华为账号登录' },
|
|
{ key: 'auth_provider_apple_enabled', value: 'true', description: 'Apple 账号登录' },
|
|
{ key: 'auth_provider_wechat_enabled', value: 'false', description: '微信登录(按需开启)' },
|
|
{ key: 'auth_provider_qq_enabled', value: 'false', description: 'QQ 登录(按需开启)' },
|
|
];
|
|
|
|
for (const setting of defaults) {
|
|
await db
|
|
.insert(appSettings)
|
|
.values(setting)
|
|
.onDuplicateKeyUpdate({ set: { description: setting.description } });
|
|
}
|
|
console.log('Auth provider settings: seeded');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Seed failed:', err);
|
|
process.exit(1);
|
|
});
|