import "dotenv/config"; import { db } from "../src/shared/db"; import { users, roles, usersToRoles, rolePermissions, questions, knowledgePoints, questionsToKnowledgePoints, exams, examQuestions, examSubmissions, submissionAnswers, homeworkAssignments, homeworkAssignmentQuestions, homeworkAssignmentTargets, homeworkSubmissions, homeworkAnswers, textbooks, chapters, 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 { sql, eq } from "drizzle-orm"; import { hash } from "bcryptjs"; import { ROLE_PERMISSIONS } from "../src/shared/lib/permissions"; // ---- 内部类型定义(避免 any) ---- interface SeedOption { id: string; text: string; isCorrect?: boolean; } interface SeedQuestionContent { text?: string; options?: SeedOption[]; correctAnswer?: string | boolean; } interface SeedQuestion { id: string; type: "single_choice" | "text" | "judgment"; difficulty: number; content: SeedQuestionContent; score: number; } interface SeedQuestionBank { chineseQuestions: SeedQuestion[]; mathQuestions: SeedQuestion[]; engQuestions?: SeedQuestion[]; } interface SeedGradeRecord { id: string; studentId: string; subjectId: string; classId: string; teacherId: string; score: number; examType: string; term: string; recordedAt: Date; } interface SeedAttendanceRecord { id: string; studentId: string; classId: string; date: Date; status: "present" | "late" | "absent"; recorderId: string; remark: string | null; } /** * 小学场景初始化脚本 * * 数据规模: * - 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("🌱 开始小学场景数据初始化..."); const start = performance.now(); // --- 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(); // --- 9. 知识点 --- const kpMap = await seedKnowledgePoints(chapterMap); // --- 10. 课表 --- await seedClassSchedule(classMap); // --- 11. 题库 --- const questionBanks = await seedQuestions(teacherMap, subjectMap, kpMap); // --- 12. 试卷(语文、数学各 1 套)+ 学生答题与批改 --- await seedExamsAndSubmissions(teacherMap, classMap, studentMap, questionBanks, subjectMap, gradeMap); // --- 13. 作业(引用试卷)+ 学生答题与批改 --- await seedHomework(teacherMap, classMap, studentMap, questionBanks); // --- 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("✓ 清理完成"); } // ============ 1. 角色 + 权限 ============ async function seedRolesAndPermissions() { console.log("👤 创建角色与权限..."); 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(roles).values(roleDefs); // 写入角色-权限映射 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} 条权限映射`); } // ============ 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: "Grade 1", order: 1 }, { key: "G2", name: "Grade 2", 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, }); } 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: "男", onboardedAt: NOW, }); 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, onboardedAt: NOW, }); await db.insert(usersToRoles).values({ userId: id, roleId: "role_teacher" }); } // 指定一年级组长 = 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" }); // --- 学生 --- // 每班 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`), phone: "1380000" + String(studentIdx).padStart(4, "0"), guardianName: "家长" + name, guardianPhone: "1380000" + String(studentIdx).padStart(4, "0"), guardianRelation: i % 2 === 0 ? "父亲" : "母亲", onboardedAt: NOW, }); await db.insert(usersToRoles).values({ userId: id, roleId: "role_student" }); } } // --- 家长 --- // 每名学生 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("涛") ? "男" : "女", onboardedAt: NOW, }); await db.insert(usersToRoles).values({ userId: id, roleId: "role_parent" }); } 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: c.name, grade: c.gradeKey === "G1" ? "Grade 1" : "Grade 2", gradeId: gradeMap[c.gradeKey], homeroom: c.code, room: c.room, invitationCode: c.code, teacherId: teacherMap[c.homeroom].id, }); } console.log(`✓ 创建 ${classDefs.length} 个班级`); return classMap; } // ============ 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() { console.log("📖 创建教材与章节..."); // 每科 1 本教材(一年级语文、一年级数学、一年级英语) // 只实现第一章第一节课 const chapterMap: Record = {}; const textbookDefs = [ { subjectCode: "CHINESE", subjectName: "Chinese", title: "一年级语文(上册)", publisher: "人民教育出版社", chapterTitle: "第一课 秋天", chapterContent: "# 第一课 秋天\n\n秋天来了,天气凉了,树叶黄了,一片片叶子从树上落下来。\n\n天空那么蓝,那么高。一群大雁往南飞,一会儿排成个\"人\"字,一会儿排成个\"一\"字。\n\n啊!秋天来了!", }, { subjectCode: "MATH", subjectName: "Mathematics", title: "一年级数学(上册)", publisher: "人民教育出版社", chapterTitle: "第一课 1-5 的认识", chapterContent: "# 第一课 1-5 的认识\n\n## 数一数\n\n1 像铅笔细又长\n2 像小鸭水上漂\n3 像耳朵听声音\n4 像小旗迎风飘\n5 像秤钩来买菜\n\n## 练习\n\n数一数图中有几只小鸟?有几朵花?", }, { subjectCode: "ENG", subjectName: "English", 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 我是", }, ]; 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: "Grade 1", publisher: tb.publisher, }); // 第一章 const ch1Id = `ch_${tb.subjectCode}_g1_1`; await db.insert(chapters).values({ id: ch1Id, textbookId: tbId, title: `第一章 ${tb.subjectName === "Chinese" ? "课文" : tb.subjectName === "Mathematics" ? "数一数" : "Greetings"}`, order: 1, parentId: null, 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: ch1Id, content: tb.chapterContent, }); } console.log(`✓ 创建 ${textbookDefs.length} 本教材,每本含第一章第一课`); return chapterMap; // { CHINESE, MATH, ENG } } // ============ 9. 知识点 ============ async function seedKnowledgePoints(chapterMap: Record) { console.log("🧠 创建知识点..."); const map: Record = {}; 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: chapterMap[kp.chapterCode], }); } console.log(`✓ 创建 ${kpDefs.length} 个知识点`); return map; } // ============ 10. 课表 ============ async function seedClassSchedule( classMap: 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" as const, difficulty: 1, content: { text: "\"秋天来了\"中,哪个字表示季节?", options: [ { 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" as const, difficulty: 2, content: { text: "课文中大雁往南飞是因为?", options: [ { 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: "text" as const, difficulty: 1, content: { text: "写出\"天\"的笔顺(用文字描述)。", correctAnswer: "横、横、撇、捺" }, score: 10, }, { id: createId(), type: "judgment" as const, difficulty: 1, content: { text: "\"一片片叶子从树上落下来\"说明秋天来了。", correctAnswer: true }, score: 10, }, { id: createId(), type: "text" as const, difficulty: 2, 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: "哪个数字表示\"3\"?", options: [ { 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, }, ]; // 英语题(作者 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, }, ]; 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( allQs.map(q => ({ id: q.id, type: q.type, difficulty: q.difficulty, content: q.content, authorId: q.authorId, })) ); // 关联知识点 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); console.log(`✓ 创建 ${allQs.length} 道题目(语${chineseQuestions.length}/数${mathQuestions.length}/英${engQuestions.length})`); return { chineseQuestions, mathQuestions, engQuestions }; } // ============ 12. 试卷 + 学生答题与批改 ============ async function seedExamsAndSubmissions( teacherMap: Record, classMap: Record, studentMap: Record, questionBanks: SeedQuestionBank, subjectMap: Record, gradeMap: Record ) { console.log("📝 创建试卷与学生答题..."); const makeGroup = (title: string, children: SeedQuestion[]) => ({ id: createId(), type: "group" as const, title, children, }); const makeQNode = (questionId: string, score: number) => ({ id: createId(), type: "question" as const, questionId, score, }); // --- 试卷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: chineseExamId, title: "一年级语文第一单元测验", description: JSON.stringify({ subject: "Chinese", grade: "Grade 1", totalScore: 50, durationMin: 40, questionCount: 5 }), creatorId: teacherMap.T_C1.id, subjectId: subjectMap.CHINESE, gradeId: gradeMap.G1, status: "published", startTime: NOW, structure: cnStructure, }); await db.insert(examQuestions).values( cnOrderedIds.map((qid, i) => ({ examId: chineseExamId, questionId: qid, score: cnScoreMap.get(qid) ?? 10, order: i, })) ); // --- 试卷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)); await db.insert(exams).values({ id: mathExamId, title: "一年级数学第一单元测验", description: JSON.stringify({ subject: "Mathematics", grade: "Grade 1", totalScore: 50, durationMin: 40, questionCount: 5 }), creatorId: teacherMap.T_M1.id, subjectId: subjectMap.MATH, gradeId: gradeMap.G1, status: "published", startTime: NOW, structure: mStructure, }); await db.insert(examQuestions).values( mOrderedIds.map((qid, i) => ({ examId: mathExamId, questionId: qid, score: mScoreMap.get(qid) ?? 10, order: i, })) ); // --- 学生答题与批改 --- // 一年级两个班的学生都参加这两套试卷 const g1Students = Object.values(studentMap).filter(s => s.classKey.startsWith("G1")); let submissionCount = 0; let answerCount = 0; 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"; // 模拟答题:前 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; await db.insert(examSubmissions).values({ id: submissionId, examId: exam.id, studentId: student.id, score: totalScore, status, submittedAt, }); submissionCount++; // 答案 const buildAnswer = (q: SeedQuestion, idx: number) => { if (q.type === "single_choice") { // 前 3 题选正确项,后 2 题选错误项 const correct = q.content.options?.find((o) => o.isCorrect); const wrong = q.content.options?.find((o) => !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(submissionAnswers).values( exam.qIds.map((qid, idx) => { const q = exam.qs.find((x) => 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; } } console.log(`✓ 创建 2 套试卷,${submissionCount} 份提交,${answerCount} 条答案`); } // ============ 13. 作业 ============ async function seedHomework( teacherMap: Record, classMap: Record, studentMap: Record, questionBanks: SeedQuestionBank ) { 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: SeedQuestion[]; 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 个学生提交作业并批改 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: SeedQuestion, idx: number) => { if (q.type === "single_choice") { const correct = q.content.options?.find((o) => o.isCorrect); const wrong = q.content.options?.find((o) => !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) => 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: SeedGradeRecord[] = []; 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: SeedAttendanceRecord[] = []; 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("❌ 初始化失败:", err); process.exit(1); });