docs: update architecture docs, audit reports, and bug tracking
- Update architecture impact map, data, feature checklist, gap audit - Add audit reports for dashboard, exam-homework, grades-diagnostic, settings-profile, textbooks - Update bug reports (admin, teacher, lesson-preparation, others, shared) - Update coding standards, DR plan, design docs, and README
This commit is contained in:
@@ -780,13 +780,16 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ v4-P1-7 改进(2026-06-23):新增 `ScoreCell` 组件(components/score-cell.tsx),根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名避免动态拼接
|
||||
- ✅ v4-P1-10 改进(2026-06-23):`grade-record-list.tsx` 使用 `ScoreCell` 替代纯文本分数展示 + 表格 `overflow-x-auto` 水平滚动
|
||||
- ✅ v4-P1-12 改进(2026-06-23):`exportGradesAction` 新增可选 `studentId` 参数,支持按学生导出(家长视角);新增 `exportStudentGradeRecordsToExcel` 导出函数(仅含成绩明细 + 个人统计,不含班级数据);parent/grades/page.tsx 传 studentId 到 ParentExportButton;ParentExportButton 接入 exportGradesAction
|
||||
- ✅ v3-P3-1 改进(2026-06-23):`batch-grade-entry.tsx` 新增"下载模板"按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8),教师可下载填好后粘贴到录入表格
|
||||
- ✅ v3-P3-2 改进(2026-06-23):`grade-record-list.tsx` 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;新增 `bulkDeleteGradeRecords` data-access 函数(使用 inArray 一次性删除避免 N+1)+ `bulkDeleteGradeRecordsAction` Server Action(限制单次最多 500 条)
|
||||
- ✅ v4-P3-2 改进(2026-06-23):`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `actions.ts` | 631+ | 18 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数) |
|
||||
| `actions.ts` | 670+ | 19 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除) |
|
||||
| `actions-analytics.ts` | 170 | 5 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验) |
|
||||
| `data-access.ts` | 428+ | 成绩 CRUD + 统计 + 草稿(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口) |
|
||||
| `data-access.ts` | 450+ | 成绩 CRUD + 统计 + 草稿(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除) |
|
||||
| `data-access-analytics.ts` | 200+ | 趋势/对比分析(P3 修复:getClassComparison 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) |
|
||||
| `data-access-ranking.ts` | 83 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) |
|
||||
| `stats-service.ts` | 285 | 统计计算纯函数(P1-1 新增:8 个纯函数 + 2 个常量 + 2 个接口;P3-10:createDefaultBuckets 改为内部函数;P3-24:buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) |
|
||||
@@ -799,13 +802,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) |
|
||||
| `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 |
|
||||
| `components/grade-trend-card.tsx` | 69 | 趋势卡片(v2-P2-9 修复:a11y;v2-P1-4:i18n;P3 修复:NaN 日期检查 + fullScore > 0 守卫) |
|
||||
| `components/grade-record-list.tsx` | 125 | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto) |
|
||||
| `components/grade-record-list.tsx` | 200+ | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto;v3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框,接入 bulkDeleteGradeRecordsAction) |
|
||||
| `components/grade-distribution-chart.tsx` | 100 | 分数分布图(v2-P1-4:i18n) |
|
||||
| `components/subject-comparison-chart.tsx` | 62 | 科目对比图(v2-P1-4:i18n) |
|
||||
| `components/class-comparison-chart.tsx` | 58 | 班级对比图(v2-P1-4:i18n) |
|
||||
| `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) |
|
||||
| `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) |
|
||||
| `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall 包装提交) |
|
||||
| `components/batch-grade-entry.tsx` | 435+ | 批量录入(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall + localStorage 安全检查 + 区分未录入与录入 0;v3-P2 新增:接入服务端草稿 saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction) |
|
||||
| `components/batch-grade-entry.tsx` | 500+ | 批量录入(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall + localStorage 安全检查 + 区分未录入与录入 0;v3-P2 新增:接入服务端草稿 saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v3-P3-1 新增:下载 CSV 录入模板按钮含学生姓名列表;v4-P3-2 新增:可折叠新手引导提示框,localStorage 记住关闭状态) |
|
||||
| `components/grade-filters.tsx` | 76 | 过滤器(v2-P1-4:i18n) |
|
||||
| `components/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) |
|
||||
| `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作) |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"generatedAt": "2026-06-17",
|
||||
"formatVersion": "1.1",
|
||||
"rule": "每次文件修改后须同步更新本文件",
|
||||
"lastUpdate": "V4 AI 模块深度增强(ai-module-v2 对标 Khanmigo/Duolingo Max/Squirrel AI/Century Tech):(V4-1) SSE 流式响应:shared/lib/ai/client.ts 新增 createAiChatCompletionStream(AsyncGenerator 逐 token 产出),新增 API 路由 /api/ai/chat/stream(POST,requirePermission(AI_CHAT)+checkDailyLimit+filterUserInput+学生苏格拉底系统提示+ReadableStream 流式输出+filterAiOutput+incrementDailyUsage+trackEvent),新增 hook useAiChatStream(fetch+ReadableStream reader+SSE 解析+AbortController 停止生成+localStorage 持久化最近 20 条)。(V4-2) Markdown 渲染:新增组件 AiMarkdownRenderer(react-markdown+remark-gfm,代码块/表格/列表+hover 复制按钮+memo 优化),AiChatPanel 全面重写(流式渲染+停止按钮+清空确认+建议提示词空状态+aria-live+错误展示)。(V4-3) 全局 AI 助手:新增组件 AiAssistantWidget(fixed 浮动按钮+Sheet 侧抽屉+usePathname 路由推断上下文+inferContextFromPath 映射 7 类场景系统提示:教师批改/备课/考试/学生错题本/学生作业/家长/管理员+useAiClientOptional 无 Provider 时隐藏+pulsing 绿色指示器),dashboard layout 全局注入 AiClientProvider+AiAssistantWidget。(V4-4) 内容安全:新增 services/content-safety.ts(filterUserInput 阻断暴力/自残/色情/毒品/黑客/PII 索取;filterAiOutput 输出二次过滤+学生场景阻断直接答案;checkDailyLimit 学生 50/教师 200/家长 30/管理员 500;incrementDailyUsage;getDailyLimit),COPPA/FERPA K12 合规。(V4-5) 多角色 AI 覆盖:types.ts 新增 ChildSummaryInput/Result、StudyPathInput/Result、AiUsageStats 类型 + AiService/AiClientService 接口扩展;schema.ts 新增 4 个 Zod schema;prompt-templates.ts 新增 CHILD_SUMMARY_SYSTEM_PROMPT(家庭教育顾问,家长友好语言)+ STUDY_PATH_SYSTEM_PROMPT(自适应学习路径设计师,3-7 步骤);ai-service.ts 新增 generateChildSummary(聚合成绩/出勤/错题本数据)+ recommendStudyPath(基于掌握度数据);actions.ts 新增 generateChildSummaryAction/recommendStudyPathAction(AI_CHAT 权限)+ getAiUsageStatsAction(AI_CONFIGURE 权限);新增组件 AiChildSummary(家长端:整体评估 Markdown+优势绿勾+改进橙警+家庭辅导建议+下一步徽章)、AiUsageDashboard(管理员端:4 统计卡片+按能力进度条+按角色徽章+Top 用户列表+最近活动日志)、AiStudyPath(学生端:当前等级横幅+学习路径步骤+连接线+状态图标+预估时间+激励消息)。(V4-6) i18n 修复与扩展:修复 AiGradingAssist/AiLessonContentGenerator/AiQuestionVariantGenerator 共 8 处错误 i18n 键引用(CardDescription 重复 title、label/placeholder/button 复用 generateContent、3 个变体类型标签全部显示「生成」);zh-CN/en ai.json 全面重写新增 chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts、grading.description/batch*、lessonPrep.description/additionalContext/insertContent、exam.variantType.*/targetDifficulty/addVariant、parent.*、admin.*、studyPath.*、widget.*、safety.* 等键。架构文档 004/005 同步更新。"
|
||||
"lastUpdate": "grades 模块 v3/v4 P3 长期问题修复(2026-06-23):(v3-P3-1) batch-grade-entry.tsx 新增下载模板按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8)。(v3-P3-2) grade-record-list.tsx 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;data-access.ts 新增 bulkDeleteGradeRecords(使用 inArray 一次性删除避免 N+1);actions.ts 新增 bulkDeleteGradeRecordsAction(GRADE_RECORD_MANAGE 权限 + 限制单次最多 500 条)。(v4-P3-2) batch-grade-entry.tsx 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态。i18n zh-CN/en grades.json 同步新增 batch.downloadTemplate/templateAriaLabel/templateStudentName/templateScore/templateRemark/templateFilename + batch.guide.* + list.bulkDelete/bulkDeleteConfirmation/bulkDeleteSelected/bulkDeleteSuccess/bulkDeleteFailed/selectAll/selectRow/clearSelection 键。--- 之前更新:V4 AI 模块深度增强(ai-module-v2 对标 Khanmigo/Duolingo Max/Squirrel AI/Century Tech):(V4-1) SSE 流式响应:shared/lib/ai/client.ts 新增 createAiChatCompletionStream(AsyncGenerator 逐 token 产出),新增 API 路由 /api/ai/chat/stream(POST,requirePermission(AI_CHAT)+checkDailyLimit+filterUserInput+学生苏格拉底系统提示+ReadableStream 流式输出+filterAiOutput+incrementDailyUsage+trackEvent),新增 hook useAiChatStream(fetch+ReadableStream reader+SSE 解析+AbortController 停止生成+localStorage 持久化最近 20 条)。(V4-2) Markdown 渲染:新增组件 AiMarkdownRenderer(react-markdown+remark-gfm,代码块/表格/列表+hover 复制按钮+memo 优化),AiChatPanel 全面重写(流式渲染+停止按钮+清空确认+建议提示词空状态+aria-live+错误展示)。(V4-3) 全局 AI 助手:新增组件 AiAssistantWidget(fixed 浮动按钮+Sheet 侧抽屉+usePathname 路由推断上下文+inferContextFromPath 映射 7 类场景系统提示:教师批改/备课/考试/学生错题本/学生作业/家长/管理员+useAiClientOptional 无 Provider 时隐藏+pulsing 绿色指示器),dashboard layout 全局注入 AiClientProvider+AiAssistantWidget。(V4-4) 内容安全:新增 services/content-safety.ts(filterUserInput 阻断暴力/自残/色情/毒品/黑客/PII 索取;filterAiOutput 输出二次过滤+学生场景阻断直接答案;checkDailyLimit 学生 50/教师 200/家长 30/管理员 500;incrementDailyUsage;getDailyLimit),COPPA/FERPA K12 合规。(V4-5) 多角色 AI 覆盖:types.ts 新增 ChildSummaryInput/Result、StudyPathInput/Result、AiUsageStats 类型 + AiService/AiClientService 接口扩展;schema.ts 新增 4 个 Zod schema;prompt-templates.ts 新增 CHILD_SUMMARY_SYSTEM_PROMPT(家庭教育顾问,家长友好语言)+ STUDY_PATH_SYSTEM_PROMPT(自适应学习路径设计师,3-7 步骤);ai-service.ts 新增 generateChildSummary(聚合成绩/出勤/错题本数据)+ recommendStudyPath(基于掌握度数据);actions.ts 新增 generateChildSummaryAction/recommendStudyPathAction(AI_CHAT 权限)+ getAiUsageStatsAction(AI_CONFIGURE 权限);新增组件 AiChildSummary(家长端:整体评估 Markdown+优势绿勾+改进橙警+家庭辅导建议+下一步徽章)、AiUsageDashboard(管理员端:4 统计卡片+按能力进度条+按角色徽章+Top 用户列表+最近活动日志)、AiStudyPath(学生端:当前等级横幅+学习路径步骤+连接线+状态图标+预估时间+激励消息)。(V4-6) i18n 修复与扩展:修复 AiGradingAssist/AiLessonContentGenerator/AiQuestionVariantGenerator 共 8 处错误 i18n 键引用(CardDescription 重复 title、label/placeholder/button 复用 generateContent、3 个变体类型标签全部显示「生成」);zh-CN/en ai.json 全面重写新增 chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts、grading.description/batch*、lessonPrep.description/additionalContext/insertContent、exam.variantType.*/targetDifficulty/addVariant、parent.*、admin.*、studyPath.*、widget.*、safety.* 等键。架构文档 004/005 同步更新。"
|
||||
},
|
||||
"architectureOverview": {
|
||||
"layers": [
|
||||
@@ -9038,6 +9038,18 @@
|
||||
"grades/actions.deleteGradeRecordAction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bulkDeleteGradeRecords",
|
||||
"signature": "(ids: string[]) => Promise<number>",
|
||||
"file": "data-access.ts",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.gradeRecords"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/actions.bulkDeleteGradeRecordsAction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getClassGradeStats",
|
||||
"signature": "(classId: string, subjectId?: string, examId?: string, scope?: DataScope, currentUserId?: string) => Promise<GradeStats | null>",
|
||||
@@ -9297,6 +9309,15 @@
|
||||
"grades/components/grade-record-list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bulkDeleteGradeRecordsAction",
|
||||
"signature": "(ids: string[]) => Promise<ActionState<number>>",
|
||||
"file": "actions.ts",
|
||||
"permission": "GRADE_RECORD_MANAGE",
|
||||
"usedBy": [
|
||||
"grades/components/grade-record-list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getGradeRecordsAction",
|
||||
"signature": "(params) => Promise<GradeRecordListItem[]>",
|
||||
@@ -10033,10 +10054,12 @@
|
||||
{
|
||||
"name": "ClassComparisonChart",
|
||||
"file": "components/class-comparison-chart.tsx",
|
||||
"purpose": "班级对比柱状图(recharts BarChart,均分/及格率/优秀率)",
|
||||
"purpose": "班级对比柱状图(recharts BarChart,均分/及格率/优秀率;v3-P3-5 新增:显著性分析区域,基于极差和样本量经验规则判断班级间差异是否显著,含可折叠详细分析展示最高/最低分班级)",
|
||||
"deps": [
|
||||
"recharts",
|
||||
"shared/components/ui/chart"
|
||||
"shared/components/ui/chart",
|
||||
"shared/components/ui/collapsible",
|
||||
"shared/lib/utils"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,6 +68,18 @@
|
||||
| | 学情诊断报告 | 基于知识点掌握度的个人/班级诊断报告 | P2 | ✅ |
|
||||
| | 成绩导出 | Excel/PDF 成绩单导出,支持自定义模板 | P1 | ✅ |
|
||||
| | 等第转换 | 分数↔等第(A/B/C/D)自动转换 | P2 | ❌ |
|
||||
| **错题本** | 错题自动采集 | 考试/作业提交后自动收录错题(去重) | P0 | ✅ |
|
||||
| | 手动添加错题 | 从题库选题手动添加到错题本 | P1 | ✅ |
|
||||
| | SM-2 间隔重复 | 4 级评级(again/hard/good/easy),科学复习调度 | P1 | ✅ |
|
||||
| | 错题复习 | 详情查看、复习记录、笔记/标签 | P0 | ✅ |
|
||||
| | 错题归档/删除 | 已掌握错题归档,支持删除 | P1 | ✅ |
|
||||
| | 知识点薄弱度分析 | 按知识点统计错误率与掌握率 | P1 | ✅ |
|
||||
| | 学科错题分布 | 按学科统计错题数量与掌握情况 | P2 | ✅ |
|
||||
| | 高频错题统计 | 班级/年级高频错题 Top N | P2 | ✅ |
|
||||
| | 学生错题视图 | 学生查看自己的错题本(统计/筛选/列表/复习) | P0 | ✅ |
|
||||
| | 教师错题分析 | 教师查看所教班级学生的错题统计与分析 | P1 | ✅ |
|
||||
| | 家长错题查看 | 家长查看子女的错题情况与学习进度 | P1 | ✅ |
|
||||
| | 管理员错题分析 | 管理员查看全校错题统计与分析 | P2 | ✅ |
|
||||
| **家校沟通** | 通知公告 | 学校/年级/班级三级公告发布,已读回执 | P0 | ✅ |
|
||||
| | 站内消息 | 教师↔家长、教师↔学生私信,支持群发 | P1 | ✅ |
|
||||
| | 家长端仪表盘 | 子女成绩/作业/考勤/课表一站式查看 | P1 | ⚠️ |
|
||||
|
||||
215
docs/architecture/audit/dashboard-audit-report-v3.md
Normal file
215
docs/architecture/audit/dashboard-audit-report-v3.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# Dashboard 模块 V3 审计报告
|
||||
|
||||
**审计日期**:2026-06-22
|
||||
**审计范围**:`src/modules/dashboard/` + 所有 dashboard 路由文件
|
||||
**前置审计**:v1(P0 修复:跨模块 DB 查询、权限、i18n 容器组件)、v2(10 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
|
||||
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
v1/v2 审计解决了表层问题。v3 审计发现了**更深层次的问题**,涉及数据完整性、i18n 完整性、死代码、类型安全、流式架构和测试缺口。最严重的是 admin dashboard 中 ContentRow 标签与值完全错配的 **P0 数据展示 bug**。
|
||||
|
||||
| 严重度 | 数量 |
|
||||
|--------|------|
|
||||
| P0 | 3 |
|
||||
| P1 | 10 |
|
||||
| P2 | 9 |
|
||||
|
||||
---
|
||||
|
||||
## P0 问题(严重)
|
||||
|
||||
### P0-1:Admin Dashboard ContentRow 标签与值错配(数据完整性)
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx`
|
||||
- **行号**:166-169(Content 区块)、180-181(Homework Activity 区块)
|
||||
- **问题**:"Content" 区块显示教材/章节/题目/考试数量,但使用了用户/班级/待批改/已发布作业的标签。图标正确(Library, BookOpen, FileText, ClipboardList),但标签错误:
|
||||
- 行 166:`label={t("stats.users")}` + `value={data.textbookCount}` → 应为 `t("stats.textbooks")`
|
||||
- 行 167:`label={t("stats.classes")}` + `value={data.chapterCount}` → 应为 `t("stats.chapters")`
|
||||
- 行 168:`label={t("stats.toGrade")}` + `value={data.questionCount}` → 应为 `t("stats.questions")`
|
||||
- 行 169:`label={t("stats.homeworkPublished")}` + `value={data.examCount}` → 应为 `t("stats.exams")`
|
||||
- 行 180:`label={t("stats.activeAssignments")}` + `value={data.homeworkAssignmentCount}` → 标签说"active"但值是总数
|
||||
- 行 181:`label={t("stats.submissionRate")}` + `value={data.homeworkSubmissionCount}` → 标签说"rate"(百分比)但值是原始计数
|
||||
- **修复**:使用与值匹配的正确翻译键。新增缺失键(`stats.textbooks`、`stats.chapters`、`stats.questions`、`stats.exams`、`stats.totalAssignments`、`stats.totalSubmissions`)到 `messages/{zh-CN,en}/dashboard.json`。
|
||||
|
||||
### P0-2:admin/error.tsx 硬编码中文,无 i18n
|
||||
|
||||
- **文件**:`src/app/(dashboard)/admin/error.tsx`
|
||||
- **行号**:12-14
|
||||
- **问题**:此错误边界有硬编码中文字符串(`"页面加载失败"`、`"抱歉,页面加载时发生了意外错误。请稍后重试。"`、`"重试"`),未导入或使用 `useTranslations`。英文用户会看到中文文本。v2 审计遗漏了此文件,因为只关注了 `dashboard/` 模块而非 `admin/` 路由错误边界。其他 dashboard error.tsx(teacher、parent、root)都正确使用了 `useTranslations`。
|
||||
- **修复**:导入 `useTranslations`,替换硬编码字符串为 `t("error.loadFailed")`、`t("error.loadFailedDesc")`、`t("error.retry")`。
|
||||
|
||||
### P0-3:userGrowth 和 homeworkTrend 永远返回空数组
|
||||
|
||||
- **文件**:`src/modules/dashboard/data-access.ts`
|
||||
- **行号**:46-47
|
||||
- **问题**:`getAdminDashboardData` 硬编码 `userGrowth: []` 和 `homeworkTrend: []`。`UserGrowthChart` 组件(admin-dashboard.tsx 行 123、133)渲染这些空数组,产生永久空图表且无空状态。架构图(行 973)标注为"待后续接入真实统计",但至今未修复。用户看到两个空白图表区域,有标题但无数据也无说明。
|
||||
- **修复**:为 `UserGrowthChart` 添加空状态(当 `data.length === 0` 时显示"暂无数据"),与其他图表组件的空状态保持一致。
|
||||
|
||||
---
|
||||
|
||||
## P1 问题(高)
|
||||
|
||||
### P1-1:admin/dashboard 路由缺失 loading.tsx
|
||||
|
||||
- **文件(缺失)**:`src/app/(dashboard)/admin/dashboard/loading.tsx`
|
||||
- **问题**:admin dashboard 路由无路由级 `loading.tsx`,回退到 `admin/loading.tsx`(通用骨架屏,不匹配 admin dashboard 布局)。Teacher、student、parent 都有 dashboard 专属 `loading.tsx`。
|
||||
- **修复**:创建 `admin/dashboard/loading.tsx`,骨架屏匹配 `AdminDashboardView` 布局。
|
||||
|
||||
### P1-2:admin/dashboard 和 student/dashboard 路由缺失 error.tsx
|
||||
|
||||
- **文件(缺失)**:`src/app/(dashboard)/admin/dashboard/error.tsx`、`src/app/(dashboard)/student/dashboard/error.tsx`
|
||||
- **问题**:这些路由无路由级错误边界。Admin 回退到 `admin/error.tsx`(有硬编码中文 — 见 P0-2)。Student 回退到 `student/error.tsx`。Teacher 和 parent 都有 dashboard 专属 `error.tsx`(含 i18n + 重试按钮)。
|
||||
- **修复**:为两个路由创建 dashboard 专属 `error.tsx`,使用 `useTranslations` 和 `reset()`。
|
||||
|
||||
### P1-3:UserGrowthChart 硬编码标签用于两个图表
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx`
|
||||
- **行号**:44
|
||||
- **问题**:`name` 属性硬编码为 `t("chart.newUsers")`。此组件在 `admin-dashboard.tsx` 中被复用于用户增长(行 123)和作业提交趋势(行 133)。作业趋势图错误地显示"新用户"作为图例/提示标签。
|
||||
- **修复**:为 `UserGrowthChart` 添加 `labelKey` 或 `name` prop,让调用方指定正确标签。
|
||||
|
||||
### P1-4:formatDate / formatLongDate 总是使用 zh-CN locale
|
||||
|
||||
- **文件**:`src/shared/lib/utils.ts`(行 8、35),及所有不传 locale 的 dashboard 组件
|
||||
- **问题**:`formatDate` 和 `formatLongDate` 默认 `locale = "zh-CN"`。所有 dashboard 组件调用时未传用户 locale:
|
||||
- `dashboard-greeting-header.tsx` 行 22
|
||||
- `admin-dashboard.tsx` 行 215
|
||||
- `teacher-homework-card.tsx` 行 69
|
||||
- `recent-submissions.tsx` 行 96
|
||||
- `student-grades-card.tsx` 行 23、105
|
||||
- `student-upcoming-assignments-card.tsx` 行 106
|
||||
|
||||
英文用户看到中文格式日期(如"2026年6月22日 周一"而非"Monday, June 22, 2026")。
|
||||
- **修复**:客户端组件用 `useLocale()`(next-intl),服务端组件用 `getLocale()`(next-intl/server),传入 `formatDate`/`formatLongDate`。
|
||||
|
||||
### P1-5:死代码 — getCachedAdminDashboard 从未使用
|
||||
|
||||
- **文件**:`src/modules/dashboard/actions.ts`
|
||||
- **行号**:146
|
||||
- **问题**:`export const getCachedAdminDashboard = cache(getAdminDashboardAction)` 定义但从未被导入或调用。`data-access.ts` 中的 `getAdminDashboardData` 已用 `cache()` 包裹。此外,用 React `cache()` 包裹调用 `requirePermission()` 的 Server Action 语义上不正确。
|
||||
- **修复**:删除行 146 及未使用的 `cache` 导入。
|
||||
|
||||
### P1-6:死代码 — AvatarImage src={undefined}
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/recent-submissions.tsx`
|
||||
- **行号**:76
|
||||
- **问题**:`<AvatarImage src={undefined} alt={item.studentName} />` 总是传 `undefined` 作为 `src`,`AvatarImage` 永远不会渲染实际图片,总是回退到 `AvatarFallback`。
|
||||
- **修复**:移除 `AvatarImage` 行,仅保留 `AvatarFallback`。
|
||||
|
||||
### P1-7:死 prop — TeacherStats isLoading 从未传入
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-stats.tsx`
|
||||
- **行号**:10、18、32、41、50、59
|
||||
- **问题**:`TeacherStats` 接受 `isLoading` prop(默认 `false`)并传给所有 4 个 `StatCard`。但 `TeacherStats` 仅在 `DashboardSection` 中渲染(`teacher-dashboard-view.tsx` 行 53),未传 `isLoading`。prop 永远为 `false`。`StudentStatsGrid` 无此 prop,造成不一致。
|
||||
- **修复**:移除 `TeacherStats` 的 `isLoading` prop 及 `StatCard` 调用。
|
||||
|
||||
### P1-8:dashboard-utils.ts 中的 `as` 类型断言违反项目规则
|
||||
|
||||
- **文件**:`src/modules/dashboard/lib/dashboard-utils.ts`
|
||||
- **行号**:114、145
|
||||
- **问题**:项目规则明确"禁止 `as` 断言"(除 `unknown` 转换或测试外)。两处违规:
|
||||
- 行 114:`})) as StudentTodayScheduleItem[] | TeacherTodayScheduleItem[]`
|
||||
- 行 145:`) as TeacherTodayScheduleItem[]`
|
||||
|
||||
根因是 `filterTodaySchedule` 重载服务于学生和教师课表,但返回类型是联合类型。
|
||||
- **修复**:将 `filterTodaySchedule` 改为泛型函数,或拆分为两个函数。
|
||||
|
||||
### P1-9:辅助函数缺失显式返回类型
|
||||
|
||||
- **文件**:
|
||||
- `teacher-schedule.tsx` 行 24:`const getStatus = (start: string, end: string) => {`
|
||||
- `student-upcoming-assignments-card.tsx` 行 30:`const getDueUrgency = (dueAt: string | null) => {`
|
||||
- **问题**:项目规则要求"函数返回值必须显式标注"。
|
||||
- **修复**:添加显式返回类型。
|
||||
|
||||
### P1-10:重复的 loading.tsx 和 error.tsx 文件
|
||||
|
||||
- **文件**:
|
||||
- `src/app/(dashboard)/dashboard/loading.tsx` 和 `src/app/(dashboard)/teacher/dashboard/loading.tsx` — 字节级完全相同
|
||||
- `src/app/(dashboard)/dashboard/error.tsx`、`teacher/dashboard/error.tsx`、`parent/dashboard/error.tsx` — 全部相同
|
||||
- **问题**:这些文件是精确副本。任何修复必须应用到所有副本,容易产生漂移。
|
||||
- **修复**:抽取共享 `DashboardLoadingSkeleton` 和 `DashboardErrorFallback` 组件到 `src/modules/dashboard/components/`,每个路由的 `loading.tsx`/`error.tsx` 渲染共享组件。
|
||||
|
||||
---
|
||||
|
||||
## P2 问题(中)
|
||||
|
||||
### P2-1:流式/Suspense 未生效 — 数据在页面级获取
|
||||
|
||||
- **文件**:所有 `page.tsx`(admin/teacher/student/parent dashboard)
|
||||
- **问题**:所有页面用 `export const dynamic = "force-dynamic"` 和 `await getDashboardAction()` 在渲染任何子组件前获取所有数据。`DashboardSection` 包裹子组件于 `<Suspense>`,但数据已在页面级解析并作为 props 传入,Suspense 永远不会在初始渲染时触发。
|
||||
- **修复**:将数据获取移入各卡片组件(使其成为异步服务端组件自行获取数据),或传入未解析的 promise 并用 React `use()` hook。这是较大的架构变更。
|
||||
|
||||
### P2-2:4 个组件不必要标记为 "use client"
|
||||
|
||||
- **文件**:
|
||||
- `dashboard-greeting-header.tsx` — 仅用 `useTranslations`、`formatLongDate`、`getGreetingKey`
|
||||
- `teacher-quick-actions.tsx` — 仅用 `useTranslations`、`Link`、`Button`
|
||||
- `teacher-dashboard-header.tsx` — 包裹上述两个
|
||||
- `student-dashboard-header.tsx` — 包裹 `DashboardGreetingHeader`
|
||||
- **问题**:这些组件标记为 `"use client"` 但不含客户端 only hook(`useState`、`useEffect`、事件处理器等)。`useTranslations` 在服务端组件中可用。转为服务端组件(用 `getTranslations` 替代 `useTranslations`)可减少客户端包大小。
|
||||
- **修复**:移除 `"use client"`,改 `useTranslations` 为 `getTranslations`(async),组件改为 `async function`。
|
||||
|
||||
### P2-3:UserGrowthChart 无空状态
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/admin-dashboard/user-growth-chart.tsx`
|
||||
- **问题**:当 `data` 为空(当前永远如此 — 见 P0-3),recharts 渲染空图表有坐标轴但无线条无说明。其他图表组件(`TeacherGradeTrends`、`StudentGradesCard`)使用 `ChartCardShell` 有正确空状态。
|
||||
- **修复**:添加空状态检查:`data.length === 0` 时渲染 `EmptyState`。
|
||||
|
||||
### P2-4:Student dashboard 空状态缺少 CTA(与 teacher 不一致)
|
||||
|
||||
- **文件**:
|
||||
- `student-today-schedule-card.tsx` 行 58-63:`EmptyState` 无 `action`
|
||||
- `student-upcoming-assignments-card.tsx` 行 59-64:`EmptyState` 无 `action`
|
||||
- **问题**:Teacher dashboard 空状态都含 CTA。Student dashboard 空状态无 CTA,用户无明确下一步。
|
||||
- **修复**:为 student 空状态添加 `action` prop。
|
||||
|
||||
### P2-5:StudentTodayScheduleCard 过时数据 — useMemo 不随时间更新
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/student-dashboard/student-today-schedule-card.tsx`
|
||||
- **行号**:25-43
|
||||
- **问题**:`useMemo(() => { ... }, [items])` 基于 `new Date()` 计算 `currentId` 和 `nextId`。依赖数组是 `[items]`,仅在 `items` 变化时重新计算。用户保持页面打开时,"进行中"和"下一个"徽章会过时。
|
||||
- **修复**:添加基于时间的重渲染机制(如 `useEffect` + `setInterval` 每分钟更新 `now` state)。
|
||||
|
||||
### P2-6:仅图标按钮缺少 aria-label
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-homework-card.tsx`
|
||||
- **行号**:22
|
||||
- **问题**:`<Button asChild size="icon" variant="ghost" className="h-8 w-8" title={...}>` 用 `title` 作 tooltip 但无 `aria-label`。屏幕阅读器可能不播报按钮用途。
|
||||
- **修复**:添加 `aria-label={t("quickActions.createNewAssignment")}`。
|
||||
|
||||
### P2-7:无组件测试 — 仅有纯函数测试
|
||||
|
||||
- **文件**:`tests/integration/dashboard/dashboard-utils.test.ts`(408 行,31 个测试覆盖 6 个纯函数)、`tests/integration/dashboard/dashboard-routing.test.ts`(6 个测试覆盖重定向逻辑)
|
||||
- **问题**:v2 添加了纯函数单测,但零组件测试、零 Server Action 测试、零 data-access 测试、零错误边界测试。`dashboard-routing.test.ts` 在用户对象上 mock `permissions`(行 41),但实际代码用 `resolvePermissions(roles)` — mock 的 `permissions` 字段被忽略,测试设置有误导性。
|
||||
- **修复**:添加组件测试(RTL)、Action 测试(mock data-access,验证权限调用)、修复路由测试。
|
||||
|
||||
### P2-8:TeacherTodoCard 排序逻辑晦涩
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-todo-card.tsx`
|
||||
- **行号**:52
|
||||
- **问题**:`.sort((a, b) => (a.variant === "urgent" ? -1 : 1) - (b.variant === "urgent" ? -1 : 1))` 难以阅读。布尔转数字的算术不透明。
|
||||
- **修复**:重写为更可读的比较函数。
|
||||
|
||||
### P2-9:TeacherSchedule 渲染两次(移动端 + 桌面端)— 重复服务端渲染
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-dashboard-view.tsx`
|
||||
- **行号**:63-67(移动端)、85-89(桌面端)
|
||||
- **问题**:`TeacherSchedule`(异步服务端组件调用 `getTranslations`)在 React 树中渲染两次 — 一次在 `lg:hidden` div,一次在 `hidden lg:block` div。两个实例都在服务端渲染并发送到客户端,使此区块 HTML 负载翻倍。
|
||||
- **修复**:渲染一次并用 CSS grid/flexbox 重排序实现响应式布局,或接受此重复为较小代价。
|
||||
|
||||
---
|
||||
|
||||
## 修复顺序
|
||||
|
||||
1. **P0-1**(ContentRow 标签)— 直接面向用户的数据 bug
|
||||
2. **P0-2**(admin/error.tsx i18n)— 直接 i18n 回归
|
||||
3. **P0-3 + P1-3 + P2-3**(空趋势数据 + 图表标签 + 空状态)— 一起修复
|
||||
4. **P1-1、P1-2**(缺失 loading.tsx/error.tsx)— 一致性
|
||||
5. **P1-4**(日期 locale)— 系统性 i18n 修复
|
||||
6. **P1-5、P1-6、P1-7**(死代码)— 快速清理
|
||||
7. **P1-8、P1-9**(类型安全)— 重构 `filterTodaySchedule`
|
||||
8. **P1-10**(重复文件)— 抽取共享组件
|
||||
9. **P2-2、P2-4、P2-6、P2-8**(增量改进)
|
||||
180
docs/architecture/audit/exam-homework-audit-report-v3.md
Normal file
180
docs/architecture/audit/exam-homework-audit-report-v3.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# 考试/作业模块审计报告 v3
|
||||
|
||||
> 基于 v2 审计报告的深度用户体验审计与同类产品对标分析
|
||||
> 生成时间:2026-06-22
|
||||
> 审计范围:`src/modules/exams/`、`src/modules/homework/`、`src/modules/proctoring/`、`src/modules/parent/`(考试相关)、`src/shared/`(考试/作业相关共享层)
|
||||
|
||||
---
|
||||
|
||||
## 1. v2 遗留项验证
|
||||
|
||||
### 1.1 遗留项状态
|
||||
|
||||
| 编号 | v2 描述 | v3 验证结果 |
|
||||
|------|---------|-------------|
|
||||
| L-1 | ExamHomeworkServicePort 已定义但未注册实现 | ❌ `registerExamHomeworkService` 全项目零调用,`instrumentation.ts` 不存在 |
|
||||
| L-2 | trackExamEvent 已定义但未在 actions 中调用 | ❌ `trackExamEvent` 全项目零调用,3 个目标文件均未导入 |
|
||||
| L-3 | useExamHomeworkFeatures hook 已创建但未在页面中使用 | ❌ hook 全项目零使用,app/ 与 modules/ 下无任何引用 |
|
||||
| L-4 | ai-pipeline/structure.ts 仍有 ~300 行 | ✅ 已降至 209 行(低于 800 行建议值) |
|
||||
| L-5 | 预存 TypeScript 错误(7 个) | ❌ 实际为 22 个,其中 8 个在 homework 模块(`data-access.ts`/`stats-service.ts` 的 `db.select().from().where()` 返回数组但代码直接访问 `.c` 属性) |
|
||||
|
||||
### 1.2 新发现的预存 TypeScript 错误
|
||||
|
||||
**位置**:`src/modules/homework/data-access.ts` 第 489-492 行、`src/modules/homework/stats-service.ts` 第 236-239 行
|
||||
|
||||
**根因**:`db.select({ c: count() }).from(table).where(condition)` 返回 `{ c: number }[]` 数组,但代码直接访问 `targetsRow?.c`,应为 `targetsRow[0]?.c`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户体验深度分析(对标同类产品)
|
||||
|
||||
### 2.1 对标产品矩阵
|
||||
|
||||
| 功能维度 | 智学网 | 猿题库 | Google Classroom | Canvas LMS | 当前实现 |
|
||||
|---------|--------|--------|------------------|------------|---------|
|
||||
| 即时自动批改 | ✅ 提交即出分 | ✅ 提交即出分 | ❌ 需教师批改 | ✅ 可配置 | ❌ 仅在批改页计算,不回写 |
|
||||
| 批量批改 | ✅ 多选+批量打分 | ❌ 逐题批改 | ❌ 无 | ✅ 批量打分 | ❌ 仅支持逐份批改 |
|
||||
| 考试分析 | ✅ 难度/区分度/知识点 | ✅ 错题统计 | ❌ 基础统计 | ✅ 完整分析 | ❌ 作业有分析,考试无分析 |
|
||||
| 多选题部分分 | ✅ 漏选得部分分 | ✅ 按选项计分 | ❌ 全对才得分 | ✅ 可配置 | ❌ 全对才得分 |
|
||||
| 提交后反馈 | ✅ 即时显示分数+错题 | ✅ 即时显示 | ❌ 等待教师 | ✅ 即时显示 | ❌ 提交后跳转列表,无反馈 |
|
||||
| 错题本 | ✅ 自动归集 | ✅ 自动归集 | ❌ 无 | ✅ 可导出 | ❌ 无错题本 |
|
||||
| 家长视图 | ✅ 考试详情+趋势 | N/A | ❌ 无 | ✅ 观察员模式 | ❌ 仅作业摘要,无考试详情 |
|
||||
| 移动端适配 | ✅ 原生 App | ✅ 原生 App | ✅ 响应式 | ✅ 响应式 | ⚠️ 响应式但触控未优化 |
|
||||
|
||||
### 2.2 关键 UX 缺陷分析
|
||||
|
||||
#### UX-1: 即时自动批改回写(P0 优先级)
|
||||
|
||||
**当前流程**:
|
||||
1. 学生提交作业 → `submitHomeworkAction` → `markHomeworkSubmitted` → 跳转列表页
|
||||
2. 教师打开批改页 → `applyAutoGrades` 在客户端计算 → 教师手动点击"提交成绩"
|
||||
|
||||
**问题**:
|
||||
- 学生提交后看不到即时成绩,体验割裂
|
||||
- 自动批改结果仅存在教师浏览器内存中,未回写 DB
|
||||
- 若教师不打开批改页,选择题/判断题永远不会有分数
|
||||
|
||||
**同类产品做法**:智学网/猿题库在学生提交瞬间服务端自动批改选择题/判断题,学生立即看到客观题分数,主观题等待教师批改。
|
||||
|
||||
**改进方案**:在 `markHomeworkSubmitted` 中调用 `applyAutoGrades` 并回写 DB,将 submission 状态设为 `graded`(若全部可自动判分)或 `submitted`(若含主观题)。
|
||||
|
||||
#### UX-2: 批量批改 UI(P1 优先级)
|
||||
|
||||
**当前**:`homework/assignments/[id]/submissions` 页面仅展示提交列表,教师需逐份点击进入批改页。
|
||||
|
||||
**同类产品**:智学网支持列表页勾选多份提交,批量设置分数(全对/全错/自定义)。
|
||||
|
||||
**改进方案**:提交列表页增加多选 checkbox + 批量操作工具栏(批量自动批改、批量设置分数)。
|
||||
|
||||
#### UX-3: 考试分析仪表盘(P1 优先级)
|
||||
|
||||
**当前**:`homework/stats-service.ts` 有作业分析(`getHomeworkAssignmentAnalytics`),但考试无分析。
|
||||
|
||||
**同类产品**:智学网考试后展示题目难度、区分度、知识点掌握度、班级对比。
|
||||
|
||||
**改进方案**:新增 `exams/components/exam-analytics-dashboard.tsx`,复用 homework stats-service 模式,基于考试关联的作业提交数据计算分析。
|
||||
|
||||
#### UX-4: 多选题部分分自动判分(P1 优先级)
|
||||
|
||||
**当前**:`computeIsCorrect` 对多选题采用"全对才得分"策略(`studentSet.size !== correctSet.size` 直接返回 false)。
|
||||
|
||||
**同类产品**:智学网/猿题库支持"漏选得部分分"(每个正确选项得分,错误选项扣分)。
|
||||
|
||||
**改进方案**:`applyAutoGrades` 增加部分分计算策略,按正确选项比例给分。
|
||||
|
||||
#### UX-5: 提交后即时反馈页(P2 优先级)
|
||||
|
||||
**当前**:学生提交后跳转到 `/student/learning/assignments` 列表页,无任何反馈。
|
||||
|
||||
**同类产品**:智学网/猿题库提交后显示成绩页(分数、对错分布、错题预览)。
|
||||
|
||||
**改进方案**:提交后跳转到 `/student/learning/assignments/[assignmentId]/result` 页面,展示分数+对错分布+错题预览。
|
||||
|
||||
#### UX-6: 错题本(P2 优先级)
|
||||
|
||||
**当前**:无错题本功能,学生无法回顾历史错题。
|
||||
|
||||
**同类产品**:智学网/猿题库自动归集错题,支持按科目/时间筛选。
|
||||
|
||||
**改进方案**:新增 `student/wrong-answers` 页面,聚合所有已批改作业中的错题。
|
||||
|
||||
#### UX-7: 家长考试详情视图(P2 优先级)
|
||||
|
||||
**当前**:`parent` 模块仅有 `ChildHomeworkSummary`(作业摘要),无考试详情。
|
||||
|
||||
**同类产品**:智学网家长端可查看孩子考试详情、错题、成绩趋势。
|
||||
|
||||
**改进方案**:新增 `parent/components/child-exam-detail.tsx`,展示孩子考试详情+成绩趋势。
|
||||
|
||||
#### UX-8: 移动端触控优化(P3 优先级)
|
||||
|
||||
**当前**:题目导航按钮 `h-8 w-8`(32px),低于 Apple HIG 建议的 44px 最小触控目标。
|
||||
|
||||
**改进方案**:移动端按钮尺寸调整为 `h-10 w-10 sm:h-8 sm:w-8`。
|
||||
|
||||
---
|
||||
|
||||
## 3. v3 改进计划
|
||||
|
||||
### 3.1 P0 优先级(核心体验)
|
||||
|
||||
| 编号 | 改进项 | 实现方案 |
|
||||
|------|--------|---------|
|
||||
| V3-1 | 修复预存 TypeScript 错误 | `data-access.ts`/`stats-service.ts` 的 `db.select()` 结果加 `[0]` 索引 |
|
||||
| V3-2 | 即时自动批改回写 | `markHomeworkSubmitted` 中调用 `applyAutoGrades` 并回写 DB |
|
||||
| V3-3 | 注册 ExamHomeworkServicePort 实现 | 新建 `src/instrumentation.ts`,注册真实实现 |
|
||||
| V3-4 | trackExamEvent 埋点接入 | 在 `createExamAction`/`submitHomeworkAction` 等 8 个关键 action 中调用 |
|
||||
| V3-5 | useExamHomeworkFeatures hook 接入 | 在 `exam-actions.tsx`/`homework-take-view.tsx` 中使用 |
|
||||
|
||||
### 3.2 P1 优先级(重要体验)
|
||||
|
||||
| 编号 | 改进项 | 实现方案 |
|
||||
|------|--------|---------|
|
||||
| V3-6 | 多选题部分分自动判分 | `applyAutoGrades` 增加部分分计算策略 |
|
||||
| V3-7 | 批量批改 UI | 提交列表页增加多选+批量操作工具栏 |
|
||||
| V3-8 | 考试分析仪表盘 | 新增 `exam-analytics-dashboard.tsx` 组件+data-access |
|
||||
|
||||
### 3.3 P2 优先级(增强体验)
|
||||
|
||||
| 编号 | 改进项 | 实现方案 |
|
||||
|------|--------|---------|
|
||||
| V3-9 | 提交后即时反馈页 | 新增 result 页面,展示分数+对错分布 |
|
||||
| V3-10 | 错题本 | 新增 `student/wrong-answers` 页面 |
|
||||
| V3-11 | 家长考试详情视图 | 新增 `child-exam-detail.tsx` 组件 |
|
||||
|
||||
### 3.4 P3 优先级(细节优化)
|
||||
|
||||
| 编号 | 改进项 | 实现方案 |
|
||||
|------|--------|---------|
|
||||
| V3-12 | 移动端触控优化 | 题目导航按钮尺寸调整为 44px 最小触控目标 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 实施顺序
|
||||
|
||||
1. V3-1: 修复预存 TypeScript 错误(阻塞后续)
|
||||
2. V3-2: 即时自动批改回写(核心体验)
|
||||
3. V3-6: 多选题部分分自动判分(与 V3-2 协同)
|
||||
4. V3-3: 注册 ExamHomeworkServicePort 实现
|
||||
5. V3-4: trackExamEvent 埋点接入
|
||||
6. V3-5: useExamHomeworkFeatures hook 接入
|
||||
7. V3-7: 批量批改 UI
|
||||
8. V3-8: 考试分析仪表盘
|
||||
9. V3-9: 提交后即时反馈页
|
||||
10. V3-10: 错题本
|
||||
11. V3-11: 家长考试详情视图
|
||||
12. V3-12: 移动端触控优化
|
||||
|
||||
---
|
||||
|
||||
## 5. 预期收益
|
||||
|
||||
| 维度 | 改进前 | 改进后 |
|
||||
|------|--------|--------|
|
||||
| 学生提交后反馈延迟 | 等待教师批改(小时-天) | 客观题即时(秒级) |
|
||||
| 教师批改效率 | 逐份手动 | 批量+自动批改 |
|
||||
| 考试后分析 | 无 | 完整分析仪表盘 |
|
||||
| 多选题评分精度 | 全对才得分 | 按选项比例得分 |
|
||||
| 家长了解孩子考试 | 无 | 考试详情+趋势 |
|
||||
| TypeScript 错误数 | 22 | 0(考试/作业模块) |
|
||||
| 死代码(已定义未使用) | 3 处 | 0 处 |
|
||||
322
docs/architecture/audit/grades-diagnostic-audit-report-v2.md
Normal file
322
docs/architecture/audit/grades-diagnostic-audit-report-v2.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# 成绩和学情诊断模块审计报告 v2
|
||||
|
||||
> 审查日期:2026-06-22
|
||||
> 审查范围:在 v1 审计(`grades-diagnostic-audit-report.md`)完成所有 P0/P1/P2 改进项之后,对 `src/modules/grades/**`、`src/modules/diagnostic/**`、相关路由层、i18n、架构图进行二次深度审计
|
||||
> 审查目的:发现 v1 修复后仍存在的代码质量、架构、类型安全、i18n、a11y、错误处理、性能、业务逻辑问题
|
||||
|
||||
---
|
||||
|
||||
## 一、v1 完成情况确认
|
||||
|
||||
v1 审计报告所有 P0/P1/P2 改进项(共 16 项)均已真实落地,代码验证通过:
|
||||
|
||||
| v1 编号 | 改进项 | 验证结果 |
|
||||
|---------|--------|----------|
|
||||
| P0-1 | 权限校验缺失 | ✅ 所有页面均调用 `requirePermission()` |
|
||||
| P0-2 | diagnostic 直查 users 表 | ✅ 已改用 `getUserNamesByIds` |
|
||||
| P0-3 | i18n 完全缺失 | ⚠️ 翻译文件已创建,但组件未接入(见 v2 P1-4) |
|
||||
| P0-4 | `/management/grade/page.tsx` 缺失 | ✅ 已补齐 |
|
||||
| P1-1 | 统计业务逻辑抽取 | ✅ `stats-service.ts` 已创建(305 行,8 个纯函数) |
|
||||
| P1-2 | 重复工具函数 | ✅ `lib/grade-utils.ts` 已创建 |
|
||||
| P1-3 | Zod 校验缺失 | ✅ 12 个 Action 已补齐 |
|
||||
| P1-4 | `as` 断言违规 | ✅ 已修复(但 stats-service.ts 新增 1 处,见 v2 P2-2) |
|
||||
| P1-5 | Error Boundary 和 Suspense | ⚠️ `widget-boundary.tsx` 已创建但未被使用(见 v2 P1-1) |
|
||||
| P1-6 | 架构图同步 | ⚠️ 部分同步,行数和路由仍有不一致(见 v2 P2-10) |
|
||||
| P2-1 | a11y 无障碍 | ⚠️ 部分修复,热力图和表单 Label 仍有问题(见 v2 P1-6、P2-7) |
|
||||
| P2-2 | Tailwind 任意值 | ✅ 已修复 |
|
||||
| P2-3 | studentId 字段语义 | ✅ 已修复(schema + types + data-access + components) |
|
||||
| P2-4 | grade_managed scope | ✅ 已修复(子查询过滤) |
|
||||
| P2-5 | parent/diagnostic 页面 | ✅ 已创建 |
|
||||
| P2-6 | SearchParams 统一 | ⚠️ 部分统一,4 个 student 路由仍自定义(见 v2 P2-8) |
|
||||
|
||||
---
|
||||
|
||||
## 二、v2 新发现问题
|
||||
|
||||
### 2.1 P1 严重问题
|
||||
|
||||
#### P1-1 WidgetBoundary 组件已定义但全项目未被使用
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [widget-boundary.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/widget-boundary.tsx) L117 | `WidgetBoundary` 组件已导出(139 行),但全项目无任何 import 语句引用它 | "每个独立的数据区块必须用 React Error Boundary 包裹" |
|
||||
| [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) L696 | 声称"已新增 WidgetBoundary 通用组件",但从未被使用 | 架构文档虚假声明 |
|
||||
|
||||
**后果**:v1 P1-5 改进项仅创建了组件但未实际应用,Error Boundary + Suspense + Skeleton 三件套未生效,单个 Widget 抛错仍会导致整个页面崩溃。
|
||||
|
||||
**改进方向**:在 9 个关键组件中应用 `WidgetBoundary`:
|
||||
- grades:`grade-trend-chart`、`grade-distribution-chart`、`class-comparison-chart`、`subject-comparison-chart`、`grade-stats-card`、`class-grade-report`
|
||||
- diagnostic:`mastery-radar-chart`、`class-diagnostic-view`、`student-diagnostic-view`
|
||||
|
||||
#### P1-2 admin/school/grades/insights 路由完全缺失 loading.tsx 和 error.tsx
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| `src/app/(dashboard)/admin/school/grades/insights/` | **loading.tsx 和 error.tsx 两者都缺失** | "路由级错误边界和加载态" |
|
||||
|
||||
**后果**:访问 `/admin/school/grades/insights` 时无骨架屏过渡,运行时错误会导致整页崩溃。
|
||||
|
||||
#### P1-3 架构数据 JSON 005 权限记录错误
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) | `/admin/school/grades` 和 `/admin/school/grades/insights` 权限记录为 `grade:manage`,实际代码使用 `school:manage` | "架构图应准确反映代码实际" |
|
||||
|
||||
**后果**:架构图与代码不一致,权限审计会得出错误结论。
|
||||
|
||||
#### P1-4 grades 和 diagnostic 模块 i18n 完全未接入
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| `src/modules/grades/components/*`(17 个文件) | 翻译文件 `grades.json` 已存在,但**没有任何组件**导入或调用 `useTranslations`,全部硬编码字符串 | "所有用户可见文本必须适配 i18n" |
|
||||
| `src/modules/diagnostic/components/*`(4 个文件) | 翻译文件 `diagnostic.json` 已存在,但 4 个组件全部硬编码英文字符串 | 同上 |
|
||||
|
||||
**后果**:v1 P0-3 仅创建了翻译文件但未接入组件,i18n 实际仍未生效。多语言用户无法切换语言。
|
||||
|
||||
**改进方向**:21 个组件全部接入 `useTranslations("grades")` 或 `useTranslations("diagnostic")`。
|
||||
|
||||
#### P1-5 exportGradesAction 安全漏洞
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L369-380 | `exportGradesAction` 调用 `exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` 时**未传递 `currentUserId: ctx.userId`** | "Server Action 必须传递用户身份到 data-access 层" |
|
||||
| [grades/actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L235-239, L303-307, L333 | `getClassGradeStatsAction`、`getClassRankingAction`、`getGradeRecordByIdAction` 均未将 `ctx.dataScope` 传递给 data-access 函数 | 同上 |
|
||||
|
||||
**后果**:学生(`class_members` scope)调用 `exportGradesAction` 时,`getGradeRecords` 中的 `if (params.scope.type === "class_members" && params.currentUserId)` 条件不成立,不会按 studentId 过滤,**学生可导出全班成绩**。
|
||||
|
||||
#### P1-6 diagnostic 缺少 stats-service.ts
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L62-90, L146-219, L222-256 | `getStudentMasterySummary`、`getClassMasterySummary`、`getKnowledgePointStats` 包含大量统计计算逻辑(averageMastery、强弱项分类、KP 聚合) | "严格三层架构,统计计算属业务逻辑层" |
|
||||
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L46-81, L84-124 | `generateDiagnosticReport`、`generateClassDiagnosticReport` 包含摘要文本生成、强弱项列表构建逻辑 | 同上 |
|
||||
|
||||
**后果**:diagnostic 模块未遵循 v1 P1-1 为 grades 模块建立的范例,统计逻辑仍混在 data-access 层,难以单独测试。
|
||||
|
||||
**改进方向**:抽取 `diagnostic/stats-service.ts`,包含 `classifyStrengthsWeaknesses`、`computeKpStats`、`computeStudentAverage` 等纯函数。
|
||||
|
||||
#### P1-7 热力图色块缺少 a11y 支持
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L128-139 | 热力图色块仅靠 `title` 属性,无 `role="img"` 和 `aria-label`,颜色编码语义无法被辅助技术感知 | "可访问性:ARIA 属性" |
|
||||
|
||||
**后果**:屏幕阅读器用户无法识别热力图色块的颜色等级含义(绿/黄/橙/红代表掌握度等级)。
|
||||
|
||||
#### P1-8 getKnowledgePointStats() 无参调用导致班级平均对比功能失效
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) L35 | 调用 `getKnowledgePointStats()`(无参数) | "函数调用应正确传参" |
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L222-256 | `getKnowledgePointStats(classId?, gradeId?)` 当两参都为 `undefined` 时,`studentIds` 为 `[]`,直接返回空数组 | 同上 |
|
||||
|
||||
**后果**:`classStats` 恒为 `[]`,`classAverageMastery` 恒为 `[]`,雷达图中班级平均对比曲线**永不显示**。架构文档标注的"班级平均对比"功能完全失效。
|
||||
|
||||
**改进方向**:页面应先查询学生所属班级,再调用 `getKnowledgePointStats(classId)`。
|
||||
|
||||
#### P1-9 updateMasteryFromSubmission 覆盖而非累积掌握度
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L93-143 | `onDuplicateKeyUpdate` 将 `totalQuestions`/`correctQuestions`/`masteryLevel` 设为**本次提交的值**,而非累积 | "掌握度应反映学习轨迹" |
|
||||
|
||||
**后果**:学生上次考 10 题 8 对(mastery=80%),本次考 1 题 1 对(mastery=100%),更新后 mastery 变为 100% 而非累积的 81.8%。掌握度随单次考试剧烈波动,无法反映真实学习轨迹。
|
||||
|
||||
**改进方向**:读取已有记录,将 `totalQuestions`/`correctQuestions` 累加后再计算,或采用加权/衰减算法。
|
||||
|
||||
### 2.2 P2 中等问题
|
||||
|
||||
#### P2-1 5 个 grades 路由和 1 个 diagnostic 路由缺失 error.tsx
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| `src/app/(dashboard)/management/grade/classes/` | 缺失 error.tsx |
|
||||
| `src/app/(dashboard)/management/grade/insights/` | 缺失 error.tsx |
|
||||
| `src/app/(dashboard)/parent/grades/` | 缺失 error.tsx |
|
||||
| `src/app/(dashboard)/student/grades/` | 缺失 error.tsx |
|
||||
| `src/app/(dashboard)/student/diagnostic/` | 缺失 error.tsx |
|
||||
|
||||
#### P2-2 lib/grade-utils.ts 跨模块直接查询 classes 表
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [lib/grade-utils.ts](file:///e:/Desktop/CICD/src/modules/grades/lib/grade-utils.ts) L6, L48-50 | 直接导入并查询 `classes` 表:`db.select({ id: classes.id }).from(classes).where(...)` | "modules 之间通过对方 data-access 通信" |
|
||||
|
||||
**改进方向**:在 `classes/data-access.ts` 新增 `getClassIdsByGradeIds(gradeIds: string[])` 函数并调用。
|
||||
|
||||
#### P2-3 死代码清理
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L93 | `updateMasteryFromSubmission` 全局零调用(架构文档标注"待扩展") |
|
||||
| [diagnostic/actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) L133, L154 | `getDiagnosticReportsAction` 和 `getDiagnosticReportByIdAction` 全局零调用,页面直接调用 data-access |
|
||||
|
||||
**改进方向**:要么删除死代码,要么让页面改为通过 Action 调用(统一权限校验入口)。本报告选择后者,保留 Action 并让页面使用。
|
||||
|
||||
#### P2-4 totalStudents 语义错误和班级平均掌握度计算偏差
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L201, L255 | `totalStudents: students.length` 是班级总人数,但 `masteredCount + notMasteredCount` 仅统计有掌握度记录的学生,数据自相矛盾 | "数据模型应语义清晰" |
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L204-205 | `averageMastery` 按记录数而非学生数平均,偏向多 KP 记录的学生 | 同上 |
|
||||
|
||||
**改进方向**:`totalStudents` 改为实际有掌握度记录的学生数(`levels.length`);`averageMastery` 先算每个学生的个人平均,再对学生平均取平均。
|
||||
|
||||
#### P2-5 多 upsert 无事务包裹
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L119-141 | `Promise.all(Array.from(kpStats.entries()).map(... db.insert(...).onDuplicateKeyUpdate(...)))` 并行执行多个 upsert,无事务包裹 | "多写操作应保证原子性" |
|
||||
|
||||
**后果**:部分成功部分失败时,掌握度数据将处于不一致状态。
|
||||
|
||||
#### P2-6 生成报告未校验掌握度数据
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [diagnostic/data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L46-81, L84-124 | `generateDiagnosticReport` 只检查 `summary` 是否为 null,不检查 `totalKnowledgePoints === 0` | "应处理空数据边界" |
|
||||
|
||||
**后果**:学生存在但无任何掌握度数据时,会生成 `overallScore: 0%`、`strengths: []`、`weaknesses: []` 的误导性报告。
|
||||
|
||||
#### P2-7 表单 Label 未关联控件
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L277, L293, L319, L334 | Class、Subject、Type、Semester 的 `<Label>` 无 `htmlFor` |
|
||||
| [grade-record-form.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-form.tsx) L88, L104, L120, L151, L166 | 5 个 `<Label>` 无 `htmlFor` |
|
||||
| [grade-query-filters.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-query-filters.tsx) L40, L57, L74, L90 | 4 个 `<Label>` 无 `htmlFor` |
|
||||
| [report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) L120-147 | 过滤器 Label 缺少 `htmlFor` |
|
||||
|
||||
#### P2-8 SearchParams 统一(剩余文件)
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx) L16 | 使用旧版 `getSearchParam, type SearchParams` from `@/shared/lib/utils` |
|
||||
| `src/app/(dashboard)/student/schedule/page.tsx` L11 | 自定义 `type SearchParams` |
|
||||
| `src/app/(dashboard)/student/learning/assignments/page.tsx` L23 | 自定义 `type SearchParams` |
|
||||
| `src/app/(dashboard)/student/learning/textbooks/page.tsx` L13 | 自定义 `type SearchParams` + 自定义 `getParam` |
|
||||
| `src/app/(dashboard)/student/learning/courses/page.tsx` L11 | 自定义 `type SearchParams` + 自定义 `getParam` |
|
||||
|
||||
#### P2-9 recorderName 硬编码和 grade-trend-card a11y
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [grades/data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L266 | `getStudentGradeSummary` 中 `recorderName: "Unknown"` 硬编码,已导入 `getUserNamesByIds` 但未用于获取录入人姓名 |
|
||||
| [grade-trend-card.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-trend-card.tsx) L37-53 | `TrendLineChart` 未包裹 `role="img"` + `aria-label`(其他 4 个图表组件均已添加) |
|
||||
|
||||
#### P2-10 架构文档行数和路由记录不一致
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.6 | grades 模块 10 个文件行数与实际不一致(如 `actions.ts` 文档 359 行,实际 398 行) |
|
||||
| [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 | diagnostic 模块 3 个文件行数与实际不一致 |
|
||||
| [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) | 缺失 `/teacher/grades/analytics` 和 `/management/grade` 路由记录 |
|
||||
|
||||
### 2.3 P3 长期问题(记录但不本次实施)
|
||||
|
||||
| 编号 | 问题 | 位置 |
|
||||
|------|------|------|
|
||||
| P3-1 | `toNumber` 工具函数在 grades 和 diagnostic 模块重复定义 | 多处 |
|
||||
| P3-2 | `byKp` 聚合逻辑重复 | diagnostic/data-access.ts L175-202 / L238-256 |
|
||||
| P3-3 | actions.ts 错误处理模板重复 14 次 | grades/actions.ts + actions-analytics.ts |
|
||||
| P3-4 | `isGradeType`/`isSemester` 类型守卫重复定义 | batch-grade-entry.tsx / grade-record-form.tsx |
|
||||
| P3-5 | `Option` 类型重复定义 3 次 | 3 个组件 |
|
||||
| P3-6 | `export.ts` 的 `avg` 函数与 `stats-service.ts` 逻辑重复 | export.ts L148 |
|
||||
| P3-7 | `TYPE_LABELS` 硬编码中文映射与 i18n 重复 | export.ts L12-17 |
|
||||
| P3-8 | `classIds` 过滤逻辑重复 3 次 | data-access.ts / export.ts |
|
||||
| P3-9 | `WidgetBoundary` 的 `WidgetErrorBoundary` 类构造函数参数类型不匹配 | widget-boundary.tsx L47 |
|
||||
| P3-10 | `createDefaultBuckets` 不必要导出 | stats-service.ts L229 |
|
||||
| P3-11 | 6 个组件内部回调函数缺失返回类型标注 | 多处 |
|
||||
| P3-12 | `batch-grade-entry.tsx` useEffect 草稿保存 bug(依赖数组含 scores) | L182-193 |
|
||||
| P3-13 | `batch-grade-entry.tsx` useMemo 依赖数组未包含 validateScore | L162-177 |
|
||||
| P3-14 | 5 处串行 DB 查询可并行化 | data-access.ts / data-access-analytics.ts 等 |
|
||||
| P3-15 | `getDiagnosticReports` 无分页 | data-access-reports.ts L127-159 |
|
||||
| P3-16 | 强弱项分类存在 60-79 盲区 | data-access.ts L77-78 |
|
||||
| P3-17 | 班级报告 strengths 无数量上限 | data-access-reports.ts L96-98 |
|
||||
| P3-18 | `getStudentMasterySummary` 内部串行可并行化 | data-access.ts L62-67 |
|
||||
| P3-19 | `getStudentMastery` 导出但仅内部使用 | data-access.ts L42 |
|
||||
| P3-20 | `grade-filters.tsx` 硬编码科目列表 | L47-53 |
|
||||
| P3-21 | `class-diagnostic-view.tsx` "View" 按钮缺少描述性 aria-label | L218-223 |
|
||||
| P3-22 | `student-diagnostic-view.tsx` "Practice" 按钮缺少描述性 aria-label | L129-133 |
|
||||
| P3-23 | 3 个表格缺少 `<caption>` | class-grade-report / student-grade-summary / batch-grade-entry |
|
||||
| P3-24 | `stats-service.ts` L110 `as GradeTrendPoint["type"]` 断言违规 | stats-service.ts |
|
||||
| P3-25 | `batch-grade-entry.tsx` JSON.parse 后 `as` 断言(灰色地带) | L75, L90, L127 |
|
||||
| P3-26 | `lib/grade-utils.ts` 61 行略超 40 行工具函数建议上限 | lib/grade-utils.ts |
|
||||
| P3-27 | data-access 写操作抛异常暴露给用户,建议结构化错误码 | data-access-reports.ts |
|
||||
| P3-28 | `grade-filters.tsx` 使用科目名称作为 value 而非科目 ID | L47-53 |
|
||||
|
||||
---
|
||||
|
||||
## 三、v2 改进优先级
|
||||
|
||||
### P1(本次实施)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|----------|------|
|
||||
| v2-P1-1 | WidgetBoundary 未被使用 | 在 9 个关键组件中应用 WidgetBoundary | ✅ 已在 3 个页面应用 |
|
||||
| v2-P1-2 | admin/school/grades/insights 缺失 loading/error | 补齐 loading.tsx 和 error.tsx | ✅ 已补齐 |
|
||||
| v2-P1-3 | 架构数据 JSON 005 权限记录错误 | 修正为 `school:manage` | ✅ 已修正 |
|
||||
| v2-P1-4 | i18n 完全未接入 | 21 个组件接入 useTranslations | ✅ 21 个组件全部接入 |
|
||||
| v2-P1-5 | exportGradesAction 安全漏洞 | 传递 currentUserId 和 dataScope | ✅ 已修复 |
|
||||
| v2-P1-6 | diagnostic 缺少 stats-service.ts | 抽取纯统计函数 | ✅ 已抽取(352 行,12 个纯函数) |
|
||||
| v2-P1-7 | 热力图色块 a11y | 添加 role="img" + aria-label | ✅ 已修复 |
|
||||
| v2-P1-8 | getKnowledgePointStats 无参调用 | 页面先查班级再传参 | ✅ 已修复 |
|
||||
| v2-P1-9 | updateMasteryFromSubmission 覆盖逻辑 | 改为累积计算 | ✅ 已改为累积模式 |
|
||||
|
||||
### P2(本次实施)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|----------|------|
|
||||
| v2-P2-1 | 5 个路由缺失 error.tsx | 补齐 | ✅ 已补齐 7 个 error.tsx |
|
||||
| v2-P2-2 | lib/grade-utils.ts 跨模块查询 | 改用 classes data-access | ✅ 已改用子查询 |
|
||||
| v2-P2-3 | 死代码清理 | 页面改用 Action 调用 | ✅ 已删除 2 个死 Action + 2 个死 schema |
|
||||
| v2-P2-4 | totalStudents 语义和平均掌握度计算 | 修正计算逻辑 | ✅ 已修正 |
|
||||
| v2-P2-5 | 多 upsert 无事务 | 包裹 db.transaction() | ✅ 已包裹事务 |
|
||||
| v2-P2-6 | 生成报告未校验掌握度数据 | 添加 totalKnowledgePoints === 0 校验 | ✅ 已添加校验 |
|
||||
| v2-P2-7 | 表单 Label 未关联控件 | 添加 htmlFor 和 id | ✅ 4 个组件已修复 |
|
||||
| v2-P2-8 | SearchParams 统一剩余文件 | 改用 @/shared/lib/search-params | ✅ 5 个文件已统一 |
|
||||
| v2-P2-9 | recorderName 硬编码和 grade-trend-card a11y | 修复 | ✅ 已修复 |
|
||||
| v2-P2-10 | 架构文档行数和路由记录 | 同步更新 | ✅ 004 和 005 已同步 |
|
||||
|
||||
### P3(长期,本次不实施)
|
||||
|
||||
P3-1 ~ P3-28 共 28 项长期改进,记录备查,后续迭代处理。
|
||||
|
||||
---
|
||||
|
||||
## 四、合规项确认(v2)
|
||||
|
||||
以下条目在 v2 审计中**已通过**:
|
||||
|
||||
- ✅ 所有 Server Action 调用 `requirePermission()`
|
||||
- ✅ 所有 Server Action 返回 `ActionState<T>`
|
||||
- ✅ 所有 Server Action 使用 `revalidatePath`
|
||||
- ✅ 无 `any` 类型
|
||||
- ✅ 无 `?!` 组合(可选链后非空断言)
|
||||
- ✅ 无模块循环依赖
|
||||
- ✅ 无 N+1 查询
|
||||
- ✅ 所有读查询函数使用 `cache()`
|
||||
- ✅ 文件行数全部合规(最大 batch-grade-entry.tsx 450 行 < 500)
|
||||
- ✅ i18n 翻译文件键完整(zh-CN 与 en 一致)
|
||||
- ✅ i18n/request.ts 已加载所有命名空间
|
||||
- ✅ studentId 可空 null 安全处理完整
|
||||
- ✅ diagnostic 跨模块依赖通过 data-access
|
||||
- ✅ grades data-access 统计逻辑已抽取到 stats-service.ts
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
本报告列出的 P1(9 项)和 P2(10 项)改进项将在本次实施中全部完成。P3 长期改进项记录备查,后续迭代处理。
|
||||
|
||||
实施顺序:
|
||||
1. P1 安全漏洞修复(v2-P1-5)
|
||||
2. P1 业务逻辑修复(v2-P1-8、v2-P1-9)
|
||||
3. P1 架构修复(v2-P1-6)
|
||||
4. P1 路由补齐(v2-P1-2)
|
||||
5. P1 a11y 修复(v2-P1-7)
|
||||
6. P1 WidgetBoundary 应用(v2-P1-1)
|
||||
7. P1 i18n 接入(v2-P1-4)
|
||||
8. P1 架构图修正(v2-P1-3)
|
||||
9. P2 改进项(v2-P2-1 ~ v2-P2-10)
|
||||
10. 验证:lint + tsc + 提交
|
||||
296
docs/architecture/audit/grades-diagnostic-audit-report-v3.md
Normal file
296
docs/architecture/audit/grades-diagnostic-audit-report-v3.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# 成绩和学情诊断模块易用性审计报告 v3
|
||||
|
||||
> 审查日期:2026-06-23
|
||||
> 审查范围:在 v1/v2 审计完成后,从**用户视角**对 `src/modules/grades/**`、`src/modules/diagnostic/**`、相关路由层进行易用性深度审计
|
||||
> 审查目的:对比同类型 K12 系统(PowerSchool、Infinite Campus、Skyward、Alma、Gradelink、RenWeb),发现功能易用性差距并实现改进
|
||||
> 审查方法:逐文件分析 44 个源文件,从教师/学生/家长/管理员四种角色视角评估每个功能的易用性
|
||||
|
||||
---
|
||||
|
||||
## 一、v2 完成情况确认
|
||||
|
||||
v2 审计报告所有 P1(9 项)和 P2(10 项)改进项均已真实落地:
|
||||
|
||||
| v2 编号 | 改进项 | 验证结果 |
|
||||
|---------|--------|----------|
|
||||
| v2-P1-1 | WidgetBoundary 应用 | ✅ 3 个页面已应用 |
|
||||
| v2-P1-2 | admin/school/grades/insights loading/error | ✅ 已补齐 |
|
||||
| v2-P1-3 | 架构 JSON 005 权限记录 | ✅ 已修正为 school:manage |
|
||||
| v2-P1-4 | i18n 接入 | ✅ 21 个组件全部接入 useTranslations |
|
||||
| v2-P1-5 | exportGradesAction 安全漏洞 | ✅ 已传递 currentUserId 和 dataScope |
|
||||
| v2-P1-6 | diagnostic stats-service.ts | ✅ 已抽取(352 行,12 个纯函数) |
|
||||
| v2-P1-7 | 热力图色块 a11y | ✅ 已添加 role="img" + aria-label |
|
||||
| v2-P1-8 | getKnowledgePointStats 无参调用 | ✅ 已修复 |
|
||||
| v2-P1-9 | updateMasteryFromSubmission 覆盖逻辑 | ✅ 已改为累积模式 |
|
||||
| v2-P2-1 ~ P2-10 | 10 项 P2 改进 | ✅ 全部完成 |
|
||||
|
||||
---
|
||||
|
||||
## 二、同类 K12 系统易用性对比
|
||||
|
||||
### 2.1 成绩录入功能对比
|
||||
|
||||
| 功能 | PowerSchool | Infinite Campus | Skyward | Alma | Gradelink | RenWeb | 本系统(v2) |
|
||||
|------|-------------|-----------------|---------|------|-----------|--------|--------------|
|
||||
| 单条录入 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 批量录入 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Excel 粘贴** | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
|
||||
| **行内编辑** | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
| **撤销功能** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **草稿自动保存** | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅(localStorage) |
|
||||
| **键盘导航** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅(Enter 跳转) |
|
||||
| **实时统计** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
|
||||
### 2.2 成绩查询功能对比
|
||||
|
||||
| 功能 | PowerSchool | Infinite Campus | Skyward | Alma | Gradelink | RenWeb | 本系统(v2) |
|
||||
|------|-------------|-----------------|---------|------|-----------|--------|--------------|
|
||||
| 学生成绩列表 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **编辑入口** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌(仅删除) |
|
||||
| 成绩趋势图 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **排名显示** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌(硬编码 0) |
|
||||
| **排名趋势** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌(Action 已实现未调用) |
|
||||
| **班级平均对比** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| 导出 Excel | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### 2.3 学情诊断功能对比
|
||||
|
||||
| 功能 | PowerSchool | Infinite Campus | Skyward | Alma | Gradelink | RenWeb | 本系统(v2) |
|
||||
|------|-------------|-----------------|---------|------|-----------|--------|--------------|
|
||||
| 知识点掌握度 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| 强弱项分析 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| 班级诊断 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **报告发布通知** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **弱项练习推荐** | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **报告导出** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **按知识点筛选学生** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
### 2.4 关键差距总结
|
||||
|
||||
对比同类系统,本系统在以下方面存在明显差距:
|
||||
|
||||
1. **成绩列表无编辑入口**:所有同类系统都支持在列表中直接编辑成绩,本系统仅有删除
|
||||
2. **不支持 Excel 粘贴**:PowerSchool/Infinite Campus/Skyward/Gradelink 都支持从 Excel 粘贴成绩,大幅提升录入效率
|
||||
3. **学生排名硬编码为 0**:所有同类系统都显示班级排名,本系统虽有 `getClassRanking` 函数但 `getStudentGradeSummary` 返回 `rank: 0`
|
||||
4. **排名趋势图未接入**:`getRankingTrendAction` 已实现但学生页面未调用,浪费已有功能
|
||||
5. **诊断报告发布无通知**:所有同类系统在报告发布时都会通知学生/家长,本系统仅更新状态
|
||||
6. **成绩录入不触发诊断更新**:成绩变化应反映到掌握度,本系统仅 exam submission 触发
|
||||
7. **无撤销功能**:Infinite Campus 支持撤销批量录入,本系统无此功能
|
||||
8. **无报告导出**:所有同类系统都支持导出诊断报告,本系统无此功能
|
||||
|
||||
---
|
||||
|
||||
## 三、v3 新发现问题
|
||||
|
||||
### 3.1 P1 严重易用性问题
|
||||
|
||||
#### v3-P1-1 成绩列表无编辑入口
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| [grade-record-list.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-list.tsx) L102-112 | 仅有删除按钮,无编辑按钮 | 教师录错成绩后只能删除重录,效率极低 |
|
||||
| [actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L156-188 | `updateGradeRecordAction` 已实现但前端从未调用 | 已有功能浪费 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Skyward、Alma、RenWeb 全部支持列表内编辑成绩。
|
||||
|
||||
**用户痛点**:教师录入 50 人成绩后发现某项分数录错,当前流程是"删除→重新打开录入页→重新填写全部字段→保存",至少 5 步操作;同类系统仅需"点击编辑→修改分数→保存"2 步。
|
||||
|
||||
**改进方向**:在 `grade-record-list.tsx` 增加编辑按钮,弹出 Dialog 复用 `GradeRecordForm` 的字段(标题、分数、满分、类型、学期、备注),调用 `updateGradeRecordAction`。
|
||||
|
||||
#### v3-P1-2 批量录入不支持 Excel 粘贴
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| [batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L119-123 | `handleScoreChange` 只接受单值输入,无 paste 事件处理 | 教师无法从 Excel 粘贴一列成绩 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Skyward、Gradelink 都支持从 Excel 复制一列分数粘贴到批量录入表格。
|
||||
|
||||
**用户痛点**:教师常在 Excel 中整理好成绩(如按学号排序的分数列),当前需要逐个手动输入 50 人分数;同类系统支持复制 Excel 一列→粘贴到第一个输入框→自动填充所有学生。
|
||||
|
||||
**改进方向**:在分数输入框添加 `onPaste` 处理器,解析剪贴板文本(按行/Tab 分割),按学生顺序自动填充。
|
||||
|
||||
#### v3-P1-3 学生排名硬编码为 0 且排名趋势图未接入
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| [data-access.ts](file:///e:/Desktop/CICD/src/modules/grades/data-access.ts) L351 | `getStudentGradeSummary` 返回 `rank: 0` 硬编码 | 学生看不到自己的班级排名 |
|
||||
| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) | 未调用 `getRankingTrendAction` | 排名趋势图功能浪费 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Skyward、Alma、Gradelink、RenWeb 全部显示学生班级排名。
|
||||
|
||||
**用户痛点**:学生/家长查看成绩时最关心"班级第几名",当前页面只显示平均分和记录列表,无法回答"孩子排第几"这个核心问题。
|
||||
|
||||
**改进方向**:
|
||||
1. `getStudentGradeSummary` 调用 `getClassRanking` 计算实际排名
|
||||
2. 学生页面接入 `getRankingTrendAction`,显示排名趋势图
|
||||
|
||||
#### v3-P1-4 诊断报告发布无通知机制
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| [diagnostic/actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) L78-100 | `publishReportAction` 仅执行 `revalidatePath`,未触发通知 | 学生/家长不知道报告已发布 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Skyward、Alma、Gradelink、RenWeb 全部在报告发布时发送通知。
|
||||
|
||||
**用户痛点**:教师发布诊断报告后,学生/家长需要主动登录查看才知道有新报告,信息传递滞后;同类系统会自动推送站内通知/邮件/短信。
|
||||
|
||||
**改进方向**:`publishReportAction` 调用 `notifications` 模块的 `createNotification`,向学生(个人报告)或全班学生(班级报告)发送站内通知。
|
||||
|
||||
#### v3-P1-5 成绩录入不触发诊断掌握度更新
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| [diagnostic/data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) L64-139 | `updateMasteryFromSubmission` 只从 exam submission 触发 | 手动录入的成绩不反映到掌握度 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Alma 的成绩变化会自动更新学情分析。
|
||||
|
||||
**用户痛点**:教师手动录入期中考试成绩后,学情诊断页面仍显示旧数据,导致诊断报告与成绩单不一致。
|
||||
|
||||
**改进方向**:在 `createGradeRecord` 和 `batchCreateGradeRecords` 后,若成绩关联了 examId,调用 `updateMasteryFromSubmission` 更新掌握度。
|
||||
|
||||
### 3.2 P2 中等易用性问题
|
||||
|
||||
#### v3-P2-1 学生成绩过滤器科目使用名称而非 ID
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [student/grades/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/grades/page.tsx) L49 | `r.subjectName !== subjectFilter` 按名称过滤,科目重名时会冲突 |
|
||||
|
||||
**改进方向**:改为按 subjectId 过滤,`GradeFilters` 组件的科目选项使用 ID 作为 value。
|
||||
|
||||
#### v3-P2-2 成绩趋势图无班级平均对比线
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [grade-trend-card.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-trend-card.tsx) | 仅显示学生个人趋势,无班级平均对比 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus、Skyward、Alma 都支持个人 vs 班级平均对比。
|
||||
|
||||
**改进方向**:`GradeTrendCard` 接收 `classAverageData` prop,在趋势图中添加第二条对比线。
|
||||
|
||||
#### v3-P2-3 批量录入无撤销功能
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) | 提交后无法撤销,录错全班成绩需要逐条删除 |
|
||||
|
||||
**同类系统对比**:Infinite Campus 支持撤销最近一次批量录入。
|
||||
|
||||
**改进方向**:`batchCreateGradeRecordsAction` 返回创建的记录 ID 列表,前端缓存到 sessionStorage,提供"撤销"按钮调用批量删除。
|
||||
|
||||
#### v3-P2-4 诊断报告无导出功能
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| diagnostic 模块 | 无导出功能,教师无法将诊断报告导出为 PDF/Excel |
|
||||
|
||||
**同类系统对比**:所有 6 个同类系统都支持导出诊断报告。
|
||||
|
||||
**改进方向**:新增 `exportDiagnosticReportAction`,导出为 Excel(复用 grades/export.ts 模式)。
|
||||
|
||||
#### v3-P2-5 班级诊断不支持按知识点筛选学生
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) | 无法按"某知识点掌握度 < 60%"筛选学生列表 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Infinite Campus 支持按知识点筛选学生。
|
||||
|
||||
**改进方向**:`class-diagnostic-view.tsx` 增加知识点筛选下拉框,筛选出该知识点掌握度低于阈值的学生。
|
||||
|
||||
#### v3-P2-6 弱项无个性化练习推荐
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx) | "Practice" 按钮无实际跳转目标 |
|
||||
|
||||
**同类系统对比**:PowerSchool、Alma 支持基于弱项推荐练习题。
|
||||
|
||||
**改进方向**:`student-diagnostic-view.tsx` 的"Practice"按钮跳转到题目库,带知识点筛选参数。
|
||||
|
||||
#### v3-P2-7 成绩分析页无学期/考试筛选
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [teacher/grades/analytics/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/grades/analytics/page.tsx) | 仅有班级/科目/年级筛选,无学期和考试筛选 |
|
||||
|
||||
**改进方向**:`AnalyticsFilters` 增加学期和考试筛选下拉框。
|
||||
|
||||
#### v3-P2-8 家长页面缺失趋势图
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| `src/app/(dashboard)/parent/grades/page.tsx` | 仅显示成绩列表,无趋势图 |
|
||||
|
||||
**改进方向**:家长页面复用 `GradeTrendCard` 显示子女成绩趋势。
|
||||
|
||||
#### v3-P2-9 管理员无全校成绩汇总视图
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| `src/app/(dashboard)/admin/school/grades/insights/page.tsx` | 仅有单班级分析,无全校汇总 |
|
||||
|
||||
**改进方向**:新增全校成绩汇总卡片(各年级平均分、及格率、优秀率对比)。
|
||||
|
||||
#### v3-P2-10 批量录入无服务端草稿自动保存
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| [batch-grade-entry.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/batch-grade-entry.tsx) L192-205 | 草稿仅保存到 localStorage,换设备丢失 |
|
||||
|
||||
**改进方向**:新增 `saveGradeDraftAction` 和 `getGradeDraftAction`,将草稿保存到 DB。
|
||||
|
||||
### 3.3 P3 长期易用性问题(记录但不本次实施)
|
||||
|
||||
| 编号 | 问题 | 位置 |
|
||||
|------|------|------|
|
||||
| v3-P3-1 | 成绩录入无模板下载 | batch-grade-entry.tsx |
|
||||
| v3-P3-2 | 成绩列表无批量操作 | grade-record-list.tsx |
|
||||
| v3-P3-3 | 诊断报告无自定义模板 | data-access-reports.ts |
|
||||
| v3-P3-4 | 成绩趋势图无日期范围选择 | grade-trend-card.tsx |
|
||||
| v3-P3-5 | 班级对比图无显著性标记 | class-comparison-chart.tsx |
|
||||
| v3-P3-6 | 学生诊断无历史对比 | student-diagnostic-view.tsx |
|
||||
| v3-P3-7 | 成绩录入无语音输入 | batch-grade-entry.tsx |
|
||||
| v3-P3-8 | 诊断报告无分享功能 | report-list.tsx |
|
||||
|
||||
---
|
||||
|
||||
## 四、v3 改进优先级
|
||||
|
||||
### P1(本次实施)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|----------|------|
|
||||
| v3-P1-1 | 成绩列表无编辑入口 | 增加编辑按钮,Dialog 内编辑 | ✅ 已完成 |
|
||||
| v3-P1-2 | 批量录入不支持 Excel 粘贴 | 添加 onPaste 处理器 | ✅ 已完成 |
|
||||
| v3-P1-3 | 学生排名硬编码且趋势图未接入 | 计算实际排名 + 接入趋势图 | ✅ 已完成 |
|
||||
| v3-P1-4 | 诊断报告发布无通知 | 对接 notifications 模块 | ✅ 已完成 |
|
||||
| v3-P1-5 | 成绩录入不触发诊断更新 | 关联 examId 时触发掌握度更新 | ✅ 已完成 |
|
||||
|
||||
### P2(本次实施)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|----------|------|
|
||||
| v3-P2-1 | 科目过滤器用名称 | 改用 subjectId | ✅ 已完成 |
|
||||
| v3-P2-2 | 趋势图无班级对比 | 添加班级平均对比线 | ✅ 已完成 |
|
||||
| v3-P2-3 | 批量录入无撤销 | 返回 ID 列表 + 撤销按钮 | ✅ 已完成 |
|
||||
| v3-P2-4 | 诊断报告无导出 | 新增 exportDiagnosticReportAction | ✅ 已完成 |
|
||||
| v3-P2-5 | 班级诊断无知识点筛选 | 增加知识点筛选下拉框 | ✅ 已完成 |
|
||||
| v3-P2-6 | 弱项无练习推荐 | Practice 按钮跳转题目库 | ✅ 已完成 |
|
||||
| v3-P2-7 | 分析页无学期/考试筛选 | AnalyticsFilters 增加筛选 | ✅ 已完成 |
|
||||
| v3-P2-8 | 家长页面无趋势图 | 复用 GradeTrendCard | ✅ 已完成 |
|
||||
| v3-P2-9 | 管理员无全校汇总 | 新增全校汇总卡片 | ✅ 已完成 |
|
||||
| v3-P2-10 | 草稿仅本地 | 新增服务端草稿保存 | ✅ 已完成 |
|
||||
|
||||
### P3(长期,本次不实施)
|
||||
|
||||
v3-P3-1 ~ v3-P3-8 共 8 项长期易用性改进,记录备查,后续迭代处理。
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
实施顺序:
|
||||
1. P1 易用性核心修复(v3-P1-1 ~ v3-P1-5)
|
||||
2. P2 易用性增强(v3-P2-1 ~ v3-P2-10)
|
||||
3. 验证:lint + tsc + 架构文档同步
|
||||
240
docs/architecture/audit/grades-diagnostic-audit-report-v4.md
Normal file
240
docs/architecture/audit/grades-diagnostic-audit-report-v4.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 成绩与诊断模块易用性审计报告 v4
|
||||
|
||||
> **审计日期**:2026-06-23
|
||||
> **审计范围**:成绩模块(grades)+ 诊断模块(diagnostic)
|
||||
> **对标系统**:PowerSchool、Infinite Campus、Skyward、Alma、Gradelink、RenWeb、Google Classroom、Canvas、超星学习通、ClassIn
|
||||
> **前置文档**:[v3 审计报告](./grades-diagnostic-audit-report-v3.md)(5 P1 + 10 P2 已全部完成)
|
||||
|
||||
---
|
||||
|
||||
## 一、v3 完成确认
|
||||
|
||||
v3 审计报告中 **5 个 P1 + 10 个 P2 改进项全部已实现并验证通过**(tsc + lint 通过,架构文档已同步)。
|
||||
|
||||
---
|
||||
|
||||
## 二、v4 新增易用性问题(深度分析)
|
||||
|
||||
本轮分析从 12 个维度对成绩和诊断模块进行了深度审查,对比 10 个同类 K12 系统,共发现 **48 个易用性问题**(成绩模块 24 项 + 诊断模块 24 项)。
|
||||
|
||||
### 严重程度分布
|
||||
|
||||
| 严重程度 | 成绩模块 | 诊断模块 | 合计 | 本次实施 |
|
||||
|---------|---------|---------|------|---------|
|
||||
| P1(核心缺陷) | 12 | 12 | 24 | 12 项 |
|
||||
| P2(易用性增强) | 22 | 20 | 42 | 0 项(下迭代) |
|
||||
| P3(长期优化) | 2 | 7 | 9 | 0 项(记录备查) |
|
||||
|
||||
### 本次实施范围
|
||||
|
||||
聚焦 P1 中影响**数据安全、通知机制、基础可读性、移动端可用性**的 12 项改进。
|
||||
|
||||
---
|
||||
|
||||
## 三、P1 改进项详情(本次实施)
|
||||
|
||||
### 数据安全修复(诊断模块,3 项)
|
||||
|
||||
#### v4-P1-1 getDiagnosticReports 无 dataScope 过滤(数据泄露)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) L115-147 |
|
||||
| 问题 | `getDiagnosticReports` 接收 filters 但无 dataScope 参数,教师调用时返回全校所有报告 |
|
||||
| 对比 | PowerSchool、Infinite Campus 严格按教师所教班级过滤 |
|
||||
| 改进 | 增加 dataScope 参数,教师仅返回所教班级学生报告 |
|
||||
|
||||
#### v4-P1-2 教师学生诊断页未校验师生关系
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) L24-32 |
|
||||
| 问题 | 仅校验 class_members 和 children,未校验 class_taught,教师可通过 URL 查看任意学生 |
|
||||
| 对比 | PowerSchool、Infinite Campus 严格校验师生关系 |
|
||||
| 改进 | 增加 class_taught 校验,查询 studentId 是否属于教师所教班级 |
|
||||
|
||||
#### v4-P1-3 学生可见草稿报告(发布流程缺陷)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [student/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/diagnostic/page.tsx) L13-16 |
|
||||
| 问题 | 学生/家长调用 getDiagnosticReports 未传 status 过滤,且组件回退到 reports[0](可能是草稿) |
|
||||
| 对比 | 所有对标系统严格区分草稿/已发布 |
|
||||
| 改进 | 学生/家长页面传 status: "published",移除组件回退逻辑 |
|
||||
|
||||
### 通知机制修复(3 项)
|
||||
|
||||
#### v4-P1-4 班级报告发布不通知学生
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) L96-109 |
|
||||
| 问题 | publishReportAction 仅当 studentId 非空时通知,班级报告 studentId=null 全班不通知 |
|
||||
| 对比 | 所有对标系统班级报告发布均通知全班 |
|
||||
| 改进 | learningDiagnosticReports 表新增 classId 字段,班级报告发布时查询全班学生批量通知 |
|
||||
|
||||
#### v4-P1-5 家长未收到子女报告发布通知
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) L102-108 |
|
||||
| 问题 | createNotification 仅通知学生本人,未查询 parent_student_relations 通知家长 |
|
||||
| 对比 | PowerSchool、Infinite Campus、超星学习通同步通知家长 |
|
||||
| 改进 | 发布通知时查询家长 userId 列表,批量发送通知 |
|
||||
|
||||
#### v4-P1-6 成绩录入无通知机制
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/grades/actions.ts) L82-130 |
|
||||
| 问题 | createGradeRecordAction 和 batchCreateGradeRecordsAction 录入后仅 revalidatePath,不触发通知 |
|
||||
| 对比 | PowerSchool、Canvas、超星学习通成绩发布自动通知学生和家长 |
|
||||
| 改进 | 录入成功后调用通知模块,通知学生本人和家长 |
|
||||
|
||||
### 可读性修复(3 项)
|
||||
|
||||
#### v4-P1-7 成绩列表缺少颜色编码
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [grade-record-list.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-list.tsx) L161-163 |
|
||||
| 问题 | 分数展示为纯文本,不及格不标红,优秀不标绿 |
|
||||
| 对比 | PowerSchool、Canvas、超星学习通均按区间着色 |
|
||||
| 改进 | 新增 ScoreCell 组件,根据得分率着色(红<60%/黄60-84%/绿≥85%) |
|
||||
|
||||
#### v4-P1-8 热力图缺少颜色图例
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L166-206 |
|
||||
| 问题 | 热力图渲染了色块但无图例说明颜色含义 |
|
||||
| 对比 | PowerSchool、Infinite Campus、Alma 热力图均带图例 |
|
||||
| 改进 | 热力图卡片底部增加图例条 |
|
||||
|
||||
#### v4-P1-9 家长页静默丢弃查询失败的子女
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [parent/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx) L31-48 |
|
||||
| 问题 | Promise.allSettled rejected 状态被静默丢弃,家长不知有子女数据加载失败 |
|
||||
| 对比 | PowerSchool、Infinite Campus 显示错误提示并允许重试 |
|
||||
| 改进 | 保留 rejected 项,渲染错误卡片提供重试按钮 |
|
||||
|
||||
### 移动端修复(2 项)
|
||||
|
||||
#### v4-P1-10 成绩列表表格移动端溢出
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [grade-record-list.tsx](file:///e:/Desktop/CICD/src/modules/grades/components/grade-record-list.tsx) L138-196 |
|
||||
| 问题 | 10 列表格无水平滚动容器,手机端溢出 |
|
||||
| 对比 | PowerSchool、Infinite Campus 移动端表格可横向滚动 |
|
||||
| 改进 | 表格容器添加 overflow-x-auto |
|
||||
|
||||
#### v4-P1-11 诊断模块表格移动端溢出
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) L242-376 |
|
||||
| 问题 | 多个表格无 overflow-x-auto 包裹,手机端溢出 |
|
||||
| 对比 | 所有对标系统移动端表格可横向滚动 |
|
||||
| 改进 | 所有 Table 外层包裹 overflow-x-auto |
|
||||
|
||||
### 家长端导出修复(1 项)
|
||||
|
||||
#### v4-P1-12 家长端导出按钮为占位实现
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [parent-export-button.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-export-button.tsx) L25-31 |
|
||||
| 问题 | handleExport 仅 setTimeout 后 toast "coming soon",无实际导出 |
|
||||
| 对比 | PowerSchool、Infinite Campus 家长端完整导出功能 |
|
||||
| 改进 | 接入 exportGradesAction,支持按 studentId 导出 |
|
||||
|
||||
---
|
||||
|
||||
## 四、P2 改进项(下迭代规划,本次不实施)
|
||||
|
||||
成绩模块 22 项 + 诊断模块 20 项,共 42 项 P2 易用性增强,记录备查。
|
||||
|
||||
### 成绩模块 P2 代表性问题
|
||||
|
||||
- v4-P2-1 MAX_SCORE 硬编码与 fullScore 不一致
|
||||
- v4-P2-2 缺少自动计算与智能填充
|
||||
- v4-P2-3 成绩表格不支持列排序
|
||||
- v4-P2-4 班级排名缺少进步/退步趋势标识
|
||||
- v4-P2-5 趋势图缺少交互式钻取
|
||||
- v4-P2-6 缺少科目相关性分析
|
||||
- v4-P2-7 缺少成绩发布状态控制
|
||||
- v4-P2-8 grade_managed scope 校验过于宽松
|
||||
- v4-P2-9 历史成绩访问无时间窗口限制
|
||||
- v4-P2-10 缺少 CSV 导出与打印友好视图
|
||||
|
||||
### 诊断模块 P2 代表性问题
|
||||
|
||||
- v4-P2-1 报告内容硬编码无模板系统
|
||||
- v4-P2-2 雷达图截断知识点名称无 tooltip
|
||||
- v4-P2-3 无学生×知识点掌握度矩阵
|
||||
- v4-P2-4 无掌握度趋势/历史分析
|
||||
- v4-P2-5 无预测性分析(at-risk 预警)
|
||||
- v4-P2-6 无掌握度下降预警
|
||||
- v4-P2-7 通知类型使用 "grade" 而非专用类型
|
||||
- v4-P2-8 grade_managed 范围未处理
|
||||
- v4-P2-9 导出 action 未校验报告归属
|
||||
- v4-P2-10 无 PDF 导出
|
||||
|
||||
---
|
||||
|
||||
## 五、P3 长期改进(记录备查)
|
||||
|
||||
成绩模块 2 项 + 诊断模块 7 项,共 9 项长期优化。
|
||||
|
||||
### 代表性问题
|
||||
- v4-P3-1 成绩录入无语音输入
|
||||
- v4-P3-2 缺少成绩录入指引与新手引导
|
||||
- v4-P3-3 无定时/自动化报告生成
|
||||
- v4-P3-4 色盲用户友好性不足
|
||||
- v4-P3-5 无知识点前置依赖图
|
||||
- v4-P3-6 雷达图键盘不可达
|
||||
- v4-P3-7 无数据置信度指示
|
||||
|
||||
---
|
||||
|
||||
## 六、实施计划
|
||||
|
||||
实施顺序:
|
||||
1. 数据安全修复(v4-P1-1 ~ v4-P1-3)— 最高优先级
|
||||
2. 通知机制修复(v4-P1-4 ~ v4-P1-6)
|
||||
3. 可读性修复(v4-P1-7 ~ v4-P1-9)
|
||||
4. 移动端修复(v4-P1-10 ~ v4-P1-11)
|
||||
5. 家长端导出修复(v4-P1-12)
|
||||
6. 验证:lint + tsc + 架构文档同步
|
||||
|
||||
---
|
||||
|
||||
## 七、实施状态跟踪
|
||||
|
||||
### P1(本次实施)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|----------|------|
|
||||
| v4-P1-1 | getDiagnosticReports 无 dataScope 过滤 | 增加 dataScope 参数 | ✅ 已完成 |
|
||||
| v4-P1-2 | 教师学生诊断页未校验师生关系 | 增加 class_taught 校验 | ✅ 已完成 |
|
||||
| v4-P1-3 | 学生可见草稿报告 | 传 status: "published" | ✅ 已完成 |
|
||||
| v4-P1-4 | 班级报告发布不通知学生 | 新增 classId 字段 + 批量通知 | ✅ 已完成 |
|
||||
| v4-P1-5 | 家长未收到报告发布通知 | 查询家长 userId 批量通知 | ✅ 已完成 |
|
||||
| v4-P1-6 | 成绩录入无通知机制 | 录入后通知学生和家长 | ✅ 已完成 |
|
||||
| v4-P1-7 | 成绩列表缺少颜色编码 | 新增 ScoreCell 组件 | ✅ 已完成 |
|
||||
| v4-P1-8 | 热力图缺少颜色图例 | 增加图例条 | ✅ 已完成 |
|
||||
| v4-P1-9 | 家长页静默丢弃查询失败 | 渲染错误卡片 | ✅ 已完成 |
|
||||
| v4-P1-10 | 成绩列表表格移动端溢出 | 添加 overflow-x-auto | ✅ 已完成 |
|
||||
| v4-P1-11 | 诊断模块表格移动端溢出 | 添加 overflow-x-auto | ✅ 已完成 |
|
||||
| v4-P1-12 | 家长端导出按钮占位 | 接入 exportGradesAction | ✅ 已完成 |
|
||||
|
||||
### P2(下迭代规划)
|
||||
|
||||
成绩模块 22 项 + 诊断模块 20 项,共 42 项,本次不实施。
|
||||
|
||||
### P3(长期,本次不实施)
|
||||
|
||||
成绩模块 2 项 + 诊断模块 7 项,共 9 项,记录备查。
|
||||
262
docs/architecture/audit/settings-profile-audit-report-v2.md
Normal file
262
docs/architecture/audit/settings-profile-audit-report-v2.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 设置和个人信息模块审计报告 v2
|
||||
|
||||
> 审查日期:2026-06-22
|
||||
> 审查范围:`src/modules/settings/**`、`src/app/(dashboard)/settings/**`、`src/app/(dashboard)/admin/settings/**`、`src/app/(dashboard)/profile/**`
|
||||
> 上一版本:`settings-profile-audit-report.md`(v1,P0/P1/P2 共 13 项已全部完成)
|
||||
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.23、`docs/architecture/005_architecture_data.json`
|
||||
|
||||
---
|
||||
|
||||
## 一、v1 完成情况回顾
|
||||
|
||||
v1 报告中的 13 项改进建议已全部完成:
|
||||
|
||||
| 编号 | 优先级 | 标题 | 状态 |
|
||||
|------|--------|------|------|
|
||||
| P0-1 | P0 | 创建 settings i18n 命名空间 | ✅ 已完成 |
|
||||
| P0-2 | P0 | 消除跨模块 action 直调(SettingsService 接口) | ✅ 已完成 |
|
||||
| P0-3 | P0 | AdminSettingsView 接入真实数据层 | ✅ 已完成(新增 system_settings 表 + data-access + actions) |
|
||||
| P1-4 | P1 | 配置驱动角色路由 | ✅ 已完成 |
|
||||
| P1-5 | P1 | 分区 Error Boundary + Suspense | ✅ 已完成 |
|
||||
| P1-6 | P1 | Profile 页面拆分 | ✅ 已完成 |
|
||||
| P1-7 | P1 | 移除 `as` 断言 | ✅ 已完成 |
|
||||
| P2-8 | P2 | 头像上传 | ✅ 已完成(AvatarUpload + actions-avatar) |
|
||||
| P2-9 | P2 | 2FA / 会话管理 | ✅ 已完成(SecurityCenterCard + actions-security) |
|
||||
| P2-10 | P2 | 通知测试按钮 | ✅ 已完成(sendTestNotificationAction) |
|
||||
| P2-11 | P2 | 语言切换集成 | ✅ 已完成(ThemePreferencesCard 集成 LocaleSwitcher) |
|
||||
| P2-12 | P2 | 埋点接口 | ✅ 已完成(SettingsService.trackEvent 预留) |
|
||||
| P2-13 | P2 | a11y 修复 | ✅ 已完成 |
|
||||
|
||||
---
|
||||
|
||||
## 二、v2 新发现的问题
|
||||
|
||||
### 2.1 安全中心 2FA 为纯占位实现(P0)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) L21-46 | `toggleTwoFactorAction` 仅将 `twoFactorEnabled` 写入 system_settings 表,未接入 TOTP 密钥绑定、一次性码校验、备份码生成等真实 2FA 流程 | P0 |
|
||||
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) L105-120 | 用户开启 2FA 后立即显示"已启用",但实际登录时不会要求二次验证,造成虚假安全感 | P0 |
|
||||
| 同文件 L70 注释 | "占位实现,仅记录用户偏好" — 注释承认未接入真实流程 | P0 |
|
||||
|
||||
**后果**:用户以为启用了 2FA 但实际无效;安全合规审计会失败。
|
||||
|
||||
**建议**:在 v2 中要么 (a) 完整实现 TOTP 流程(绑定 authenticator + 验证一次性码 + 备份码),要么 (b) 将开关改为"即将推出"禁用状态,避免误导。
|
||||
|
||||
### 2.2 通知测试按钮为纯占位实现(P1)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [actions-notifications.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-notifications.ts) L29-39 | `sendTestNotificationAction` 仅 `console.info` + `Promise.resolve()`,未调用真实通知发送服务 | P1 |
|
||||
| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L119-133 | 点击测试按钮后总是显示"测试通知已发送",但用户不会收到任何通知 | P1 |
|
||||
|
||||
**后果**:用户以为测试通知已发送但收不到,无法真正验证渠道配置。
|
||||
|
||||
**建议**:接入 `notifications/dispatcher.ts` 的真实发送逻辑,或暂时将按钮改为禁用状态并标注"功能开发中"。
|
||||
|
||||
### 2.3 头像上传未清理旧文件(P1)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [actions-avatar.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-avatar.ts) L15-34 | `updateUserAvatarAction` 更新 `users.image` 字段后,旧头像文件仍留在文件存储中,无清理逻辑 | P1 |
|
||||
| [avatar-upload.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/avatar-upload.tsx) L108-124 | `handleRemove` 调用 `removeUserAvatarAction` 仅清空 `users.image`,未删除实际文件 | P1 |
|
||||
|
||||
**后果**:存储成本累积;孤儿文件无法回收。
|
||||
|
||||
**建议**:在 `removeUserAvatarAction` 和 `updateUserAvatarAction` 中,更新数据库前先记录旧 URL,更新成功后异步调用 `files/data-access.deleteFile` 清理旧文件。
|
||||
|
||||
### 2.4 SecurityCenterCard 缺少"登出其他会话"功能(P1)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) 全文 | 仅展示登录历史,无法远程登出其他设备的会话 | P1 |
|
||||
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) 全文 | 无 `revokeSessionAction` 或类似 Server Action | P1 |
|
||||
|
||||
**后果**:用户发现可疑登录后无法主动处置,只能修改密码被动应对。
|
||||
|
||||
**建议**:新增 `revokeSessionAction(sessionToken: string)`,删除 `sessions` 表对应记录;UI 在每条登录历史旁显示"登出"按钮(当前会话除外)。
|
||||
|
||||
### 2.5 AdminSettingsView 缺少表单变更检测(P1)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) L107-122 | `handleSave` 无条件保存,即使用户未修改任何字段也会触发 upsert 全部 16 个设置项 | P1 |
|
||||
| 同文件 L415 | "Reset" 按钮直接 `setValues(DEFAULT_VALUES)` 而非恢复到加载时的值,会丢失未保存的服务端数据 | P1 |
|
||||
|
||||
**后果**:无谓的数据库写入;Reset 语义错误。
|
||||
|
||||
**建议**:维护 `dirty` 状态(`JSON.stringify(values) !== JSON.stringify(loadedValues)`),Save 按钮禁用直到 dirty;Reset 恢复到 `loadedValues` 而非 `DEFAULT_VALUES`。
|
||||
|
||||
### 2.6 i18n 键 `settings.profile.avatar` 在 `profilePage` 命名空间下缺失(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) | 使用 `<AvatarUpload>` 但页面其他文本使用 `settings.profilePage.*` 命名空间,而 AvatarUpload 内部使用 `settings.profile.avatar.*`,命名空间不一致 | P2 |
|
||||
|
||||
**后果**:i18n 命名空间结构混乱,维护时易混淆。
|
||||
|
||||
**建议**:统一为 `settings.profile.avatar.*` 或 `settings.profilePage.avatar.*`,二选一。
|
||||
|
||||
### 2.7 SecurityCenterCard 未传递 `currentDeviceLabel`(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L182 | `<SecurityCenterCard />` 未传递 `currentDeviceLabel` prop | P2 |
|
||||
| [security-center-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/security-center-card.tsx) L192-194 | `isCurrent` 判断永远为 `false`,"当前会话"徽章永远不会显示 | P2 |
|
||||
|
||||
**后果**:用户无法在登录历史中识别当前会话。
|
||||
|
||||
**建议**:在 Server Component 层获取 `headers().get("user-agent")`,通过 props 传递到 `SecurityCenterCard`。
|
||||
|
||||
### 2.8 头像上传未限制文件名长度(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [avatar-upload.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/avatar-upload.tsx) L49-57 | `validateFile` 仅校验类型和大小,未校验文件名长度 | P2 |
|
||||
|
||||
**后果**:超长文件名可能导致数据库 `varchar` 字段截断或存储错误。
|
||||
|
||||
**建议**:添加 `file.name.length > 255` 校验。
|
||||
|
||||
### 2.9 通知偏好表单未做 dirty 检测(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L98-117 | Save 按钮始终可点击,无 dirty 检测 | P2 |
|
||||
|
||||
**后果**:用户误点 Save 触发不必要的 Server Action 调用。
|
||||
|
||||
**建议**:维护 dirty 状态,Save 按钮在无变更时禁用。
|
||||
|
||||
### 2.10 AdminSettingsView 文件行数接近上限(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) | 425 行,接近 500 行建议上限 | P2 |
|
||||
|
||||
**后果**:可读性下降,维护困难。
|
||||
|
||||
**建议**:将 4 个 Card 拆分为独立子组件(`SchoolInfoCard` / `SecurityPolicyCard` / `FileUploadCard` / `NotificationConfigCard`),主组件仅负责表单状态和提交逻辑。
|
||||
|
||||
### 2.11 缺少单元测试(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| `src/modules/settings/**/*.test.ts` | 整个 settings 模块无任何单元测试文件 | P2 |
|
||||
|
||||
**后果**:重构无回归保障;纯函数(`toSettingItem`、`parseUserAgent`、`formatRelativeTime`)无法独立验证。
|
||||
|
||||
**建议**:为以下纯函数添加单元测试:
|
||||
- `actions-system-settings.ts` 的 `toSettingItem`(值类型转换)
|
||||
- `security-center-card.tsx` 的 `parseUserAgent`、`formatRelativeTime`
|
||||
- `lib/student-overview-data.ts` 的 `buildStudentOverviewData`、`computeStudentStats`
|
||||
|
||||
### 2.12 2FA 状态查询存在 N+1 问题(P2)
|
||||
|
||||
| 位置 | 问题 | 严重性 |
|
||||
|------|------|--------|
|
||||
| [actions-security.ts](file:///e:/Desktop/CICD/src/modules/settings/actions-security.ts) L48-62 | `getTwoFactorStatus` 对每个用户分别查询 3 次 `system_settings` 表(enabled / method / enabledAt),共 3 次 DB 往返 | P2 |
|
||||
|
||||
**后果**:每次加载安全中心页面额外 3 次 DB 查询。
|
||||
|
||||
**建议**:使用 `getSystemSettingsByCategory("security_policy")` 一次查询所有 security_policy 分类下的设置,在内存中过滤当前用户的键。
|
||||
|
||||
---
|
||||
|
||||
## 三、改进优先级建议(v2)
|
||||
|
||||
### P0(紧急,影响安全/合规)
|
||||
|
||||
1. **2FA 真实实现或禁用开关**:要么完整实现 TOTP 流程,要么将开关改为"即将推出"禁用状态,避免虚假安全感。
|
||||
|
||||
### P1(重要,影响功能完整性)
|
||||
|
||||
2. **通知测试按钮接入真实发送逻辑**:调用 `notifications/dispatcher.ts` 发送真实通知,或暂时禁用按钮。
|
||||
3. **头像上传清理旧文件**:在 `removeUserAvatarAction` 和 `updateUserAvatarAction` 中添加旧文件清理逻辑。
|
||||
4. **会话远程登出**:新增 `revokeSessionAction`,UI 添加"登出"按钮。
|
||||
5. **AdminSettingsView 表单 dirty 检测**:Save 按钮在无变更时禁用;Reset 恢复到加载值。
|
||||
|
||||
### P2(优化,提升质量)
|
||||
|
||||
6. **统一 i18n 命名空间**:`settings.profile.avatar` 与 `settings.profilePage` 二选一。
|
||||
7. **SecurityCenterCard 传递 currentDeviceLabel**:Server Component 层获取 user-agent 传入。
|
||||
8. **头像上传文件名长度校验**:添加 `file.name.length > 255` 校验。
|
||||
9. **通知偏好表单 dirty 检测**:Save 按钮在无变更时禁用。
|
||||
10. **AdminSettingsView 拆分子组件**:4 个 Card 拆分为独立组件。
|
||||
11. **添加单元测试**:为纯函数添加测试覆盖。
|
||||
12. **2FA 状态查询优化**:合并 3 次 DB 查询为 1 次。
|
||||
|
||||
---
|
||||
|
||||
## 四、v2 实施计划
|
||||
|
||||
### 4.1 P0:2FA 真实实现或禁用
|
||||
|
||||
**方案选择**:考虑到完整 TOTP 实现需要额外的库(`otplib`)和 UI(QR 码扫描、备份码展示),v2 阶段先将开关改为"即将推出"禁用状态,避免虚假安全感。完整 TOTP 实现留待 v3。
|
||||
|
||||
**改动范围**:
|
||||
- `security-center-card.tsx`:Switch 添加 `disabled` 属性,显示"即将推出"徽章
|
||||
- i18n:添加 `twoFactor.comingSoon` 键
|
||||
|
||||
### 4.2 P1:通知测试按钮接入真实逻辑
|
||||
|
||||
**方案选择**:调用 `notifications/dispatcher.ts` 的 `dispatchNotification` 函数发送真实通知。
|
||||
|
||||
**改动范围**:
|
||||
- `actions-notifications.ts`:导入 `dispatchNotification`,根据 channel 调用对应渠道
|
||||
- 失败时返回具体错误信息
|
||||
|
||||
### 4.3 P1:头像上传清理旧文件
|
||||
|
||||
**改动范围**:
|
||||
- `actions-avatar.ts`:在更新前记录旧 image URL,更新成功后调用 `files/data-access.deleteFileByUrl` 清理
|
||||
- 需要先确认 `files/data-access` 是否有 `deleteFileByUrl` 函数,若无则新增
|
||||
|
||||
### 4.4 P1:会话远程登出
|
||||
|
||||
**改动范围**:
|
||||
- `actions-security.ts`:新增 `revokeSessionAction(sessionToken: string)`
|
||||
- `security-center-card.tsx`:每条登录历史旁添加"登出"按钮(当前会话除外)
|
||||
- i18n:添加 `recentLogins.revoke` / `revokeSuccess` / `revokeFailure` 键
|
||||
|
||||
### 4.5 P1:AdminSettingsView dirty 检测
|
||||
|
||||
**改动范围**:
|
||||
- `admin-settings-view.tsx`:维护 `loadedValues` 状态,计算 `isDirty`,Save 按钮禁用逻辑,Reset 恢复到 `loadedValues`
|
||||
|
||||
### 4.6 P2:其他优化项
|
||||
|
||||
逐项实施,每项改动范围较小,详见各小节。
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
v2 改动完成后需同步更新:
|
||||
|
||||
### 5.1 `004_architecture_impact_map.md` §2.23
|
||||
|
||||
- 更新"已知问题":标注 v2 新增/修复项
|
||||
- 更新"文件清单":新增测试文件、拆分后的子组件
|
||||
|
||||
### 5.2 `005_architecture_data.json`
|
||||
|
||||
- `modules.settings.exports`:新增 `revokeSessionAction` 等
|
||||
- `modules.settings.knownIssues`:更新 v2 状态
|
||||
- `dependencyMatrix`:settings → notifications 依赖(通知测试真实发送)
|
||||
|
||||
---
|
||||
|
||||
## 六、验收标准
|
||||
|
||||
v2 完成后应满足:
|
||||
|
||||
1. `npm run lint` 零错误(warnings 可接受)
|
||||
2. `npx tsc --noEmit` 零错误
|
||||
3. 2FA 开关为禁用状态或完整 TOTP 实现(二选一)
|
||||
4. 通知测试按钮发送真实通知或禁用(二选一)
|
||||
5. 头像更换/删除后旧文件被清理
|
||||
6. 安全中心可远程登出其他会话
|
||||
7. AdminSettingsView Save 按钮在无变更时禁用
|
||||
8. 至少 3 个纯函数有单元测试
|
||||
9. 架构图 004/005 已同步更新
|
||||
224
docs/architecture/audit/textbooks-audit-report-v2.md
Normal file
224
docs/architecture/audit/textbooks-audit-report-v2.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# 教材(Textbooks)模块审计报告 v2
|
||||
|
||||
> 审计日期:2026-06-22
|
||||
> 审计范围:`src/modules/textbooks/**`、`src/app/(dashboard)/teacher/textbooks/**`、`src/app/(dashboard)/student/learning/textbooks/**`
|
||||
> 对比基准:[textbooks-audit-report.md](./textbooks-audit-report.md)(v1)
|
||||
> 参照规则:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、v1 改进项完成状态总览
|
||||
|
||||
### 1.1 完成度统计
|
||||
|
||||
| 优先级 | 总数 | 已完成 | 部分完成 | 未完成 |
|
||||
|--------|------|--------|----------|--------|
|
||||
| P0 | 4 | 3 | 1(P0-3 i18n) | 0 |
|
||||
| P1 | 8 | 7 | 1(P1-6 类型断言) | 0 |
|
||||
| P2 | 6 | 4 | 1(P2-5 架构图同步) | 1(图谱方向键导航) |
|
||||
| **合计** | **18** | **14** | **3** | **1** |
|
||||
|
||||
### 1.2 各项状态明细
|
||||
|
||||
| 编号 | 标题 | 状态 | 关键证据 |
|
||||
|------|------|------|----------|
|
||||
| P0-1 | 跨模块 UI 依赖解耦 | ✅ 已完成 | `knowledge-point-dialogs.tsx` 改为 render prop,页面层注入 |
|
||||
| P0-2 | 前端权限硬编码 canEdit | ✅ 已完成 | `textbook-reader.tsx` 使用 `usePermission().hasPermission()` |
|
||||
| P0-3 | 全模块 i18n 改造 | ⚠️ 部分完成 | 约 85%,`chapter-sidebar-list.tsx`/`actions.ts`/`section-error-boundary.tsx` 未接入 |
|
||||
| P0-4 | Server Action 资源归属校验 | ✅ 已完成 | `actions.ts` 全部写 Action 调用 `verify*` 函数 |
|
||||
| P1-1 | data-access 数据范围过滤 | ✅ 已完成 | `getTextbooksWithScope` + 学生端按年级过滤 |
|
||||
| P1-2 | Error Boundary | ✅ 已完成 | 4 个 `error.tsx` + `section-error-boundary.tsx` |
|
||||
| P1-3 | 消除重复组件 | ✅ 已完成 | 删除 `knowledge-point-panel.tsx` 和 `create-knowledge-point-dialog.tsx` |
|
||||
| P1-4 | 抽取学科/年级配置 | ✅ 已完成 | `constants.ts` 集中管理 `SUBJECTS`/`GRADES`/`SUBJECT_COLORS` |
|
||||
| P1-5 | 导出纯函数并补单测 | ✅ 已完成 | `utils.ts` + `graph-layout.ts` + 两个测试文件 |
|
||||
| P1-6 | 修复类型断言 | ⚠️ 部分完成 | v1 的 3 处已修复,残留 3 处 `as string` |
|
||||
| P1-7 | 图谱 a11y | ✅ 已完成 | `role="img"`/`aria-label`/`<title>`/`aria-pressed` |
|
||||
| P1-8 | 统一删除确认 | ✅ 已完成 | `textbook-settings-dialog.tsx` 用 `AlertDialog` |
|
||||
| P2-1 | 统一空状态 | ✅ 已完成 | 全部使用 `EmptyState` |
|
||||
| P2-2 | 知识点高亮性能优化 | ✅ 已完成 | 单遍 alternation 正则 + `useMemo` |
|
||||
| P2-3 | 知识点懒加载 | ✅ 已完成 | 按章节懒加载 + 缓存 + 派生加载状态 |
|
||||
| P2-4 | 移动端阅读优化 | ✅ 已完成 | `Sheet` 抽屉 + 桌面端内联复用 |
|
||||
| P2-5 | 架构图同步 | ⚠️ 部分完成 | 005 JSON 新函数已加,`knownIssues`/`uiDeps`/`components` 过期 |
|
||||
| P2-6 | 埋点接口预留 | ✅ 已完成 | `analytics.tsx` 定义接口 + Provider + Hook |
|
||||
|
||||
---
|
||||
|
||||
## 二、v2 新发现的问题
|
||||
|
||||
### 2.1 i18n 完整性(P0,v1 遗留)
|
||||
|
||||
#### 问题 v2-1 | `chapter-sidebar-list.tsx` 完全未接入 i18n(P0)
|
||||
|
||||
- **位置**:[chapter-sidebar-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx)
|
||||
- **现象**:第 90 行 `"Toggle"`、第 118 行 `"Add Subchapter"`、第 130 行 `"Delete Chapter"`、第 258 行 `"Order updated"`、第 278 行 `"Cannot delete chapter with subchapters"`、第 332-341 行删除对话框文案全部硬编码英文
|
||||
- **翻译键已存在**:`dialog.chapter.deleteTitle`/`delete`/`deleting`/`cannotDeleteWithSubchapters`/`addSubchapter` 等
|
||||
- **影响**:中文用户看到英文文案,i18n 覆盖率不完整
|
||||
|
||||
#### 问题 v2-2 | `actions.ts` 错误消息全部硬编码英文(P0)
|
||||
|
||||
- **位置**:[actions.ts](file:///e:/Desktop/CICD/src/modules/textbooks/actions.ts)
|
||||
- **现象**:约 20+ 条消息硬编码,如第 47 行 `"Chapter does not belong to this textbook"`、第 56 行 `"Failed to reorder chapters"`
|
||||
- **影响**:用户看到的 toast 消息无法本地化
|
||||
- **建议**:Server Action 内使用 `getTranslations("textbooks.action")` 获取翻译
|
||||
|
||||
#### 问题 v2-3 | `section-error-boundary.tsx` 默认文案硬编码中文(P1)
|
||||
|
||||
- **位置**:[section-error-boundary.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/section-error-boundary.tsx) 第 52/55/59 行
|
||||
- **现象**:默认 fallback `"区块加载失败"` / `"请重试或刷新页面"` / `"重试"` 硬编码
|
||||
- **影响**:英文用户看到中文默认值
|
||||
|
||||
### 2.2 学科/年级显示未本地化(P1)
|
||||
|
||||
#### 问题 v2-4 | `textbook-card.tsx` 学科显示未本地化(P1)
|
||||
|
||||
- **位置**:[textbook-card.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-card.tsx) 第 41 行
|
||||
- **现象**:`{textbook.subject}` 直接显示原始值(如 "Mathematics"),未通过 `t(\`subject.${labelKey}\`)` 转换
|
||||
|
||||
#### 问题 v2-5 | 页面层学科/年级显示未本地化(P1)
|
||||
|
||||
- **位置**:
|
||||
- [teacher/textbooks/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) 第 64、66 行
|
||||
- [student/learning/textbooks/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx) 第 47、49 行
|
||||
- **现象**:`{textbook.subject}` 和 `{textbook.grade}` 直接显示原始值
|
||||
|
||||
### 2.3 类型安全(P2)
|
||||
|
||||
#### 问题 v2-6 | 残留 `as string` 断言(P2)
|
||||
|
||||
- **位置**:
|
||||
- [chapter-sidebar-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx) 第 226、257 行:`active.id as string`
|
||||
- [graph-layout.ts](file:///e:/Desktop/CICD/src/modules/textbooks/graph-layout.ts) 第 123 行:`kp.parentId as string`
|
||||
- **建议**:用类型守卫或 narrowing 替代
|
||||
|
||||
### 2.4 重复代码(P2)
|
||||
|
||||
#### 问题 v2-7 | `findParent` 与 `utils.ts` 的 `findChapterParent` 重复(P2)
|
||||
|
||||
- **位置**:[chapter-sidebar-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx) 第 215-224 行
|
||||
- **现象**:内联 `findParent` 函数与 `utils.ts` 导出的 `findChapterParent` 功能完全相同
|
||||
- **建议**:替换为 `import { findChapterParent } from "../utils"`
|
||||
|
||||
#### 问题 v2-8 | 4 个 `error.tsx` 文件几乎完全相同(P2)
|
||||
|
||||
- **位置**:4 个 `error.tsx` 文件
|
||||
- **现象**:内容完全一致(仅函数名不同)
|
||||
- **建议**:抽取为共享组件 `TextbookRouteError`
|
||||
|
||||
#### 问题 v2-9 | `student/learning/textbooks/page.tsx` 重复定义 `getParam`(P2)
|
||||
|
||||
- **位置**:[student/learning/textbooks/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/textbooks/page.tsx) 第 13-18 行
|
||||
- **现象**:本地定义 `getParam`,但 `@/shared/lib/search-params` 已导出
|
||||
- **建议**:统一从 `@/shared/lib/search-params` 导入
|
||||
|
||||
### 2.5 a11y 改进(P2)
|
||||
|
||||
#### 问题 v2-10 | 拖拽手柄无 `aria-label`(P2)
|
||||
|
||||
- **位置**:[chapter-sidebar-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx) 第 71-73 行
|
||||
- **现象**:`<div {...attributes} {...listeners}>` 拖拽手柄仅含 `GripVertical` 图标,无 `aria-label`
|
||||
|
||||
#### 问题 v2-11 | 知识点高亮 span 无可交互语义(P2)
|
||||
|
||||
- **位置**:[textbook-content-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx) 第 133-148 行
|
||||
- **现象**:高亮的知识点 `<span>` 仅 `data-kp-id` + `title`,无 `role="button"`/`aria-label`/`tabIndex`
|
||||
|
||||
#### 问题 v2-12 | 移动端抽屉触发按钮无 `aria-expanded`(P2)
|
||||
|
||||
- **位置**:[textbook-reader.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx) 第 361-368 行
|
||||
- **现象**:`<Button>` 未关联 `aria-expanded`/`aria-controls`
|
||||
|
||||
### 2.6 性能与状态管理(P2)
|
||||
|
||||
#### 问题 v2-13 | `textbook-reader.tsx` textbookId 变化时未清理缓存(P2)
|
||||
|
||||
- **位置**:[textbook-reader.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx) 第 110-142 行
|
||||
- **现象**:`requestedChaptersRef` 是 ref,当 textbookId 变化(用户切换教材)时不会清理,可能导致缓存命中错误章节的数据
|
||||
- **建议**:在 `useEffect` 中增加 textbookId 变化时清理 `kpsByChapter` 和 `requestedChaptersRef`
|
||||
|
||||
#### 问题 v2-14 | `TextbookContentPanel` 存在未使用的 props(P2)
|
||||
|
||||
- **位置**:[textbook-content-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx) 第 23-46 行
|
||||
- **现象**:`knowledgePoints`/`createDialogOpen`/`isCreating`/`onCreateKnowledgePoint` 4 个 props 在接口中定义但函数体内未解构使用
|
||||
- **建议**:移除这 4 个 props 及对应的传参
|
||||
|
||||
### 2.7 架构图同步(P2,v1 遗留)
|
||||
|
||||
#### 问题 v2-15 | 架构图 005 JSON 与 004 MD 同步不完整(P2)
|
||||
|
||||
- **005 JSON 未同步部分**:
|
||||
- `knownIssues` 数组仍列出所有 v1 的 P0/P1 问题为未解决
|
||||
- `uiDeps` 仍标注 "P0 待解耦",但代码已通过 render prop 解耦
|
||||
- `components` 数组仍列出已删除的组件,遗漏新增的 `SectionErrorBoundary`
|
||||
- `hooks` 签名函数名简写与实际不一致
|
||||
- 遗漏 `analytics.tsx`/`constants.ts`/`utils.ts`/`graph-layout.ts` 等新文件
|
||||
- **004 MD 未同步部分**:
|
||||
- §2.5 仍写 "⚠️ UI 层跨模块依赖(P0 待解耦)" — 已修复
|
||||
- "已知问题"列表未更新
|
||||
- 文件行数过期:`actions.ts` 317→377、`data-access.ts` 514→619、组件数 11→12
|
||||
|
||||
---
|
||||
|
||||
## 三、v2 改进优先级建议
|
||||
|
||||
### P0(紧急,i18n 完整性收尾)
|
||||
|
||||
1. **`chapter-sidebar-list.tsx` 接入 i18n**:替换所有硬编码英文为 `t(...)` 调用
|
||||
2. **`actions.ts` 接入 i18n**:使用 `getTranslations("textbooks.action")` 替换硬编码消息
|
||||
3. **`section-error-boundary.tsx` 默认文案 i18n**:默认值改为从 i18n 获取或使用翻译键
|
||||
|
||||
### P1(重要)
|
||||
|
||||
1. **学科/年级显示本地化**:`textbook-card.tsx`、`teacher/textbooks/[id]/page.tsx`、`student/learning/textbooks/[id]/page.tsx` 中 `{textbook.subject}`/`{textbook.grade}` 改为 `t(...)` 调用
|
||||
2. **架构图同步**(P2-5 收尾):更新 005 JSON 的 `knownIssues`/`uiDeps`/`components`/`hooks` 签名;更新 004 MD §2.5 的"已知问题"列表和文件清单行数
|
||||
3. **移除 `TextbookContentPanel` 的 4 个未使用 props**
|
||||
|
||||
### P2(优化)
|
||||
|
||||
1. **类型断言清理**:`chapter-sidebar-list.tsx` 的 `as string`、`graph-layout.ts` 的 `as string`
|
||||
2. **重复代码消除**:`findParent` 重复、4 个 error.tsx 重复、`getParam` 重复
|
||||
3. **a11y 补全**:拖拽手柄 aria-label、高亮 span role/aria-label、移动端抽屉 aria-expanded
|
||||
4. **`textbook-reader.tsx` textbookId 变化时清理缓存**
|
||||
5. **`highlightKnowledgePoints` 补 Markdown 边界测试**
|
||||
|
||||
---
|
||||
|
||||
## 四、行业差距对比(v1 第三节中仍未完成的项目)
|
||||
|
||||
以下 v1 报告中"行业差距对比"的项目在 v2 中仍未实现,作为长期路线图保留:
|
||||
|
||||
| 差距项 | 优先级 | 说明 |
|
||||
|--------|--------|------|
|
||||
| 富媒体嵌入(图片/音频/视频/公式/3D) | 长期 | 仍仅 Markdown + RichTextEditor |
|
||||
| 公式编辑(LaTeX/MathML) | 长期 | 无 |
|
||||
| 翻阅式阅读(页码/书签/进度记忆) | 长期 | 仍滚动 + URL chapterId |
|
||||
| 朗读/TTS | 长期 | 无 |
|
||||
| 笔记/划线/高亮/书签 | 长期 | 仅有"选区创建知识点" |
|
||||
| 知识图谱缩放/拖拽/力导向 | 长期 | 仍静态 SVG 树状布局 |
|
||||
| 知识点多级层级/跨章节关联/前置后置依赖 | 长期 | 仅 parentId 树 + chapterId 归属 |
|
||||
| admin 多教师协作编辑 + 版本历史 | 长期 | 无版本管理 |
|
||||
| parent 角色教材查看 | 长期 | 无 parent 入口 |
|
||||
| 章节跨级拖拽移动 | 长期 | reorderChapters 仅支持同级排序 |
|
||||
| 全文搜索(标题+正文+知识点) | 长期 | 仅列表页按 title/subject/grade/publisher 模糊搜索 |
|
||||
| 阅读进度条/章节完成度 | 长期 | 无 |
|
||||
| 知识点难度标注/教师标注重点 | 长期 | 仅有 level 字段,无 UI 录入 |
|
||||
|
||||
---
|
||||
|
||||
## 五、总结
|
||||
|
||||
### 关键成果(v1 → v2)
|
||||
|
||||
1. **架构解耦**:P0-1 跨模块 UI 依赖通过 render prop 完全解耦
|
||||
2. **权限安全**:P0-2 前端权限接入 `usePermission`,P0-4 Server Action 资源归属校验全覆盖,P1-1 学生端数据范围过滤
|
||||
3. **可维护性**:P1-3 重复组件删除,P1-4 配置集中化,P1-5 纯函数抽离 + 单测
|
||||
4. **用户体验**:P1-2 Error Boundary 全覆盖,P1-8 删除确认统一,P2-1 空状态统一,P2-2 高亮性能优化,P2-3 懒加载,P2-4 移动端抽屉
|
||||
5. **可扩展性**:P2-6 埋点接口预留
|
||||
|
||||
### 主要遗留(v2 需解决)
|
||||
|
||||
1. **i18n 完整性**:`chapter-sidebar-list.tsx`、`actions.ts`、`section-error-boundary.tsx` 三处未接入,学科/年级显示未本地化
|
||||
2. **架构图同步**:005 JSON 的 `knownIssues`/`uiDeps`/`components` 过期,004 MD §2.5 已知问题未更新
|
||||
3. **类型断言**:3 处 `as string` 可改善
|
||||
4. **重复代码**:`findParent`/`error.tsx`/`getParam` 三处重复
|
||||
5. **a11y**:拖拽手柄 aria-label、高亮 span 可交互性、移动端抽屉 aria-expanded
|
||||
6. **未使用 props**:`TextbookContentPanel` 的 4 个 props
|
||||
@@ -0,0 +1,543 @@
|
||||
# 备课模块重构设计 — 课文锚点画布
|
||||
|
||||
**日期**:2026-06-22
|
||||
**状态**:已确认,待实现
|
||||
**作者**:brainstorming session
|
||||
|
||||
## 背景与目标
|
||||
|
||||
当前备课模块基于 React Flow 节点图编辑器(v2 nodes+edges),支持 12 种 Block 类型、版本管理、自动保存、模板系统。但存在以下问题:
|
||||
|
||||
1. **创建课案时无法选择教材/章节**(UI 缺失),所有课案 `textbookId/chapterId` 都是 null
|
||||
2. **TextStudyBlock 与教材课文完全脱节**,教师手动粘贴纯文本到 textarea
|
||||
3. **编辑器内无法切换关联的教材/章节**
|
||||
4. **节点与课文无关联**,无法体现教学流程的时间线
|
||||
|
||||
**本次重构目标**:以课文正文为核心主体,教学节点围绕课文组织,通过锚点机制建立节点与课文位置的关联,形成教学流程时间线。
|
||||
|
||||
## 核心设计决策
|
||||
|
||||
### 决策 1:1 课案 = 1 课文
|
||||
|
||||
一个课案对应一篇课文(如《秋天》第一课时)。课文正文在画布中央作为核心主体,教学目标/重难点/导入/新授等节点围绕课文组织。
|
||||
|
||||
### 决策 2:画布式锚点布局
|
||||
|
||||
保留 React Flow 画布交互(缩放/平移/节点拖动/连线),课文作为特殊节点类型 `textbook_content` 嵌在画布中央,`draggable: false`(不可移动)但可缩放。
|
||||
|
||||
### 决策 3:两种锚定方式
|
||||
|
||||
- **范围锚定(range)**:选中一段文字 → 关联节点。文本背景色 = 节点颜色,默认 `opacity: 0`(完全透明),选中时 `opacity: 0.3`
|
||||
- **点锚定(point)**:点击文本某位置 → 插入占位符标记(①②③)。默认 `opacity: 0.3`(半透明),选中时 `opacity: 1`(不透明)
|
||||
|
||||
### 决策 4:连线透明度策略
|
||||
|
||||
- 锚点连线(anchor):默认 10%,选中节点时 100%
|
||||
- 流程连线(flow):节点间教学流程连线,正常显示
|
||||
|
||||
### 决策 5:每个节点类型完全定制字段
|
||||
|
||||
每个节点类型有独特的字段和交互,不是统一富文本。详见第 2 节。
|
||||
|
||||
### 决策 6:默认骨架 10 节点
|
||||
|
||||
创建课案时强制选择教材/章节,自动生成 10 个默认教学节点 + 1 个正文节点。
|
||||
|
||||
### 决策 7:实时拖动
|
||||
|
||||
节点拖动改为实时更新位置(`onNodeDrag`),而非当前的 `onNodeDragStop` 才更新。
|
||||
|
||||
### 决策 8:颜色保持现有方案
|
||||
|
||||
节点颜色复用 `lib/node-summary.ts` 的 `getNodeColor`,不改变现有配色。
|
||||
|
||||
## 第 1 节:整体架构与数据模型
|
||||
|
||||
### 1.1 整体布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 顶部工具栏:标题 | 教材/章节 | 保存状态 | 版本 | 保存按钮 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────┐ ┌──────────────┐ ┌─────────┐ │
|
||||
│ │ 导入 │───→│ 课文正文 │←───│ 新授 │ │
|
||||
│ │ 节点 │ │ (固定中央) │ │ 节点 │ │
|
||||
│ └─────────┘ │ │ └─────────┘ │
|
||||
│ │ 天气凉了① │ │
|
||||
│ ┌─────────┐ │ 天空那么蓝②│ ┌─────────┐ │
|
||||
│ │ 文本研习│───→│ ... │←───│ 练习 │ │
|
||||
│ │ 节点 │ │ │ └─────────┘ │
|
||||
│ └─────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ 教学目标│ │ 重难点 │ │ 作业 │ (未锚定节点) │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │
|
||||
│ [+ 添加节点] [+] [-] [⌖] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 数据模型升级(v2 → v3)
|
||||
|
||||
```typescript
|
||||
// 新增:正文节点类型
|
||||
interface TextbookContentNodeData {
|
||||
chapterId: string;
|
||||
content: string; // Markdown 正文(缓存)
|
||||
zoom: number; // 缩放比例 0.5-2.0
|
||||
}
|
||||
|
||||
// 新增:正文节点(继承 LessonPlanNode)
|
||||
interface TextbookContentNode extends LessonPlanNode {
|
||||
type: "textbook_content";
|
||||
data: TextbookContentNodeData;
|
||||
draggable: false; // 不可拖动
|
||||
}
|
||||
|
||||
// 新增:锚点类型
|
||||
type AnchorType = "range" | "point";
|
||||
|
||||
interface NodeAnchor {
|
||||
id: string;
|
||||
nodeId: string; // 关联的教学节点 ID
|
||||
type: AnchorType;
|
||||
start: number; // 正文纯文本偏移量
|
||||
end?: number; // range 锚定的结束偏移
|
||||
textPreview?: string; // range 锚定的文字预览
|
||||
}
|
||||
|
||||
// 新增:边类型
|
||||
type EdgeType = "anchor" | "flow";
|
||||
|
||||
interface AnchorEdge extends LessonPlanEdge {
|
||||
type: "anchor";
|
||||
source: string; // 教学节点 ID
|
||||
target: string; // 正文节点 ID
|
||||
anchorId: string; // 关联的 NodeAnchor ID
|
||||
}
|
||||
|
||||
interface FlowEdge extends LessonPlanEdge {
|
||||
type: "flow"; // 教学流程连线(如 导入→新授)
|
||||
}
|
||||
|
||||
// 升级:LessonPlanDocument v3
|
||||
interface LessonPlanDocument {
|
||||
version: 3;
|
||||
textbookContentNodeId: string; // 正文节点 ID(唯一)
|
||||
nodes: (LessonPlanNode | TextbookContentNode)[];
|
||||
edges: (AnchorEdge | FlowEdge)[];
|
||||
anchors: NodeAnchor[]; // 新增:锚点数组
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 迁移策略
|
||||
|
||||
- `migrateV2ToV3(doc, chapterId?, chapterContent?)`:将 v2 文档升级为 v3
|
||||
- 如果有关联的 chapterId,创建 `TextbookContentNode` 注入正文
|
||||
- 现有节点保留,`edges` 保留为 `flow` 类型
|
||||
- `anchors` 初始化为空数组
|
||||
- `normalizeDocument` 优先识别 v3,v2 自动迁移
|
||||
|
||||
## 第 2 节:节点类型与定制字段
|
||||
|
||||
### 2.1 节点类型清单(11 种 + 1 正文节点)
|
||||
|
||||
| 类型 | BlockType | 定制字段 | 输入 | 输出 |
|
||||
|------|-----------|---------|------|------|
|
||||
| 教学目标 | `objective` | `objectives: { dimension, text }[]` | 三维目标列表 | 结构化目标 |
|
||||
| 重难点 | `key_point` | `keyPoints: { type: "key"\|"difficult", text }[]` | 重点/难点分组 | 结构化重难点 |
|
||||
| 导入 | `import` | `method, prompt, durationMin` | 导入方式+提问+时长 | 导入脚本 |
|
||||
| 新授 | `new_teaching` | `teachingPoints: { knowledgePointIds, outline, boardNotes }[]` | 知识点+讲解要点+板书 | 教学步骤 |
|
||||
| 练习 | `exercise` | `items: ExerciseItem[]; purpose` | 题目列表+用途 | 可发布作业 |
|
||||
| 小结 | `summary` | `summaryPoints: string[]; homeworkPreview` | 要点列表+作业预告 | 总结文本 |
|
||||
| 作业 | `homework` | `assignments: { type, refId?, description }[]` | 作业项列表 | 作业清单 |
|
||||
| 板书设计 | `blackboard` | `layout, content, knowledgePointIds` | 布局+内容 | 板书图 |
|
||||
| 教学反思 | `reflection` | `reflection: { aspect, text }[]` | 反维度反思 | 反思记录 |
|
||||
| 文本研习 | `text_study` | `annotations: TextStudyAnnotation[]` | 课文批注(与正文联动) | 批注列表 |
|
||||
| 富文本 | `rich_text` | `html, knowledgePointIds` | 自由富文本 | HTML |
|
||||
| **正文** | `textbook_content` | `chapterId, content, zoom` | 教材章节 Markdown | 只读正文 |
|
||||
|
||||
### 2.2 数据类型定义
|
||||
|
||||
```typescript
|
||||
// 教学目标
|
||||
interface ObjectiveBlockData {
|
||||
objectives: {
|
||||
dimension: "knowledge" | "process" | "emotion";
|
||||
text: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 重难点
|
||||
interface KeyPointBlockData {
|
||||
keyPoints: {
|
||||
type: "key" | "difficult";
|
||||
text: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 导入
|
||||
interface ImportBlockData {
|
||||
method: "question" | "situation" | "review" | "other";
|
||||
prompt: string;
|
||||
durationMin: number;
|
||||
}
|
||||
|
||||
// 新授
|
||||
interface NewTeachingBlockData {
|
||||
teachingPoints: {
|
||||
knowledgePointIds: string[];
|
||||
outline: string;
|
||||
boardNotes: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 小结
|
||||
interface SummaryBlockData {
|
||||
summaryPoints: string[];
|
||||
homeworkPreview: string;
|
||||
}
|
||||
|
||||
// 作业
|
||||
interface HomeworkBlockData {
|
||||
assignments: {
|
||||
type: "exercise" | "reading" | "writing";
|
||||
refId?: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 板书设计
|
||||
interface BlackboardBlockData {
|
||||
layout: "structure" | "mindmap" | "text";
|
||||
content: string;
|
||||
knowledgePointIds: string[];
|
||||
}
|
||||
|
||||
// 教学反思
|
||||
interface ReflectionBlockData {
|
||||
reflection: {
|
||||
aspect: "effectiveness" | "problems" | "improvements";
|
||||
text: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// BlockData 联合类型扩展
|
||||
type BlockData =
|
||||
| RichTextBlockData
|
||||
| TextStudyBlockData
|
||||
| ExerciseBlockData
|
||||
| ObjectiveBlockData
|
||||
| KeyPointBlockData
|
||||
| ImportBlockData
|
||||
| NewTeachingBlockData
|
||||
| SummaryBlockData
|
||||
| HomeworkBlockData
|
||||
| BlackboardBlockData
|
||||
| ReflectionBlockData
|
||||
| TextbookContentNodeData;
|
||||
```
|
||||
|
||||
### 2.3 BlockRegistry 配置驱动
|
||||
|
||||
每个节点类型在 `block-registry.tsx` 注册:
|
||||
- `component`: 对应的编辑组件
|
||||
- `icon`: 节点图标
|
||||
- `defaultTitle`: 默认标题(i18n 键)
|
||||
- `defaultData`: 初始数据
|
||||
- `summaryExtractor`: 节点卡片摘要函数
|
||||
- `color`: 节点颜色(复用现有 `getNodeColor`)
|
||||
|
||||
### 2.4 默认骨架(10 节点)
|
||||
|
||||
创建课案时自动生成:
|
||||
1. 教学目标(未锚定,全局)
|
||||
2. 重难点(未锚定,全局)
|
||||
3. 导入(锚定到正文开头)
|
||||
4. 文本研习(锚定到正文,范围锚定)
|
||||
5. 新授(锚定到正文中部)
|
||||
6. 练习(锚定到正文,点锚定)
|
||||
7. 小结(锚定到正文结尾)
|
||||
8. 作业(未锚定,课后)
|
||||
9. 板书设计(未锚定,全局)
|
||||
10. 教学反思(未锚定,课后)
|
||||
|
||||
## 第 3 节:正文节点与锚点交互
|
||||
|
||||
### 3.1 正文节点组件(TextbookContentNode)
|
||||
|
||||
```typescript
|
||||
// components/nodes/textbook-content-node.tsx
|
||||
interface Props {
|
||||
data: TextbookContentNodeData;
|
||||
selectedNodeId: string | null;
|
||||
anchors: NodeAnchor[];
|
||||
onAddAnchor: (anchor: NodeAnchor) => void;
|
||||
onRemoveAnchor: (anchorId: string) => void;
|
||||
onSelectNode: (nodeId: string | null) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**渲染流程**:
|
||||
1. `ReactMarkdown` 渲染 `data.content`(复用教材模块的 `remarkGfm + remarkBreaks + rehypeSanitize`)
|
||||
2. 渲染前调用 `injectPlaceholders(content, anchors)` 在对应偏移位置插入占位符标记
|
||||
3. 范围锚定的文字用 `<span class="range-anchor">` 包裹,背景色 = 节点颜色
|
||||
4. 点锚定的位置插入 `<span class="point-anchor">①</span>` 标记
|
||||
5. 缩放通过 `transform: scale(data.zoom)` 实现
|
||||
|
||||
### 3.2 占位符注入算法
|
||||
|
||||
```typescript
|
||||
// lib/anchor-injector.ts
|
||||
|
||||
// 将 Markdown 渲染为纯文本,记录偏移映射
|
||||
function buildOffsetMap(markdown: string): {
|
||||
plainText: string;
|
||||
mdToPlain: Map<number, number>;
|
||||
}
|
||||
|
||||
// 在纯文本中注入占位符标记
|
||||
function injectPlaceholders(
|
||||
markdown: string,
|
||||
anchors: NodeAnchor[]
|
||||
): string {
|
||||
// 1. buildOffsetMap 得到 plainText + 映射
|
||||
// 2. 按 start 排序 anchors(倒序,避免偏移变化)
|
||||
// 3. 对 range 锚定:在 [start, end] 范围包裹 <span class="range-anchor">
|
||||
// 4. 对 point 锚定:在 start 位置插入 <span class="point-anchor">①</span>
|
||||
// 5. 返回注入标记后的 HTML(供 ReactMarkdown 的 components 自定义渲染)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 CSS 透明度规则
|
||||
|
||||
```css
|
||||
/* 范围锚定:文本背景色 = 节点颜色 */
|
||||
.range-anchor {
|
||||
background-color: var(--node-color);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.range-anchor.active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 点锚定:占位符标记 */
|
||||
.point-anchor {
|
||||
display: inline-block;
|
||||
background-color: var(--node-color);
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.point-anchor.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.point-anchor:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 连线默认 10% */
|
||||
.react-flow__edge.anchor {
|
||||
opacity: 0.1;
|
||||
}
|
||||
.react-flow__edge.anchor.active {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 两种锚定交互流程
|
||||
|
||||
**范围锚定(选文本 → 关联节点)**:
|
||||
1. 教师在正文选中一段文字
|
||||
2. 选中后浮动菜单出现:"关联节点 →"
|
||||
3. 下拉列表显示所有未锚定的教学节点 + "新建节点"
|
||||
4. 选择后创建 `NodeAnchor { type: "range", start, end, textPreview }`
|
||||
5. 创建 `AnchorEdge { source: nodeId, target: textbookContentNodeId, anchorId }`
|
||||
6. 正文对应文字被 `<span class="range-anchor">` 包裹
|
||||
|
||||
**点锚定(点击位置 → 插入占位符)**:
|
||||
1. 教师在正文某位置点击(光标位置或点击空白处)
|
||||
2. 弹出菜单:"在此处插入节点 →"
|
||||
3. 下拉列表显示所有未锚定的教学节点 + "新建节点"
|
||||
4. 选择后创建 `NodeAnchor { type: "point", start }`
|
||||
5. 创建 `AnchorEdge`
|
||||
6. 正文对应位置插入 `<span class="point-anchor">①</span>`
|
||||
|
||||
### 3.5 选中节点的视觉反馈
|
||||
|
||||
```typescript
|
||||
function getActiveAnchorIds(anchors: NodeAnchor[], selectedNodeId: string | null): Set<string> {
|
||||
if (!selectedNodeId) return new Set();
|
||||
return new Set(anchors.filter(a => a.nodeId === selectedNodeId).map(a => a.id));
|
||||
}
|
||||
|
||||
const activeAnchorIds = getActiveAnchorIds(anchors, selectedNodeId);
|
||||
// 对每个占位符:activeAnchorIds.has(anchor.id) ? "active" : ""
|
||||
```
|
||||
|
||||
### 3.6 正文内容变更处理
|
||||
|
||||
- 正文来自教材模块的 `chapter.content`,教师不可编辑正文本身
|
||||
- 如果教材章节内容更新,课案中的正文缓存需要同步
|
||||
- 提供"同步正文"按钮,调用 `getChapterContentAction(chapterId)` 刷新
|
||||
- 同步后锚点偏移量可能失效,用 `textPreview` 做模糊匹配尝试重新定位
|
||||
- 无法定位的锚点标记为"失效",提示教师重新锚定
|
||||
|
||||
## 第 4 节:创建课案流程
|
||||
|
||||
### 4.1 入口 1:从备课模块新建
|
||||
|
||||
`template-picker.tsx` 改造为强制选择教材/章节:
|
||||
1. 选择学科/年级
|
||||
2. 选择教材(从 `getTextbooksAction` 获取)
|
||||
3. 选择章节(从 `getChaptersByTextbookIdAction` 获取章节树)
|
||||
4. 输入课案标题
|
||||
5. 点击创建 → 自动拉取章节正文 + 生成默认骨架
|
||||
|
||||
### 4.2 入口 2:从教材阅读器进入
|
||||
|
||||
在 `textbook-reader.tsx` 的章节内容面板添加"为此课文备课"按钮:
|
||||
- 仅教师角色可见
|
||||
- 校验教师教授科目与教材学科匹配
|
||||
- 点击后跳转到 `/teacher/lesson-plans/new?textbookId=xxx&chapterId=xxx`
|
||||
- `template-picker.tsx` 读取 URL 参数自动预选
|
||||
|
||||
### 4.3 默认骨架生成
|
||||
|
||||
```typescript
|
||||
function buildDefaultSkeleton(chapterId: string, chapterContent: string): LessonPlanDocument {
|
||||
const textbookContentNodeId = createId();
|
||||
const textbookNode: TextbookContentNode = {
|
||||
id: textbookContentNodeId,
|
||||
type: "textbook_content",
|
||||
position: { x: 400, y: 200 }, // 画布中央
|
||||
draggable: false,
|
||||
data: { chapterId, content: chapterContent, zoom: 1 },
|
||||
};
|
||||
|
||||
const defaultNodes = [
|
||||
{ type: "objective", position: { x: 80, y: 80 }, anchor: null },
|
||||
{ type: "key_point", position: { x: 80, y: 180 }, anchor: null },
|
||||
{ type: "import", position: { x: 80, y: 280 }, anchor: { type: "point", start: 0 } },
|
||||
{ type: "text_study", position: { x: 80, y: 380 }, anchor: { type: "range", start: 0, end: 10 } },
|
||||
{ type: "new_teaching", position: { x: 720, y: 80 }, anchor: { type: "range", start: 50, end: 60 } },
|
||||
{ type: "exercise", position: { x: 720, y: 180 }, anchor: { type: "point", start: 100 } },
|
||||
{ type: "summary", position: { x: 720, y: 280 }, anchor: { type: "point", start: 200 } },
|
||||
{ type: "homework", position: { x: 80, y: 480 }, anchor: null },
|
||||
{ type: "blackboard", position: { x: 720, y: 380 }, anchor: null },
|
||||
{ type: "reflection", position: { x: 720, y: 480 }, anchor: null },
|
||||
];
|
||||
|
||||
// 生成 nodes + anchors + edges
|
||||
return { version: 3, textbookContentNodeId, nodes, edges, anchors };
|
||||
}
|
||||
```
|
||||
|
||||
## 第 5 节:编辑器交互改进
|
||||
|
||||
### 5.1 实时拖动
|
||||
|
||||
修改 `use-lesson-plan-editor.ts`:
|
||||
- 当前:`onNodeDragStop` 才调用 `updateNodePosition`
|
||||
- 改为:`onNodeDrag` 实时调用 `updateNodePosition`(每次拖动事件都更新)
|
||||
|
||||
### 5.2 顶部工具栏增加教材/章节切换
|
||||
|
||||
- 显示当前教材/章节名称
|
||||
- 点击可切换教材/章节
|
||||
- 切换后重新拉取正文内容,更新 `TextbookContentNode.data`
|
||||
- 锚点可能失效,提示教师
|
||||
|
||||
### 5.3 添加节点菜单
|
||||
|
||||
- 左下角"+ 添加节点"按钮
|
||||
- 弹出 11 种节点类型菜单(不含 textbook_content)
|
||||
- 选择后在画布空闲位置创建节点
|
||||
|
||||
### 5.4 节点编辑面板
|
||||
|
||||
- 点击节点 → 右侧 `NodeEditPanel` 显示对应编辑组件
|
||||
- `BlockRenderer` 配置驱动渲染
|
||||
- 正文节点不可编辑内容,但可缩放(zoom 控件)
|
||||
|
||||
## 第 6 节:错误处理与边界情况
|
||||
|
||||
### 6.1 正文内容为空
|
||||
|
||||
- 如果章节无 `content`,正文节点显示"暂无课文内容,请在教材模块编辑"
|
||||
- 锚点功能禁用
|
||||
|
||||
### 6.2 锚点失效
|
||||
|
||||
- 正文同步后,用 `textPreview` 模糊匹配重新定位
|
||||
- 无法定位的锚点标记 `invalid: true`
|
||||
- UI 显示"锚点已失效,请重新选择"
|
||||
- 教师可删除失效锚点或重新锚定
|
||||
|
||||
### 6.3 数据迁移失败
|
||||
|
||||
- v2 文档无 chapterId:创建空正文节点,提示"请选择教材/章节"
|
||||
- 迁移过程异常:保留 v2 原始数据,记录错误日志
|
||||
|
||||
## 第 7 节:测试策略
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
- `lib/anchor-injector.ts`:占位符注入算法
|
||||
- `lib/document-migration.ts`:v2 → v3 迁移
|
||||
- `lib/node-summary.ts`:新节点类型的摘要提取
|
||||
|
||||
### 7.2 集成测试
|
||||
|
||||
- 创建课案 → 选择教材/章节 → 验证默认骨架生成
|
||||
- 选中正文文字 → 关联节点 → 验证锚点创建
|
||||
- 点击正文位置 → 插入占位符 → 验证点锚定
|
||||
- 选中节点 → 验证透明度变化
|
||||
- 正文同步 → 验证锚点重定位
|
||||
|
||||
### 7.3 E2E 测试
|
||||
|
||||
- 完整备课流程:创建 → 编辑 → 锚定 → 保存 → 版本回退
|
||||
|
||||
## 实现范围
|
||||
|
||||
本次重构涉及以下文件(预估):
|
||||
|
||||
**新增**:
|
||||
- `src/modules/lesson-preparation/components/nodes/textbook-content-node.tsx`
|
||||
- `src/modules/lesson-preparation/components/anchor-context-menu.tsx`
|
||||
- `src/modules/lesson-preparation/lib/anchor-injector.ts`
|
||||
- `src/modules/lesson-preparation/components/blocks/objective-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/key-point-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/import-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/new-teaching-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/summary-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/homework-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/blackboard-block.tsx`
|
||||
- `src/modules/lesson-preparation/components/blocks/reflection-block.tsx`(重构)
|
||||
|
||||
**修改**:
|
||||
- `src/modules/lesson-preparation/types.ts`(v3 数据模型)
|
||||
- `src/modules/lesson-preparation/constants.ts`(BlockType 枚举)
|
||||
- `src/modules/lesson-preparation/config/block-registry.tsx`(注册新节点)
|
||||
- `src/modules/lesson-preparation/lib/document-migration.ts`(v2→v3)
|
||||
- `src/modules/lesson-preparation/lib/node-summary.ts`(新节点摘要)
|
||||
- `src/modules/lesson-preparation/hooks/use-lesson-plan-editor.ts`(实时拖动 + 锚点操作)
|
||||
- `src/modules/lesson-preparation/components/node-editor.tsx`(正文节点 + 连线透明度)
|
||||
- `src/modules/lesson-preparation/components/lesson-plan-editor.tsx`(教材/章节切换)
|
||||
- `src/modules/lesson-preparation/components/template-picker.tsx`(强制选教材)
|
||||
- `src/modules/lesson-preparation/components/blocks/text-study-block.tsx`(与正文联动)
|
||||
- `src/modules/lesson-preparation/data-access.ts`(buildDefaultSkeleton)
|
||||
- `src/modules/lesson-preparation/actions.ts`(创建课案传入 chapterId)
|
||||
- `src/modules/textbooks/components/textbook-reader.tsx`("为此课文备课"按钮)
|
||||
- `src/shared/i18n/messages/zh-CN/lesson-preparation.json`(新 i18n 键)
|
||||
- `src/shared/i18n/messages/en/lesson-preparation.json`(新 i18n 键)
|
||||
- `src/app/globals.css`(锚点 CSS)
|
||||
Reference in New Issue
Block a user