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:
@@ -7031,19 +7031,121 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "toggleTwoFactorAction",
|
||||
"name": "setupTwoFactorAction",
|
||||
"file": "actions-security.ts",
|
||||
"permission": "USER_PROFILE_UPDATE",
|
||||
"signature": "(enabled: boolean) => Promise<ActionState<TwoFactorStatus>>",
|
||||
"purpose": "启用/禁用 2FA(P2-9 新增:占位实现;v2 已禁用开关,显示'即将推出'提示,避免虚假安全感)",
|
||||
"signature": "() => Promise<ActionState<TwoFactorSetupData>>",
|
||||
"purpose": "2FA 启用流程第一步:生成 TOTP 密钥 + QR 码 Data URL(v3 新增:完整 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 哈希存储)+ 启用 2FA(v3 新增)",
|
||||
"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/<60;v4-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 秒轮询 getUnreadMessageCountAction;V2-P2-1 优化:轮询间隔提取为 POLL_INTERVAL_MS 常量)"
|
||||
"purpose": "未读消息计数徽章(侧边栏 Messages 导航项旁显示,每 30 秒轮询 getUnreadMessageCountAction;V2-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=draft,studentId 存生成者 ID)",
|
||||
"purpose": "生成班级诊断报告(聚合班级掌握度,识别薄弱知识点,status=draft,studentId 存生成者 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 prop,null 时隐藏练习按钮)",
|
||||
"purpose": "学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示;v3-P2 新增:practiceHrefBase prop,null 时隐藏练习按钮;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,渲染 GradeTrendCard;v3-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"
|
||||
|
||||
Reference in New Issue
Block a user