feat(attendance,elective): 实现所有 P2 长期改进项

P2 修复(来自审计报告):
- 2.4.4: Server Action 错误消息 i18n 化(attendance/elective 全部 Action)
- 2.5.3: 抽取 AttendancePageLayout 组件复用(admin/teacher 页面)
- 2.5.4: 抽取 ElectivePageLayout 组件复用(admin/teacher 列表页)
- 2.6.3: 考勤月历键盘导航(tabIndex + 方向键 + Home/End + role=grid)
- 2.8.2: getStudentAttendanceSummary 分页优化(SQL 聚合统计 + LIMIT 分页)
- 2.8.3: resolveCourseDisplayNames 缓存优化(React cache 去重)
- 2.1.4: elective data-access 跨模块依赖接口抽象(resolvers.ts 可注入)

P2 建议项:
- 选课时间冲突检测(parseSchedule + isScheduleConflict 纯函数 + checkScheduleConflict)
- 学分上限校验(MAX_CREDIT_PER_TERM + checkCreditLimit)
- 考勤/选课数据导出 Excel(export.ts + API 路由扩展)

新增文件:
- src/modules/attendance/components/attendance-page-layout.tsx
- src/modules/elective/components/elective-page-layout.tsx
- src/modules/elective/resolvers.ts
- src/modules/attendance/export.ts
- src/modules/elective/export.ts

校验:
- npm run lint 通过(exit 0)
- npx tsc --noEmit attendance/elective/parent 相关零错误
This commit is contained in:
SpecialX
2026-06-23 09:02:41 +08:00
parent c766951374
commit e2e0487a3b
50 changed files with 1514 additions and 411 deletions

View File

