347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
import "dotenv/config"
|
||
import { db } from "@/shared/db"
|
||
import { users, exams, questions, knowledgePoints, examSubmissions, examQuestions, submissionAnswers } from "@/shared/db/schema"
|
||
import { createId } from "@paralleldrive/cuid2"
|
||
import { faker } from "@faker-js/faker"
|
||
import { eq } from "drizzle-orm"
|
||
|
||
/**
|
||
* Seed Script for Next_Edu
|
||
*
|
||
* Usage:
|
||
* 1. Ensure DATABASE_URL is set in .env
|
||
* 2. Run with tsx: npx tsx docs/scripts/seed-exams.ts
|
||
*/
|
||
|
||
const SUBJECTS = ["Mathematics", "Physics", "English", "Chemistry", "Biology"]
|
||
const GRADES = ["Grade 10", "Grade 11", "Grade 12"]
|
||
const DIFFICULTY = [1, 2, 3, 4, 5]
|
||
|
||
async function seed() {
|
||
console.log("🌱 Starting seed process...")
|
||
|
||
// 1. Create a Teacher User if not exists
|
||
const teacherEmail = "teacher@example.com"
|
||
let teacherId = "user_teacher_123"
|
||
|
||
const existingTeacher = await db.query.users.findFirst({
|
||
where: eq(users.email, teacherEmail)
|
||
})
|
||
|
||
if (!existingTeacher) {
|
||
console.log("Creating teacher user...")
|
||
await db.insert(users).values({
|
||
id: teacherId,
|
||
name: "Senior Teacher",
|
||
email: teacherEmail,
|
||
role: "teacher",
|
||
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Teacher",
|
||
})
|
||
} else {
|
||
teacherId = existingTeacher.id
|
||
console.log("Teacher user exists:", teacherId)
|
||
}
|
||
|
||
// 1b. Create Students
|
||
console.log("Creating students...")
|
||
const studentIds: string[] = []
|
||
for (let i = 0; i < 5; i++) {
|
||
const sId = createId()
|
||
studentIds.push(sId)
|
||
await db.insert(users).values({
|
||
id: sId,
|
||
name: faker.person.fullName(),
|
||
email: faker.internet.email(),
|
||
role: "student",
|
||
image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${sId}`,
|
||
})
|
||
}
|
||
|
||
// 2. Create Knowledge Points
|
||
console.log("Creating knowledge points...")
|
||
const kpIds: string[] = []
|
||
for (const subject of SUBJECTS) {
|
||
for (let i = 0; i < 3; i++) {
|
||
const kpId = createId()
|
||
kpIds.push(kpId)
|
||
await db.insert(knowledgePoints).values({
|
||
id: kpId,
|
||
name: `${subject} - ${faker.science.unit()}`,
|
||
description: faker.lorem.sentence(),
|
||
level: 1,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 3. Create Questions
|
||
console.log("Creating questions...")
|
||
const questionIds: string[] = []
|
||
for (let i = 0; i < 50; i++) {
|
||
const qId = createId()
|
||
questionIds.push(qId)
|
||
|
||
const type = faker.helpers.arrayElement(["single_choice", "multiple_choice", "text", "judgment"])
|
||
|
||
await db.insert(questions).values({
|
||
id: qId,
|
||
content: {
|
||
text: faker.lorem.paragraph(),
|
||
options: type.includes("choice") ? [
|
||
{ id: "A", text: faker.lorem.sentence(), isCorrect: true },
|
||
{ id: "B", text: faker.lorem.sentence(), isCorrect: false },
|
||
{ id: "C", text: faker.lorem.sentence(), isCorrect: false },
|
||
{ id: "D", text: faker.lorem.sentence(), isCorrect: false },
|
||
] : undefined
|
||
},
|
||
type: type as any,
|
||
difficulty: faker.helpers.arrayElement(DIFFICULTY),
|
||
authorId: teacherId,
|
||
})
|
||
}
|
||
|
||
// 4. Create Exams & Submissions
|
||
console.log("Creating exams and submissions...")
|
||
for (let i = 0; i < 15; i++) {
|
||
const examId = createId()
|
||
const subject = faker.helpers.arrayElement(SUBJECTS)
|
||
const grade = faker.helpers.arrayElement(GRADES)
|
||
const status = faker.helpers.arrayElement(["draft", "published", "archived"])
|
||
|
||
const scheduledAt = faker.date.soon({ days: 30 })
|
||
|
||
const meta = {
|
||
subject,
|
||
grade,
|
||
difficulty: faker.helpers.arrayElement(DIFFICULTY),
|
||
totalScore: 100,
|
||
durationMin: faker.helpers.arrayElement([45, 60, 90, 120]),
|
||
questionCount: faker.number.int({ min: 10, max: 30 }),
|
||
tags: [faker.word.sample(), faker.word.sample()],
|
||
scheduledAt: scheduledAt.toISOString()
|
||
}
|
||
|
||
await db.insert(exams).values({
|
||
id: examId,
|
||
title: `${subject} ${faker.helpers.arrayElement(["Midterm", "Final", "Quiz", "Unit Test"])}`,
|
||
description: JSON.stringify(meta),
|
||
creatorId: teacherId,
|
||
startTime: scheduledAt,
|
||
status: status as any,
|
||
})
|
||
|
||
// Link some questions to this exam (random 5 questions)
|
||
const selectedQuestions = faker.helpers.arrayElements(questionIds, 5)
|
||
await db.insert(examQuestions).values(
|
||
selectedQuestions.map((qId, idx) => ({
|
||
examId,
|
||
questionId: qId,
|
||
score: 20, // 5 * 20 = 100
|
||
order: idx
|
||
}))
|
||
)
|
||
|
||
// Create submissions for published exams
|
||
if (status === "published") {
|
||
const submittingStudents = faker.helpers.arrayElements(studentIds, faker.number.int({ min: 1, max: 3 }))
|
||
for (const studentId of submittingStudents) {
|
||
const submissionId = createId()
|
||
const submissionStatus = faker.helpers.arrayElement(["submitted", "graded"])
|
||
|
||
await db.insert(examSubmissions).values({
|
||
id: submissionId,
|
||
examId,
|
||
studentId,
|
||
score: submissionStatus === "graded" ? faker.number.int({ min: 60, max: 100 }) : null,
|
||
status: submissionStatus,
|
||
submittedAt: faker.date.recent(),
|
||
})
|
||
|
||
// Generate answers for this submission
|
||
for (const qId of selectedQuestions) {
|
||
await db.insert(submissionAnswers).values({
|
||
id: createId(),
|
||
submissionId: submissionId,
|
||
questionId: qId,
|
||
answerContent: { answer: faker.lorem.sentence() }, // Mock answer
|
||
score: submissionStatus === "graded" ? faker.number.int({ min: 0, max: 20 }) : null,
|
||
feedback: submissionStatus === "graded" ? faker.lorem.sentence() : null,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Create a specific Primary School Chinese Exam (小学语文)
|
||
console.log("Creating Primary School Chinese Exam...")
|
||
const chineseExamId = createId()
|
||
const chineseQuestions = []
|
||
|
||
// 5a. Pinyin Questions
|
||
const pinyinQ1 = createId()
|
||
const pinyinQ2 = createId()
|
||
chineseQuestions.push({ id: pinyinQ1, score: 5 }, { id: pinyinQ2, score: 5 })
|
||
|
||
await db.insert(questions).values([
|
||
{
|
||
id: pinyinQ1,
|
||
content: { text: "看拼音写词语:chūn tiān ( )" },
|
||
type: "text",
|
||
difficulty: 1,
|
||
authorId: teacherId,
|
||
},
|
||
{
|
||
id: pinyinQ2,
|
||
content: { text: "看拼音写词语:huā duǒ ( )" },
|
||
type: "text",
|
||
difficulty: 1,
|
||
authorId: teacherId,
|
||
}
|
||
])
|
||
|
||
// 5b. Vocabulary Questions
|
||
const vocabQ1 = createId()
|
||
const vocabQ2 = createId()
|
||
chineseQuestions.push({ id: vocabQ1, score: 5 }, { id: vocabQ2, score: 5 })
|
||
|
||
await db.insert(questions).values([
|
||
{
|
||
id: vocabQ1,
|
||
content: {
|
||
text: "选词填空:今天天气真( )。",
|
||
options: [
|
||
{ id: "A", text: "美好", isCorrect: false },
|
||
{ id: "B", text: "晴朗", isCorrect: true },
|
||
{ id: "C", text: "快乐", isCorrect: false }
|
||
]
|
||
},
|
||
type: "single_choice",
|
||
difficulty: 2,
|
||
authorId: teacherId,
|
||
},
|
||
{
|
||
id: vocabQ2,
|
||
content: {
|
||
text: "下列词语中,书写正确的是( )。",
|
||
options: [
|
||
{ id: "A", text: "漂扬", isCorrect: false },
|
||
{ id: "B", text: "飘扬", isCorrect: true },
|
||
{ id: "C", text: "票扬", isCorrect: false }
|
||
]
|
||
},
|
||
type: "single_choice",
|
||
difficulty: 2,
|
||
authorId: teacherId,
|
||
}
|
||
])
|
||
|
||
// 5c. Reading Comprehension Questions
|
||
const readingQ1 = createId()
|
||
const readingQ2 = createId()
|
||
chineseQuestions.push({ id: readingQ1, score: 10 }, { id: readingQ2, score: 10 })
|
||
|
||
await db.insert(questions).values([
|
||
{
|
||
id: readingQ1,
|
||
content: {
|
||
text: "阅读短文《小兔子乖乖》,回答问题:\n\n小兔子乖乖,把门儿开开...\n\n文中提到的动物是?",
|
||
options: [
|
||
{ id: "A", text: "大灰狼", isCorrect: false },
|
||
{ id: "B", text: "小兔子", isCorrect: true },
|
||
{ id: "C", text: "小花猫", isCorrect: false }
|
||
]
|
||
},
|
||
type: "single_choice",
|
||
difficulty: 3,
|
||
authorId: teacherId,
|
||
},
|
||
{
|
||
id: readingQ2,
|
||
content: { text: "请用一句话形容小兔子。" },
|
||
type: "text",
|
||
difficulty: 3,
|
||
authorId: teacherId,
|
||
}
|
||
])
|
||
|
||
// 5d. Construct Exam Structure
|
||
const chineseExamStructure = [
|
||
{
|
||
id: createId(),
|
||
type: "group",
|
||
title: "第一部分:基础知识",
|
||
children: [
|
||
{
|
||
id: createId(),
|
||
type: "group",
|
||
title: "一、看拼音写词语",
|
||
children: [
|
||
{ id: createId(), type: "question", questionId: pinyinQ1, score: 5 },
|
||
{ id: createId(), type: "question", questionId: pinyinQ2, score: 5 }
|
||
]
|
||
},
|
||
{
|
||
id: createId(),
|
||
type: "group",
|
||
title: "二、词语积累",
|
||
children: [
|
||
{ id: createId(), type: "question", questionId: vocabQ1, score: 5 },
|
||
{ id: createId(), type: "question", questionId: vocabQ2, score: 5 }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: createId(),
|
||
type: "group",
|
||
title: "第二部分:阅读理解",
|
||
children: [
|
||
{
|
||
id: createId(),
|
||
type: "group",
|
||
title: "三、短文阅读",
|
||
children: [
|
||
{ id: createId(), type: "question", questionId: readingQ1, score: 10 },
|
||
{ id: createId(), type: "question", questionId: readingQ2, score: 10 }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
|
||
await db.insert(exams).values({
|
||
id: chineseExamId,
|
||
title: "小学语文三年级上册期末考试",
|
||
description: JSON.stringify({
|
||
subject: "Chinese",
|
||
grade: "Grade 3",
|
||
difficulty: 3,
|
||
totalScore: 40,
|
||
durationMin: 90,
|
||
questionCount: 6,
|
||
tags: ["期末", "语文", "三年级"]
|
||
}),
|
||
structure: chineseExamStructure,
|
||
creatorId: teacherId,
|
||
status: "published",
|
||
startTime: new Date(),
|
||
})
|
||
|
||
// Link questions to exam
|
||
await db.insert(examQuestions).values(
|
||
chineseQuestions.map((q, idx) => ({
|
||
examId: chineseExamId,
|
||
questionId: q.id,
|
||
score: q.score,
|
||
order: idx
|
||
}))
|
||
)
|
||
|
||
console.log("✅ Seed completed successfully!")
|
||
process.exit(0)
|
||
}
|
||
|
||
seed().catch((err) => {
|
||
console.error("❌ Seed failed:", err)
|
||
process.exit(1)
|
||
})
|