专项练习(adaptive-practice)模块审计报告
审计日期:2026-06-25
审计范围:src/modules/adaptive-practice/ 全部文件 + src/app/(dashboard)/{student,teacher,management/grade}/practice/ 路由 + 跨模块集成点(error-book)
对标系统:Khan Academy、IXL Learning、DreamBox、Smartick、学而思网校、猿辅导、作业帮、超星学习通、ClassIn、智学网
前置文档:004 架构影响地图、005 架构数据
一、现有实现概要
1.1 文件分布与代码量
1.2 路由分布
| 路由 |
文件 |
角色 |
/student/practice |
page.tsx + loading.tsx + error.tsx |
student |
/student/practice/[sessionId] |
page.tsx + loading.tsx + error.tsx |
student |
/teacher/practice |
page.tsx + loading.tsx + error.tsx |
teacher / grade_head / teaching_head |
/management/grade/practice |
page.tsx + loading.tsx + error.tsx |
grade_head / teaching_head |
❌ /parent/practice |
缺失 |
parent(有 ADAPTIVE_PRACTICE_READ 权限但无页面) |
1.3 数据流与依赖关系
模块内:app/page.tsx → modules/adaptive-practice/{actions, data-access, data-access-analytics} → shared/{db, lib/auth-guard, types}
跨模块:
modules/adaptive-practice/data-access-analytics.ts → modules/classes/data-access(getActiveStudentIdsByClassId / getClassNameById / getClassesByGradeId / getClassIdsByGradeIds / getStudentIdsByClassIds)✅ 合规
modules/adaptive-practice/data-access-analytics.ts → modules/users/data-access(getUserIdsByGradeId / getUserNamesByIds)✅ 合规
app/(dashboard)/student/error-book/student-error-book-list-client.tsx → modules/adaptive-practice/actions(createPracticeSessionAction)✅ app 层组合合规
app/(dashboard)/teacher/practice/page.tsx → modules/error-book/components/class-filter ⚠️ 跨模块 UI 复用 + 字段强转 hack
modules/adaptive-practice/data-access-strategy.ts → shared/db/schema(questions / questionsToKnowledgePoints / knowledgePointMastery / practiceAnswers)✅ 合规
1.4 架构图同步状态
004_architecture_impact_map.md §2.30 与 005_architecture_data.json 的 adaptivePractice 节点已完整覆盖:
- DB Schema(practiceSessions / practiceAnswers)、Server Actions(7 个)、Data Access、4 种出题策略、教师/年级宏观数据分析
- 依赖矩阵、权限点、DataScope 行级权限、自动判分规则
架构图遗漏:
- ❌ 未记录
parent 角色路由(因为页面本身缺失,架构图未列出 /parent/practice)
- ❌ 未记录
error-book 模块通过 onStartVariantPractice props 注入的解耦关系
- ❌ 未记录
practice-starter.tsx、practice-session-view.tsx、practice-history.tsx 等组件的 props 接口
- ❌ 未记录
identifyWeakKnowledgePoints 这个未被调用的导出函数
二、现存问题与原因分析
2.1 架构与耦合问题
P0-1 跨模块 UI 复用通过字段强转 hack 实现
| 项 |
内容 |
| 位置 |
teacher/practice/page.tsx L31-32, L108-116 |
| 问题 |
教师练习分析页直接 import @/modules/error-book/components/class-filter 和 @/modules/error-book/types,并通过字段重命名把 TeacherClassPracticeOverview 强转为 ClassErrorOverview:totalErrorItems ← totalSessions、averageMasteryRate ← averageAccuracy、dueReviewCount: 0。语义完全错位,"练习数"被当成"错题数"展示。 |
| 规则 |
违反"模块间只能通过对方 data-access 通信"和"避免 as 断言"。 |
| 后果 |
任何一方修改 ClassErrorOverview 或 ClassFilter 字段都会破坏练习分析页;筛选器 tooltip 显示"错题数"误导用户。 |
P0-2 PracticeStarter 硬编码路由跳转与 Action 直调
| 项 |
内容 |
| 位置 |
practice-starter.tsx L20, L82, L129 |
| 问题 |
组件直接 import { createPracticeSessionAction } from "../actions" 并 router.push("/student/practice/${sessionId}")。组件无法被其他角色(如 parent 监督子女练习、teacher 课堂演示)复用。 |
| 规则 |
违反"完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions"(同一模块内允许,但路由硬编码违反"可复用"原则)。 |
| 后果 |
组件无法跨角色复用;测试需要 mock 整个 Action 模块;路由变更需改组件。 |
P0-3 PracticeSessionView 单文件 550 行,承担 5 个组件职责
| 项 |
内容 |
| 位置 |
practice-session-view.tsx |
| 问题 |
单文件包含 PracticeSessionView / QuestionCard / AnswerInput / AnswerResult / PracticeResultView 五个组件 + extractOptions 辅助函数。AnswerInput 内部对 4 种题型的渲染逻辑高度相似却重复编写。 |
| 规则 |
违反"React 组件建议 ≤ 500 行"和"最大化复用:识别共用 UI 块抽象为泛型组件"。 |
| 后果 |
难以单测、难以独立复用 QuestionCard(例如在错题本详情弹窗中预览变式题)。 |
2.2 权限与安全问题
P0-4 后端 completePracticeSession 不校验是否全部题已答
| 项 |
内容 |
| 位置 |
data-access.ts L416-442 |
| 问题 |
completePracticeSession 只检查 status === "in_progress",未校验 answeredQuestions === totalQuestions。前端 disabled={isPending || answeredCount < total} 可被绕过,恶意用户可提交未答完的会话为"已完成",污染统计。 |
| 规则 |
违反"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"。 |
| 后果 |
统计数据失真,影响教师宏观数据分析。 |
P0-5 submitPracticeAnswer 不防并发提交
| 项 |
内容 |
| 位置 |
data-access.ts L320-410 |
| 问题 |
检查 answerRecord.status === "answered" 后再更新,但中间无事务/行锁。学生快速双击提交按钮可绕过检查,导致同一题被二次判分,updateSessionStats 重复累加 answeredQuestions 和 correctCount。 |
| 规则 |
违反"安全性:Server Action 二次校验"。 |
| 后果 |
统计数据被双重累加,正确率失真。 |
P0-6 getPracticeSessionsAction 未校验 studentId 与 ctx 的强一致性
| 项 |
内容 |
| 位置 |
actions.ts L31-56 |
| 问题 |
当 ctx.dataScope.type === "all"(admin)时,可传任意 studentId 查询;admin 角色确实可查任意学生,但 audit 模块规则要求"权限校验需要 parentId 和 studentId 双重校验"。教师角色 dataScope.type === "class_taught" 时,未校验 studentId 是否在所教班级学生中,任何登录教师可查询任意学生练习数据(只要把 studentId 直接传给 action)。 |
| 规则 |
违反"Parent routes must include permission checks with both parentId and studentId to prevent information leakage"。 |
| 后果 |
教师越权查看非本班学生练习记录,数据泄露。 |
2.3 国际化遗漏(i18n)
P0-7 error.tsx 大量硬编码中文
P0-8 actions.ts 错误消息硬编码中文
| 项 |
内容 |
| 位置 |
actions.ts L53, L77, L129, L137, L154, L161, L178, L184, L204, L235, L263 |
| 问题 |
Server Action 返回的 message 字段硬编码中文:"获取练习列表失败"、"练习会话不存在或无权访问"、"提交格式错误"、"输入验证失败"、"未找到符合条件的题目"、"已创建练习会话"、"已跳过此题"、"回答正确"、"回答错误"、"练习已完成"、"练习已放弃" 等。这些消息通过 ActionState 返回到前端 toast 展示给用户。 |
| 规则 |
违反"所有用户可见文本必须适配 i18n"。 |
| 后果 |
英文用户看到中文 toast。 |
P0-9 data-access.ts 抛出错误消息硬编码中文
| 项 |
内容 |
| 位置 |
data-access.ts L336, L340, L352, L356, L382 |
| 问题 |
throw new Error("练习会话不存在或无权访问") 等中文消息直接抛给上层,最终被 handleActionError 包装后展示给用户。 |
| 规则 |
违反"所有用户可见文本必须适配 i18n"。 |
| 后果 |
与 P0-8 同。 |
P0-10 业务数据写入翻译文本
| 项 |
内容 |
| 位置 |
practice-starter.tsx L113-117 |
| 问题 |
sourceMeta = { recommendedKnowledgePointIds, reason: t("toasts.aiRecommendedReason") } —— 把翻译文本作为业务数据写入数据库 practice_sessions.source_meta.reason 字段。语言切换后历史记录的 reason 不一致;数据库存储多语言文本违反数据归一化。 |
| 规则 |
违反"业务数据不应包含翻译文本"(架构规范)。 |
| 后果 |
数据库冗余、语言切换不一致、跨语言环境数据污染。 |
2.4 类型安全问题
P1-1 data-access.ts 多处 as 类型断言绕过严格模式
| 项 |
内容 |
| 位置 |
data-access.ts L38 row.practiceType as PracticeType、L39 row.status as PracticeStatus、L60 row.status as PracticeAnswerStatus、L174 session.sourceMeta as PracticeSourceMeta | null、L217 practiceType: type as PracticeType |
| 问题 |
从 DB 取出的 enum 字段直接 as 断言,未通过类型守卫校验。如果 DB 数据被脏写(如手工改库),运行时会把无效值当作合法值处理。 |
| 规则 |
违反"禁止 as 断言(除类型收窄外)",且"未知类型用 unknown 并做类型守卫"。 |
| 后果 |
类型系统失效,潜在运行时错误。 |
P1-2 actions.ts L144 双重 as 断言
| 项 |
内容 |
| 位置 |
actions.ts L144 parsed.data.sourceMeta as unknown as PracticeSourceMeta |
| 问题 |
z.record(z.string(), z.unknown()) 返回 Record<string, unknown>,通过 as unknown as 双重断言绕过类型系统。Zod schema 没有按 PracticeSourceMeta 联合类型做判别式校验。 |
| 规则 |
违反"禁止 as 断言"。 |
| 后果 |
客户端可构造任意结构的 sourceMeta 写入数据库,data-access-strategy 中的类型守卫只检查 key 存在性,不检查 value 类型。 |
P1-3 data-access-strategy.ts 类型守卫不充分
| 项 |
内容 |
| 位置 |
data-access-strategy.ts L325-343 |
| 问题 |
类型守卫仅检查 "errorBookItemIds" in meta 等 key 存在性,不验证 errorBookItemIds 是 string[]、sourceQuestionIds 是 string[]。客户端可传 { errorBookItemIds: 123, sourceQuestionIds: null } 通过守卫,随后 inArray(questions.id, sourceQuestionIds) 抛 SQL 错误。 |
| 规则 |
违反"未知类型用 unknown 并做类型守卫"。 |
| 后果 |
SQL 异常泄露内部信息。 |
P1-4 practice-session-view.tsx L360 数组断言
| 项 |
内容 |
| 位置 |
practice-session-view.tsx L360 userAnswer as string[] |
| 问题 |
多选题把 userAnswer 强转为 string[],但 userAnswer 类型为 unknown。 |
| 规则 |
违反"禁止 as 断言"。 |
| 后果 |
类型不安全。 |
2.5 业务逻辑缺陷
P0-11 "错题变式"策略实际不做变式
| 项 |
内容 |
| 位置 |
data-access-strategy.ts L40-70 |
| 问题 |
函数名 selectForErrorVariant,类型定义 QuestionSelectionResult.variants: Map<string, unknown>,但实现直接查询原题返回,variants: new Map() 始终为空。注释 L34 写明"不依赖 AI 生成变式题",但对外仍以"错题变式"命名,UI 上展示为"错题变式"练习类型。功能与名称严重不符。 |
| 规则 |
违反"组件必须为纯函数,使用 function 声明"中的语义诚实原则。 |
| 后果 |
用户期待"变式题"实际是原题重做,体验落差;与 AI 模块定义的 AiQuestionVariantGenerator 能力割裂。 |
P0-12 "薄弱章节"策略未自动识别薄弱知识点
| 项 |
内容 |
| 位置 |
data-access-strategy.ts L135-180, L298-319 |
| 问题 |
selectForWeakChapter 接收 sourceMeta.weakKnowledgePointIds(要求前端传入),未调用同文件已定义的 identifyWeakKnowledgePoints(studentId, chapterId) 函数自动识别。WeakChapterSourceMeta.chapterId 字段定义了但策略中未使用。用户必须先在另一处查看薄弱知识点再手动选择,体验割裂。 |
| 规则 |
违反"组合优先:逻辑复用一律抽取为自定义 hooks"和"最大化复用"。 |
| 后果 |
"薄弱章节"功能名不副实;identifyWeakKnowledgePoints 成为死代码。 |
P0-13 createPracticeSession 返回空 sessionId 表示失败
| 项 |
内容 |
| 位置 |
data-access.ts L266-268 |
| 问题 |
失败时 return { sessionId: "", selectedCount: 0 },调用方通过判断 selectedCount === 0 识别失败。空字符串作为 ID 是反模式,与成功的 { sessionId: "xxx", selectedCount: 0 }(理论上可能)混淆。 |
| 规则 |
违反"函数返回值必须显式标注"和"明确处理边界状态"。 |
| 后果 |
调用方判断逻辑脆弱;后续重构易引入 bug。 |
P1-5 出题策略使用 ORDER BY RAND() 性能差
| 项 |
内容 |
| 位置 |
data-access-strategy.ts L116, L173, L225 |
| 问题 |
知识点专项、薄弱章节、AI 推荐三种策略都用 sql\RAND()`。MySQL ORDER BY RAND()` 在大表上会全表扫描排序,题库上万题时性能急剧下降。 |
| 规则 |
违反"性能:优先使用 React Server Components 获取初始数据"。 |
| 后果 |
大题库下出题延迟可达数秒。 |
P1-6 getTeacherClassPracticeOverviews / getGradeClassPracticeComparison N+1 查询
| 项 |
内容 |
| 位置 |
data-access-analytics.ts L276-340, L432-496 |
| 问题 |
Promise.all(classIds.map(...)) 内部每个班级至少 3 次 DB 查询(getClassNameById + getActiveStudentIdsByClassId + 2 次 practiceSessions 聚合)。10 个班级 = 30 次查询。 |
| 规则 |
违反"性能:优先使用 RSC"和工程规范"批量查询应合并"。 |
| 后果 |
班级多时延迟累积。 |
2.6 错误处理与边界缺失
P0-14 答题提交失败后无重试机制
| 项 |
内容 |
| 位置 |
practice-session-view.tsx L81-107 |
| 问题 |
handleSubmit 失败仅 toast.error,不保留失败状态、不提供重试按钮。学生网络抖动时需要手动重新选择答案再提交,且因为 setResults 未更新,UI 上仍显示"未作答"。 |
| 规则 |
违反"明确处理空数据、无权限、网络异常等边界状态"。 |
| 后果 |
网络异常时学生困惑、流失答题意愿。 |
P0-15 缺少细粒度 React Error Boundary
| 项 |
内容 |
| 位置 |
teacher/practice/page.tsx 全文 |
| 问题 |
教师分析页一个数据区块失败会导致整页回退到 error.tsx。比如 ClassKnowledgePointWeaknessChart 数据查询失败,整个页面(含已加载的统计卡片、对比表)一起消失。 |
| 规则 |
违反"每个独立的数据区块必须用 React Error Boundary 包裹"。 |
| 后果 |
局部错误导致整页不可用。 |
P0-16 缺少流式渲染与骨架屏细粒度
| 项 |
内容 |
| 位置 |
teacher/practice/page.tsx L122-133 |
| 问题 |
Promise.all([...]) 阻塞所有数据加载完成才渲染任何内容,仅外层 Suspense 包裹整页。无区块级 Suspense + 骨架屏。 |
| 规则 |
违反"异步数据使用 React Suspense + 骨架屏"和"支持流式渲染"。 |
| 后果 |
首屏白屏时间长达数秒。 |
2.7 可测试性缺失
P1-7 自动判分纯函数未导出
| 项 |
内容 |
| 位置 |
data-access.ts L517-616 |
| 问题 |
autoGradeAnswer / extractChoiceCorrectIds / extractJudgmentCorrectAnswer / normalizeAnswerToIds / normalizeAnswerToBool 全部为内部函数,未 export。无法单测,判分正确性无保障。 |
| 规则 |
违反"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。 |
| 后果 |
判分 bug 难以回归。 |
P1-8 出题策略未单独导出
| 项 |
内容 |
| 位置 |
data-access-strategy.ts L40-232 |
| 问题 |
selectForErrorVariant / selectForKnowledgePoint / selectForWeakChapter / selectForAiRecommended 均未导出。仅 selectQuestionsForPractice 入口可测,无法针对单策略测试。 |
| 规则 |
违反"导出清晰的接口类型以便 mock"。 |
| 后果 |
策略调整需要端到端测试。 |
2.8 可访问性(a11y)缺失
P1-9 题目内容用 JSON.stringify 展示
| 项 |
内容 |
| 位置 |
practice-session-view.tsx L287-293 |
| 问题 |
题目内容若不是字符串,直接 JSON.stringify(content, null, 2) 渲染在 <pre> 中。学生看到 {"options":[{"id":"a","text":"..."}]} 这种 JSON 而非可读题目。 |
| 规则 |
违反"a11y:语义化标签、ARIA 属性、键盘导航"。 |
| 后果 |
用户体验极差,无法正常答题。 |
P1-10 自定义 checkbox 缺少 aria-label
| 项 |
内容 |
| 位置 |
practice-starter.tsx L192-204 |
| 问题 |
<input type="checkbox"> 原生元素而非 shadcn Checkbox,且无 aria-label。屏幕阅读器无法识别知识点名称。 |
| 规则 |
违反"a11y:ARIA 属性"。 |
| 后果 |
视障用户无法使用。 |
2.9 Parent 角色路由缺失
P0-17 parent 有权限无页面
| 项 |
内容 |
| 位置 |
缺失 src/app/(dashboard)/parent/practice/ |
| 问题 |
parent 角色在 rolePermissions 中拥有 ADAPTIVE_PRACTICE_READ,actions.ts 已实现 ctx.dataScope.type === "children" 分支,但无 /parent/practice 路由。家长无法查看子女练习记录、统计、错题变式入口。 |
| 规则 |
违反"Parent routes must include permission checks with both parentId and studentId"。 |
| 后果 |
家长无法监督子女学习,与 parent/error-book 等同级模块功能不对等。 |
三、行业差距对比
3.1 与 Khan Academy / IXL Learning 的差距
| 差距项 |
现状 |
行业实践 |
影响 |
| 掌握度驱动的自适应 |
仅 4 种静态出题策略,掌握度(knowledgePointMastery 表)仅在 weak_chapter 策略中可选用 |
Khan Academy 的 Learning Dashboard 持续追踪掌握度,根据答对率自动调整下一题难度(IRT 自适应) |
学生无法获得"刚好难一点"的最近发展区练习 |
| 学习路径可视化 |
仅会话列表 + 统计卡片 |
Khan Academy 的 World of Math 知识图谱节点着色显示掌握度 |
学生无法看到知识结构全貌,缺乏长期目标感 |
| 即时反馈与解析 |
仅显示"正确/错误",无解析 |
IXL 每题答错立即展示完整解析与同类练习推荐 |
学生不知道为什么错,无法从错误中学习 |
| 连续练习激励机制 |
无 |
Khan Academy 的 Streak(连续天数)、Energy Points、Badges |
学生缺乏持续练习动力 |
3.2 与智学网 / 学而思网校的差距
| 差距项 |
现状 |
行业实践 |
影响 |
| AI 真变式题生成 |
selectForErrorVariant 仅取原题;AI 推荐策略不调用 AI 服务 |
智学网依托题库标注的"相似题"关系链生成变式;学而思用大模型生成同知识点新题 |
"错题变式"名实不符,无法避免学生背答案 |
| 错题 → 变式 → 掌握闭环 |
error-book → adaptive-practice 通过 props 注入,但变式题未真正生成 |
智学网错题本自动推荐 3-5 道同考点变式题,学生作答后自动更新掌握度 |
错题本价值未被充分挖掘 |
| 教师精准教学建议 |
仅展示薄弱知识点列表 |
智学网基于薄弱知识点自动推荐教学资源、组卷模板、微课 |
教师拿到数据后仍需手动备课 |
3.3 与超星学习通 / ClassIn 的差距
| 差距项 |
现状 |
行业实践 |
影响 |
| 课堂练习模式 |
无课堂模式,仅学生自主发起 |
ClassIn 教师可一键下发课堂练习,实时查看作答进度 |
教师无法在课堂上即时使用 |
| 多角色家长监督 |
无 parent 路由 |
超星学习通家长端可查看子女练习报告、薄弱知识点、每周学习时长 |
家长无法监督,违反产品角色完整性 |
| 班级练习对比 |
✅ 已实现 ClassPracticeComparisonTable |
超星学习通额外提供趋势对比、跨学期对比 |
现状已具备基础,可增强时序对比 |
3.4 关键交互差距
| 差距项 |
现状 |
行业实践 |
| 题目内容渲染 |
JSON.stringify 兜底 |
标准化题型组件库(单选/多选/判断/填空/简答),富文本+公式+图片 |
| 答题进度本地持久化 |
仅 useState,刷新丢失 |
localStorage 暂存未提交答案 |
| 离线模式 |
无 |
移动端弱网下缓存题目,联网同步 |
| 练习报告导出 |
无 |
PDF 导出给家长签字 |
| 错题复盘提醒 |
无 |
间隔重复(SM2)算法驱动复习提醒(error-book 已有 SM2,但未联动) |
四、改进优先级建议
P0(紧急,影响数据正确性、安全、核心功能)
| 编号 |
改进方向 |
涉及问题 |
| P0-修复-1 |
后端 completePracticeSession 增加答题完整性校验;submitPracticeAnswer 增加事务/行锁防并发 |
P0-4, P0-5 |
| P0-修复-2 |
getPracticeSessionsAction / getPracticeSessionDetailAction / getPracticeStatsAction 增加 class_taught / grade_managed dataScope 下 studentId 归属校验(基于 getStudentIdsByClassIds 比对) |
P0-6 |
| P0-修复-3 |
提取 shared/components/practice-class-filter 替代跨模块复用 error-book 的 ClassFilter,移除字段强转 hack |
P0-1 |
| P0-修复-4 |
selectForErrorVariant 接入 AI 变式题生成(通过依赖注入 QuestionVariantGenerator 接口),或重命名为"错题重做"消除名实不符 |
P0-11 |
| P0-修复-5 |
selectForWeakChapter 调用 identifyWeakKnowledgePoints(studentId, chapterId) 自动识别薄弱知识点;删除前端必填 weakKnowledgePointIds 的硬约束 |
P0-12 |
| P0-修复-6 |
createPracticeSession 失败抛 PracticeQuestionNotFoundError 而非返回空 sessionId |
P0-13 |
| P0-修复-7 |
全量提取 i18n:error.tsx、actions.ts、data-access.ts 中的硬编码中文;翻译键结构见 §五重构方案 |
P0-7, P0-8, P0-9 |
| P0-修复-8 |
practice-starter.tsx 移除 reason: t("toasts.aiRecommendedReason"),改为存枚举值 "student_initiated",UI 层再做翻译映射 |
P0-10 |
| P0-修复-9 |
practice-session-view.tsx 拆分为 practice-session-view.tsx + question-card.tsx + answer-input.tsx + answer-result.tsx + practice-result-view.tsx;引入 QuestionRenderer(复用 homework 模块同款)替代 JSON.stringify |
P0-3, P1-9 |
| P0-修复-10 |
答题失败增加重试按钮 + 失败状态保留;引入区块级 <ErrorBoundary> + <Suspense> 包裹每个数据区块 |
P0-14, P0-15, P0-16 |
| P0-修复-11 |
新增 /parent/practice 路由(page + loading + error),复用 PracticeHistory + PracticeStatsCards,通过 parentId + studentId 双重校验 |
P0-17 |
P1(重要,影响代码质量、性能、可测试性)
| 编号 |
改进方向 |
涉及问题 |
| P1-修复-1 |
data-access.ts 用类型守卫 isPracticeType / isPracticeStatus 替换 as 断言;schema.ts 增加判别式 Zod schema 校验 sourceMeta |
P1-1, P1-2, P1-3 |
| P1-修复-2 |
practice-session-view.tsx L360 改用 Array.isArray(userAnswer) && userAnswer.every(v => typeof v === "string") 类型守卫 |
P1-4 |
| P1-修复-3 |
出题策略改用 ORDER BY questions.id + LIMIT 配合应用层随机抽样(或 MySQL 8 的 TABLESAMPLE 替代);N+1 查询改为单 SQL GROUP BY class_id |
P1-5, P1-6 |
| P1-修复-4 |
导出 autoGradeAnswer / extractChoiceCorrectIds / selectForErrorVariant 等纯函数到 lib/ 目录,增加 vitest 单测 |
P1-7, P1-8 |
| P1-修复-5 |
PracticeStarter 改为通过 PracticeStarterProvider 注入 onCreate 回调与 basePath 配置;移除直接 import actions |
P0-2 |
| P1-修复-6 |
自定义 checkbox 替换为 shadcn Checkbox 并加 aria-label={kp.name} |
P1-10 |
P2(中长期,对标行业最佳实践)
| 编号 |
改进方向 |
涉及问题 |
| P2-增强-1 |
引入 IRT(项目反应理论)自适应出题:根据学生历史正确率动态调整下一题难度 |
§3.1 |
| P2-增强-2 |
答题后展示解析 + 推荐同类练习(联动 questions 模块的相似题关系链) |
§3.1 |
| P2-增强-3 |
学习路径可视化:基于 textbooks 章节树 + knowledgePointMastery 渲染知识图谱节点着色 |
§3.1 |
| P2-增强-4 |
连续练习激励:Streak / Energy Points / Badges,存 users 表扩展字段 |
§3.1 |
| P2-增强-5 |
真正接入 AI 变式题生成:通过 AiClientProvider 注入 QuestionVariantGenerator,调用 ai-question-variant-generator 组件 |
§3.2 |
| P2-增强-6 |
间隔重复复习提醒:联动 error-book 的 SM2 算法,到期错题自动出现在"错题变式"入口 |
§3.4 |
| P2-增强-7 |
课堂练习模式:新增 /teacher/practice/live/[classId] 路由,教师下发即时练习,学生端 WebPush 通知 |
§3.3 |
| P2-增强-8 |
练习报告 PDF 导出:服务端生成 PDF 供家长签字 |
§3.4 |
| P2-增强-9 |
答题进度 localStorage 持久化:刷新不丢未提交答案 |
§3.4 |
| P2-增强-10 |
data-access-analytics.ts 拆分为 data-access-analytics-class.ts(班级维度)+ data-access-analytics-grade.ts(年级维度)+ data-access-analytics-shared.ts(共享类型与工具) |
§1.1 |
五、重构方案设计
5.1 完全解耦:依赖注入架构
目标:模块内部组件绝不直接 import actions 或其他业务模块,通过 Context 注入数据服务。
5.1.1 定义数据服务接口(services/practice-service.ts)
5.1.2 Context Provider(context/practice-service-provider.tsx)
5.1.3 app 层注入
5.2 组合优先:组件拆分与组合
5.2.1 组件树
5.2.2 自定义 hooks 抽取
5.3 国际化就绪
5.3.1 翻译文件结构(messages/zh-CN/practice.json 扩展)
{
"page": { "title": "...", "description": "..." },
"starter": { ... },
"session": { ... },
"result": { ... },
"history": { ... },
"toasts": { ... },
"stats": { ... },
"types": { ... },
"status": { ... },
"teacher": { ... },
"grade": { ... },
"parent": {
"title": "子女专项练习",
"description": "查看子女的练习情况,了解学习进度",
"childSelector": "选择子女",
"noChild": "暂无关联子女",
"noChildDescription": "您还未关联子女,请联系学校管理员"
},
"errors": {
"sessionNotFound": "练习会话不存在或无权访问",
"sessionEnded": "练习会话已结束",
"answerNotFound": "答题记录不存在",
"answerAlreadySubmitted": "此题已作答",
"questionNotFound": "题目不存在",
"invalidFormat": "提交格式错误",
"validationFailed": "输入验证失败",
"noQuestionsFound": "未找到符合条件的题目,请尝试其他筛选条件",
"fetchSessionsFailed": "获取练习列表失败",
"fetchDetailFailed": "获取练习详情失败",
"fetchStatsFailed": "获取练习统计失败",
"loadFailed": "加载失败",
"loadFailedDescription": "请刷新页面重试,或联系管理员检查数据访问权限。",
"pageErrorPractice": "加载练习分析数据时发生错误",
"pageErrorGrade": "加载年级练习数据时发生错误"
},
"messages": {
"sessionCreated": "已创建练习会话,共 {count} 道题目",
"answerSubmitted": "答案已提交",
"answerCorrect": "回答正确",
"answerIncorrect": "回答错误",
"answerSkipped": "已跳过此题",
"sessionCompleted": "练习已完成",
"sessionAbandoned": "练习已放弃"
},
"reasons": {
"student_initiated": "学生自主发起 AI 推荐练习",
"teacher_assigned": "教师布置",
"parent_suggested": "家长建议"
}
}
5.3.2 Server Action 错误返回结构化错误码
5.4 最大化复用:泛型组件与配置驱动
5.4.1 角色配置驱动渲染
5.4.2 通用统计卡片泛型组件
5.5 错误与边界处理
5.5.1 区块级 ErrorBoundary + Suspense
5.5.2 答题失败重试
5.6 可测试性
5.6.1 纯函数抽取(lib/grading.ts、lib/source-meta.ts)
5.6.2 单测示例(lib/grading.test.ts)
5.7 可扩展性:配置驱动
新增角色或功能只需修改 config/role-config.ts:
5.8 企业级补充
5.8.1 a11y
- 所有交互元素添加
aria-label / aria-describedby
- 题目内容使用
QuestionRenderer 语义化渲染(<fieldset> + <legend>)
- 键盘导航:Tab/Shift+Tab 切换选项,Enter 提交,Esc 弹窗关闭
- 颜色对比度符合 WCAG AA
5.8.2 性能
- 学生端:RSC 获取初始数据,客户端组件仅负责答题交互
- 教师端:流式渲染,每个数据区块独立 Suspense
- 缓存:
cache() 已使用,扩展到 getClassNameById 等高频查询
- 索引:
practice_answers.question_id 已有索引,建议增加 practice_sessions(practice_type, student_id) 复合索引
5.8.3 安全性
- data-access 层所有查询结合
ctx.userId / ctx.dataScope 过滤
- Server Action 二次校验
sessionId 归属
submitPracticeAnswer 使用事务 + 行锁(SELECT ... FOR UPDATE)
5.8.4 监控埋点
六、架构图同步说明
6.1 需要补充的节点
| 文档 |
节点 |
说明 |
| 004 §2.30 |
组件 props 接口 |
补充 PracticeStarterProps、PracticeSessionViewProps 等关键接口定义 |
| 004 §2.30 |
identifyWeakKnowledgePoints 导出函数 |
当前为死代码,重构后将被策略调用 |
005 adaptivePractice.exports |
纯函数 lib 导出 |
新增 lib/grading.ts、lib/source-meta.ts、lib/strategy.ts 的导出函数 |
005 routes.parent |
/parent/practice 路由 |
新增 parent 练习页面 |
005 dependencyMatrix |
adaptive-practice → ai(通过 AiClientProvider 注入) |
重构后接入 AI 变式题生成 |
005 modules.error-book.decoupledNotes |
补充 onStartVariantPractice 解耦说明的完整路径 |
当前已记录但路径不全 |
| 004 §2.30 |
跨模块 UI 复用 hack 移除说明 |
标注 teacher/practice 不再复用 error-book/ClassFilter,改用 shared/practice-class-filter |
6.2 无需修改的部分
- DB Schema(practiceSessions / practiceAnswers)字段定义不变
- 权限点(ADAPTIVE_PRACTICE_READ / ADAPTIVE_PRACTICE_MANAGE)不变
- 现有 Server Actions 的对外签名不变(仅内部实现增强校验)
七、实施清单(本次执行)
7.1 P0 修复项(本次完整实施)
7.2 P1 修复项(本次完整实施)
7.3 P2 长期项(记录备查,不在本次实施范围)
7.4 验证步骤
npx tsc --noEmit 零错误 ✅(本次新增/修改文件零错误;预存错误 exams/lesson-preparation/standards/attendance/homework/textbooks 与本次改动无关)
npm run lint 零错误零警告 ✅(8 个本次改动文件 eslint --quiet 零警告)
- 单测:
npm test -- adaptive-practice ⏳ 待补齐
- 手动验证:⏳ 待人工验证
- 学生发起 4 种练习 + 答题 + 完成/放弃
- 教师查看班级分析(含错误边界测试)
- 家长查看子女练习(新路由)
- error-book 发起变式练习(重做)
- 中英文切换显示
- 同步更新架构图 004 / 005 ✅(见 §六)