@@ -7031,19 +7031,121 @@
]
},
{
"name": "toggleTwoFactorAction",
"name": "setupTwoFactorAction",
"file": "actions-security.ts",
"permission": "USER_PROFILE_UPDATE",
"signature": "(enabled: boolean) => Promise<ActionState<TwoFactorStatus>>",
"purpose": "启用/禁用 2FAP2-9 新增占位实现v2 已禁用开关,显示'即将推出'提示,避免虚假安全感",
"signature": "() => Promise<ActionState<TwoFactorSetupData>>",
"purpose": "2FA 启用流程第一步:生成 TOTP 密钥 + QR 码 Data URLv3 新增:完整 TOTP 实现,替代 v2 的占位开关",
"deps": [
"requirePermission",
"data-access-system-settings.upsertSystemSetting"
"users/data-access.getUserProfile",
"data-access-two-factor.getTwoFactorEnabled",
"data-access-two-factor.setTotpSecret",
"lib/totp.generateTotpSecret",
"lib/totp.buildOtpAuthUrl",
"lib/totp.generateQrCodeDataUrl"
],
"usedBy": [
"components/security-center-card.tsx"
]
},
{
"name": "verifyTwoFactorAction",
"file": "actions-security.ts",
"permission": "USER_PROFILE_UPDATE",
"signature": "(token: string) => Promise<ActionState<{ backupCodes: string[]; status: TwoFactorStatus }>>",
"purpose": "2FA 启用流程第二步:校验一次性码 + 生成 10 个备份码bcrypt 哈希存储)+ 启用 2FAv3 新增)",
"deps": [
"requirePermission",
"data-access-two-factor.getTotpSecret",
"data-access-two-factor.setBackupCodesHashed",
"data-access-two-factor.setTwoFactorEnabled",
"data-access-two-factor.setTwoFactorEnabledAt",
"lib/totp.verifyTotpCode",
"lib/totp.generateBackupCodes",
"lib/totp.hashBackupCodes"
],
"usedBy": [
"components/security-center-card.tsx"
]
},
{
"name": "disableTwoFactorAction",
"file": "actions-security.ts",
"permission": "USER_PROFILE_UPDATE",
"signature": "(token: string) => Promise<ActionState<TwoFactorStatus>>",
"purpose": "关闭 2FA需提供有效 TOTP 码或备份码确认身份清除密钥和备份码v3 新增)",
"deps": [
"requirePermission",
"data-access-two-factor.getTwoFactorEnabled",
"data-access-two-factor.getTotpSecret",
"data-access-two-factor.getBackupCodesHashed",
"data-access-two-factor.setTwoFactorEnabled",
"data-access-two-factor.setTwoFactorEnabledAt",
"data-access-two-factor.deleteTotpSecret",
"data-access-two-factor.deleteBackupCodes",
"data-access-two-factor.setBackupCodesHashed",
"lib/totp.verifyTotpCode",
"lib/totp.verifyBackupCode",
"lib/totp.consumeBackupCode"
],
"usedBy": [
"components/security-center-card.tsx"
]
},
{
"name": "regenerateBackupCodesAction",
"file": "actions-security.ts",
"permission": "USER_PROFILE_UPDATE",
"signature": "(token: string) => Promise<ActionState<{ backupCodes: string[]; status: TwoFactorStatus }>>",
"purpose": "重新生成备份码:需 TOTP 码确认身份使旧备份码失效v3 新增)",
"deps": [
"requirePermission",
"data-access-two-factor.getTwoFactorEnabled",
"data-access-two-factor.getTotpSecret",
"data-access-two-factor.setBackupCodesHashed",
"lib/totp.verifyTotpCode",
"lib/totp.generateBackupCodes",
"lib/totp.hashBackupCodes"
],
"usedBy": [
"components/security-center-card.tsx"
]
},
{
"name": "preflightTwoFactorAction",
"file": "actions-security.ts",
"permission": "(public, login 前预检)",
"signature": "(email: string) => Promise<{ required: boolean }>",
"purpose": "登录预检:根据邮箱查询用户是否启用 2FA登录表单据此展示 2FA 输入框v3 新增;不验证密码,防邮箱枚举)",
"deps": [
"shared.db",
"shared.db.schema.users",
"data-access-two-factor.getTwoFactorEnabled"
],
"usedBy": [
"modules/auth/components/login-form.tsx"
]
},
{
"name": "verifyTwoFactorForLogin",
"file": "actions-security.ts",
"permission": "(internal, 供 auth.ts 调用)",
"signature": "(params: { userId: string; token?: string }) => Promise<{ required: boolean; valid: boolean }>",
"purpose": "登录时 2FA 校验:检查用户是否启用 2FA 并校验 TOTP 码或备份码(消耗备份码);由 auth.ts authorize 回调调用v3 新增)",
"deps": [
"data-access-two-factor.getTwoFactorEnabled",
"data-access-two-factor.getTotpSecret",
"data-access-two-factor.getBackupCodesHashed",
"data-access-two-factor.setBackupCodesHashed",
"lib/totp.verifyTotpCode",
"lib/totp.verifyBackupCode",
"lib/totp.consumeBackupCode"
],
"usedBy": [
"auth.ts"
]
},
{
"name": "revokeAllOtherSessionsAction",
"file": "actions-security.ts",
@@ -9657,7 +9759,7 @@
{
"name": "GradeDistributionChart",
"file": "components/grade-distribution-chart.tsx",
"purpose": "分数分布柱状图recharts BarChart彩色区间 90-100/80-89/70-79/60-69/<60",
"purpose": "分数分布柱状图recharts BarChart彩色区间 90-100/80-89/70-79/60-69/<60v4-P3-4 改进:每个分数段使用不同 SVG pattern + 颜色双重编码,色盲友好",
"deps": [
"recharts",
"shared/components/ui/chart"
@@ -10864,7 +10966,7 @@
{
"name": "UnreadMessageBadge",
"file": "components/unread-message-badge.tsx",
"purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 60 秒轮询 getUnreadMessageCountActionV2-P2-1 优化:轮询间隔提取为 POLL_INTERVAL_MS 常量)"
"purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 30 秒轮询 getUnreadMessageCountActionV2-P2-1 优化:轮询间隔提取为 POLL_INTERVAL_MS 常量V2-P3 优化:间隔从 60s 缩短为 30s 与通知组件保持一致"
}
],
"hooks": [
@@ -10922,7 +11024,7 @@
"data-access.getNotifications"
],
"usedBy": [
"notification-dropdown.tsx",
"hooks/use-notification-stream.ts",
"notification-list.tsx"
]
},
@@ -10936,7 +11038,7 @@
"data-access.getUnreadNotificationCount"
],
"usedBy": [
"notification-dropdown.tsx"
"hooks/use-notification-stream.ts"
]
},
{
@@ -12629,22 +12731,21 @@
"name": "getStudentMastery",
"signature": "(studentId: string) => Promise<MasteryWithKnowledgePoint[]>",
"file": "data-access.ts",
"purpose": "获取学生在所有知识点的掌握度(含知识点名称,按掌握度降序)",
"purpose": "获取学生在所有知识点的掌握度(含知识点名称,按掌握度降序)。P3-19 修复:移除 export改为模块内部函数",
"deps": [
"shared.db",
"shared.db.schema.knowledgePointMastery",
"shared.db.schema.knowledgePoints"
],
"usedBy": [
"data-access.getStudentMasterySummary",
"teacher/diagnostic/student/[studentId]/page.tsx"
"data-access.getStudentMasterySummary"
]
},
{
"name": "getStudentMasterySummary",
"signature": "(studentId: string) => Promise<StudentMasterySummary | null>",
"file": "data-access.ts",
"purpose": "获取学生掌握度摘要平均掌握度、强项≥80%、弱项<60%",
"purpose": "获取学生掌握度摘要平均掌握度、强项≥80%、弱项<80%[P3-16修复]。P3-18 修复getUserNamesByIds 与 getStudentMastery 并行查询",
"deps": [
"shared.db",
"shared.db.schema.users",
@@ -12710,11 +12811,12 @@
"name": "generateDiagnosticReport",
"signature": "(studentId: string, period: string, generatedBy: string) => Promise<string>",
"file": "data-access-reports.ts",
"purpose": "生成个人诊断报告(计算 overallScore、强项/弱项列表、复习建议status=draft",
"purpose": "生成个人诊断报告(计算 overallScore、强项/弱项列表、复习建议status=draft。P3-27 修复:使用 DiagnosticReportError 结构化错误码P3-1 修复toNumber 从 grades 模块导入",
"deps": [
"shared.db",
"shared.db.schema.learningDiagnosticReports",
"data-access.getStudentMasterySummary",
"grades.lib.grade-utils.toNumber",
"@paralleldrive/cuid2"
],
"usedBy": [
@@ -12725,7 +12827,7 @@
"name": "generateClassDiagnosticReport",
"signature": "(classId: string, period: string, generatedBy: string) => Promise<string>",
"file": "data-access-reports.ts",
"purpose": "生成班级诊断报告聚合班级掌握度识别薄弱知识点status=draftstudentId 存生成者 ID",
"purpose": "生成班级诊断报告聚合班级掌握度识别薄弱知识点status=draftstudentId 存生成者 ID。P3-27 修复:使用 DiagnosticReportError 结构化错误码",
"deps": [
"shared.db",
"shared.db.schema.learningDiagnosticReports",
@@ -12738,19 +12840,20 @@
},
{
"name": "getDiagnosticReports",
"signature": "(filters: DiagnosticReportQueryParams) => Promise<DiagnosticReportWithDetails[]>",
"signature": "(filters: DiagnosticReportQueryParams, scope?: DataScope) => Promise<DiagnosticReportListResult>",
"file": "data-access-reports.ts",
"purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名v3 修复conditions 显式标注 SQL[] 类型,移除 round2 死代码",
"purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名。P3-15 修复:支持分页 limit/offset返回 { reports, total } 结构Promise.all 并行查询总数和数据",
"deps": [
"shared.db",
"shared.db.schema.learningDiagnosticReports",
"shared.db.schema.users"
"shared.db.schema.users",
"grades.lib.grade-utils.toNumber"
],
"usedBy": [
"actions.getDiagnosticReportsAction",
"teacher/diagnostic/page.tsx",
"teacher/diagnostic/student/[studentId]/page.tsx",
"student/diagnostic/page.tsx"
"student/diagnostic/page.tsx",
"parent/diagnostic/page.tsx"
]
},
{
@@ -12987,7 +13090,7 @@
"name": "StudentMasterySummary",
"type": "interface",
"file": "types.ts",
"definition": "{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<60), allMastery }",
"definition": "{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<80)[P3-16修复消除60-79盲区], allMastery }",
"usedBy": [
"data-access.getStudentMasterySummary",
"data-access-reports.generateDiagnosticReport",
@@ -13042,12 +13145,21 @@
"name": "DiagnosticReportQueryParams",
"type": "interface",
"file": "types.ts",
"definition": "{ studentId?, reportType?, status?, period? }",
"definition": "{ studentId?, reportType?, status?, period?, limit?(P3-15), offset?(P3-15) }",
"usedBy": [
"data-access-reports.getDiagnosticReports",
"actions.getDiagnosticReportsAction"
]
},
{
"name": "DiagnosticReportListResult",
"type": "interface",
"file": "types.ts",
"definition": "{ reports: DiagnosticReportWithDetails[], total: number }P3-15 修复:分页查询结果)",
"usedBy": [
"data-access-reports.getDiagnosticReports"
]
},
{
"name": "MasteryRadarPoint",
"type": "interface",
@@ -13075,7 +13187,7 @@
{
"name": "StudentDiagnosticView",
"file": "components/student-diagnostic-view.tsx",
"purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示v3-P2 新增practiceHrefBase propnull 时隐藏练习按钮)",
"purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示v3-P2 新增practiceHrefBase propnull 时隐藏练习按钮P3-22 改进:练习按钮添加 aria-label 含知识点名",
"props": "{ studentId, summary, classAverage?, reports?, practiceHrefBase?: string | null }",
"deps": [
"usePermission",
@@ -16131,6 +16243,12 @@
"type": "data-access",
"description": "关联考试提交(examSubmissions/submissionAnswers)"
},
{
"from": "diagnostic",
"to": "grades",
"type": "lib-import",
"description": "复用 toNumber 工具函数(P3-1 修复:从 grades/lib/grade-utils 导入)"
},
{
"from": "elective",
"to": "school",
@@ -17467,7 +17585,7 @@
"grades/data-access-analytics.getClassAverageTrend"
],
"permission": "grade_record:read",
"description": "家长成绩视图(按 DataScope.children 过滤v4 新增 ParentExportButton 占位v3-P2 更新:为每个子女并行查询 getClassAverageTrend渲染 GradeTrendCard"
"description": "家长成绩视图(按 DataScope.children 过滤v4 新增 ParentExportButton 占位v3-P2 更新:为每个子女并行查询 getClassAverageTrend渲染 GradeTrendCardv3-P3-4 更新GradeTrendCard 新增日期范围选择器,通过 nuqs trendRange URL 参数持久化"
},
"/parent/diagnostic": {
"component": "子女学情诊断",
@@ -17642,6 +17760,30 @@
],
"studentMode": "强制苏格拉底式引导系统提示"
},
"/api/notifications/stream": {
"methods": [
"GET"
],
"handler": "通知实时推送 SSE 端点ReadableStream + setInterval 定时推送)",
"auth": "MESSAGE_READ",
"validation": "requirePermission 权限校验",
"protocol": "Server-Sent Events",
"events": [
"update — 未读数 + 最新通知列表(连接建立时立即推送,之后每 15 秒推送)",
"error — 权限拒绝或内部错误",
"[DONE] — 连接超时5 分钟)自动关闭"
],
"pushStrategy": "连接建立立即推送 + 15 秒间隔定时推送 + 5 分钟超时自动关闭",
"module": "notifications",
"deps": [
"requirePermission",
"data-access.getUnreadNotificationCount",
"data-access.getNotifications"
],
"usedBy": [
"hooks/use-notification-stream.ts"
]
},
"/api/onboarding/complete": {
"methods": [
"POST"