feat(exams,homework,proctoring): 长期问题修复与竞品差距补齐

P1-1 跨模块直查消除:
- homework/data-access-classes.ts 移除对 exams/subjects 表的 JOIN 直查
- 改为调用 exams/data-access.getExamSubjectIdMap + school/data-access.getSubjectNameMapByIds
- school/data-access.ts 新增 getSubjectNameMapByIds 批量科目名称映射函数

P1-2 as 断言消除(exam-mode-config.tsx):
- 移除全部 10 处 as 类型断言
- 改用 useFormContext 替代 Control prop,避免 Control<T> 不变型问题
- exam-form.tsx 调用方简化为 <ExamModeConfig />(已集成到考试表单)

P1-3 as 断言消除(proctoring-dashboard.tsx):
- 用类型守卫函数 isProctoringEventType + toProctoringEventTypes
  替代 Object.keys(...) as ProctoringEventType[] 断言

P0-竞品倒计时(对标智学网/猿题库):
- 新增 hooks/use-exam-countdown.ts 考试倒计时 Hook
- homework-take-view.tsx 集成限时/监考模式倒计时显示与到时自动提交
- data-access.ts 的 getStudentHomeworkTakeData 新增 examModeConfig + startedAt 字段
- types.ts 扩展 StudentHomeworkTakeData 类型
- i18n 补充 timedExam/timeRemaining/timeUpAutoSubmit 翻译键

架构文档同步:
- 004/005 更新 homework/proctoring/school/exams 模块导出与依赖关系
- 005 新增 homework.hooks.useExamCountdown 与 school.dataAccess.getSubjectNameMapByIds
- 005 依赖矩阵 homework→school 补充 getSubjectNameMapByIds

验证:tsc --noEmit 零错误,eslint 零错误(3 个预存 warning 无关)
This commit is contained in:
SpecialX
2026-06-23 09:34:24 +08:00
parent 2c0f81391b
commit 036a2f2839
12 changed files with 915 additions and 136 deletions

View File

