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 警告
This commit is contained in:
102
scripts/seed.ts
102
scripts/seed.ts
@@ -20,6 +20,56 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小学场景初始化脚本
|
||||
*
|
||||
@@ -73,22 +123,22 @@ async function seed() {
|
||||
await seedEnrollmentsAndParents(classMap, studentMap, parentMap);
|
||||
|
||||
// --- 8. 教材 + 章节(每科第一章第一节课)---
|
||||
const chapterMap = await seedTextbooksAndChapters(subjectMap, gradeMap);
|
||||
const chapterMap = await seedTextbooksAndChapters();
|
||||
|
||||
// --- 9. 知识点 ---
|
||||
const kpMap = await seedKnowledgePoints(chapterMap);
|
||||
|
||||
// --- 10. 课表 ---
|
||||
await seedClassSchedule(classMap, subjectMap);
|
||||
await seedClassSchedule(classMap);
|
||||
|
||||
// --- 11. 题库 ---
|
||||
const questionBanks = await seedQuestions(teacherMap, subjectMap, kpMap);
|
||||
|
||||
// --- 12. 试卷(语文、数学各 1 套)+ 学生答题与批改 ---
|
||||
await seedExamsAndSubmissions(teacherMap, classMap, studentMap, subjectMap, questionBanks, gradeMap);
|
||||
await seedExamsAndSubmissions(teacherMap, classMap, studentMap, questionBanks);
|
||||
|
||||
// --- 13. 作业(引用试卷)+ 学生答题与批改 ---
|
||||
await seedHomework(teacherMap, classMap, studentMap, questionBanks, subjectMap);
|
||||
await seedHomework(teacherMap, classMap, studentMap, questionBanks);
|
||||
|
||||
// --- 14. 成绩记录 ---
|
||||
await seedGradeRecords(teacherMap, classMap, studentMap, subjectMap, academicYearId);
|
||||
@@ -451,10 +501,7 @@ async function seedEnrollmentsAndParents(
|
||||
|
||||
// ============ 8. 教材 + 章节 ============
|
||||
|
||||
async function seedTextbooksAndChapters(
|
||||
subjectMap: Record<string, string>,
|
||||
gradeMap: Record<string, string>
|
||||
) {
|
||||
async function seedTextbooksAndChapters() {
|
||||
console.log("📖 创建教材与章节...");
|
||||
// 每科 1 本教材(一年级语文、一年级数学、一年级英语)
|
||||
// 只实现第一章第一节课
|
||||
@@ -558,8 +605,7 @@ async function seedKnowledgePoints(chapterMap: Record<string, string>) {
|
||||
// ============ 10. 课表 ============
|
||||
|
||||
async function seedClassSchedule(
|
||||
classMap: Record<string, { id: string }>,
|
||||
subjectMap: Record<string, string>
|
||||
classMap: Record<string, { id: string }>
|
||||
) {
|
||||
console.log("📅 创建课表...");
|
||||
// 每班每天安排语数外,简化为周一三五各 2 节
|
||||
@@ -807,13 +853,11 @@ async function seedExamsAndSubmissions(
|
||||
teacherMap: Record<string, { id: string }>,
|
||||
classMap: Record<string, { id: string }>,
|
||||
studentMap: Record<string, { id: string; classKey: string }>,
|
||||
subjectMap: Record<string, string>,
|
||||
questionBanks: { chineseQuestions: any[]; mathQuestions: any[] },
|
||||
gradeMap: Record<string, string>
|
||||
questionBanks: SeedQuestionBank
|
||||
) {
|
||||
console.log("📝 创建试卷与学生答题...");
|
||||
|
||||
const makeGroup = (title: string, children: any[]) => ({
|
||||
const makeGroup = (title: string, children: SeedQuestion[]) => ({
|
||||
id: createId(),
|
||||
type: "group" as const,
|
||||
title,
|
||||
@@ -926,12 +970,12 @@ async function seedExamsAndSubmissions(
|
||||
submissionCount++;
|
||||
|
||||
// 答案
|
||||
const buildAnswer = (q: any, idx: number) => {
|
||||
const buildAnswer = (q: SeedQuestion, idx: number) => {
|
||||
if (q.type === "single_choice") {
|
||||
// 前 3 题选正确项,后 2 题选错误项
|
||||
const correct = q.content.options.find((o: any) => o.isCorrect);
|
||||
const wrong = q.content.options.find((o: any) => !o.isCorrect);
|
||||
return { answer: idx < 3 ? correct.id : wrong.id };
|
||||
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 ? "标准答案" : "学生答案" };
|
||||
@@ -939,7 +983,7 @@ async function seedExamsAndSubmissions(
|
||||
|
||||
await db.insert(submissionAnswers).values(
|
||||
exam.qIds.map((qid, idx) => {
|
||||
const q = exam.qs.find((x: any) => x.id === qid)!;
|
||||
const q = exam.qs.find((x) => x.id === qid)!;
|
||||
const score = isGraded ? (perScores[idx] ?? 0) : null;
|
||||
return {
|
||||
id: createId(),
|
||||
@@ -963,8 +1007,7 @@ async function seedHomework(
|
||||
teacherMap: Record<string, { id: string }>,
|
||||
classMap: Record<string, { id: string }>,
|
||||
studentMap: Record<string, { id: string; classKey: string }>,
|
||||
questionBanks: { chineseQuestions: any[]; mathQuestions: any[]; engQuestions: any[] },
|
||||
subjectMap: Record<string, string>
|
||||
questionBanks: SeedQuestionBank
|
||||
) {
|
||||
console.log("📚 创建作业...");
|
||||
// 1 个语文作业(引用语文题)+ 1 个数学作业(引用数学题)
|
||||
@@ -972,7 +1015,7 @@ async function seedHomework(
|
||||
const dueAt = new Date(now.getTime() + 7 * 86400_000);
|
||||
const lateDueAt = new Date(now.getTime() + 9 * 86400_000);
|
||||
|
||||
const assignments: { id: string; title: string; creatorId: string; qIds: string[]; qs: any[]; targetClassKey: string }[] = [
|
||||
const assignments: { id: string; title: string; creatorId: string; qIds: string[]; qs: SeedQuestion[]; targetClassKey: string }[] = [
|
||||
{
|
||||
id: "hw_chinese_g1",
|
||||
title: "一年级语文第一课作业",
|
||||
@@ -1039,7 +1082,6 @@ async function seedHomework(
|
||||
);
|
||||
|
||||
// 前 3 个学生提交作业并批改
|
||||
const scoreMap = new Map(hw.qs.map(q => [q.id, q.score] as const));
|
||||
for (let i = 0; i < Math.min(3, targets.length); i++) {
|
||||
const student = targets[i];
|
||||
const submissionId = createId();
|
||||
@@ -1067,11 +1109,11 @@ async function seedHomework(
|
||||
});
|
||||
hwSubmissionCount++;
|
||||
|
||||
const buildAnswer = (q: any, idx: number) => {
|
||||
const buildAnswer = (q: SeedQuestion, idx: number) => {
|
||||
if (q.type === "single_choice") {
|
||||
const correct = q.content.options.find((o: any) => o.isCorrect);
|
||||
const wrong = q.content.options.find((o: any) => !o.isCorrect);
|
||||
return { answer: idx < 3 ? correct.id : wrong.id };
|
||||
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 ? "标准答案" : "学生答案" };
|
||||
@@ -1079,7 +1121,7 @@ async function seedHomework(
|
||||
|
||||
await db.insert(homeworkAnswers).values(
|
||||
hw.qIds.map((qid, idx) => {
|
||||
const q = hw.qs.find((x: any) => x.id === qid)!;
|
||||
const q = hw.qs.find((x) => x.id === qid)!;
|
||||
const score = isGraded ? (perScores[idx] ?? 0) : null;
|
||||
return {
|
||||
id: createId(),
|
||||
@@ -1117,7 +1159,7 @@ async function seedGradeRecords(
|
||||
ENG: teacherMap.T_E1.id,
|
||||
};
|
||||
|
||||
const records: any[] = [];
|
||||
const records: SeedGradeRecord[] = [];
|
||||
for (const s of g1c1Students) {
|
||||
for (const [subCode, sid] of Object.entries(subjectMap)) {
|
||||
records.push({
|
||||
@@ -1151,7 +1193,7 @@ async function seedAttendanceRecords(
|
||||
// 为一年级1班最近 5 天的考勤
|
||||
const g1c1Students = Object.values(studentMap).filter(s => s.classKey === "G1C1");
|
||||
const recorder = teacherMap.T_C1.id;
|
||||
const records: any[] = [];
|
||||
const records: SeedAttendanceRecord[] = [];
|
||||
const statuses = ["present", "present", "present", "present", "late", "absent"] as const;
|
||||
|
||||
for (let d = 0; d < 5; d++) {
|
||||
|
||||
Reference in New Issue
Block a user