571 lines
20 KiB
TypeScript
571 lines
20 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
||
import * as bcrypt from 'bcryptjs';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('🌱 开始创建种子数据...\n');
|
||
|
||
// 0. 清空数据 (按依赖顺序反向删除)
|
||
console.log('🧹 清空现有数据...');
|
||
await prisma.submissionDetail.deleteMany();
|
||
await prisma.studentSubmission.deleteMany();
|
||
await prisma.assignment.deleteMany();
|
||
await prisma.examNode.deleteMany();
|
||
await prisma.exam.deleteMany();
|
||
await prisma.questionKnowledge.deleteMany();
|
||
await prisma.question.deleteMany();
|
||
await prisma.knowledgePoint.deleteMany();
|
||
await prisma.textbookLesson.deleteMany();
|
||
await prisma.textbookUnit.deleteMany();
|
||
await prisma.textbook.deleteMany();
|
||
await prisma.subject.deleteMany();
|
||
await prisma.classMember.deleteMany();
|
||
await prisma.class.deleteMany();
|
||
await prisma.grade.deleteMany();
|
||
await prisma.school.deleteMany();
|
||
await prisma.applicationUser.deleteMany();
|
||
console.log(' ✅ 数据已清空');
|
||
|
||
// 1. 创建学校
|
||
console.log('📚 创建学校...');
|
||
const school = await prisma.school.create({
|
||
data: {
|
||
id: 'school-demo-001',
|
||
name: '北京示范高中',
|
||
regionCode: '110101',
|
||
address: '北京市东城区示范路100号',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
});
|
||
console.log(` ✅ 创建学校: ${school.name}`);
|
||
|
||
// 2. 创建年级
|
||
console.log('\n🎓 创建年级...');
|
||
const grades = await Promise.all([
|
||
prisma.grade.create({
|
||
data: {
|
||
id: 'grade-1',
|
||
schoolId: school.id,
|
||
name: '高一年级',
|
||
sortOrder: 1,
|
||
enrollmentYear: 2024,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.grade.create({
|
||
data: {
|
||
id: 'grade-2',
|
||
schoolId: school.id,
|
||
name: '高二年级',
|
||
sortOrder: 2,
|
||
enrollmentYear: 2023,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 ${grades.length} 个年级`);
|
||
|
||
// 3. 创建科目
|
||
console.log('\n📖 创建科目...');
|
||
const subjects = await Promise.all([
|
||
prisma.subject.create({
|
||
data: {
|
||
id: 'subject-math',
|
||
name: '数学',
|
||
code: 'MATH',
|
||
icon: '📐',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.subject.create({
|
||
data: {
|
||
id: 'subject-physics',
|
||
name: '物理',
|
||
code: 'PHYS',
|
||
icon: '⚡',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.subject.create({
|
||
data: {
|
||
id: 'subject-english',
|
||
name: '英语',
|
||
code: 'ENG',
|
||
icon: '🔤',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 ${subjects.length} 个科目`);
|
||
|
||
// 4. 创建教师账号
|
||
console.log('\n👨🏫 创建教师账号...');
|
||
const teacherPassword = await bcrypt.hash('123456', 10);
|
||
const teachers = await Promise.all([
|
||
prisma.applicationUser.create({
|
||
data: {
|
||
id: 'teacher-001',
|
||
realName: '李明',
|
||
studentId: 'T2024001',
|
||
email: 'liming@school.edu',
|
||
phone: '13800138001',
|
||
gender: 'Male',
|
||
currentSchoolId: school.id,
|
||
accountStatus: 'Active',
|
||
passwordHash: teacherPassword,
|
||
bio: '数学教师,教龄10年',
|
||
avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=teacher1',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.applicationUser.create({
|
||
data: {
|
||
id: 'teacher-002',
|
||
realName: '张伟',
|
||
studentId: 'T2024002',
|
||
email: 'zhangwei@school.edu',
|
||
phone: '13800138002',
|
||
gender: 'Male',
|
||
currentSchoolId: school.id,
|
||
accountStatus: 'Active',
|
||
passwordHash: teacherPassword,
|
||
bio: '物理教师,教龄8年',
|
||
avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=teacher2',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 ${teachers.length} 个教师账号 (密码: 123456)`);
|
||
|
||
// 5. 创建学生账号
|
||
console.log('\n👨🎓 创建学生账号...');
|
||
const studentPassword = await bcrypt.hash('123456', 10);
|
||
const students = [];
|
||
for (let i = 1; i <= 10; i++) {
|
||
const student = await prisma.applicationUser.create({
|
||
data: {
|
||
id: `student-${String(i).padStart(3, '0')}`,
|
||
realName: `学生${i}号`,
|
||
studentId: `S2024${String(i).padStart(3, '0')}`,
|
||
email: `student${i}@school.edu`,
|
||
phone: `1380013${String(8000 + i)}`,
|
||
gender: i % 2 === 0 ? 'Female' : 'Male',
|
||
currentSchoolId: school.id,
|
||
accountStatus: 'Active',
|
||
passwordHash: studentPassword,
|
||
avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=student${i}`,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
});
|
||
students.push(student);
|
||
}
|
||
console.log(` ✅ 创建 ${students.length} 个学生账号 (密码: 123456)`);
|
||
|
||
// 6. 创建班级
|
||
console.log('\n🏫 创建班级...');
|
||
const class1 = await prisma.class.create({
|
||
data: {
|
||
id: 'class-001',
|
||
gradeId: grades[0].id,
|
||
name: '高一(1)班',
|
||
inviteCode: 'ABC123',
|
||
headTeacherId: teachers[0].id,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
console.log(` ✅ 创建班级: ${class1.name} (邀请码: ${class1.inviteCode})`);
|
||
|
||
// 7. 添加班级成员
|
||
console.log('\n👥 添加班级成员...');
|
||
// 添加教师
|
||
await prisma.classMember.create({
|
||
data: {
|
||
id: 'cm-teacher-001',
|
||
classId: class1.id,
|
||
userId: teachers[0].id,
|
||
roleInClass: 'Teacher',
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
// 添加学生
|
||
for (let i = 0; i < students.length; i++) {
|
||
await prisma.classMember.create({
|
||
data: {
|
||
id: `cm-student-${String(i + 1).padStart(3, '0')}`,
|
||
classId: class1.id,
|
||
userId: students[i].id,
|
||
roleInClass: i === 0 ? 'Monitor' : 'Student',
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
}
|
||
console.log(` ✅ 添加 1 个教师和 ${students.length} 个学生到班级`);
|
||
|
||
// 8. 创建教材
|
||
console.log('\n📚 创建教材...');
|
||
const textbook = await prisma.textbook.create({
|
||
data: {
|
||
id: 'textbook-math-1',
|
||
subjectId: subjects[0].id,
|
||
name: '普通高中教科书·数学A版(必修第一册)',
|
||
publisher: '人民教育出版社',
|
||
versionYear: '2024',
|
||
coverUrl: 'https://placehold.co/300x400/007AFF/ffffff?text=Math',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
});
|
||
console.log(` ✅ 创建教材: ${textbook.name}`);
|
||
|
||
// 9. 创建单元和课节
|
||
console.log('\n📑 创建单元和课节...');
|
||
const unit1 = await prisma.textbookUnit.create({
|
||
data: {
|
||
id: 'unit-001',
|
||
textbookId: textbook.id,
|
||
name: '第一章 集合与常用逻辑用语',
|
||
sortOrder: 1,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
});
|
||
|
||
const lessons = await Promise.all([
|
||
prisma.textbookLesson.create({
|
||
data: {
|
||
id: 'lesson-001',
|
||
unitId: unit1.id,
|
||
name: '1.1 集合的概念',
|
||
sortOrder: 1,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.textbookLesson.create({
|
||
data: {
|
||
id: 'lesson-002',
|
||
unitId: unit1.id,
|
||
name: '1.2 集合间的基本关系',
|
||
sortOrder: 2,
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 1 个单元和 ${lessons.length} 个课节`);
|
||
|
||
// 10. 创建知识点
|
||
console.log('\n🎯 创建知识点...');
|
||
const knowledgePoints = await Promise.all([
|
||
prisma.knowledgePoint.create({
|
||
data: {
|
||
id: 'kp-001',
|
||
lessonId: lessons[0].id,
|
||
name: '集合的含义',
|
||
difficulty: 1,
|
||
description: '理解集合的基本概念',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
}),
|
||
prisma.knowledgePoint.create({
|
||
data: {
|
||
id: 'kp-002',
|
||
lessonId: lessons[1].id,
|
||
name: '子集的概念',
|
||
difficulty: 2,
|
||
description: '掌握子集的定义和性质',
|
||
createdBy: 'system',
|
||
updatedBy: 'system'
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 ${knowledgePoints.length} 个知识点`);
|
||
|
||
// 11. 创建题目
|
||
console.log('\n📝 创建题目...');
|
||
const questions = await Promise.all([
|
||
prisma.question.create({
|
||
data: {
|
||
id: 'question-001',
|
||
subjectId: subjects[0].id,
|
||
content: '<p>已知集合 A = {1, 2, 3}, B = {2, 3, 4}, 则 A ∩ B = ( )</p>',
|
||
questionType: 'SingleChoice',
|
||
difficulty: 2,
|
||
answer: 'B',
|
||
explanation: '集合 A 与 B 的公共元素为 2 和 3',
|
||
optionsConfig: {
|
||
options: [
|
||
{ label: 'A', content: '{1}' },
|
||
{ label: 'B', content: '{2, 3}' },
|
||
{ label: 'C', content: '{1, 2, 3, 4}' },
|
||
{ label: 'D', content: '∅' }
|
||
]
|
||
},
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
}),
|
||
prisma.question.create({
|
||
data: {
|
||
id: 'question-002',
|
||
subjectId: subjects[0].id,
|
||
content: '<p>若集合 A ⊆ B,则下列说法正确的是 ( )</p>',
|
||
questionType: 'SingleChoice',
|
||
difficulty: 2,
|
||
answer: 'C',
|
||
explanation: '子集定义:A的所有元素都在B中',
|
||
optionsConfig: {
|
||
options: [
|
||
{ label: 'A', content: 'A ∪ B = A' },
|
||
{ label: 'B', content: 'A ∩ B = B' },
|
||
{ label: 'C', content: 'A ∩ B = A' },
|
||
{ label: 'D', content: 'A ∪ B = ∅' }
|
||
]
|
||
},
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
}),
|
||
prisma.question.create({
|
||
data: {
|
||
id: 'question-003',
|
||
subjectId: subjects[0].id,
|
||
content: '<p>函数 f(x) = x² - 2x + 1 的最小值是 ______</p>',
|
||
questionType: 'FillBlank',
|
||
difficulty: 3,
|
||
answer: '0',
|
||
explanation: '配方法:f(x) = (x-1)², 最小值为 0',
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建 ${questions.length} 个题目`);
|
||
|
||
// 12. 创建试卷
|
||
console.log('\n📋 创建试卷...');
|
||
const exam = await prisma.exam.create({
|
||
data: {
|
||
id: 'exam-001',
|
||
subjectId: subjects[0].id,
|
||
title: '高一数学第一单元测试',
|
||
totalScore: 100,
|
||
suggestedDuration: 90,
|
||
status: 'Published',
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
console.log(` ✅ 创建试卷: ${exam.title}`);
|
||
|
||
// 13. 创建试卷节点
|
||
console.log('\n🌳 创建试卷结构...');
|
||
const groupNode = await prisma.examNode.create({
|
||
data: {
|
||
id: 'node-group-001',
|
||
examId: exam.id,
|
||
nodeType: 'Group',
|
||
title: '一、选择题',
|
||
description: '本大题共 2 小题,每小题 5 分,共 10 分',
|
||
score: 10,
|
||
sortOrder: 1,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
|
||
await Promise.all([
|
||
prisma.examNode.create({
|
||
data: {
|
||
id: 'node-q-001',
|
||
examId: exam.id,
|
||
parentNodeId: groupNode.id,
|
||
nodeType: 'Question',
|
||
questionId: questions[0].id,
|
||
score: 5,
|
||
sortOrder: 1,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
}),
|
||
prisma.examNode.create({
|
||
data: {
|
||
id: 'node-q-002',
|
||
examId: exam.id,
|
||
parentNodeId: groupNode.id,
|
||
nodeType: 'Question',
|
||
questionId: questions[1].id,
|
||
score: 5,
|
||
sortOrder: 2,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
}),
|
||
prisma.examNode.create({
|
||
data: {
|
||
id: 'node-q-003',
|
||
examId: exam.id,
|
||
nodeType: 'Question',
|
||
questionId: questions[2].id,
|
||
title: '二、填空题',
|
||
score: 10,
|
||
sortOrder: 2,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
})
|
||
]);
|
||
console.log(` ✅ 创建试卷结构(1个分组,3道题目)`);
|
||
|
||
// 14. 创建作业
|
||
console.log('\n📮 创建作业...');
|
||
const assignment = await prisma.assignment.create({
|
||
data: {
|
||
id: 'assignment-001',
|
||
examId: exam.id,
|
||
classId: class1.id,
|
||
title: '第一单元课后练习',
|
||
startTime: new Date('2025-11-26T00:00:00Z'),
|
||
endTime: new Date('2025-12-31T23:59:59Z'),
|
||
allowLateSubmission: false,
|
||
autoScoreEnabled: true,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
console.log(` ✅ 创建作业: ${assignment.title}`);
|
||
|
||
// 15. 为所有学生创建提交记录并模拟答题/批改
|
||
console.log('\n📬 创建学生提交记录并模拟答题...');
|
||
for (let i = 0; i < students.length; i++) {
|
||
const status = i < 5 ? 'Graded' : (i < 8 ? 'Submitted' : 'Pending');
|
||
const score = status === 'Graded' ? Math.floor(Math.random() * 20) + 80 : null; // 80-100分
|
||
|
||
const submission = await prisma.studentSubmission.create({
|
||
data: {
|
||
id: `submission-${String(i + 1).padStart(3, '0')}`,
|
||
assignmentId: assignment.id,
|
||
studentId: students[i].id,
|
||
submissionStatus: status,
|
||
submitTime: status !== 'Pending' ? new Date() : null,
|
||
totalScore: score,
|
||
timeSpentSeconds: status !== 'Pending' ? 3600 : null,
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
|
||
// 如果已提交或已批改,创建答题详情
|
||
if (status !== 'Pending') {
|
||
// 题目1:单选题 (正确答案 B)
|
||
await prisma.submissionDetail.create({
|
||
data: {
|
||
id: uuidv4(),
|
||
submissionId: submission.id,
|
||
examNodeId: 'node-q-001',
|
||
studentAnswer: i % 3 === 0 ? 'A' : 'B', // 部分答错
|
||
score: status === 'Graded' ? (i % 3 === 0 ? 0 : 5) : null,
|
||
judgement: status === 'Graded' ? (i % 3 === 0 ? 'Incorrect' : 'Correct') : null,
|
||
createdBy: students[i].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
|
||
// 题目2:单选题 (正确答案 C)
|
||
await prisma.submissionDetail.create({
|
||
data: {
|
||
id: uuidv4(),
|
||
submissionId: submission.id,
|
||
examNodeId: 'node-q-002',
|
||
studentAnswer: 'C', // 全部答对
|
||
score: status === 'Graded' ? 5 : null,
|
||
judgement: status === 'Graded' ? 'Correct' : null,
|
||
createdBy: students[i].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
|
||
// 题目3:填空题 (正确答案 0)
|
||
await prisma.submissionDetail.create({
|
||
data: {
|
||
id: uuidv4(),
|
||
submissionId: submission.id,
|
||
examNodeId: 'node-q-003',
|
||
studentAnswer: '0',
|
||
score: status === 'Graded' ? 10 : null,
|
||
judgement: status === 'Graded' ? 'Correct' : null,
|
||
teacherComment: status === 'Graded' ? '做得好!' : null,
|
||
createdBy: students[i].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
}
|
||
}
|
||
console.log(` ✅ 为 ${students.length} 个学生创建提交记录 (5个已批改, 3个已提交, 2个未提交)`);
|
||
|
||
// 创建更多试卷以测试列表
|
||
console.log('\n📄 创建更多试卷数据...');
|
||
for (let i = 2; i <= 15; i++) {
|
||
await prisma.exam.create({
|
||
data: {
|
||
id: `exam-${String(i).padStart(3, '0')}`,
|
||
subjectId: subjects[i % 3].id,
|
||
title: `模拟试卷 ${i}`,
|
||
totalScore: 100,
|
||
suggestedDuration: 90,
|
||
status: i % 2 === 0 ? 'Published' : 'Draft',
|
||
createdAt: new Date(Date.now() - i * 86400000), // 过去的时间
|
||
createdBy: teachers[0].id,
|
||
updatedBy: teachers[0].id
|
||
}
|
||
});
|
||
}
|
||
console.log(` ✅ 创建额外 14 份试卷`);
|
||
|
||
console.log('\n✨ 种子数据创建完成!\n');
|
||
console.log('═══════════════════════════════════════');
|
||
console.log('📊 数据统计:');
|
||
console.log('═══════════════════════════════════════');
|
||
console.log(` 学校: 1 所`);
|
||
console.log(` 年级: ${grades.length} 个`);
|
||
console.log(` 科目: ${subjects.length} 个`);
|
||
console.log(` 教师: ${teachers.length} 个`);
|
||
console.log(` 学生: ${students.length} 个`);
|
||
console.log(` 班级: 1 个`);
|
||
console.log(` 教材: 1 本`);
|
||
console.log(` 题目: ${questions.length} 道`);
|
||
console.log(` 试卷: 1 份`);
|
||
console.log(` 作业: 1 个`);
|
||
console.log('═══════════════════════════════════════\n');
|
||
console.log('🔑 测试账号:');
|
||
console.log('═══════════════════════════════════════');
|
||
console.log(' 教师账号: liming@school.edu / 123456');
|
||
console.log(' 学生账号: student1@school.edu / 123456');
|
||
console.log(' 班级邀请码: ABC123');
|
||
console.log('═══════════════════════════════════════\n');
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('❌ 种子数据创建失败:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|