教材(Textbooks)模块审计报告 v3
审计日期:2026-06-24
审计范围:src/modules/textbooks/**、src/app/(dashboard)/teacher/textbooks/**、src/app/(dashboard)/student/learning/textbooks/**
对比基准:textbooks-audit-report-v2.md(v2)
参照规则:docs/architecture/004_architecture_impact_map.md、docs/architecture/005_architecture_data.json、.trae/rules/project_rules.md
一、现有实现概要
1.1 模块定位
教材模块是 K12 系统的"标杆模块"(架构图 004 第 2.5 节明确标注),承担教材与知识体系管理职责:
- 教材/章节树形结构 CRUD
- 知识点 CRUD + Markdown 内容编辑
- 知识图谱可视化(React Flow + dagre 布局)
- 知识点前置依赖管理(含循环检测)
- 学生/班级掌握度聚合查询
1.2 文件分布与规模
| 层 |
文件数 |
总行数 |
最大文件 |
备注 |
| 模块核心(modules/textbooks) |
26 |
~3,800 |
data-access.ts 662 行 |
含 5 个 hooks、16 个组件 |
| App 层 - 教师 |
6 |
~350 |
[id]/page.tsx 66 行 |
含 loading/error |
| App 层 - 学生 |
6 |
~240 |
page.tsx 86 行 |
含 loading/error |
| 测试 |
2 |
~200 |
utils.test.ts |
纯函数单测 |
1.3 数据流概览
1.4 架构图记录完整性评估
| 架构图节点 |
记录状态 |
差异 |
| 004 § 2.5 textbooks |
✅ 完整 |
导出函数、依赖、已知问题、文件清单均已记录 |
| 005 textbooks 节点 |
✅ 完整 |
actions/dataAccess/hooks/types/components/files 均已记录 |
| 已知问题记录 |
⚠️ 部分过期 |
005 knownIssues 仍写"i18n 覆盖率约 98%",实际审计发现残留硬编码英文异常文案、a11y 缺失等问题未记录 |
结论:架构图对教材模块的覆盖度约 90%,主要差距在 knownIssues 字段未同步最新审计发现。
二、现存问题与原因分析
2.1 安全与权限维度(P0)
问题 T-AUDIT-01 | 学生端页面缺少 requirePermission 显式校验
- 位置:student/learning/textbooks/page.tsx、student/learning/textbooks/[id]/page.tsx
- 现象:两个学生端页面均未调用
requirePermission(Permissions.TEXTBOOK_READ),仅靠 getCurrentStudentUser() 隐式校验角色。对比教师端 teacher/textbooks/page.tsx 第 18 行显式调用 requirePermission。
- 违反规则:
project_rules.md → "Server Action 规范" → "每个 Action 必须调用 requirePermission() 进行权限校验";以及"安全规范" → "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"
- 原因:学生端页面是 RSC 直接调用 data-access(非 Server Action),开发者认为
getCurrentStudentUser 已隐含角色校验,但权限点 TEXTBOOK_READ 未被显式校验,无法通过权限矩阵灵活调整
- 后果:若未来权限矩阵调整(如临时禁止某学生查看教材),学生端无法响应;权限审计日志缺失"教材阅读"事件
问题 T-AUDIT-02 | getKnowledgeGraphDataAction 缺少数据范围过滤
- 位置:actions.ts 第 373-413 行
- 现象:
getKnowledgeGraphDataAction 仅校验 TEXTBOOK_READ 权限,未按学生年级做 scope 过滤(对比 getTextbooksWithScope 有 scope.grade 过滤)。学生可传任意 textbookId 查看跨年级教材的图谱数据
- 违反规则:
project_rules.md → "安全规范" → "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"
- 原因:图谱查询是 Task 7 新增功能,开发时复用了教师端逻辑,未区分学生数据范围
- 后果:学生可越权查看非本年级教材的知识图谱与掌握度数据
问题 T-AUDIT-03 | class-mastery 视图缺少教师-班级归属校验
- 位置:actions.ts 第 393-403 行
- 现象:
getKnowledgeGraphDataAction 的 class-mastery 分支调用 getClassStudents({ status: "active" }) 获取全量班级学生 ID,未校验当前教师是否拥有这些班级
- 违反规则:
project_rules.md → "安全规范" → "Server Action 二次校验"
- 原因:直接调用 classes 模块的
getClassStudents 未传入教师 scope
- 后果:任何持有
TEXTBOOK_READ 权限的教师可查看全校所有班级的掌握度数据
2.2 数据完整性维度(P0)
问题 T-AUDIT-04 | deleteChapter 未清理孤儿前置依赖记录
- 位置:data-access.ts 第 259-298 行
- 现象:递归删除章节及其知识点时,未删除
knowledgePointPrerequisites 表中引用被删知识点的边(包括作为 prerequisiteKpId 的记录)
- 违反规则:数据完整性最佳实践
- 原因:删除逻辑只关注了
knowledgePoints 表本身,遗漏了外键关联表
- 后果:图谱渲染时出现指向已删除知识点的孤儿边,循环检测算法
hasCycleAfterAddingEdge 可能因引用不存在节点而异常
2.3 类型安全维度(P1)
问题 T-AUDIT-05 | as 断言违反规范
- 位置:
- 违反规则:
project_rules.md → "TypeScript 规则" → "禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)"
- 原因:React Flow 的
NodeProps 类型与自定义数据结构不匹配,开发者用 as unknown as 绕过类型检查
- 后果:类型安全被削弱,运行时可能因数据形状不符而崩溃
问题 T-AUDIT-06 | 类型与 DB Schema 不一致
- 位置:types.ts
- 现象:
- 第 42 行
KnowledgePoint.chapterId?: string(可选),但 DB schema 中该字段非空
- 第 13 行
Textbook.grade: string | null(可空),但 CreateTextbookSchema.grade 要求 min(1) 非空
- 违反规则:
project_rules.md → "TypeScript 规则" → "禁止 any"
- 原因:类型手动定义而非从 Drizzle schema 推断(types.ts 第 1-3 行注释承认)
- 后果:下游需大量
?? undefined 兜底(data-access.ts 第 320、347 行),类型契约不可信
2.4 错误处理维度(P0/P1)
问题 T-AUDIT-07 | 前置依赖操作无 try/catch,loading 状态卡死
- 位置:knowledge-graph.tsx 第 202-219 行(
handleAddPrerequisite)、第 222-235 行(handleRemovePrerequisite)
- 现象:两个函数均无 try/catch,若 action 抛异常,
setIsSavingPrereq(false) 不会执行
- 违反规则:
project_rules.md → "错误处理" 隐含要求
- 原因:开发者假设 Server Action 不会抛异常(实际网络异常、服务端 500 均会抛)
- 后果:用户点击"添加前置依赖"失败后,按钮永久 loading,需刷新页面
问题 T-AUDIT-08 | textbook-reader.tsx onCreateKnowledgePoint 无 try/catch
- 位置:textbook-reader.tsx 第 177-181 行
- 现象:
onCreateKnowledgePoint 回调中调用 handleCreateKnowledgePoint,无 try/catch,异常时 setIsCreating(false) 不执行
- 后果:创建知识点失败时,UI 永久显示创建中状态
问题 T-AUDIT-09 | section-error-boundary.tsx 缺 componentDidCatch
- 位置:section-error-boundary.tsx
- 现象:仅实现
getDerivedStateFromError,未实现 componentDidCatch,错误被吞没未上报
- 违反规则:React Error Boundary 最佳实践
- 后果:生产环境无法追踪章节内容渲染错误
问题 T-AUDIT-10 | 4 个 error.tsx 完全重复且未记录日志
- 位置:4 个 error.tsx 文件(教师/学生 × 列表/详情)
- 现象:内容完全相同(仅函数名不同),
error 参数接收但未 console.error 记录、未展示 error.digest、未区分错误类型
- 违反规则:DRY 原则、Next.js error.tsx 最佳实践
- 后果:错误无法追踪;4 份重复代码维护成本高
2.5 国际化维度(P1)
问题 T-AUDIT-11 | data-access 层硬编码英文异常文案
- 位置:data-access.ts 第 188、244、383 行
- 现象:
throw new Error("Textbook not found") / "Chapter not found" 等英文硬编码
- 违反规则:
project_rules.md → "所有用户可见文本必须适配 i18n"
- 原因:data-access 层无法直接调用
getTranslations(需 async context)
- 后果:错误冒泡到
handleActionError 后,非英文用户看到英文错误信息
问题 T-AUDIT-12 | use-graph-data.ts 错误状态值类型不一致
- 位置:use-graph-data.ts 第 60、66 行
- 现象:
error 状态有时是服务端已翻译文案(result.message),有时是原始 i18n key 字符串("graph.error.loadFailed"),有时是 JS Error.message
- 后果:下游组件无法判断该直接显示还是再
t() 翻译
2.6 可访问性维度(P1)
问题 T-AUDIT-13 | 章节项可点击 div 无键盘支持
- 位置:chapter-sidebar-list.tsx 第 100-110 行
- 现象:章节项点击区是
<div onClick>,缺少 role="button"、tabIndex={0}、onKeyDown
- 违反规则:WCAG 2.1 AA → 键盘可达性
- 后果:键盘用户无法选中章节
问题 T-AUDIT-14 | 图标按钮缺 aria-label
问题 T-AUDIT-15 | loading.tsx 缺少 ARIA 标记
- 位置:4 个 loading.tsx 文件
- 现象:根容器缺少
role="status" 和 aria-busy="true"
- 后果:屏幕阅读器无法感知加载状态
问题 T-AUDIT-16 | error.tsx 缺少 role="alert"
- 位置:4 个 error.tsx 文件
- 现象:错误容器缺少
role="alert",屏幕阅读器不会自动播报错误
2.7 跨模块耦合维度(P1)
问题 T-AUDIT-17 | 跨模块 URL 硬编码
- 位置:
- 现象:组件内硬编码角色前缀 URL(
/teacher/...),无法复用于学生/家长端
- 违反规则:
project_rules.md → "前端组件禁止使用 role === "xxx" 硬编码"(URL 硬编码角色前缀同理)
- 原因:组件未通过 props 注入路由前缀
- 后果:组件无法跨角色复用;新增角色需修改组件源码
2.8 代码质量维度(P2)
问题 T-AUDIT-18 | use-kp-crud.ts 行数超限
- 位置:hooks/use-kp-crud.ts 122 行
- 现象:超过 Hook 上限 80 行
- 违反规则:
project_rules.md → "自定义 Hook:建议 ≤ 80 行"
- 原因:CRUD 操作集中在一个 Hook
- 后果:维护困难
问题 T-AUDIT-19 | 表单字段代码重复
问题 T-AUDIT-20 | 死代码
问题 T-AUDIT-21 | textbook-content-panel.tsx props 膨胀
问题 T-AUDIT-22 | 函数返回类型未显式标注
- 位置:
use-knowledge-point-actions.ts、use-kp-dialog-state.ts、use-text-selection.ts、use-graph-data.ts、constants.ts(getSubjectColor 等)
- 违反规则:
project_rules.md → "函数返回值必须显式标注,特别是 Promise<T>"
- 后果:类型推导不透明
2.9 性能与可测试性维度(P2)
问题 T-AUDIT-23 | textbook-reader.tsx 命令式 DOM 操作
- 位置:textbook-reader.tsx 第 229-235 行
- 现象:
document.querySelector(\[data-kp-id="${highlightedKpId}"]`)+el.classList.add/remove`
- 风险:模板字符串选择器存在选择器注入风险;命令式 DOM 操作与 React 数据流冲突
- 后果:难以测试、潜在 XSS
问题 T-AUDIT-24 | graph-layout.ts O(n²) 性能
- 位置:graph-layout.ts 第 56、64、89、103 行
- 现象:多次
knowledgePoints.some(...) 线性查找
- 后果:大图谱(>100 节点)性能差
问题 T-AUDIT-25 | cache() 包裹影响可测试性
三、行业差距对比
基于对 PowerSchool、Veracross、钉钉教育、智学网、班级小管家、ClassIn、智慧树等 K12 产品的调研,教材模块相比行业优秀实践存在以下差距:
3.1 功能差距
| 差距项 |
行业实践 |
当前实现 |
影响角色 |
| 多角色视图缺失 |
admin 全校教材管理、parent 查看子女教材 |
仅 teacher/student 两端 |
admin、parent |
| 教材版本管理 |
支持多版本教材对比、历史版本回滚 |
无版本概念 |
teacher、admin |
| 协作编辑 |
多教师协作编辑同一教材(锁机制/CRDT) |
单人编辑,无锁 |
teacher |
| 离线阅读 |
移动端缓存章节内容离线访问 |
无离线缓存 |
student |
| PDF/打印导出 |
章节导出为 PDF、打印友好版式 |
无导出功能 |
teacher、student |
| 阅读进度追踪 |
记录学生阅读进度、停留时长 |
无进度追踪 |
student、teacher |
| 教材资源附件 |
章节关联视频、音频、PDF 附件 |
仅 Markdown 文本 |
teacher、student |
| 知识点掌握度联动 |
图谱节点点击跳转至相关题目/作业练习 |
仅展示掌握度,无跳转练习入口 |
student |
| 教材评论/批注 |
教师可在章节内添加批注,学生可见 |
无批注功能 |
teacher、student |
| 智能推荐 |
基于掌握度推荐复习路径 |
analytics.tsx 仅预留接口,未实现 |
student |
3.2 UI/UX 差距
| 差距项 |
行业实践 |
当前实现 |
影响 |
| 骨架屏布局不匹配 |
骨架屏与实际页面结构一致 |
[id]/loading.tsx 渲染 grid+sidebar,实际是阅读器布局 |
用户体验割裂 |
| 空状态引导 |
空状态提供"创建教材"CTA 按钮 |
仅文字提示 |
转化率低 |
| 筛选持久化 |
筛选条件持久化到 URL(nuqs) |
部分持久化,不一致 |
刷新丢失筛选 |
| 移动端阅读体验 |
上下滑动阅读、字号调节、夜间模式 |
有 Sheet 抽屉,无字号/主题调节 |
阅读体验差 |
| 图谱交互 |
节点拖拽、缩放、小地图、方向键导航 |
有拖拽/缩放,缺方向键导航 |
键盘用户不可达 |
3.3 架构差距
| 差距项 |
行业实践 |
当前实现 |
影响 |
| 配置驱动 |
角色配置决定渲染哪些 Widget |
组件内硬编码角色 URL |
新增角色需改组件 |
| 依赖注入 |
通过 Context 注入数据服务 |
组件直接调用 data-access |
难以 mock 测试 |
| 错误恢复 |
错误边界提供重试按钮 + 自动上报 |
仅 router.refresh,无上报 |
错误不可追踪 |
| 监控埋点 |
关键操作(创建/删除/阅读)埋点 |
analytics.tsx 仅 stub |
无行为数据 |
四、改进优先级建议
4.1 P0(紧急,安全与数据完整性)
| 编号 |
问题 |
改进方向 |
影响范围 |
| T-FIX-01 |
T-AUDIT-01 学生端缺 requirePermission |
学生端两个页面添加 requirePermission(Permissions.TEXTBOOK_READ) |
2 文件 |
| T-FIX-02 |
T-AUDIT-02 图谱缺 scope 过滤 |
getKnowledgeGraphDataAction 学生端按年级 scope 过滤 textbookId |
actions.ts |
| T-FIX-03 |
T-AUDIT-03 class-mastery 缺教师-班级校验 |
传入教师 scope,校验班级归属 |
actions.ts |
| T-FIX-04 |
T-AUDIT-04 deleteChapter 孤儿记录 |
级联删除 knowledgePointPrerequisites 表相关记录 |
data-access.ts |
| T-FIX-05 |
T-AUDIT-07/08 前置依赖/创建知识点无 try/catch |
补 try/catch/finally,finally 中复位 loading |
knowledge-graph.tsx、textbook-reader.tsx |
| T-FIX-06 |
T-AUDIT-09 Error Boundary 缺 componentDidCatch |
添加 componentDidCatch 记录错误 |
section-error-boundary.tsx |
4.2 P1(重要,规范合规)
| 编号 |
问题 |
改进方向 |
影响范围 |
| T-FIX-07 |
T-AUDIT-05 as 断言 |
用 NodeProps<GraphLayoutNodeData> 泛型替代双重断言;data-access 用 satisfies |
3 文件 |
| T-FIX-08 |
T-AUDIT-06 类型与 DB Schema 不一致 |
chapterId 改为必填;grade 与 schema 对齐 |
types.ts |
| T-FIX-09 |
T-AUDIT-10 error.tsx 重复 |
抽取 shared/components/error-boundary.tsx 共享组件 |
4 文件 → 1 共享 |
| T-FIX-10 |
T-AUDIT-11 data-access 硬编码英文 |
改用错误码,actions 层翻译 |
data-access.ts |
| T-FIX-11 |
T-AUDIT-12 错误状态值类型不一致 |
统一为 i18n key 或错误码 |
use-graph-data.ts |
| T-FIX-12 |
T-AUDIT-13 章节项无键盘支持 |
添加 role="button"/tabIndex/onKeyDown |
chapter-sidebar-list.tsx |
| T-FIX-13 |
T-AUDIT-14 图标按钮缺 aria-label |
添加 aria-label |
graph-node-detail-panel.tsx |
| T-FIX-14 |
T-AUDIT-15/16 loading/error 缺 ARIA |
添加 role="status"/aria-busy/role="alert" |
8 文件 |
| T-FIX-15 |
T-AUDIT-17 跨模块 URL 硬编码 |
通过 props 注入 hrefBase,移除角色前缀 |
4 文件 |
| T-FIX-16 |
T-AUDIT-18 use-kp-crud 行数超限 |
按 create/update/delete 拆为 3 个子 Hook |
use-kp-crud.ts |
4.3 P2(优化,代码质量)
| 编号 |
问题 |
改进方向 |
影响范围 |
| T-FIX-17 |
T-AUDIT-19 表单字段重复 |
抽取 TextbookFormFields 共享组件 |
2 文件 |
| T-FIX-18 |
T-AUDIT-20 死代码 |
删除 verifyKnowledgePointBelongsToChapter、ReorderChaptersSchema、canEdit prop |
3 文件 |
| T-FIX-19 |
T-AUDIT-21 props 膨胀 |
拆分为编辑态/内容态/选区态 Context |
textbook-content-panel.tsx |
| T-FIX-20 |
T-AUDIT-22 返回类型未标注 |
显式标注所有 Hook/工具函数返回类型 |
5 文件 |
| T-FIX-21 |
T-AUDIT-23 命令式 DOM 操作 |
改用 React 状态驱动高亮 |
textbook-reader.tsx |
| T-FIX-22 |
T-AUDIT-24 O(n²) 性能 |
预建 Set 替代 some() |
graph-layout.ts |
| T-FIX-23 |
T-AUDIT-25 cache() 可测试性 |
导出非 cache 版本供测试 |
data-access.ts |
4.4 中长期计划(P3,功能增强)
| 编号 |
功能 |
优先级 |
说明 |
| T-FEAT-01 |
admin/parent 端教材视图 |
中 |
复用现有组件,新增配置驱动 |
| T-FEAT-02 |
教材版本管理 |
低 |
新增 textbook_versions 表 |
| T-FEAT-03 |
PDF/打印导出 |
中 |
服务端生成 PDF |
| T-FEAT-04 |
阅读进度追踪 |
中 |
新增 reading_progress 表 |
| T-FEAT-05 |
教材资源附件 |
中 |
章节关联 files 模块 |
| T-FEAT-06 |
智能推荐学习路径 |
低 |
接入 AI 模块 |
| T-FEAT-07 |
图谱方向键导航 |
低 |
React Flow 键盘交互 |
| T-FEAT-08 |
监控埋点接入 |
中 |
实现 analytics.tsx 真实逻辑 |
五、架构图同步说明
5.1 需要更新的节点
| 文档 |
节点 |
修改内容 |
| 004 § 2.5 |
已知问题 |
新增 v3 审计发现:学生端缺 requirePermission、图谱缺 scope 过滤、deleteChapter 孤儿记录、error.tsx 重复、a11y 缺失等 |
| 004 § 2.5 |
文件清单 |
更新行数(如 use-kp-crud 拆分后行数变化) |
| 005 textbooks.knownIssues |
knownIssues |
同步上述新发现 |
| 005 textbooks.files |
files |
同步行数变化 |
5.2 无需修改的部分
- 导出函数清单:本次审计未发现新增/删除/重命名导出函数
- 依赖关系:未新增跨模块依赖
- 权限点:未新增权限点
- 数据库表:未新增表(中长期计划中的新表待实施时再同步)
附录:审计方法说明
- 审计基准:2026-06-24 代码库快照
- 审计工具:人工阅读全部 26 个模块文件 + 13 个 app 层文件
- 覆盖维度:安全/权限、数据完整性、类型安全、错误处理、i18n、a11y、跨模块耦合、代码质量、性能、可测试性
- 未覆盖:E2E 测试、性能压测、安全渗透测试(建议后续补充)