@@ -2431,6 +2431,8 @@
"content",
"link",
"isRead",
"priority",
"isArchived",
"createdAt"
],
"usedBy": [
@@ -2570,6 +2572,7 @@
"id",
"studentId",
"generatedBy",
"classId",
"reportType",
"period",
"summary",
@@ -2581,10 +2584,17 @@
"createdAt",
"updatedAt"
],
"indexes": [
"diagnostic_student_idx (studentId)",
"diagnostic_generated_by_idx (generatedBy)",
"diagnostic_status_idx (status)",
"diagnostic_report_type_idx (reportType)",
"diagnostic_class_idx (classId)"
],
"usedBy": [
"diagnostic"
],
"description": "学情诊断报告reportType: individual/class/gradestatus: draft/published/archived"
"description": "学情诊断报告reportType: individual/class/gradestatus: draft/published/archivedv4-P1-4 新增 classId 字段 varchar(128) references classes.id onDelete:set null用于班级报告关联班级发布时批量通知全班学生"
},
"electiveCourses": {
"fields": [
@@ -4092,6 +4102,17 @@
"file": "homework-submission-result.tsx",
"purpose": "V3-9 新增:提交后即时反馈页。学生提交后立即看到分数汇总(总分/满分、得分率 Progress、对错分布正确/错误/部分正确/待批改)、错题预览(题目文本、学生答案、正确答案)。对标智学网/猿题库提交后反馈。"
}
],
"hooks": [
{
"name": "useExamCountdown",
"file": "hooks/use-exam-countdown.ts",
"signature": "(options: { durationMinutes: number | null, startedAt: string | null, onExpire?: () => void, enabled?: boolean }) => ExamCountdownState | null",
"purpose": "P0-竞品新增:考试倒计时 Hook对标智学网/猿题库。每秒更新剩余时间剩余≤5分钟标记紧急状态到时触发 onExpire 回调自动提交。setInterval + setState 仅在 interval 回调中异步调用,符合 react-hooks/purity 与 set-state-in-effect 规则。",
"usedBy": [
"homework/components/homework-take-view.HomeworkTakeView"
]
}
]
}
},
@@ -6291,6 +6312,15 @@
"usedBy": [
"classes/actions.createTeacherClassAction"
]
},
{
"name": "getSubjectNameMapByIds",
"signature": "(subjectIds: string[]) => Promise<Map<string, string | null>>",
"purpose": "按 ID 批量获取科目名称映射跨模块接口P1-1 新增供 homework/data-access-classes 调用,替代直查 subjects 表)",
"usedBy": [
"homework/data-access-classes.getHomeworkAssignmentsWithSubject",
"homework/data-access-classes.getPublishedHomeworkAssignmentsWithSubject"
]
}
],
"schema": [
@@ -7595,6 +7625,74 @@
"SettingsView"
]
}
],
"lib": [
{
"name": "parseUserAgent",
"file": "lib/security-utils.ts",
"signature": "(ua: string | null) => { device: string; browser: string }",
"purpose": "解析 User-Agent 字符串为设备类型 + 浏览器名称v2 抽取的纯函数,便于单测)"
},
{
"name": "formatRelativeTime",
"file": "lib/security-utils.ts",
"signature": "(iso: string, locale: string) => string",
"purpose": "将 ISO 时间戳格式化为相对时间v2 抽取的纯函数)"
},
{
"name": "generateTotpSecret",
"file": "lib/totp.ts",
"signature": "() => string",
"purpose": "生成 TOTP base32 密钥v3 新增,基于 otplib v13"
},
{
"name": "buildOtpAuthUrl",
"file": "lib/totp.ts",
"signature": "(params: { serviceName: string; accountName: string; secret: string }) => string",
"purpose": "构建 otpauth:// URL 用于二维码扫描v3 新增)"
},
{
"name": "generateQrCodeDataUrl",
"file": "lib/totp.ts",
"signature": "(otpAuthUrl: string) => Promise<string>",
"purpose": "将 otpauth URL 转换为 QR 码 Data URLbase64 PNGv3 新增)"
},
{
"name": "verifyTotpCode",
"file": "lib/totp.ts",
"signature": "(token: string, secret: string) => boolean",
"purpose": "校验 TOTP 一次性码±30s 窗口容差v3 新增)"
},
{
"name": "generateBackupCodes",
"file": "lib/totp.ts",
"signature": "() => string[]",
"purpose": "生成 10 个 8 位备份码去除易混淆字符v3 新增)"
},
{
"name": "hashBackupCodes",
"file": "lib/totp.ts",
"signature": "(codes: string[]) => Promise<string>",
"purpose": "将备份码列表 bcrypt 哈希为 JSON 数组字符串v3 新增)"
},
{
"name": "verifyBackupCode",
"file": "lib/totp.ts",
"signature": "(input: string, storedHashedJson: string) => Promise<number>",
"purpose": "校验备份码,返回匹配索引或 -1v3 新增)"
},
{
"name": "consumeBackupCode",
"file": "lib/totp.ts",
"signature": "(storedHashedJson: string, usedIndex: number) => Promise<string>",
"purpose": "从哈希列表中移除已使用的备份码v3 新增)"
},
{
"name": "countRemainingBackupCodes",
"file": "lib/totp.ts",
"signature": "(storedHashedJson: string) => number",
"purpose": "统计剩余备份码数量v3 新增)"
}
]
}
},
@@ -9038,6 +9136,14 @@
"signature": "(prevState, formData) => Promise<ActionState<string>>",
"file": "actions.ts",
"permission": "GRADE_RECORD_MANAGE",
"purpose": "创建单条成绩记录。v4-P1-6 增强:成绩录入后通知学生和家长(调用 notifyGradeEntered内部使用 parent/data-access.getParentIdsByStudentIds 批量查询家长 ID通知失败不阻断录入v3-P1-5录入后更新诊断掌握度updateMasteryFromExamScore",
"deps": [
"requirePermission",
"data-access.createGradeRecord",
"diagnostic/data-access.updateMasteryFromExamScore",
"grades/lib/notify-grade-entered.notifyGradeEntered",
"revalidatePath"
],
"usedBy": [
"grades/components/grade-record-form"
]
@@ -9047,6 +9153,15 @@
"signature": "(prevState, formData) => Promise<ActionState<number>>",
"file": "actions.ts",
"permission": "GRADE_RECORD_MANAGE",
"purpose": "批量创建成绩记录db.transaction 原子操作。v4-P1-6 增强:批量录入后通知所有相关学生和家长(调用 notifyGradeEnteredv3-P1-5录入后更新诊断掌握度v3-P2-3返回创建的记录 ID 列表供前端撤销",
"deps": [
"requirePermission",
"data-access.batchCreateGradeRecords",
"diagnostic/data-access.updateMasteryFromExamScore",
"grades/lib/notify-grade-entered.notifyGradeEntered",
"shared/lib/action-utils.safeJsonParse",
"revalidatePath"
],
"usedBy": [
"grades/components/batch-grade-entry"
]
@@ -9117,18 +9232,21 @@
},
{
"name": "exportGradesAction",
"signature": "(params: { classId: string; subjectId?: string; examId?: string; reportType?: \"detail\" | \"class\" }) => Promise<ActionState<{ buffer: string; filename: string }>>",
"signature": "(params: { classId?: string; studentId?: string; subjectId?: string; examId?: string; reportType?: \"detail\" | \"class\" }) => Promise<ActionState<{ buffer: string; filename: string }>>",
"file": "actions.ts",
"permission": "GRADE_RECORD_READ",
"purpose": "导出成绩到 Exceldetail=成绩明细+统计汇总class=班级多科目横向对比总表),返回 base64 buffer",
"purpose": "导出成绩到 Exceldetail=成绩明细+统计汇总class=班级多科目横向对比总表),返回 base64 buffer。v4-P1-12 增强:新增可选 studentId 参数支持按学生导出(家长视角)——当提供 studentId 且 scope 为 children 时校验该学生属于家长子女,调用 exportStudentGradeRecordsToExcel 导出单学生成绩单",
"deps": [
"requirePermission",
"export.exportGradeRecordsToExcel",
"export.exportClassGradeReportToExcel",
"export.formatDateForFile"
"export.exportStudentGradeRecordsToExcel",
"export.formatDateForFile",
"actions.assertClassInScope"
],
"usedBy": [
"grades/components/export-button.tsx"
"grades/components/export-button.tsx",
"parent/components/parent-export-button.tsx"
]
},
{
@@ -9874,6 +9992,18 @@
"usedBy": [
"admin/school/grades/insights/page.tsx"
]
},
{
"name": "ScoreCell",
"file": "components/score-cell.tsx",
"purpose": "v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%(不及格 text-red-600/黄60-84%(及格未优秀 text-yellow-600/绿≥85%(优秀 text-green-600使用语义化 Tailwind 类名避免动态拼接fullScore<=0 时不着色(避免除零)",
"props": "{ score: number; fullScore: number; className?: string }",
"deps": [
"shared/lib/utils.cn"
],
"usedBy": [
"grades/components/grade-record-list.tsx"
]
}
]
}
@@ -10385,6 +10515,22 @@
"usedBy": [
"parent/children/[studentId]/page.tsx"
]
},
{
"name": "getParentIdsByStudentIds",
"signature": "(studentIds: string[]) => Promise<string[]>",
"file": "data-access.ts",
"purpose": "v4-P1-5 新增:批量查询多个学生的家长 userId 列表,用于通知场景——报告/成绩发布时同步通知所有相关家长。返回去重后的 parentId 数组,使用 react.cache 包装实现请求级 memoization空数组直接返回 []",
"deps": [
"shared.db",
"shared.db.schema.parentStudentRelations",
"drizzle-orm.inArray",
"react.cache"
],
"usedBy": [
"diagnostic/actions.publishReportAction",
"grades/lib/notify-grade-entered.notifyGradeEntered"
]
}
],
"types": [
@@ -10914,7 +11060,7 @@
"name": "Notification",
"type": "type",
"file": "types.ts",
"definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, createdAt }",
"definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, priority: NotificationPriority, isArchived, createdAt }",
"usedBy": [
"notification-dropdown",
"notification-list"
@@ -10961,7 +11107,7 @@
"name": "CreateNotificationInput",
"type": "type",
"file": "types.ts",
"definition": "{ userId, type: NotificationType, title, content?, link? }",
"definition": "{ userId, type: NotificationType, title, content?, link?, priority? }",
"usedBy": [
"createNotification"
]
@@ -11134,6 +11280,22 @@
"notification-dropdown.tsx",
"notification-list.tsx"
]
},
{
"name": "archiveNotificationAction",
"permission": "MESSAGE_READ",
"signature": "(notificationId: string) => Promise<ActionState<string>>",
"purpose": "V2-P2-13b 新增:将单条通知归档(归档后不在默认列表显示);含 trackEvent 埋点 notification.archived",
"deps": [
"requirePermission",
"schema.NotificationIdSchema",
"data-access.archiveNotification",
"trackEvent",
"revalidatePath"
],
"usedBy": [
"notification-list.tsx"
]
}
],
"dispatcher": [
@@ -11173,9 +11335,9 @@
"dataAccess": [
{
"name": "getNotifications",
"signature": "(userId: string, params?: { page?, pageSize?, unreadOnly? }) => Promise<PaginatedResult<Notification>>",
"signature": "(userId: string, params?: { page?, pageSize?, unreadOnly?, unarchivedOnly?, priority? }) => Promise<PaginatedResult<Notification>>",
"file": "data-access.ts",
"purpose": "获取用户站内通知列表(分页,支持 unreadOnly 过滤P0-4 / P1-5 修复后从 messaging 迁移)",
"purpose": "获取用户站内通知列表(分页,支持 unreadOnly/unarchivedOnly/priority 过滤P0-4 / P1-5 修复后从 messaging 迁移V2-P2-13b 新增归档和优先级筛选,默认仅返回未归档",
"deps": [
"shared.db",
"shared.db.schema.messageNotifications",
@@ -11190,7 +11352,7 @@
"name": "createNotification",
"signature": "(input: CreateNotificationInput) => Promise<string>",
"file": "data-access.ts",
"purpose": "创建站内通知记录(写入 message_notifications 表P0-4 / P1-5 修复后从 messaging 迁移)",
"purpose": "创建站内通知记录(写入 message_notifications 表P0-4 / P1-5 修复后从 messaging 迁移V2-P2-13b 支持 priority 可选参数,默认 normal",
"deps": [
"shared.db",
"shared.db.schema.messageNotifications",
@@ -11240,6 +11402,32 @@
"待扩展"
]
},
{
"name": "archiveNotification",
"signature": "(id: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13b 新增将单条通知归档isArchived 置 true归档后不在默认列表显示",
"deps": [
"shared.db",
"shared.db.schema.messageNotifications"
],
"usedBy": [
"archiveNotificationAction"
]
},
{
"name": "unarchiveNotification",
"signature": "(id: string, userId: string) => Promise<void>",
"file": "data-access.ts",
"purpose": "V2-P2-13b 新增取消归档通知isArchived 置 false",
"deps": [
"shared.db",
"shared.db.schema.messageNotifications"
],
"usedBy": [
"待扩展"
]
},
{
"name": "getUserContactInfo",
"signature": "(userId: string) => Promise<ChannelRecipient>",
@@ -11422,11 +11610,22 @@
"messaging (via re-export)"
]
},
{
"name": "NotificationPriority",
"type": "type",
"file": "types.ts",
"definition": "'low' | 'normal' | 'high' | 'urgent'",
"usedBy": [
"data-access",
"components.notification-list",
"messaging (via re-export)"
]
},
{
"name": "Notification",
"type": "interface",
"file": "types.ts",
"definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, createdAt }",
"definition": "{ id, userId, type: NotificationType, title, content: string | null, link: string | null, isRead, priority: NotificationPriority, isArchived, createdAt }",
"usedBy": [
"data-access.getNotifications",
"messaging (via re-export)"
@@ -11462,7 +11661,7 @@
"name": "GetNotificationsParams",
"type": "interface",
"file": "types.ts",
"definition": "{ page?, pageSize?, unreadOnly? }",
"definition": "{ page?, pageSize?, unreadOnly?, unarchivedOnly?, priority? }",
"usedBy": [
"data-access.getNotifications",
"messaging (via re-export)"
@@ -11472,7 +11671,7 @@
"name": "CreateNotificationInput",
"type": "interface",
"file": "types.ts",
"definition": "{ userId, type: NotificationType, title, content?, link? }",
"definition": "{ userId, type: NotificationType, title, content?, link?, priority? }",
"usedBy": [
"data-access.createNotification",
"channels.in-app-channel",
@@ -12028,6 +12227,35 @@
"usedBy": [
"teacher/attendance/stats/page.tsx"
]
},
{
"name": "AttendancePageLayout",
"file": "components/attendance-page-layout.tsx",
"purpose": "考勤页面布局骨架P2-3 新增:抽取 admin/teacher 考勤页重复的 header/stats/filters/children 布局结构,统一 spacing 与响应式)",
"deps": [
"shared/lib/utils.cn"
],
"usedBy": [
"admin/attendance/page.tsx",
"teacher/attendance/page.tsx"
]
}
],
"importExport": [
{
"name": "exportAttendanceRecordsToExcel",
"signature": "(params: { scope: DataScope; currentUserId?: string; classId?: string; status?: string; date?: string }) => Promise<Buffer>",
"file": "export.ts",
"purpose": "导出考勤记录到 ExcelP2-11 新增Sheet1 考勤明细—学生/班级/日期/状态/备注/记录人/创建时间Sheet2 统计汇总—总记录数/到场/缺勤/迟到/早退/请假/出勤率;列头使用 next-intl getTranslations 国际化)",
"deps": [
"shared.lib.excel.exportToExcel",
"data-access.getAttendanceRecords",
"data-access.getAttendanceStats",
"next-intl/server.getTranslations"
],
"usedBy": [
"app/api/export/route.ts"
]
}
]
}
@@ -12768,7 +12996,7 @@
{
"name": "ProctoringDashboard",
"file": "components/proctoring-dashboard.tsx",
"purpose": "教师监考面板实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制)"
"purpose": "教师监考面板实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制;✅ P1-3 已修复:用类型守卫函数 isProctoringEventType + toProctoringEventTypes 替代 Object.keys(...) as ProctoringEventType[] 断言"
},
{
"name": "AntiCheatMonitor",
@@ -12778,7 +13006,7 @@
{
"name": "ExamModeConfig",
"file": "components/exam-mode-config.tsx",
"purpose": "考试模式配置(react-hook-form Controller作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项)"
"purpose": "考试模式配置(✅ P1-2 已修复:已集成到 exam-form.tsxuseFormContext 替代 Control prop 避免 Control<T> 不变型问题,移除全部 10 处 as 类型断言;作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项)"
}
]
}
@@ -13162,7 +13390,7 @@
"name": "DiagnosticReport",
"type": "interface",
"file": "types.ts",
"definition": "{ id, studentId, generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }",
"definition": "{ id, studentId, classId(v4-P1-4 新增: 班级报告关联班级 ID个人报告为 null), generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }",
"usedBy": [
"data-access-reports",
"types.DiagnosticReportWithDetails"
@@ -13616,6 +13844,35 @@
"usedBy": [
"actions.dropCourseAction"
]
},
{
"name": "checkScheduleConflict",
"file": "data-access-operations.ts",
"signature": "(tx: Tx, studentId: string, courseId: string) => Promise<boolean>",
"purpose": "查询学生已选课程时间段,与新课程时间段逐一调用 isScheduleConflict 判断冲突P2-9 新增选课时间冲突检测selectCourse 调用)",
"deps": [
"shared.db",
"shared.db.schema.courseSelections",
"shared.db.schema.electiveCourses",
"lib.isScheduleConflict"
],
"usedBy": [
"data-access-operations.selectCourse"
]
},
{
"name": "checkCreditLimit",
"file": "data-access-operations.ts",
"signature": "(tx: Tx, studentId: string, courseId: string) => Promise<{ exceeded: boolean; current: number; max: number }>",
"purpose": "查询学生本学期已选课程学分总和,加上新课程学分后判断是否超过 MAX_CREDIT_PER_TERM(10)P2-10 新增学分上限校验selectCourse 调用)",
"deps": [
"shared.db",
"shared.db.schema.courseSelections",
"shared.db.schema.electiveCourses"
],
"usedBy": [
"data-access-operations.selectCourse"
]
}
],
"types": [
@@ -13781,6 +14038,129 @@
"actions.dropCourseAction",
"shared/components/ui/*"
]
},
{
"name": "ElectivePageLayout",
"file": "components/elective-page-layout.tsx",
"purpose": "选课页面布局骨架P2-4 新增:抽取 admin/teacher 选课页重复的 header/children 布局结构,统一 spacing",
"deps": [
"shared/lib/utils.cn"
],
"usedBy": [
"admin/elective/page.tsx",
"teacher/elective/page.tsx"
]
}
],
"resolvers": [
{
"name": "CourseDisplayResolver",
"type": "interface",
"file": "resolvers.ts",
"definition": "{ getUserNamesByIds(ids: string[]): Promise<Map<string,{name:string|null}>>; getSubjectOptions(): Promise<Array<{id,name}>>; getGradeOptions(): Promise<Array<{id,name}>> }",
"purpose": "课程显示名解析接口P2-8 新增:抽象跨模块依赖 users/school data-access便于测试注入 mock",
"usedBy": [
"data-access.resolveCourseDisplayNames",
"data-access-selections.resolveStudentDisplayNames"
]
},
{
"name": "StudentGradeResolver",
"type": "interface",
"file": "resolvers.ts",
"definition": "{ getStudentActiveGradeId(studentId: string): Promise<string|null> }",
"purpose": "学生年级解析接口P2-8 新增:抽象跨模块依赖 classes data-access便于测试注入 mock",
"usedBy": [
"data-access-selections.getStudentGradeId"
]
},
{
"name": "getCourseDisplayResolver",
"signature": "() => CourseDisplayResolver",
"file": "resolvers.ts",
"purpose": "获取当前课程显示名解析器(默认委托 users/school data-access测试可通过 setCourseDisplayResolver 注入 mock",
"usedBy": [
"data-access.resolveCourseDisplayNames",
"data-access-selections.resolveStudentDisplayNames"
]
},
{
"name": "getStudentGradeResolver",
"signature": "() => StudentGradeResolver",
"file": "resolvers.ts",
"purpose": "获取当前学生年级解析器(默认委托 classes data-access测试可通过 setStudentGradeResolver 注入 mock",
"usedBy": [
"data-access-selections.getStudentGradeId"
]
},
{
"name": "setCourseDisplayResolver",
"signature": "(resolver: CourseDisplayResolver) => void",
"file": "resolvers.ts",
"purpose": "注入自定义课程显示名解析器(仅测试用,生产代码勿调)"
},
{
"name": "setStudentGradeResolver",
"signature": "(resolver: StudentGradeResolver) => void",
"file": "resolvers.ts",
"purpose": "注入自定义学生年级解析器(仅测试用,生产代码勿调)"
},
{
"name": "resetResolvers",
"signature": "() => void",
"file": "resolvers.ts",
"purpose": "重置解析器为默认实现(测试 afterEach 清理用)"
}
],
"lib": [
{
"name": "parseSchedule",
"signature": "(schedule: string) => { day: number; startMinutes: number; endMinutes: number } | null",
"file": "data-access-operations.ts",
"purpose": "纯函数:解析时间段字符串(支持 '周一 14:00-15:30' / 'Mon 14:00-15:30' 两种格式),返回星期 0-6 + 起止分钟数;无法解析返回 nullP2-9 新增)",
"usedBy": [
"data-access-operations.isScheduleConflict",
"data-access-operations.checkScheduleConflict"
]
},
{
"name": "isScheduleConflict",
"signature": "(a: string, b: string) => boolean",
"file": "data-access-operations.ts",
"purpose": "纯函数:判断两个时间段是否冲突(同一天且时间区间重叠);任一无法解析返回 falseP2-9 新增,可独立单测)",
"usedBy": [
"data-access-operations.checkScheduleConflict"
]
}
],
"importExport": [
{
"name": "exportElectiveCoursesToExcel",
"signature": "(params: { status?: string; teacherId?: string }) => Promise<Buffer>",
"file": "export.ts",
"purpose": "导出选修课程列表到 ExcelP2-11 新增:课程名/学科/教师/年级/容量/已选/状态/模式/学分/时间段;列头使用 next-intl getTranslations 国际化)",
"deps": [
"shared.lib.excel.exportToExcel",
"data-access.getElectiveCourses",
"next-intl/server.getTranslations"
],
"usedBy": [
"app/api/export/route.ts"
]
},
{
"name": "exportCourseSelectionsToExcel",
"signature": "(params: { courseId: string }) => Promise<Buffer>",
"file": "export.ts",
"purpose": "导出课程选课名单到 ExcelP2-11 新增:学生名/状态/优先级/选课时间/录取时间;列头使用 next-intl getTranslations 国际化)",
"deps": [
"shared.lib.excel.exportToExcel",
"data-access-selections.getCourseSelections",
"next-intl/server.getTranslations"
],
"usedBy": [
"app/api/export/route.ts"
]
}
]
}
@@ -14833,7 +15213,7 @@
},
"messageNotifications": {
"owner": "notifications",
"description": "消息通知"
"description": "消息通知(type/title/content/link/isRead/priority/isArchived/createdAtV2-P2-13b 新增 priority 和 isArchived 字段)"
},
"notificationLogs": {
"owner": "notifications",
@@ -14911,7 +15291,7 @@
},
"learningDiagnosticReports": {
"owner": "diagnostic",
"description": "学情诊断报告(individual/class/grade)"
"description": "学情诊断报告(individual/class/gradev4-P1-4 新增 classId 字段关联班级)"
}
}
},
@@ -15194,7 +15574,8 @@
"data-access.getClassTeacherById"
],
"school": [
"data-access.getSubjectOptions"
"data-access.getSubjectOptions",
"data-access.getSubjectNameMapByIds"
],
"users": [
"data-access.getUserWithRole",
@@ -15400,7 +15781,7 @@
"data-access.deleteFileAttachment"
]
},
"note": "组件层通过 SettingsService 接口注入解耦,不直接 import messaging/actions页面层 app/(dashboard)/settings/page.tsx 负责注入 users/actions + messaging/actions 实现。P0-3/P2-8/P2-9/P2-10/P2-11 已修复AdminSettingsView 接入真实数据层system_settings 表、头像上传、2FA/登录历史、通知测试按钮、语言切换集成。v2 已增强:2FA 开关改为禁用状态避免虚假安全感、通知测试接入真实发送、头像上传清理旧文件、会话远程登出、AdminSettingsView/通知偏好表单 dirty 检测、currentDeviceLabel 标记当前会话、文件名长度校验、2FA 查询 N+1 优化、新增 30 个单元测试。"
"note": "组件层通过 SettingsService 接口注入解耦,不直接 import messaging/actions页面层 app/(dashboard)/settings/page.tsx 负责注入 users/actions + messaging/actions 实现。P0-3/P2-8/P2-9/P2-10/P2-11 已修复AdminSettingsView 接入真实数据层system_settings 表、头像上传、2FA/登录历史、通知测试按钮、语言切换集成。v2 已增强通知测试接入真实发送、头像上传清理旧文件、会话远程登出、AdminSettingsView/通知偏好表单 dirty 检测、currentDeviceLabel 标记当前会话、文件名长度校验、2FA 查询 N+1 优化。v3 已增强:完整 TOTP 2FA 流程otplib v13 + qrcode— 启用(QR码扫描+验证码校验+10个备份码 bcrypt 哈希存储)/关闭(需 TOTP 或备份码确认)/重新生成备份码;登录流程接入 2FApreflightTwoFactorAction 预检 + auth.ts authorize 校验);新增 29 个 TOTP 单元测试(总计 59 个。依赖新增otplib、qrcode。"
},
"users": {
"dependsOn": [
@@ -16314,7 +16695,19 @@
"from": "elective",
"to": "school",
"type": "data-access",
"description": "关联年级(grades)/科目(subjects)"
"description": "关联年级(grades)/科目(subjects)P2-8通过 resolvers.ts CourseDisplayResolver 接口抽象,默认实现委托 school data-access"
},
{
"from": "elective",
"to": "users",
"type": "data-access",
"description": "查询教师/学生显示名P2-8通过 resolvers.ts CourseDisplayResolver 接口抽象,默认实现委托 users data-access.getUserNamesByIds"
},
{
"from": "elective",
"to": "classes",
"type": "data-access",
"description": "查询学生所在年级P2-8通过 resolvers.ts StudentGradeResolver 接口抽象,默认实现委托 classes data-access.getStudentActiveGradeId"
},
{
"from": "attendance",
@@ -17908,11 +18301,11 @@
"methods": [
"POST"
],
"handler": "Excel 导出grades/users/attendance",
"handler": "Excel 导出grades/users/attendance/electiveCourses/courseSelections/audit/login/dataChange",
"auth": "requireAuth",
"module": "shared.lib.excel + users/grades",
"module": "shared.lib.excel + users/grades/attendance/elective/audit",
"validation": "JSON body { type, params }",
"description": "按 type 分发exportGradeRecordsToExcel/exportUsersToExcel返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流"
"description": "按 type 分发grades→exportGradeRecordsToExcel, users→exportUsersToExcel, attendance→exportAttendanceRecordsToExcel(需 ATTENDANCE_READ), electiveCourses→exportElectiveCoursesToExcel(需 ELECTIVE_READ), courseSelections→exportCourseSelectionsToExcel(需 ELECTIVE_MANAGE), audit/login/dataChange→audit/actions 导出(需 AUDIT_LOG_READ)。返回 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 二进制流"
},
"/api/import": {
"methods": [