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:
@@ -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/grade;status: draft/published/archived)"
|
||||
"description": "学情诊断报告(reportType: individual/class/grade;status: draft/published/archived;v4-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 URL(base64 PNG)(v3 新增)"
|
||||
},
|
||||
{
|
||||
"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": "校验备份码,返回匹配索引或 -1(v3 新增)"
|
||||
},
|
||||
{
|
||||
"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 增强:批量录入后通知所有相关学生和家长(调用 notifyGradeEntered);v3-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": "导出成绩到 Excel(detail=成绩明细+统计汇总,class=班级多科目横向对比总表),返回 base64 buffer",
|
||||
"purpose": "导出成绩到 Excel(detail=成绩明细+统计汇总,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": "导出考勤记录到 Excel(P2-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.tsx,useFormContext 替代 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 + 起止分钟数;无法解析返回 null(P2-9 新增)",
|
||||
"usedBy": [
|
||||
"data-access-operations.isScheduleConflict",
|
||||
"data-access-operations.checkScheduleConflict"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "isScheduleConflict",
|
||||
"signature": "(a: string, b: string) => boolean",
|
||||
"file": "data-access-operations.ts",
|
||||
"purpose": "纯函数:判断两个时间段是否冲突(同一天且时间区间重叠);任一无法解析返回 false(P2-9 新增,可独立单测)",
|
||||
"usedBy": [
|
||||
"data-access-operations.checkScheduleConflict"
|
||||
]
|
||||
}
|
||||
],
|
||||
"importExport": [
|
||||
{
|
||||
"name": "exportElectiveCoursesToExcel",
|
||||
"signature": "(params: { status?: string; teacherId?: string }) => Promise<Buffer>",
|
||||
"file": "export.ts",
|
||||
"purpose": "导出选修课程列表到 Excel(P2-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": "导出课程选课名单到 Excel(P2-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/createdAt;V2-P2-13b 新增 priority 和 isArchived 字段)"
|
||||
},
|
||||
"notificationLogs": {
|
||||
"owner": "notifications",
|
||||
@@ -14911,7 +15291,7 @@
|
||||
},
|
||||
"learningDiagnosticReports": {
|
||||
"owner": "diagnostic",
|
||||
"description": "学情诊断报告(individual/class/grade)"
|
||||
"description": "学情诊断报告(individual/class/grade;v4-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 或备份码确认)/重新生成备份码;登录流程接入 2FA(preflightTwoFactorAction 预检 + 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": [
|
||||
|
||||
Reference in New Issue
Block a user