duoqi-api/db/seeds/index.ts
Wang Zhuoxuan a2282975ca feat: 集成阿里云融合认证实现手机号一键登录与登录方式管理
- 新增 POST /auth/fusion/token 获取 SDK 鉴权 Token
- 新增 POST /auth/fusion/verify 用 verifyToken 换取手机号并登录/注册
- 新增 GET /auth/providers 按平台返回可用登录方式列表
- 新增 PUT /admin/auth-providers 管理端热切换第三方登录开关
- 新增 appSettings 表存储运行时配置,支持不重启生效
- 修复 schema 中超长外键名称导致的 db:push 失败
2026-05-27 22:50:11 +08:00

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