- Update ESLint, Prettier, Tailwind, TypeScript, Vitest, Playwright configs - Update Dockerfile and CI/CD workflows (ci, dr-drill, security) - Add/Update DB backup, restore, health-check, security-scan scripts - Update project rules and .gitignore
1353 lines
46 KiB
TypeScript
1353 lines
46 KiB
TypeScript
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<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: "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<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: "男",
|
||
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<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,
|
||
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<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`),
|
||
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<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("涛") ? "男" : "女",
|
||
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<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" ? "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<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: "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<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,
|
||
subjectMap: Record<string, string>,
|
||
gradeMap: Record<string, string>
|
||
) {
|
||
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<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);
|
||
});
|