Files
NextEdu/scripts/seed.ts
SpecialX b86255f0ea feat(P2): 实现选课管理、考试监考、学情诊断三大功能模块
## 新增功能模块

### 1. 选课管理(elective)
- 新增表:electiveCourses、courseSelections
- 新增权限:ELECTIVE_MANAGE/ELECTIVE_READ/ELECTIVE_SELECT
- 支持先到先得 + 抽签两种选课模式
- admin/teacher/student 三端页面

### 2. 考试监考(proctoring)
- exams 表扩展:examMode/durationMinutes/antiCheatEnabled 等字段
- 新增表:examProctoringEvents
- 新增权限:EXAM_PROCTOR/EXAM_PROCTOR_READ
- 教师监考面板 + 学生端防作弊监控
- API:/api/proctoring/event 接收事件上报

### 3. 学情诊断报告(diagnostic)
- 新增表:knowledgePointMastery、learningDiagnosticReports
- 新增权限:DIAGNOSTIC_MANAGE/DIAGNOSTIC_READ
- 基于提交答案自动计算知识点掌握度
- 生成个人/班级诊断报告(强项/弱项/建议)
- 雷达图可视化

## 其他改动
- 项目规则:单文件行数限制从 300 行调整为企业级规范(组件≤500/Actions≤800/硬上限1000)
- scripts/seed.ts:消除全部 any 类型,定义内部类型,0 lint 错误
- 架构文档 004/005 同步更新三个新模块
- 迁移文件 0001_heavy_sage.sql 生成

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
2026-06-17 19:12:51 +08:00

1346 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
// --- 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<string, string> = {};
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<string, string> = {};
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,
});
}
console.log(`✓ 创建 1 所学校、${gradeDefs.length} 个年级、1 个学年`);
return { schoolId, gradeMap, academicYearId };
}
// ============ 4. 用户 ============
async function seedUsers(
schoolId: string,
gradeMap: Record<string, string>
) {
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<string, { id: string; name: string; subject: string }> = {};
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" });
}
// 指定一年级组长 = 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<string, { id: string; name: string; classKey: string }> = {};
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" });
}
}
// --- 家长 ---
// 每名学生 1 位家长
const parentMap: Record<string, { id: string; name: string; childKey: string }> = {};
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" });
}
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<string, string>,
teacherMap: Record<string, { id: string; name: string; subject: string }>
) {
console.log("🏫 创建班级...");
const classMap: Record<string, { id: string; gradeKey: string; homeroomTeacherKey: string }> = {};
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" ? "一年级" : "二年级",
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<string, { id: string; gradeKey: string; homeroomTeacherKey: string }>,
teacherMap: Record<string, { id: string; name: string; subject: string }>,
subjectMap: Record<string, string>
) {
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<string, { CHINESE: string; MATH: string; ENG: string }> = {
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<string, { id: string }>,
studentMap: Record<string, { id: string; classKey: string }>,
parentMap: Record<string, { id: string; childKey: string }>
) {
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<string, string> = {};
const textbookDefs = [
{
subjectCode: "CHINESE",
subjectName: "语文",
title: "一年级语文(上册)",
publisher: "人民教育出版社",
chapterTitle: "第一课 秋天",
chapterContent: "# 第一课 秋天\n\n秋天来了天气凉了树叶黄了一片片叶子从树上落下来。\n\n天空那么蓝那么高。一群大雁往南飞一会儿排成个\"人\"字,一会儿排成个\"一\"字。\n\n啊秋天来了",
},
{
subjectCode: "MATH",
subjectName: "数学",
title: "一年级数学(上册)",
publisher: "人民教育出版社",
chapterTitle: "第一课 1-5 的认识",
chapterContent: "# 第一课 1-5 的认识\n\n## 数一数\n\n1 像铅笔细又长\n2 像小鸭水上漂\n3 像耳朵听声音\n4 像小旗迎风飘\n5 像秤钩来买菜\n\n## 练习\n\n数一数图中有几只小鸟有几朵花",
},
{
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 我是",
},
];
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: `# 第一章\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<string, string>) {
console.log("🧠 创建知识点...");
const map: Record<string, string> = {};
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<string, { id: string }>
) {
console.log("📅 创建课表...");
// 每班每天安排语数外,简化为周一三五各 2 节
const rows: { id: string; classId: string; weekday: number; startTime: string; endTime: string; course: string; location: string }[] = [];
const subjectNames: Record<string, string> = { 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<string, { id: string }>,
subjectMap: Record<string, string>,
kpMap: Record<string, string>
) {
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<string, { id: string }>,
classMap: Record<string, { id: string }>,
studentMap: Record<string, { id: string; classKey: string }>,
questionBanks: SeedQuestionBank
) {
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: "语文", grade: "一年级", 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: "数学", grade: "一年级", 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<number>((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<string, { id: string }>,
classMap: Record<string, { id: string }>,
studentMap: Record<string, { id: string; classKey: string }>,
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<number>((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<string, { id: string }>,
classMap: Record<string, { id: string }>,
studentMap: Record<string, { id: string; classKey: string }>,
subjectMap: Record<string, string>,
academicYearId: string
) {
console.log("📊 创建成绩记录...");
// 为一年级1班学生录入语数外成绩
const g1c1Students = Object.values(studentMap).filter(s => s.classKey === "G1C1");
const subjectTeacher: Record<string, string> = {
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<string, { id: string }>,
classMap: Record<string, { id: string }>,
studentMap: Record<string, { id: string; classKey: string }>
) {
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<string, { id: string }>,
classMap: Record<string, { id: string }>,
subjectMap: Record<string, string>,
academicYearId: string,
chapterMap: Record<string, string>
) {
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<string, string>,
classMap: Record<string, { id: string }>
) {
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);
});