chore: initial import to Nexus_Edu
This commit is contained in:
526
backend/prisma/schema.prisma
Normal file
526
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,526 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["fullTextIndex"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 基础审计字段模型
|
||||
// =============================================
|
||||
|
||||
model ApplicationUser {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
realName String @map("real_name") @db.VarChar(50)
|
||||
studentId String? @map("student_id") @db.VarChar(20)
|
||||
avatarUrl String? @map("avatar_url") @db.VarChar(500)
|
||||
gender Gender @default(Male)
|
||||
currentSchoolId String? @map("current_school_id") @db.VarChar(36)
|
||||
accountStatus AccountStatus @map("account_status") @default(Active)
|
||||
email String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(20)
|
||||
bio String? @db.Text
|
||||
passwordHash String @map("password_hash") @db.VarChar(255)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
// Relations
|
||||
classMemberships ClassMember[]
|
||||
createdQuestions Question[] @relation("CreatedQuestions")
|
||||
createdExams Exam[] @relation("CreatedExams")
|
||||
submissions StudentSubmission[]
|
||||
messages Message[]
|
||||
|
||||
@@index([studentId])
|
||||
@@index([email])
|
||||
@@index([phone])
|
||||
@@index([currentSchoolId])
|
||||
@@map("application_users")
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
Male
|
||||
Female
|
||||
}
|
||||
|
||||
enum AccountStatus {
|
||||
Active
|
||||
Suspended
|
||||
Graduated
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 组织架构模块
|
||||
// =============================================
|
||||
|
||||
model School {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
name String @db.VarChar(100)
|
||||
regionCode String @map("region_code") @db.VarChar(20)
|
||||
address String? @db.VarChar(200)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
grades Grade[]
|
||||
|
||||
@@index([regionCode])
|
||||
@@map("schools")
|
||||
}
|
||||
|
||||
model Grade {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
schoolId String @map("school_id") @db.VarChar(36)
|
||||
name String @db.VarChar(50)
|
||||
sortOrder Int @map("sort_order")
|
||||
enrollmentYear Int @map("enrollment_year")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
school School @relation(fields: [schoolId], references: [id])
|
||||
classes Class[]
|
||||
|
||||
@@index([schoolId])
|
||||
@@index([enrollmentYear])
|
||||
@@map("grades")
|
||||
}
|
||||
|
||||
model Class {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
gradeId String @map("grade_id") @db.VarChar(36)
|
||||
name String @db.VarChar(50)
|
||||
inviteCode String @unique @map("invite_code") @db.VarChar(10)
|
||||
headTeacherId String? @map("head_teacher_id") @db.VarChar(36)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
grade Grade @relation(fields: [gradeId], references: [id])
|
||||
members ClassMember[]
|
||||
assignments Assignment[]
|
||||
schedules Schedule[]
|
||||
|
||||
@@index([gradeId])
|
||||
@@map("classes")
|
||||
}
|
||||
|
||||
model ClassMember {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
classId String @map("class_id") @db.VarChar(36)
|
||||
userId String @map("user_id") @db.VarChar(36)
|
||||
roleInClass ClassRole @map("role_in_class") @default(Student)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
user ApplicationUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([classId, userId])
|
||||
@@index([userId])
|
||||
@@map("class_members")
|
||||
}
|
||||
|
||||
enum ClassRole {
|
||||
Student
|
||||
Monitor
|
||||
Committee
|
||||
Teacher
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 教材与知识图谱模块
|
||||
// =============================================
|
||||
|
||||
model Subject {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
name String @db.VarChar(50)
|
||||
code String @unique @db.VarChar(20)
|
||||
icon String? @db.VarChar(50)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
textbooks Textbook[]
|
||||
questions Question[]
|
||||
exams Exam[]
|
||||
|
||||
@@map("subjects")
|
||||
}
|
||||
|
||||
model Textbook {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
subjectId String @map("subject_id") @db.VarChar(36)
|
||||
name String @db.VarChar(100)
|
||||
publisher String @db.VarChar(100)
|
||||
versionYear String @map("version_year") @db.VarChar(20)
|
||||
coverUrl String? @map("cover_url") @db.VarChar(500)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
subject Subject @relation(fields: [subjectId], references: [id])
|
||||
units TextbookUnit[]
|
||||
|
||||
@@index([subjectId])
|
||||
@@map("textbooks")
|
||||
}
|
||||
|
||||
model TextbookUnit {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
textbookId String @map("textbook_id") @db.VarChar(36)
|
||||
name String @db.VarChar(100)
|
||||
sortOrder Int @map("sort_order")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
textbook Textbook @relation(fields: [textbookId], references: [id], onDelete: Cascade)
|
||||
lessons TextbookLesson[]
|
||||
|
||||
@@index([textbookId])
|
||||
@@index([sortOrder])
|
||||
@@map("textbook_units")
|
||||
}
|
||||
|
||||
model TextbookLesson {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
unitId String @map("unit_id") @db.VarChar(36)
|
||||
name String @db.VarChar(100)
|
||||
sortOrder Int @map("sort_order")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
unit TextbookUnit @relation(fields: [unitId], references: [id], onDelete: Cascade)
|
||||
knowledgePoints KnowledgePoint[]
|
||||
|
||||
@@index([unitId])
|
||||
@@index([sortOrder])
|
||||
@@map("textbook_lessons")
|
||||
}
|
||||
|
||||
model KnowledgePoint {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
lessonId String @map("lesson_id") @db.VarChar(36)
|
||||
parentKnowledgePointId String? @map("parent_knowledge_point_id") @db.VarChar(36)
|
||||
name String @db.VarChar(200)
|
||||
difficulty Int
|
||||
description String? @db.Text
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
lesson TextbookLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
||||
parentKnowledgePoint KnowledgePoint? @relation("KnowledgePointHierarchy", fields: [parentKnowledgePointId], references: [id], onDelete: Cascade)
|
||||
childKnowledgePoints KnowledgePoint[] @relation("KnowledgePointHierarchy")
|
||||
questionAssociations QuestionKnowledge[]
|
||||
|
||||
@@index([lessonId])
|
||||
@@index([parentKnowledgePointId])
|
||||
@@index([difficulty])
|
||||
@@map("knowledge_points")
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 题库资源模块
|
||||
// =============================================
|
||||
|
||||
model Question {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
subjectId String @map("subject_id") @db.VarChar(36)
|
||||
content String @db.Text
|
||||
optionsConfig Json? @map("options_config")
|
||||
questionType QuestionType @map("question_type")
|
||||
answer String @db.Text
|
||||
explanation String? @db.Text
|
||||
difficulty Int @default(3)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
subject Subject @relation(fields: [subjectId], references: [id])
|
||||
creator ApplicationUser @relation("CreatedQuestions", fields: [createdBy], references: [id])
|
||||
knowledgePoints QuestionKnowledge[]
|
||||
examNodes ExamNode[]
|
||||
|
||||
@@index([subjectId])
|
||||
@@index([questionType])
|
||||
@@index([difficulty])
|
||||
@@fulltext([content])
|
||||
@@map("questions")
|
||||
}
|
||||
|
||||
model QuestionKnowledge {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
questionId String @map("question_id") @db.VarChar(36)
|
||||
knowledgePointId String @map("knowledge_point_id") @db.VarChar(36)
|
||||
weight Int @default(100)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||
knowledgePoint KnowledgePoint @relation(fields: [knowledgePointId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([questionId, knowledgePointId])
|
||||
@@index([knowledgePointId])
|
||||
@@map("question_knowledge")
|
||||
}
|
||||
|
||||
enum QuestionType {
|
||||
SingleChoice
|
||||
MultipleChoice
|
||||
TrueFalse
|
||||
FillBlank
|
||||
Subjective
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 试卷工程模块
|
||||
// =============================================
|
||||
|
||||
model Exam {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
subjectId String @map("subject_id") @db.VarChar(36)
|
||||
title String @db.VarChar(200)
|
||||
totalScore Decimal @map("total_score") @default(0) @db.Decimal(5, 1)
|
||||
suggestedDuration Int @map("suggested_duration")
|
||||
status ExamStatus @default(Draft)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
subject Subject @relation(fields: [subjectId], references: [id])
|
||||
creator ApplicationUser @relation("CreatedExams", fields: [createdBy], references: [id])
|
||||
nodes ExamNode[]
|
||||
assignments Assignment[]
|
||||
|
||||
@@index([subjectId])
|
||||
@@index([status])
|
||||
@@index([createdBy])
|
||||
@@map("exams")
|
||||
}
|
||||
|
||||
model ExamNode {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
examId String @map("exam_id") @db.VarChar(36)
|
||||
parentNodeId String? @map("parent_node_id") @db.VarChar(36)
|
||||
nodeType NodeType @map("node_type")
|
||||
questionId String? @map("question_id") @db.VarChar(36)
|
||||
title String? @db.VarChar(200)
|
||||
description String? @db.Text
|
||||
score Decimal @db.Decimal(5, 1)
|
||||
sortOrder Int @map("sort_order")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
exam Exam @relation(fields: [examId], references: [id], onDelete: Cascade)
|
||||
parentNode ExamNode? @relation("ExamNodeHierarchy", fields: [parentNodeId], references: [id], onDelete: Cascade)
|
||||
childNodes ExamNode[] @relation("ExamNodeHierarchy")
|
||||
question Question? @relation(fields: [questionId], references: [id])
|
||||
submissionDetails SubmissionDetail[]
|
||||
|
||||
@@index([examId])
|
||||
@@index([parentNodeId])
|
||||
@@index([sortOrder])
|
||||
@@index([questionId])
|
||||
@@map("exam_nodes")
|
||||
}
|
||||
|
||||
enum ExamStatus {
|
||||
Draft
|
||||
Published
|
||||
}
|
||||
|
||||
enum NodeType {
|
||||
Group
|
||||
Question
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 教学执行模块
|
||||
// =============================================
|
||||
|
||||
model Assignment {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
examId String @map("exam_id") @db.VarChar(36)
|
||||
classId String @map("class_id") @db.VarChar(36)
|
||||
title String @db.VarChar(200)
|
||||
startTime DateTime @map("start_time")
|
||||
endTime DateTime @map("end_time")
|
||||
allowLateSubmission Boolean @map("allow_late_submission") @default(false)
|
||||
autoScoreEnabled Boolean @map("auto_score_enabled") @default(true)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
exam Exam @relation(fields: [examId], references: [id])
|
||||
class Class @relation(fields: [classId], references: [id])
|
||||
submissions StudentSubmission[]
|
||||
|
||||
@@index([examId])
|
||||
@@index([classId])
|
||||
@@index([startTime])
|
||||
@@index([endTime])
|
||||
@@map("assignments")
|
||||
}
|
||||
|
||||
model StudentSubmission {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
assignmentId String @map("assignment_id") @db.VarChar(36)
|
||||
studentId String @map("student_id") @db.VarChar(36)
|
||||
submissionStatus SubmissionStatus @map("submission_status") @default(Pending)
|
||||
submitTime DateTime? @map("submit_time")
|
||||
timeSpentSeconds Int? @map("time_spent_seconds")
|
||||
totalScore Decimal? @map("total_score") @db.Decimal(5, 1)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
||||
student ApplicationUser @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||
details SubmissionDetail[]
|
||||
|
||||
@@unique([assignmentId, studentId])
|
||||
@@index([studentId])
|
||||
@@index([submitTime])
|
||||
@@index([submissionStatus])
|
||||
@@map("student_submissions")
|
||||
}
|
||||
|
||||
model SubmissionDetail {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
submissionId String @map("submission_id") @db.VarChar(36)
|
||||
examNodeId String @map("exam_node_id") @db.VarChar(36)
|
||||
studentAnswer String? @map("student_answer") @db.Text
|
||||
gradingData Json? @map("grading_data")
|
||||
score Decimal? @db.Decimal(5, 1)
|
||||
judgement JudgementResult?
|
||||
teacherComment String? @map("teacher_comment") @db.Text
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String @map("updated_by") @db.VarChar(36)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
submission StudentSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
||||
examNode ExamNode @relation(fields: [examNodeId], references: [id])
|
||||
|
||||
@@unique([submissionId, examNodeId])
|
||||
@@index([examNodeId])
|
||||
@@index([judgement])
|
||||
@@map("submission_details")
|
||||
}
|
||||
|
||||
enum SubmissionStatus {
|
||||
Pending
|
||||
Submitted
|
||||
Grading
|
||||
Graded
|
||||
}
|
||||
|
||||
enum JudgementResult {
|
||||
Correct
|
||||
Incorrect
|
||||
Partial
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 辅助功能模块
|
||||
// =============================================
|
||||
|
||||
model Message {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
userId String @map("user_id") @db.VarChar(36)
|
||||
title String @db.VarChar(200)
|
||||
content String @db.Text
|
||||
type String @db.VarChar(20) // Announcement, Notification, Alert
|
||||
senderName String @map("sender_name") @db.VarChar(50)
|
||||
isRead Boolean @default(false) @map("is_read")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user ApplicationUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("messages")
|
||||
}
|
||||
|
||||
model Schedule {
|
||||
id String @id @default(uuid()) @db.VarChar(36)
|
||||
classId String @map("class_id") @db.VarChar(36)
|
||||
subject String @db.VarChar(50)
|
||||
room String? @db.VarChar(50)
|
||||
dayOfWeek Int @map("day_of_week") // 1-7
|
||||
period Int // 1-8
|
||||
startTime String @map("start_time") @db.VarChar(10) // HH:mm
|
||||
endTime String @map("end_time") @db.VarChar(10) // HH:mm
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([classId])
|
||||
@@map("schedules")
|
||||
}
|
||||
570
backend/prisma/seed.ts
Normal file
570
backend/prisma/seed.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user