diff --git a/scripts/seed.ts b/scripts/seed.ts index 8759d06..c550d7d 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -1,642 +1,1303 @@ import "dotenv/config"; import { db } from "../src/shared/db"; -import { - users, roles, usersToRoles, +import { + users, roles, usersToRoles, rolePermissions, questions, knowledgePoints, questionsToKnowledgePoints, - exams, examQuestions, - homeworkAssignments, - homeworkAssignmentQuestions, - homeworkAssignmentTargets, - homeworkSubmissions, - homeworkAnswers, + exams, examQuestions, examSubmissions, submissionAnswers, + homeworkAssignments, homeworkAssignmentQuestions, + homeworkAssignmentTargets, homeworkSubmissions, homeworkAnswers, textbooks, chapters, - schools, - grades, - classes, classEnrollments, classSchedule, - subjects + schools, grades, classes, classEnrollments, classSchedule, + classSubjectTeachers, subjects, academicYears, + parentStudentRelations, + announcements, + gradeRecords, + coursePlans, coursePlanItems, + attendanceRecords, } from "../src/shared/db/schema"; import { createId } from "@paralleldrive/cuid2"; -import { faker } from "@faker-js/faker"; -import { sql } from "drizzle-orm"; +import { sql, eq } from "drizzle-orm"; import { hash } from "bcryptjs"; +import { ROLE_PERMISSIONS } from "../src/shared/lib/permissions"; /** - * Enterprise-Grade Seed Script for Next_Edu - * - * Scenarios Covered: - * 1. IAM: RBAC with multiple roles (Teacher & Grade Head). - * 2. Knowledge Graph: Nested Knowledge Points (Math -> Algebra -> Linear Equations). - * 3. Question Bank: Rich Text Content & Nested Questions (Reading Comprehension). - * 4. Exams: JSON Structure for Sectioning. + * 小学场景初始化脚本 + * + * 数据规模: + * - 1 所小学 + * - 2 个年级(一年级、二年级) + * - 每个年级 2 个班级(共 4 个班) + * - 每班 2 个老师(1 班主任 + 1 任课),跨班凑齐语数外 3 科 + * - 每班 6 名学生(共 24 名学生) + * - 每名学生 1 位家长(共 24 位家长) + * - 语数外 3 科,每科 1 本教材(含第一章第一节课) + * - 2 套试卷(语文、数学),含学生答题与批改 + * - 配套作业、成绩、考勤、公告、课程计划数据 + * + * 所有账号密码统一为:123456 */ +// ============ 类型与常量 ============ + +const PASSWORD = "123456"; +const NOW = new Date(); + +// ============ 主流程 ============ + async function seed() { - console.log("🌱 Starting Database Seed..."); + console.log("🌱 开始小学场景数据初始化..."); const start = performance.now(); - // --- 0. Cleanup (Optional: Truncate tables for fresh start) --- - // Note: Order matters due to foreign keys if checks are enabled. - // Ideally, use: SET FOREIGN_KEY_CHECKS = 0; - try { - await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`); - const tables = [ - "class_schedule", "class_enrollments", "classes", - "homework_answers", "homework_submissions", "homework_assignment_targets", "homework_assignment_questions", "homework_assignments", - "submission_answers", "exam_submissions", "exam_questions", "exams", - "questions_to_knowledge_points", "questions", "knowledge_points", - "chapters", "textbooks", - "grades", "schools", "subjects", - "users_to_roles", "roles", "users", "accounts", "sessions" - ]; - for (const table of tables) { - await db.execute(sql.raw(`TRUNCATE TABLE \`${table}\`;`)); + // --- 0. 清理数据 --- + await cleanup(); + + // --- 1. 角色 + 权限 --- + await seedRolesAndPermissions(); + + // --- 2. 学科 --- + const subjectMap = await seedSubjects(); + + // --- 3. 学校 + 年级 + 学年 --- + const { schoolId, gradeMap, academicYearId } = await seedSchoolAndGrades(); + + // --- 4. 用户(admin + 老师 + 学生 + 家长)--- + const { adminId, teacherMap, studentMap, parentMap } = await seedUsers(schoolId, gradeMap); + + // --- 5. 班级 + 师生关系 --- + const classMap = await seedClasses(schoolId, gradeMap, teacherMap); + + // --- 6. 班级-学科-老师关联(凑齐语数外)--- + await seedClassSubjectTeachers(classMap, teacherMap, subjectMap); + + // --- 7. 学生分班 + 家长-学生关联 --- + await seedEnrollmentsAndParents(classMap, studentMap, parentMap); + + // --- 8. 教材 + 章节(每科第一章第一节课)--- + const chapterMap = await seedTextbooksAndChapters(subjectMap, gradeMap); + + // --- 9. 知识点 --- + const kpMap = await seedKnowledgePoints(chapterMap); + + // --- 10. 课表 --- + await seedClassSchedule(classMap, subjectMap); + + // --- 11. 题库 --- + const questionBanks = await seedQuestions(teacherMap, subjectMap, kpMap); + + // --- 12. 试卷(语文、数学各 1 套)+ 学生答题与批改 --- + await seedExamsAndSubmissions(teacherMap, classMap, studentMap, subjectMap, questionBanks, gradeMap); + + // --- 13. 作业(引用试卷)+ 学生答题与批改 --- + await seedHomework(teacherMap, classMap, studentMap, questionBanks, subjectMap); + + // --- 14. 成绩记录 --- + await seedGradeRecords(teacherMap, classMap, studentMap, subjectMap, academicYearId); + + // --- 15. 考勤记录 --- + await seedAttendanceRecords(teacherMap, classMap, studentMap); + + // --- 16. 课程计划 --- + await seedCoursePlans(teacherMap, classMap, subjectMap, academicYearId, chapterMap); + + // --- 17. 公告 --- + await seedAnnouncements(adminId, gradeMap, classMap); + + const end = performance.now(); + console.log(`\n✅ 初始化完成,耗时 ${(end - start).toFixed(0)}ms`); + console.log("\n📋 登录账号(密码统一为 123456):"); + console.log(` 管理员: admin@xiaoxue.edu.cn`); + console.log(` 语文老师/一年级1班班主任: t_chinese_1@xiaoxue.edu.cn`); + console.log(` 数学老师/一年级1班任课: t_math_1@xiaoxue.edu.cn`); + console.log(` 英语老师/一年级2班任课: t_english_1@xiaoxue.edu.cn`); + console.log(` 学生(一年级1班): student_g1c1_1@xiaoxue.edu.cn`); + console.log(` 家长: parent_g1c1_1@xiaoxue.edu.cn`); + process.exit(0); +} + +// ============ 0. 清理 ============ + +async function cleanup() { + console.log("🧹 清理现有数据..."); + await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`); + const tables = [ + "attendance_records", + "course_plan_items", "course_plans", + "grade_records", + "announcements", + "homework_answers", "homework_submissions", "homework_assignment_targets", "homework_assignment_questions", "homework_assignments", + "submission_answers", "exam_submissions", "exam_questions", "exams", + "questions_to_knowledge_points", "questions", "knowledge_points", + "chapters", "textbooks", + "class_schedule", "class_subject_teachers", "class_enrollments", "classes", + "parent_student_relations", + "grades", "academic_years", "schools", "subjects", + "role_permissions", "users_to_roles", "roles", "users", "accounts", "sessions", + "notification_preferences", "password_security", "message_notifications", "messages", + "data_change_logs", "login_logs", "audit_logs", + "scheduling_rules", "schedule_changes", "attendance_rules", + "file_attachments", "ai_providers", + ]; + for (const t of tables) { + try { + await db.execute(sql.raw(`TRUNCATE TABLE \`${t}\`;`)); + } catch { + // 表可能不存在,跳过 } - await db.execute(sql`SET FOREIGN_KEY_CHECKS = 1;`); - console.log("🧹 Cleaned up existing data."); - } catch (e) { - console.warn("⚠️ Cleanup warning (might be fresh DB):", e); } + await db.execute(sql`SET FOREIGN_KEY_CHECKS = 1;`); + console.log("✓ 清理完成"); +} - // --- 1. IAM & Roles --- - console.log("👤 Seeding IAM..."); - - // Roles - const roleMap = { - admin: "role_admin", - teacher: "role_teacher", - student: "role_student", - grade_head: "role_grade_head", - teaching_head: "role_teaching_head" - }; +// ============ 1. 角色 + 权限 ============ - await db.insert(roles).values([ - { id: roleMap.admin, name: "admin", description: "System Administrator" }, - { id: roleMap.teacher, name: "teacher", description: "Academic Instructor" }, - { id: roleMap.student, name: "student", description: "Learner" }, - { id: roleMap.grade_head, name: "grade_head", description: "Head of Grade Year" }, - { id: roleMap.teaching_head, name: "teaching_head", description: "Teaching Research Lead" } - ]); +async function seedRolesAndPermissions() { + console.log("👤 创建角色与权限..."); - // Users - const passwordHash = await hash("123456", 10); - const usersData = [ - { - id: "user_admin", - name: "Admin User", - email: "admin@next-edu.com", - role: "admin", // Legacy field - image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Admin", - password: passwordHash - }, - { - id: "user_teacher_math", - name: "Mr. Math", - email: "math@next-edu.com", - role: "teacher", - image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Math", - password: passwordHash - }, - { - id: "user_student_1", - name: "Alice Student", - email: "alice@next-edu.com", - role: "student", - image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice", - password: passwordHash - } + const roleDefs = [ + { id: "role_admin", name: "admin", description: "系统管理员" }, + { id: "role_teacher", name: "teacher", description: "教师" }, + { id: "role_student", name: "student", description: "学生" }, + { id: "role_parent", name: "parent", description: "家长" }, + { id: "role_grade_head", name: "grade_head", description: "年级组长" }, + { id: "role_teaching_head", name: "teaching_head", description: "教研组长" }, ]; - await db.insert(users).values(usersData); + await db.insert(roles).values(roleDefs); - // Assign Roles (RBAC) - await db.insert(usersToRoles).values([ - { userId: "user_admin", roleId: roleMap.admin }, - { userId: "user_teacher_math", roleId: roleMap.teacher }, - // Math teacher is also a Grade Head - { userId: "user_teacher_math", roleId: roleMap.grade_head }, - { userId: "user_student_1", roleId: roleMap.student }, - ]); + // 写入角色-权限映射 + const permsData: { roleId: string; permission: string }[] = []; + for (const role of roleDefs) { + const perms = ROLE_PERMISSIONS[role.name] ?? []; + for (const p of perms) { + permsData.push({ roleId: role.id, permission: p }); + } + } + await db.insert(rolePermissions).values(permsData); + console.log(`✓ 创建 ${roleDefs.length} 个角色,${permsData.length} 条权限映射`); +} - const extraStudentIds: string[] = []; - for (let i = 0; i < 12; i++) { - const studentId = createId(); - extraStudentIds.push(studentId); - await db.insert(users).values({ - id: studentId, - name: faker.person.fullName(), - email: faker.internet.email().toLowerCase(), - role: "student", - image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${studentId}`, - password: passwordHash, +// ============ 2. 学科 ============ + +async function seedSubjects() { + console.log("📚 创建学科..."); + const map: Record = {}; + const subs = [ + { name: "语文", code: "CHINESE", order: 1 }, + { name: "数学", code: "MATH", order: 2 }, + { name: "英语", code: "ENG", order: 3 }, + ]; + for (const s of subs) { + const id = createId(); + map[s.code] = id; + await db.insert(subjects).values({ id, name: s.name, code: s.code, order: s.order }); + } + console.log(`✓ 创建 ${subs.length} 个学科`); + return map; // { CHINESE, MATH, ENG } +} + +// ============ 3. 学校 + 年级 + 学年 ============ + +async function seedSchoolAndGrades() { + console.log("🏫 创建学校、年级、学年..."); + const schoolId = "school_xiaoxue"; + await db.insert(schools).values({ + id: schoolId, + name: "阳光小学", + code: "SUNXX", + }); + + const academicYearId = createId(); + await db.insert(academicYears).values({ + id: academicYearId, + name: "2025-2026学年", + startDate: new Date("2025-09-01"), + endDate: new Date("2026-07-15"), + isActive: true, + }); + + const gradeMap: Record = {}; + const gradeDefs = [ + { key: "G1", name: "一年级", order: 1 }, + { key: "G2", name: "二年级", order: 2 }, + ]; + for (const g of gradeDefs) { + const id = createId(); + gradeMap[g.key] = id; + await db.insert(grades).values({ + id, + schoolId, + name: g.name, + order: g.order, }); - await db.insert(usersToRoles).values({ userId: studentId, roleId: roleMap.student }); + } + console.log(`✓ 创建 1 所学校、${gradeDefs.length} 个年级、1 个学年`); + return { schoolId, gradeMap, academicYearId }; +} + +// ============ 4. 用户 ============ + +async function seedUsers( + schoolId: string, + gradeMap: Record +) { + console.log("👥 创建用户..."); + const passwordHash = await hash(PASSWORD, 10); + const avatar = (seed: string) => `https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}`; + + // --- Admin --- + const adminId = "user_admin"; + await db.insert(users).values({ + id: adminId, + name: "系统管理员", + email: "admin@xiaoxue.edu.cn", + password: passwordHash, + image: avatar("admin"), + gender: "男", + }); + await db.insert(usersToRoles).values({ userId: adminId, roleId: "role_admin" }); + + // --- 老师 --- + // 8 个老师:每班 2 个(班主任+任课),跨班凑齐语数外 + // 一年级1班:班主任 T_C1(语文),任课 T_M1(数学),英语由 T_E1 跨班 + // 一年级2班:班主任 T_C2(语文),任课 T_E1(英语),数学由 T_M1 跨班 + // 二年级1班:班主任 T_C3(语文),任课 T_M2(数学),英语由 T_E2 跨班 + // 二年级2班:班主任 T_C4(语文),任课 T_E2(英语),数学由 T_M2 跨班 + const teacherMap: Record = {}; + const teacherDefs = [ + { key: "T_C1", name: "王语文", subject: "CHINESE", email: "t_chinese_1@xiaoxue.edu.cn", gender: "女" as const }, + { key: "T_M1", name: "李数学", subject: "MATH", email: "t_math_1@xiaoxue.edu.cn", gender: "男" as const }, + { key: "T_C2", name: "赵语文", subject: "CHINESE", email: "t_chinese_2@xiaoxue.edu.cn", gender: "女" as const }, + { key: "T_E1", name: "张英语", subject: "ENG", email: "t_english_1@xiaoxue.edu.cn", gender: "女" as const }, + { key: "T_C3", name: "陈语文", subject: "CHINESE", email: "t_chinese_3@xiaoxue.edu.cn", gender: "男" as const }, + { key: "T_M2", name: "刘数学", subject: "MATH", email: "t_math_2@xiaoxue.edu.cn", gender: "男" as const }, + { key: "T_C4", name: "孙语文", subject: "CHINESE", email: "t_chinese_4@xiaoxue.edu.cn", gender: "女" as const }, + { key: "T_E2", name: "周英语", subject: "ENG", email: "t_english_2@xiaoxue.edu.cn", gender: "女" as const }, + ]; + + for (const t of teacherDefs) { + const id = `user_${t.key}`; + teacherMap[t.key] = { id, name: t.name, subject: t.subject }; + await db.insert(users).values({ + id, + name: t.name, + email: t.email, + password: passwordHash, + image: avatar(t.key), + gender: t.gender, + }); + await db.insert(usersToRoles).values({ userId: id, roleId: "role_teacher" }); } - const schoolId = "school_nextedu" - const grade10Id = "grade_10" + // 指定一年级组长 = T_C1,二年级组长 = T_C3 + await db.update(grades).set({ gradeHeadId: teacherMap.T_C1.id }).where(eq(grades.id, gradeMap.G1)); + await db.update(grades).set({ gradeHeadId: teacherMap.T_C3.id }).where(eq(grades.id, gradeMap.G2)); + // 同时给年级组长角色 + await db.insert(usersToRoles).values({ userId: teacherMap.T_C1.id, roleId: "role_grade_head" }); + await db.insert(usersToRoles).values({ userId: teacherMap.T_C3.id, roleId: "role_grade_head" }); - await db.insert(schools).values([ - { id: schoolId, name: "Next_Edu School", code: "NEXTEDU" }, - { id: "school_demo_2", name: "Demo School No.2", code: "DEMO2" }, - ]) + // --- 学生 --- + // 每班 6 名学生,共 24 名 + const studentMap: Record = {}; + const classKeys = ["G1C1", "G1C2", "G2C1", "G2C2"]; + const surnames = ["小明", "小红", "小华", "小亮", "小芳", "小强", "小丽", "小军", "小燕", "小杰", "小琳", "小涛"]; + let studentIdx = 0; + for (const ck of classKeys) { + for (let i = 0; i < 6; i++) { + const key = `S_${ck}_${i + 1}`; + const id = `user_${key.toLowerCase()}`; + const name = surnames[studentIdx % surnames.length] + (studentIdx >= surnames.length ? "2" : ""); + studentIdx++; + studentMap[key] = { id, name, classKey: ck }; + await db.insert(users).values({ + id, + name, + email: `student_${ck.toLowerCase()}_${i + 1}@xiaoxue.edu.cn`, + password: passwordHash, + image: avatar(key), + gender: i % 2 === 0 ? "男" : "女", + birthDate: new Date(`201${ck.startsWith("G1") ? 8 : 7}-0${(i % 9) + 1}-15`), + guardianName: "家长" + name, + guardianPhone: "1380000" + String(studentIdx).padStart(4, "0"), + guardianRelation: i % 2 === 0 ? "父亲" : "母亲", + }); + await db.insert(usersToRoles).values({ userId: id, roleId: "role_student" }); + } + } - // --- Seeding Subjects --- - await db.insert(subjects).values([ - { id: createId(), name: "语文", code: "CHINESE", order: 1 }, - { id: createId(), name: "数学", code: "MATH", order: 2 }, - { id: createId(), name: "英语", code: "ENG", order: 3 }, - { id: createId(), name: "美术", code: "ART", order: 4 }, - { id: createId(), name: "体育", code: "PE", order: 5 }, - { id: createId(), name: "科学", code: "SCI", order: 6 }, - { id: createId(), name: "社会", code: "SOC", order: 7 }, - { id: createId(), name: "音乐", code: "MUSIC", order: 8 }, - ]) + // --- 家长 --- + // 每名学生 1 位家长 + const parentMap: Record = {}; + for (const [sKey, sInfo] of Object.entries(studentMap)) { + const pKey = `P_${sKey}`; + const id = `user_${pKey.toLowerCase()}`; + const name = "家长" + sInfo.name; + parentMap[pKey] = { id, name, childKey: sKey }; + await db.insert(users).values({ + id, + name, + email: `parent_${sInfo.classKey.toLowerCase()}_${sKey.split("_")[2]}@xiaoxue.edu.cn`, + password: passwordHash, + image: avatar(pKey), + gender: sInfo.name.includes("明") || sInfo.name.includes("华") || sInfo.name.includes("亮") || sInfo.name.includes("强") || sInfo.name.includes("军") || sInfo.name.includes("涛") ? "男" : "女", + }); + await db.insert(usersToRoles).values({ userId: id, roleId: "role_parent" }); + } - await db.insert(grades).values([ - { - id: grade10Id, + console.log(`✓ 创建 1 管理员、${teacherDefs.length} 老师、${Object.keys(studentMap).length} 学生、${Object.keys(parentMap).length} 家长`); + return { adminId, teacherMap, studentMap, parentMap }; +} + +// ============ 5. 班级 ============ + +async function seedClasses( + schoolId: string, + gradeMap: Record, + teacherMap: Record +) { + console.log("🏫 创建班级..."); + const classMap: Record = {}; + + const classDefs = [ + { key: "G1C1", name: "一年级1班", gradeKey: "G1", homeroom: "T_C1", room: "1号楼101", code: "G1C101" }, + { key: "G1C2", name: "一年级2班", gradeKey: "G1", homeroom: "T_C2", room: "1号楼102", code: "G1C102" }, + { key: "G2C1", name: "二年级1班", gradeKey: "G2", homeroom: "T_C3", room: "2号楼101", code: "G2C101" }, + { key: "G2C2", name: "二年级2班", gradeKey: "G2", homeroom: "T_C4", room: "2号楼102", code: "G2C102" }, + ]; + + for (const c of classDefs) { + const id = `class_${c.key}`; + classMap[c.key] = { id, gradeKey: c.gradeKey, homeroomTeacherKey: c.homeroom }; + await db.insert(classes).values({ + id, + schoolName: "阳光小学", schoolId, - name: "Grade 10", - order: 10, - gradeHeadId: "user_teacher_math", - teachingHeadId: "user_teacher_math", - }, - ]) + name: c.name, + grade: c.gradeKey === "G1" ? "一年级" : "二年级", + gradeId: gradeMap[c.gradeKey], + homeroom: c.code, + room: c.room, + invitationCode: c.code, + teacherId: teacherMap[c.homeroom].id, + }); + } + console.log(`✓ 创建 ${classDefs.length} 个班级`); + return classMap; +} - await db.insert(classes).values([ +// ============ 6. 班级-学科-老师关联 ============ + +async function seedClassSubjectTeachers( + classMap: Record, + teacherMap: Record, + subjectMap: Record +) { + console.log("🔗 建立班级-学科-老师关联..."); + // 每班 3 科(语数外),由跨班老师凑齐 + // G1C1: 语文 T_C1, 数学 T_M1, 英语 T_E1 + // G1C2: 语文 T_C2, 数学 T_M1, 英语 T_E1 + // G2C1: 语文 T_C3, 数学 T_M2, 英语 T_E2 + // G2C2: 语文 T_C4, 数学 T_M2, 英语 T_E2 + const mapping: Record = { + G1C1: { CHINESE: "T_C1", MATH: "T_M1", ENG: "T_E1" }, + G1C2: { CHINESE: "T_C2", MATH: "T_M1", ENG: "T_E1" }, + G2C1: { CHINESE: "T_C3", MATH: "T_M2", ENG: "T_E2" }, + G2C2: { CHINESE: "T_C4", MATH: "T_M2", ENG: "T_E2" }, + }; + + const rows: { classId: string; subjectId: string; teacherId: string }[] = []; + for (const [ck, teachers] of Object.entries(mapping)) { + for (const [subCode, tKey] of Object.entries(teachers)) { + rows.push({ + classId: classMap[ck].id, + subjectId: subjectMap[subCode], + teacherId: teacherMap[tKey].id, + }); + } + } + await db.insert(classSubjectTeachers).values(rows); + console.log(`✓ 创建 ${rows.length} 条班级-学科-老师关联`); +} + +// ============ 7. 学生分班 + 家长-学生关联 ============ + +async function seedEnrollmentsAndParents( + classMap: Record, + studentMap: Record, + parentMap: Record +) { + console.log("👨‍🎓 学生分班 + 家长关联..."); + const enrollments: { classId: string; studentId: string; status: "active" }[] = []; + for (const s of Object.values(studentMap)) { + enrollments.push({ + classId: classMap[s.classKey].id, + studentId: s.id, + status: "active", + }); + } + await db.insert(classEnrollments).values(enrollments); + + const parentRelations: { parentId: string; studentId: string; relation: string }[] = []; + for (const p of Object.values(parentMap)) { + const child = studentMap[p.childKey]; + parentRelations.push({ + parentId: p.id, + studentId: child.id, + relation: child.name.includes("明") || child.name.includes("华") || child.name.includes("亮") || child.name.includes("强") || child.name.includes("军") || child.name.includes("涛") ? "父亲" : "母亲", + }); + } + await db.insert(parentStudentRelations).values(parentRelations); + console.log(`✓ ${enrollments.length} 条分班记录、${parentRelations.length} 条家长关联`); +} + +// ============ 8. 教材 + 章节 ============ + +async function seedTextbooksAndChapters( + subjectMap: Record, + gradeMap: Record +) { + console.log("📖 创建教材与章节..."); + // 每科 1 本教材(一年级语文、一年级数学、一年级英语) + // 只实现第一章第一节课 + const chapterMap: Record = {}; + + const textbookDefs = [ { - id: "class_10_3", - schoolName: "Next_Edu School", - schoolId, - name: "Grade 10 · Class 3", - grade: "Grade 10", - gradeId: grade10Id, - homeroom: "10-3", - room: "Room 304", - invitationCode: "100003", - teacherId: "user_teacher_math", + subjectCode: "CHINESE", + subjectName: "语文", + title: "一年级语文(上册)", + publisher: "人民教育出版社", + chapterTitle: "第一课 秋天", + chapterContent: "# 第一课 秋天\n\n秋天来了,天气凉了,树叶黄了,一片片叶子从树上落下来。\n\n天空那么蓝,那么高。一群大雁往南飞,一会儿排成个\"人\"字,一会儿排成个\"一\"字。\n\n啊!秋天来了!", }, { - id: "class_10_7", - schoolName: "Next_Edu School", - schoolId, - name: "Grade 10 · Class 7", - grade: "Grade 10", - gradeId: grade10Id, - homeroom: "10-7", - room: "Room 201", - invitationCode: "100007", - teacherId: "user_teacher_math", + subjectCode: "MATH", + subjectName: "数学", + title: "一年级数学(上册)", + publisher: "人民教育出版社", + chapterTitle: "第一课 1-5 的认识", + chapterContent: "# 第一课 1-5 的认识\n\n## 数一数\n\n1 像铅笔细又长\n2 像小鸭水上漂\n3 像耳朵听声音\n4 像小旗迎风飘\n5 像秤钩来买菜\n\n## 练习\n\n数一数图中有几只小鸟?有几朵花?", }, - ]); - - await db.insert(classEnrollments).values([ - { classId: "class_10_3", studentId: "user_student_1", status: "active" }, - ...extraStudentIds.slice(0, 8).map((studentId) => ({ classId: "class_10_3", studentId, status: "active" as const })), - ...extraStudentIds.slice(8, 12).map((studentId) => ({ classId: "class_10_7", studentId, status: "active" as const })), - ]); - - await db.insert(classSchedule).values([ - { id: "cs_001", classId: "class_10_3", weekday: 1, startTime: "09:00", endTime: "09:45", course: "Mathematics", location: "Room 304" }, - { id: "cs_002", classId: "class_10_3", weekday: 3, startTime: "14:00", endTime: "14:45", course: "Physics", location: "Lab A" }, - { id: "cs_003", classId: "class_10_7", weekday: 2, startTime: "11:00", endTime: "11:45", course: "Mathematics", location: "Room 201" }, - ]); - - await db.insert(textbooks).values([ { - id: "tb_01", - title: "Advanced Mathematics Grade 10", - subject: "Mathematics", - grade: "Grade 10", - publisher: "Next Education Press", + subjectCode: "ENG", + subjectName: "英语", + title: "一年级英语(上册)", + publisher: "外语教学与研究出版社", + chapterTitle: "Unit 1 Hello", + chapterContent: "# Unit 1 Hello\n\n## Let's talk\n\n- Hello! I'm Mike.\n- Hi! I'm Sarah.\n- Goodbye!\n- Bye!\n\n## Words\n\n- hello 你好\n- goodbye 再见\n- I'm = I am 我是", }, - ]) + ]; - await db.insert(chapters).values([ - { - id: "ch_01", - textbookId: "tb_01", - title: "Chapter 1: Real Numbers", + for (const tb of textbookDefs) { + const tbId = `tb_${tb.subjectCode}_g1`; + await db.insert(textbooks).values({ + id: tbId, + title: tb.title, + subject: tb.subjectName, + grade: "一年级", + publisher: tb.publisher, + }); + + // 第一章 + const ch1Id = `ch_${tb.subjectCode}_g1_1`; + await db.insert(chapters).values({ + id: ch1Id, + textbookId: tbId, + title: `第一章 ${tb.subjectName === "语文" ? "课文" : tb.subjectName === "数学" ? "数一数" : "Greetings"}`, order: 1, parentId: null, - content: "# Chapter 1: Real Numbers\n\nIn this chapter, we will explore the properties of real numbers...", - }, - { - id: "ch_01_01", - textbookId: "tb_01", - title: "1.1 Introduction to Real Numbers", + content: `# 第一章\n\n${tb.subjectName}第一章导引内容。`, + }); + + // 第一课 + const ch1_1Id = `ch_${tb.subjectCode}_g1_1_1`; + chapterMap[tb.subjectCode] = ch1_1Id; + await db.insert(chapters).values({ + id: ch1_1Id, + textbookId: tbId, + title: tb.chapterTitle, order: 1, - parentId: "ch_01", - content: "## 1.1 Introduction\n\nReal numbers include rational and irrational numbers.", - }, - ]) + parentId: ch1Id, + content: tb.chapterContent, + }); + } + console.log(`✓ 创建 ${textbookDefs.length} 本教材,每本含第一章第一课`); + return chapterMap; // { CHINESE, MATH, ENG } +} - // --- 2. Knowledge Graph (Tree) --- - console.log("🧠 Seeding Knowledge Graph..."); - - const kpMathId = createId(); - const kpAlgebraId = createId(); - const kpLinearId = createId(); +// ============ 9. 知识点 ============ - await db.insert(knowledgePoints).values([ - { id: kpMathId, name: "Mathematics", level: 0 }, - { id: kpAlgebraId, name: "Algebra", parentId: kpMathId, level: 1 }, - { id: kpLinearId, name: "Linear Equations", parentId: kpAlgebraId, level: 2 }, - ]); +async function seedKnowledgePoints(chapterMap: Record) { + console.log("🧠 创建知识点..."); + const map: Record = {}; - await db.insert(knowledgePoints).values([ - { - id: "kp_01", - name: "Real Numbers", - description: "Definition and properties of real numbers", + const kpDefs = [ + { key: "KP_CHINESE_READ", name: "朗读", chapterCode: "CHINESE", desc: "正确流利地朗读课文" }, + { key: "KP_CHINESE_CHAR", name: "生字认读", chapterCode: "CHINESE", desc: "认识课文中的生字" }, + { key: "KP_MATH_COUNT", name: "数数", chapterCode: "MATH", desc: "能数出 1-5 的数量" }, + { key: "KP_MATH_WRITE", name: "数字书写", chapterCode: "MATH", desc: "正确书写 1-5" }, + { key: "KP_ENG_GREET", name: "问候语", chapterCode: "ENG", desc: "掌握 hello/goodbye" }, + { key: "KP_ENG_INTRO", name: "自我介绍", chapterCode: "ENG", desc: "使用 I'm... 介绍自己" }, + ]; + + for (const kp of kpDefs) { + const id = `kp_${kp.key.toLowerCase()}`; + map[kp.key] = id; + await db.insert(knowledgePoints).values({ + id, + name: kp.name, + description: kp.desc, level: 1, order: 1, - chapterId: "ch_01", - }, - { - id: "kp_02", - name: "Rational Numbers", - description: "Numbers that can be expressed as a fraction", - level: 2, - order: 1, - chapterId: "ch_01_01", - }, - ]) + chapterId: chapterMap[kp.chapterCode], + }); + } + console.log(`✓ 创建 ${kpDefs.length} 个知识点`); + return map; +} - // --- 3. Question Bank (Rich Content) --- - console.log("📚 Seeding Question Bank..."); +// ============ 10. 课表 ============ - const mathExamQuestions: Array<{ - id: string; - type: "single_choice" | "text" | "judgment"; - difficulty: number; - content: unknown; - score: number; - }> = [ +async function seedClassSchedule( + classMap: Record, + subjectMap: Record +) { + console.log("📅 创建课表..."); + // 每班每天安排语数外,简化为周一三五各 2 节 + const rows: { id: string; classId: string; weekday: number; startTime: string; endTime: string; course: string; location: string }[] = []; + const subjectNames: Record = { CHINESE: "语文", MATH: "数学", ENG: "英语" }; + const slots = [ + { weekday: 1, start: "08:00", end: "08:40", sub: "CHINESE" }, + { weekday: 1, start: "08:50", end: "09:30", sub: "MATH" }, + { weekday: 3, start: "08:00", end: "08:40", sub: "ENG" }, + { weekday: 3, start: "08:50", end: "09:30", sub: "CHINESE" }, + { weekday: 5, start: "08:00", end: "08:40", sub: "MATH" }, + { weekday: 5, start: "08:50", end: "09:30", sub: "ENG" }, + ]; + for (const [ck, cInfo] of Object.entries(classMap)) { + for (let i = 0; i < slots.length; i++) { + const s = slots[i]; + rows.push({ + id: `cs_${ck}_${i}`, + classId: cInfo.id, + weekday: s.weekday, + startTime: s.start, + endTime: s.end, + course: subjectNames[s.sub], + location: "教室", + }); + } + } + await db.insert(classSchedule).values(rows); + console.log(`✓ 创建 ${rows.length} 条课表记录`); +} + +// ============ 11. 题库 ============ + +async function seedQuestions( + teacherMap: Record, + subjectMap: Record, + kpMap: Record +) { + console.log("❓ 创建题库..."); + + // 语文题(作者 T_C1) + const chineseAuthorId = teacherMap.T_C1.id; + const chineseQuestions = [ { id: createId(), - type: "single_choice", + type: "single_choice" as const, difficulty: 1, - score: 4, content: { - text: "1) What is 2 + 2?", + text: "\"秋天来了\"中,哪个字表示季节?", options: [ - { id: "A", text: "3", isCorrect: false }, - { id: "B", text: "4", isCorrect: true }, - { id: "C", text: "5", isCorrect: false }, - { id: "D", text: "6", isCorrect: false }, + { id: "A", text: "秋", isCorrect: true }, + { id: "B", text: "天", isCorrect: false }, + { id: "C", text: "来", isCorrect: false }, + { id: "D", text: "了", isCorrect: false }, ], }, + score: 10, }, { id: createId(), - type: "single_choice", + type: "single_choice" as const, difficulty: 2, - score: 4, content: { - text: "2) If f(x) = 2x + 1, then f(3) = ?", + text: "课文中大雁往南飞是因为?", options: [ - { id: "A", text: "5", isCorrect: false }, - { id: "B", text: "7", isCorrect: true }, - { id: "C", text: "8", isCorrect: false }, - { id: "D", text: "10", isCorrect: false }, + { id: "A", text: "春天来了", isCorrect: false }, + { id: "B", text: "秋天来了", isCorrect: true }, + { id: "C", text: "夏天来了", isCorrect: false }, + { id: "D", text: "冬天来了", isCorrect: false }, ], }, + score: 10, }, { id: createId(), - type: "single_choice", - difficulty: 2, - score: 4, - content: { - text: "3) Solve 3x - 5 = 7. What is x?", - options: [ - { id: "A", text: "3", isCorrect: false }, - { id: "B", text: "4", isCorrect: true }, - { id: "C", text: "5", isCorrect: false }, - { id: "D", text: "6", isCorrect: false }, - ], - }, - }, - { - id: createId(), - type: "single_choice", - difficulty: 3, - score: 4, - content: { - text: "4) Which is a factor of x^2 - 9?", - options: [ - { id: "A", text: "(x - 3)", isCorrect: true }, - { id: "B", text: "(x + 9)", isCorrect: false }, - { id: "C", text: "(x - 9)", isCorrect: false }, - { id: "D", text: "(x^2 + 9)", isCorrect: false }, - ], - }, - }, - { - id: createId(), - type: "single_choice", - difficulty: 2, - score: 4, - content: { - text: "5) If a^2 = 49 and a > 0, then a = ?", - options: [ - { id: "A", text: "-7", isCorrect: false }, - { id: "B", text: "0", isCorrect: false }, - { id: "C", text: "7", isCorrect: true }, - { id: "D", text: "49", isCorrect: false }, - ], - }, - }, - { - id: createId(), - type: "single_choice", - difficulty: 3, - score: 4, - content: { - text: "6) Simplify (x^2 y)(x y^3).", - options: [ - { id: "A", text: "x^2 y^3", isCorrect: false }, - { id: "B", text: "x^3 y^4", isCorrect: true }, - { id: "C", text: "x^3 y^3", isCorrect: false }, - { id: "D", text: "x^4 y^4", isCorrect: false }, - ], - }, - }, - { - id: createId(), - type: "single_choice", - difficulty: 2, - score: 4, - content: { - text: "7) The slope of the line y = -3x + 2 is:", - options: [ - { id: "A", text: "2", isCorrect: false }, - { id: "B", text: "-3", isCorrect: true }, - { id: "C", text: "3", isCorrect: false }, - { id: "D", text: "-2", isCorrect: false }, - ], - }, - }, - { - id: createId(), - type: "single_choice", + type: "text" as const, difficulty: 1, - score: 4, - content: { - text: "8) The probability of getting heads in one fair coin toss is:", - options: [ - { id: "A", text: "0", isCorrect: false }, - { id: "B", text: "1/4", isCorrect: false }, - { id: "C", text: "1/2", isCorrect: true }, - { id: "D", text: "1", isCorrect: false }, - ], - }, + content: { text: "写出\"天\"的笔顺(用文字描述)。", correctAnswer: "横、横、撇、捺" }, + score: 10, }, { id: createId(), - type: "single_choice", - difficulty: 2, - score: 4, - content: { - text: "9) In an arithmetic sequence with a1 = 2 and d = 3, a5 = ?", - options: [ - { id: "A", text: "11", isCorrect: false }, - { id: "B", text: "12", isCorrect: false }, - { id: "C", text: "14", isCorrect: true }, - { id: "D", text: "17", isCorrect: false }, - ], - }, + type: "judgment" as const, + difficulty: 1, + content: { text: "\"一片片叶子从树上落下来\"说明秋天来了。", correctAnswer: true }, + score: 10, }, { id: createId(), - type: "single_choice", + type: "text" as const, difficulty: 2, - score: 4, + content: { text: "用\"一会儿...一会儿...\"造句。", correctAnswer: "一会儿...一会儿..." }, + score: 10, + }, + ]; + + // 数学题(作者 T_M1) + const mathAuthorId = teacherMap.T_M1.id; + const mathQuestions = [ + { + id: createId(), + type: "single_choice" as const, + difficulty: 1, content: { - text: "10) The solution set of x^2 = 0 is:", + text: "哪个数字表示\"3\"?", options: [ - { id: "A", text: "{0}", isCorrect: true }, - { id: "B", text: "{1}", isCorrect: false }, - { id: "C", text: "{-1, 1}", isCorrect: false }, - { id: "D", text: "Empty set", isCorrect: false }, + { id: "A", text: "1", isCorrect: false }, + { id: "B", text: "2", isCorrect: false }, + { id: "C", text: "3", isCorrect: true }, + { id: "D", text: "4", isCorrect: false }, ], }, + score: 10, }, + { + id: createId(), + type: "single_choice" as const, + difficulty: 2, + content: { + text: "2 + 3 = ?", + options: [ + { id: "A", text: "4", isCorrect: false }, + { id: "B", text: "5", isCorrect: true }, + { id: "C", text: "6", isCorrect: false }, + { id: "D", text: "3", isCorrect: false }, + ], + }, + score: 10, + }, + { + id: createId(), + type: "text" as const, + difficulty: 1, + content: { text: "数一数:🍎🍎🍎🍎 有几个苹果?(填数字)", correctAnswer: "4" }, + score: 10, + }, + { + id: createId(), + type: "judgment" as const, + difficulty: 1, + content: { text: "数字 5 比 3 大。", correctAnswer: true }, + score: 10, + }, + { + id: createId(), + type: "text" as const, + difficulty: 2, + content: { text: "小红有 2 支铅笔,妈妈又给她 2 支,现在有几支?(列式计算)", correctAnswer: "2+2=4" }, + score: 10, + }, + ]; - { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "11) Fill in the blank: √81 = ____.", correctAnswer: "9" } }, - { id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "12) Fill in the blank: (a - b)^2 = ____.", correctAnswer: ["a^2 - 2ab + b^2", "a² - 2ab + b²"] } }, - { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "13) Fill in the blank: 2^5 = ____.", correctAnswer: "32" } }, - { id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "14) Fill in the blank: The area of a circle with radius r is ____.", correctAnswer: ["πr^2", "pi r^2", "πr²"] } }, - { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "15) Fill in the blank: If x = -2, then x^3 = ____.", correctAnswer: "-8" } }, + // 英语题(作者 T_E1) + const engAuthorId = teacherMap.T_E1.id; + const engQuestions = [ + { + id: createId(), + type: "single_choice" as const, + difficulty: 1, + content: { + text: "How do you say \"你好\" in English?", + options: [ + { id: "A", text: "Goodbye", isCorrect: false }, + { id: "B", text: "Hello", isCorrect: true }, + { id: "C", text: "Bye", isCorrect: false }, + { id: "D", text: "Thanks", isCorrect: false }, + ], + }, + score: 10, + }, + { + id: createId(), + type: "single_choice" as const, + difficulty: 2, + content: { + text: "\"I'm Mike.\" means:", + options: [ + { id: "A", text: "我是 Mike", isCorrect: true }, + { id: "B", text: "再见 Mike", isCorrect: false }, + { id: "C", text: "你好 Mike", isCorrect: false }, + { id: "D", text: "谢谢 Mike", isCorrect: false }, + ], + }, + score: 10, + }, + { + id: createId(), + type: "text" as const, + difficulty: 1, + content: { text: "Fill in the blank: _____! I'm Sarah. (你好)", correctAnswer: "Hello" }, + score: 10, + }, + { + id: createId(), + type: "judgment" as const, + difficulty: 1, + content: { text: "\"Goodbye\" means 再见.", correctAnswer: true }, + score: 10, + }, + { + id: createId(), + type: "text" as const, + difficulty: 2, + content: { text: "Introduce yourself in English. (用英语介绍自己)", correctAnswer: "Hello! I'm ..." }, + score: 10, + }, + ]; - { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "16) If x > y, then x + 1 > y + 1.", correctAnswer: true } }, - { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "17) The graph of y = 2x is a parabola.", correctAnswer: false } }, - { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "18) The sum of interior angles of a triangle is 180°.", correctAnswer: true } }, - { id: createId(), type: "judgment", difficulty: 2, score: 2, content: { text: "19) (x + y)^2 = x^2 + y^2.", correctAnswer: false } }, - { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "20) 0 is a positive number.", correctAnswer: false } }, - - { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "21) Solve the system: x + y = 5, x - y = 1." } }, - { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "22) Expand and simplify: (2x - 3)(x + 4)." } }, - { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "23) In a right triangle with legs 6 and 8, find the hypotenuse and the area." } }, + const allQs = [ + ...chineseQuestions.map(q => ({ ...q, authorId: chineseAuthorId })), + ...mathQuestions.map(q => ({ ...q, authorId: mathAuthorId })), + ...engQuestions.map(q => ({ ...q, authorId: engAuthorId })), ]; await db.insert(questions).values( - mathExamQuestions.map((q) => ({ + allQs.map(q => ({ id: q.id, type: q.type, difficulty: q.difficulty, content: q.content, - authorId: "user_teacher_math", + authorId: q.authorId, })) ); - await db.insert(questionsToKnowledgePoints).values({ - questionId: mathExamQuestions[0].id, - knowledgePointId: kpLinearId + // 关联知识点 + const q2kp: { questionId: string; knowledgePointId: string }[] = []; + chineseQuestions.forEach((q, i) => { + q2kp.push({ questionId: q.id, knowledgePointId: i % 2 === 0 ? kpMap.KP_CHINESE_READ : kpMap.KP_CHINESE_CHAR }); }); + mathQuestions.forEach((q, i) => { + q2kp.push({ questionId: q.id, knowledgePointId: i % 2 === 0 ? kpMap.KP_MATH_COUNT : kpMap.KP_MATH_WRITE }); + }); + engQuestions.forEach((q, i) => { + q2kp.push({ questionId: q.id, knowledgePointId: i % 2 === 0 ? kpMap.KP_ENG_GREET : kpMap.KP_ENG_INTRO }); + }); + await db.insert(questionsToKnowledgePoints).values(q2kp); - // --- 4. Exams (New Structure) --- - console.log("📝 Seeding Exams..."); + console.log(`✓ 创建 ${allQs.length} 道题目(语${chineseQuestions.length}/数${mathQuestions.length}/英${engQuestions.length})`); + return { chineseQuestions, mathQuestions, engQuestions }; +} - const examId = createId(); +// ============ 12. 试卷 + 学生答题与批改 ============ - const makeGroup = (title: string, children: unknown[]) => ({ +async function seedExamsAndSubmissions( + teacherMap: Record, + classMap: Record, + studentMap: Record, + subjectMap: Record, + questionBanks: { chineseQuestions: any[]; mathQuestions: any[] }, + gradeMap: Record +) { + console.log("📝 创建试卷与学生答题..."); + + const makeGroup = (title: string, children: any[]) => ({ id: createId(), - type: "group", + type: "group" as const, title, children, }); - - const makeQuestionNode = (questionId: string, score: number) => ({ + const makeQNode = (questionId: string, score: number) => ({ id: createId(), - type: "question", + type: "question" as const, questionId, score, }); - const choiceIds = mathExamQuestions.slice(0, 10).map((q) => q.id); - const fillIds = mathExamQuestions.slice(10, 15).map((q) => q.id); - const judgmentIds = mathExamQuestions.slice(15, 20).map((q) => q.id); - const shortAnswerIds = mathExamQuestions.slice(20, 23).map((q) => q.id); - - const examStructure = [ - makeGroup( - "第一部分:单项选择题(共10题,每题4分,共40分)", - choiceIds.map((id) => makeQuestionNode(id, 4)) - ), - makeGroup( - "第二部分:填空题(共5题,每题4分,共20分)", - fillIds.map((id) => makeQuestionNode(id, 4)) - ), - makeGroup( - "第三部分:判断题(共5题,每题2分,共10分)", - judgmentIds.map((id) => makeQuestionNode(id, 2)) - ), - makeGroup( - "第四部分:解答题(共3题,每题10分,共30分)", - shortAnswerIds.map((id) => makeQuestionNode(id, 10)) - ), + // --- 试卷1:一年级语文第一单元测验 --- + const chineseExamId = "exam_chinese_g1_unit1"; + const cnQs = questionBanks.chineseQuestions; + const cnStructure = [ + makeGroup("一、选择题(每题10分)", [makeQNode(cnQs[0].id, 10), makeQNode(cnQs[1].id, 10)]), + makeGroup("二、填空题(每题10分)", [makeQNode(cnQs[2].id, 10), makeQNode(cnQs[4].id, 10)]), + makeGroup("三、判断题(每题10分)", [makeQNode(cnQs[3].id, 10)]), ]; + const cnOrderedIds = [cnQs[0].id, cnQs[1].id, cnQs[2].id, cnQs[4].id, cnQs[3].id]; + const cnScoreMap = new Map(cnQs.map(q => [q.id, q.score] as const)); await db.insert(exams).values({ - id: examId, - title: "Grade 10 Mathematics Final Exam (Seed)", - description: JSON.stringify({ - subject: "Mathematics", - grade: "Grade 10", - difficulty: 3, - totalScore: 100, - durationMin: 120, - questionCount: 23, - tags: ["seed", "math", "grade10", "final"], - }), - creatorId: "user_teacher_math", + id: chineseExamId, + title: "一年级语文第一单元测验", + description: JSON.stringify({ subject: "语文", grade: "一年级", totalScore: 50, durationMin: 40, questionCount: 5 }), + creatorId: teacherMap.T_C1.id, + subjectId: subjectMap.CHINESE, + gradeId: gradeMap.G1, status: "published", - startTime: new Date(), - structure: examStructure as unknown + startTime: NOW, + structure: cnStructure, }); - - // Link questions physically (Source of Truth) - const orderedQuestionIds = [...choiceIds, ...fillIds, ...judgmentIds, ...shortAnswerIds]; - const scoreById = new Map(mathExamQuestions.map((q) => [q.id, q.score] as const)); - await db.insert(examQuestions).values( - orderedQuestionIds.map((questionId, order) => ({ - examId, - questionId, - score: scoreById.get(questionId) ?? 0, - order, + cnOrderedIds.map((qid, i) => ({ + examId: chineseExamId, + questionId: qid, + score: cnScoreMap.get(qid) ?? 10, + order: i, })) ); - console.log("📌 Seeding Homework Assignments..."); + // --- 试卷2:一年级数学第一单元测验 --- + const mathExamId = "exam_math_g1_unit1"; + const mQs = questionBanks.mathQuestions; + const mStructure = [ + makeGroup("一、选择题(每题10分)", [makeQNode(mQs[0].id, 10), makeQNode(mQs[1].id, 10)]), + makeGroup("二、填空题(每题10分)", [makeQNode(mQs[2].id, 10), makeQNode(mQs[4].id, 10)]), + makeGroup("三、判断题(每题10分)", [makeQNode(mQs[3].id, 10)]), + ]; + const mOrderedIds = [mQs[0].id, mQs[1].id, mQs[2].id, mQs[4].id, mQs[3].id]; + const mScoreMap = new Map(mQs.map(q => [q.id, q.score] as const)); - const assignmentId = createId(); - const now = new Date(); - const dueAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); - const lateDueAt = new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000); - - await db.insert(homeworkAssignments).values({ - id: assignmentId, - sourceExamId: examId, - title: "Grade 10 Mathematics Final - Homework (Seed)", - description: "Auto-generated homework assignment from seeded math exam.", - structure: examStructure as unknown, + await db.insert(exams).values({ + id: mathExamId, + title: "一年级数学第一单元测验", + description: JSON.stringify({ subject: "数学", grade: "一年级", totalScore: 50, durationMin: 40, questionCount: 5 }), + creatorId: teacherMap.T_M1.id, + subjectId: subjectMap.MATH, + gradeId: gradeMap.G1, status: "published", - creatorId: "user_teacher_math", - availableAt: now, - dueAt, - allowLate: true, - lateDueAt, - maxAttempts: 2, + startTime: NOW, + structure: mStructure, }); - - await db.insert(homeworkAssignmentQuestions).values( - orderedQuestionIds.map((questionId, order) => ({ - assignmentId, - questionId, - score: scoreById.get(questionId) ?? 0, - order, + await db.insert(examQuestions).values( + mOrderedIds.map((qid, i) => ({ + examId: mathExamId, + questionId: qid, + score: mScoreMap.get(qid) ?? 10, + order: i, })) ); - const targetStudentIds = ["user_student_1", ...extraStudentIds.slice(0, 4)]; - await db.insert(homeworkAssignmentTargets).values( - targetStudentIds.map((studentId) => ({ - assignmentId, - studentId, - })) - ); + // --- 学生答题与批改 --- + // 一年级两个班的学生都参加这两套试卷 + const g1Students = Object.values(studentMap).filter(s => s.classKey.startsWith("G1")); + let submissionCount = 0; + let answerCount = 0; - const scoreForQuestion = (questionId: string) => scoreById.get(questionId) ?? 0; - const buildAnswer = (questionId: string, type: string) => { - if (type === "single_choice") return { answer: "B" }; - if (type === "judgment") return { answer: true }; - return { answer: "Seed answer" }; - }; + for (const exam of [ + { id: chineseExamId, qIds: cnOrderedIds, qs: cnQs }, + { id: mathExamId, qIds: mOrderedIds, qs: mQs }, + ]) { + for (let si = 0; si < g1Students.length; si++) { + const student = g1Students[si]; + const submissionId = createId(); + const submittedAt = new Date(NOW.getTime() - (si + 1) * 3600_000); + // 前 6 个学生已批改,后 6 个已提交未批改 + const isGraded = si < 6; + const status = isGraded ? "graded" : "submitted"; - const questionTypeById = new Map(mathExamQuestions.map((q) => [q.id, q.type] as const)); + // 模拟答题:前 3 题答对,后 2 题部分对 + const perScores = exam.qIds.map((_, idx) => { + if (!isGraded) return null; + if (idx < 3) return 10; + if (idx === 3) return 6; + return 8; + }); + const totalScore = isGraded ? perScores.reduce((s, v) => s + Number(v ?? 0), 0) : null; - const submissionIds: string[] = []; - for (let i = 0; i < 3; i++) { - const studentId = targetStudentIds[i]; - const submissionId = createId(); - submissionIds.push(submissionId); - const submittedAt = new Date(now.getTime() - (i + 1) * 24 * 60 * 60 * 1000); - const status = i === 0 ? "graded" : i === 1 ? "graded" : "submitted"; + await db.insert(examSubmissions).values({ + id: submissionId, + examId: exam.id, + studentId: student.id, + score: totalScore, + status, + submittedAt, + }); + submissionCount++; - const perQuestionScores = orderedQuestionIds.map((qid, idx) => { - const max = scoreForQuestion(qid); - if (status !== "graded") return null; - if (max <= 0) return 0; - if (idx % 7 === 0) return Math.max(0, max - 1); - return max; - }); + // 答案 + const buildAnswer = (q: any, idx: number) => { + if (q.type === "single_choice") { + // 前 3 题选正确项,后 2 题选错误项 + const correct = q.content.options.find((o: any) => o.isCorrect); + const wrong = q.content.options.find((o: any) => !o.isCorrect); + return { answer: idx < 3 ? correct.id : wrong.id }; + } + if (q.type === "judgment") return { answer: idx < 3 }; + return { answer: idx < 3 ? "标准答案" : "学生答案" }; + }; - const totalScore = - status === "graded" - ? perQuestionScores.reduce((sum, s) => sum + Number(s ?? 0), 0) - : null; - - await db.insert(homeworkSubmissions).values({ - id: submissionId, - assignmentId, - studentId, - attemptNo: 1, - score: totalScore, - status, - startedAt: submittedAt, - submittedAt, - isLate: false, - createdAt: submittedAt, - updatedAt: submittedAt, - }); - - await db.insert(homeworkAnswers).values( - orderedQuestionIds.map((questionId, idx) => { - const questionType = questionTypeById.get(questionId) ?? "text"; - const score = status === "graded" ? (perQuestionScores[idx] ?? 0) : null; - return { - id: createId(), - submissionId, - questionId, - answerContent: buildAnswer(questionId, questionType), - score, - feedback: status === "graded" ? (score === scoreForQuestion(questionId) ? "Good" : "Check calculation") : null, - createdAt: submittedAt, - updatedAt: submittedAt, - }; - }) - ); + await db.insert(submissionAnswers).values( + exam.qIds.map((qid, idx) => { + const q = exam.qs.find((x: any) => x.id === qid)!; + const score = isGraded ? (perScores[idx] ?? 0) : null; + return { + id: createId(), + submissionId, + questionId: qid, + answerContent: buildAnswer(q, idx), + score, + feedback: isGraded ? (score === 10 ? "回答正确!" : "还需努力") : null, + }; + }) + ); + answerCount += exam.qIds.length; + } } - - const end = performance.now(); - console.log(`✅ Seed completed in ${(end - start).toFixed(2)}ms`); - process.exit(0); + console.log(`✓ 创建 2 套试卷,${submissionCount} 份提交,${answerCount} 条答案`); } +// ============ 13. 作业 ============ + +async function seedHomework( + teacherMap: Record, + classMap: Record, + studentMap: Record, + questionBanks: { chineseQuestions: any[]; mathQuestions: any[]; engQuestions: any[] }, + subjectMap: Record +) { + console.log("📚 创建作业..."); + // 1 个语文作业(引用语文题)+ 1 个数学作业(引用数学题) + const now = new Date(); + const dueAt = new Date(now.getTime() + 7 * 86400_000); + const lateDueAt = new Date(now.getTime() + 9 * 86400_000); + + const assignments: { id: string; title: string; creatorId: string; qIds: string[]; qs: any[]; targetClassKey: string }[] = [ + { + id: "hw_chinese_g1", + title: "一年级语文第一课作业", + creatorId: teacherMap.T_C1.id, + qIds: questionBanks.chineseQuestions.map(q => q.id), + qs: questionBanks.chineseQuestions, + targetClassKey: "G1C1", + }, + { + id: "hw_math_g1", + title: "一年级数学第一课作业", + creatorId: teacherMap.T_M1.id, + qIds: questionBanks.mathQuestions.map(q => q.id), + qs: questionBanks.mathQuestions, + targetClassKey: "G1C1", + }, + ]; + + let hwSubmissionCount = 0; + let hwAnswerCount = 0; + + for (const hw of assignments) { + // 引用一份"虚拟试卷"作为 sourceExamId —— 但 homeworkAssignments 要求 sourceExamId notNull + // 我们复用已创建的试卷 ID + const sourceExamId = hw.id.includes("chinese") ? "exam_chinese_g1_unit1" : "exam_math_g1_unit1"; + + const structure = [ + { + id: createId(), + type: "group" as const, + title: "作业题", + children: hw.qIds.map(qid => ({ id: createId(), type: "question" as const, questionId: qid, score: 10 })), + }, + ]; + + await db.insert(homeworkAssignments).values({ + id: hw.id, + sourceExamId, + title: hw.title, + description: "自动生成的课后作业", + structure, + status: "published", + creatorId: hw.creatorId, + availableAt: now, + dueAt, + allowLate: true, + lateDueAt, + maxAttempts: 2, + }); + + await db.insert(homeworkAssignmentQuestions).values( + hw.qIds.map((qid, i) => ({ + assignmentId: hw.id, + questionId: qid, + score: 10, + order: i, + })) + ); + + // 目标学生:一年级1班 + const targets = Object.values(studentMap).filter(s => s.classKey === hw.targetClassKey); + await db.insert(homeworkAssignmentTargets).values( + targets.map(t => ({ assignmentId: hw.id, studentId: t.id })) + ); + + // 前 3 个学生提交作业并批改 + const scoreMap = new Map(hw.qs.map(q => [q.id, q.score] as const)); + for (let i = 0; i < Math.min(3, targets.length); i++) { + const student = targets[i]; + const submissionId = createId(); + const submittedAt = new Date(now.getTime() - (i + 1) * 86400_000); + const isGraded = i < 2; + const status = isGraded ? "graded" : "submitted"; + + const perScores = hw.qIds.map((_, idx) => { + if (!isGraded) return null; + if (idx < 3) return 10; + return 6; + }); + const totalScore = isGraded ? perScores.reduce((s, v) => s + Number(v ?? 0), 0) : null; + + await db.insert(homeworkSubmissions).values({ + id: submissionId, + assignmentId: hw.id, + studentId: student.id, + attemptNo: 1, + score: totalScore, + status, + startedAt: new Date(submittedAt.getTime() - 3600_000), + submittedAt, + isLate: false, + }); + hwSubmissionCount++; + + const buildAnswer = (q: any, idx: number) => { + if (q.type === "single_choice") { + const correct = q.content.options.find((o: any) => o.isCorrect); + const wrong = q.content.options.find((o: any) => !o.isCorrect); + return { answer: idx < 3 ? correct.id : wrong.id }; + } + if (q.type === "judgment") return { answer: idx < 3 }; + return { answer: idx < 3 ? "标准答案" : "学生答案" }; + }; + + await db.insert(homeworkAnswers).values( + hw.qIds.map((qid, idx) => { + const q = hw.qs.find((x: any) => x.id === qid)!; + const score = isGraded ? (perScores[idx] ?? 0) : null; + return { + id: createId(), + submissionId, + questionId: qid, + answerContent: buildAnswer(q, idx), + score, + feedback: isGraded ? (score === 10 ? "很好" : "继续努力") : null, + createdAt: submittedAt, + updatedAt: submittedAt, + }; + }) + ); + hwAnswerCount += hw.qIds.length; + } + } + console.log(`✓ 创建 ${assignments.length} 个作业,${hwSubmissionCount} 份提交,${hwAnswerCount} 条答案`); +} + +// ============ 14. 成绩记录 ============ + +async function seedGradeRecords( + teacherMap: Record, + classMap: Record, + studentMap: Record, + subjectMap: Record, + academicYearId: string +) { + console.log("📊 创建成绩记录..."); + // 为一年级1班学生录入语数外成绩 + const g1c1Students = Object.values(studentMap).filter(s => s.classKey === "G1C1"); + const subjectTeacher: Record = { + CHINESE: teacherMap.T_C1.id, + MATH: teacherMap.T_M1.id, + ENG: teacherMap.T_E1.id, + }; + + const records: any[] = []; + for (const s of g1c1Students) { + for (const [subCode, sid] of Object.entries(subjectMap)) { + records.push({ + id: createId(), + studentId: s.id, + classId: classMap.G1C1.id, + subjectId: sid, + examId: null, + academicYearId, + title: `${subCode === "CHINESE" ? "语文" : subCode === "MATH" ? "数学" : "英语"}第一单元测验`, + score: String(80 + Math.floor(Math.random() * 20)), + fullScore: "100", + type: "exam" as const, + semester: "1" as const, + recordedBy: subjectTeacher[subCode], + }); + } + } + await db.insert(gradeRecords).values(records); + console.log(`✓ 创建 ${records.length} 条成绩记录`); +} + +// ============ 15. 考勤记录 ============ + +async function seedAttendanceRecords( + teacherMap: Record, + classMap: Record, + studentMap: Record +) { + console.log("📋 创建考勤记录..."); + // 为一年级1班最近 5 天的考勤 + const g1c1Students = Object.values(studentMap).filter(s => s.classKey === "G1C1"); + const recorder = teacherMap.T_C1.id; + const records: any[] = []; + const statuses = ["present", "present", "present", "present", "late", "absent"] as const; + + for (let d = 0; d < 5; d++) { + const date = new Date(NOW.getTime() - d * 86400_000); + for (let i = 0; i < g1c1Students.length; i++) { + const s = g1c1Students[i]; + records.push({ + id: createId(), + studentId: s.id, + classId: classMap.G1C1.id, + date, + status: statuses[i % statuses.length], + remark: statuses[i % statuses.length] === "late" ? "迟到10分钟" : null, + recordedBy: recorder, + }); + } + } + await db.insert(attendanceRecords).values(records); + console.log(`✓ 创建 ${records.length} 条考勤记录`); +} + +// ============ 16. 课程计划 ============ + +async function seedCoursePlans( + teacherMap: Record, + classMap: Record, + subjectMap: Record, + academicYearId: string, + chapterMap: Record +) { + console.log("📝 创建课程计划..."); + // 一年级1班的语数外课程计划 + const plans: { id: string; classKey: string; subjectCode: string; teacherKey: string }[] = [ + { id: "cp_g1c1_chinese", classKey: "G1C1", subjectCode: "CHINESE", teacherKey: "T_C1" }, + { id: "cp_g1c1_math", classKey: "G1C1", subjectCode: "MATH", teacherKey: "T_M1" }, + { id: "cp_g1c1_eng", classKey: "G1C1", subjectCode: "ENG", teacherKey: "T_E1" }, + ]; + + let itemCount = 0; + for (const p of plans) { + await db.insert(coursePlans).values({ + id: p.id, + classId: classMap[p.classKey].id, + subjectId: subjectMap[p.subjectCode], + teacherId: teacherMap[p.teacherKey].id, + academicYearId, + semester: "1", + totalHours: 36, + completedHours: 4, + weeklyHours: 3, + startDate: new Date("2025-09-01"), + endDate: new Date("2026-01-15"), + syllabus: "一年级上学期教学大纲", + objectives: "掌握基础知识,培养学习兴趣", + status: "active", + createdBy: teacherMap[p.teacherKey].id, + }); + + // 第一周课程项 + await db.insert(coursePlanItems).values([ + { + id: createId(), + planId: p.id, + week: 1, + topic: "第一课", + content: "导入新课,讲解基础知识", + hours: 2, + textbookChapter: chapterMap[p.subjectCode], + notes: "注意课堂互动", + isCompleted: true, + completedAt: new Date("2025-09-02"), + }, + { + id: createId(), + planId: p.id, + week: 2, + topic: "第一课练习", + content: "巩固练习,答疑解惑", + hours: 1, + textbookChapter: chapterMap[p.subjectCode], + notes: "", + isCompleted: true, + completedAt: new Date("2025-09-09"), + }, + { + id: createId(), + planId: p.id, + week: 3, + topic: "第二课", + content: "新授课", + hours: 2, + textbookChapter: null, + notes: "", + isCompleted: false, + }, + ]); + itemCount += 3; + } + console.log(`✓ 创建 ${plans.length} 个课程计划,${itemCount} 条课程项`); +} + +// ============ 17. 公告 ============ + +async function seedAnnouncements( + adminId: string, + gradeMap: Record, + classMap: Record +) { + console.log("📢 创建公告..."); + await db.insert(announcements).values([ + { + id: "ann_school_1", + title: "2025-2026学年开学通知", + content: "尊敬的各位老师、家长、同学:\n\n新学期开始了!请同学们按时到校,做好上课准备。\n\n阳光小学", + type: "school", + status: "published", + authorId: adminId, + publishedAt: NOW, + }, + { + id: "ann_grade_g1", + title: "一年级家长会通知", + content: "一年级家长会将于本周五下午 3 点在各班教室召开,请家长准时参加。", + type: "grade", + status: "published", + targetGradeId: gradeMap.G1, + authorId: adminId, + publishedAt: NOW, + }, + { + id: "ann_class_g1c1", + title: "一年级1班作业提醒", + content: "请同学们按时完成语文、数学作业,周一上交。", + type: "class", + status: "published", + targetClassId: classMap.G1C1.id, + authorId: adminId, + publishedAt: NOW, + }, + ]); + console.log("✓ 创建 3 条公告"); +} + +// ============ 启动 ============ + seed().catch((err) => { - console.error("❌ Seed failed:", err); + console.error("❌ 初始化失败:", err); process.exit(1); }); diff --git a/src/proxy.ts b/src/proxy.ts index 7045208..2bfec2c 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -43,7 +43,10 @@ export async function proxy(request: NextRequest) { return NextResponse.next() } - const token = await getToken({ req: request }) + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + }) // Not authenticated → redirect to login if (!token) {