Files
NextEdu/docs/architecture/audit/textbooks-audit-report.md
SpecialX 2548f70f40 docs(textbooks): 新增教材模块审计报告并同步架构图
- 新增 docs/architecture/audit/textbooks-audit-report.md,覆盖三层架构、权限、i18n、类型安全、错误边界、组件复用、a11y、可测试性、性能、安全等维度的审计,并给出 P0/P1/P2 改进优先级与重构方案要点

- 同步 004 架构影响地图 §2.5:修正 actions/data-access 行数与导出函数名(移除不存在的读 Action,补充 reorderChaptersAction),补充跨模块 UI 依赖、已知问题清单

- 同步 005 架构数据 JSON:补充 getKnowledgePointOptions 跨模块接口、uiDeps、knownIssues、auditReport 字段,修正 getTextbooks/getTextbookById 的 usedBy 以包含学生端页面
2026-06-22 15:38:26 +08:00

511 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 教材Textbooks模块审计报告
> 审计日期2026-06-22
> 审计范围:`src/modules/textbooks/**`、`src/app/(dashboard)/teacher/textbooks/**`、`src/app/(dashboard)/student/learning/textbooks/**`
> 参照规则:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
---
## 一、现有实现概要
### 1.1 文件分布
教材模块作为 K12 系统的"标杆模块"(架构图原文),文件分布如下:
| 层 | 文件 | 行数 | 职责 |
|------|------|------|------|
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts) | 514 | 教材/章节/知识点 CRUD + 跨模块查询接口 |
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/textbooks/actions.ts) | 317 | 13 个 Server Action含权限校验 |
| 类型 | [types.ts](file:///e:/Desktop/CICD/src/modules/textbooks/types.ts) | 45 | Textbook / Chapter / KnowledgePoint 类型 |
| 校验 | [schema.ts](file:///e:/Desktop/CICD/src/modules/textbooks/schema.ts) | 64 | Zod 校验 schema |
| Hook | [hooks/use-knowledge-point-actions.ts](file:///e:/Desktop/CICD/src/modules/textbooks/hooks/use-knowledge-point-actions.ts) | 121 | 知识点增删改状态机 |
| Hook | [hooks/use-text-selection.ts](file:///e:/Desktop/CICD/src/modules/textbooks/hooks/use-text-selection.ts) | 57 | 文本选区捕获 |
| 组件 | [components/textbook-reader.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx) | 319 | 阅读器主壳Tabs目录/知识点/图谱) |
| 组件 | [components/textbook-content-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx) | 170 | Markdown 渲染 + 编辑切换 |
| 组件 | [components/chapter-sidebar-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx) | 348 | 递归章节树 + 拖拽排序 |
| 组件 | [components/knowledge-point-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-list.tsx) | 107 | 知识点列表 |
| 组件 | [components/knowledge-graph.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx) | 181 | 知识图谱 SVG 可视化 |
| 组件 | [components/knowledge-point-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-panel.tsx) | 157 | 知识点面板(旧版,与 list 重叠) |
| 组件 | [components/knowledge-point-dialogs.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-dialogs.tsx) | 148 | 创建/编辑知识点弹窗集合 |
| 组件 | [components/textbook-card.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-card.tsx) | 121 | 教材卡片 |
| 组件 | [components/textbook-filters.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-filters.tsx) | 71 | 筛选栏 |
| 组件 | [components/textbook-form-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-form-dialog.tsx) | 134 | 新建教材弹窗 |
| 组件 | [components/textbook-settings-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-settings-dialog.tsx) | 160 | 教材设置/删除弹窗 |
| 组件 | [components/create-chapter-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/create-chapter-dialog.tsx) | 95 | 新建章节弹窗 |
| 组件 | [components/create-knowledge-point-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/create-knowledge-point-dialog.tsx) | 95 | 新建知识点弹窗(旧版) |
| 页面 | [teacher/textbooks/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/textbooks/page.tsx) | 68 | 教师端列表页RSC |
| 页面 | [teacher/textbooks/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) | 65 | 教师端详情页RSC |
| 页面 | [student/learning/textbooks/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/textbooks/page.tsx) | 66 | 学生端列表页RSC |
| 页面 | [student/learning/textbooks/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx) | 64 | 学生端详情页RSC |
| 骨架屏 | 4 个 `loading.tsx` | — | 列表/详情骨架屏 |
### 1.2 数据流
```
page.tsx (RSC)
└─ getTextbooks / getTextbookById / getChaptersByTextbookId / getKnowledgePointsByTextbookId (data-access)
└─ db (drizzle) → textbooks / chapters / knowledgePoints 表
└─ <TextbookReader> (client)
├─ <ChapterSidebarList> → deleteChapterAction / reorderChaptersAction
├─ <TextbookContentPanel> → updateChapterContentAction
├─ <KnowledgePointList> → useKnowledgePointActions → create/update/deleteKnowledgePointAction
└─ <KnowledgePointDialogs> → ⚠️ 直接 import @/modules/questions/components/create-question-dialog
```
### 1.3 架构图记录完整性
经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.5 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json),架构图对教材模块的记录**存在以下偏差**(详见第五节):
- 行数统计过期:图记 `actions.ts 276 行 / data-access.ts 428 行`,实际为 `317 / 514`
- 导出函数名错误:图记 `getTextbooksAction / getTextbookByIdAction / getChaptersAction / getKnowledgePointsAction` 等"读 Action",实际不存在——读操作直接走 data-accessRSC未包装成 Action。
- 组件文件数:图记"12 文件",实际 11 个组件文件。
- 未记录跨模块 UI 依赖:`knowledge-point-dialogs.tsx` 直接 import questions 模块的 `CreateQuestionDialog`,图未标注。
---
## 二、现存问题与原因分析
### 2.1 架构解耦
#### 问题 2.1.1 跨模块直接 import 业务组件P0
- **位置**[knowledge-point-dialogs.tsx#L16](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-dialogs.tsx#L16)
- **现象**`import { CreateQuestionDialog } from "@/modules/questions/components/create-question-dialog"`
- **违反规则**:项目规则"该模块必须作为独立功能单元……模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"以及"模块间只能通过对方 data-access 通信"。
- **原因**:教材知识点页希望"一键创建相关题目",直接耦合了 questions 模块的弹窗组件,而非通过接口注入或事件回调。
- **后果**questions 模块任何对 `CreateQuestionDialog` props/位置的变更都会破坏教材模块编译;无法独立测试、独立部署教材模块;新增 admin/parent 角色时无法替换该弹窗实现。
#### 问题 2.1.2 前端权限硬编码 `canEdit`P0
- **位置**
- [teacher/textbooks/[id]/page.tsx#L60](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/textbooks/[id]/page.tsx#L60)`canEdit={true}`
- [student/learning/textbooks/[id]/page.tsx#L58](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx#L58):未传 `canEdit`(默认 `false`
- **违反规则**:项目规则"前端权限判断统一使用 `usePermission().hasPermission()`,严禁出现 `role === "xxx"` 硬编码"。此处虽未出现 `role ===`,但用"路由前缀"teacher/student隐式决定编辑权本质等价于角色硬编码。
- **原因**:图省事直接按路由写死布尔值,未接入权限上下文。
- **后果**:一旦 admin 也需编辑教材、或 teacher 在某些场景被回收 `TEXTBOOK_UPDATE`,前端仍会展示编辑按钮,造成"按钮可见但点击 403"的体验;权限策略变更需改多处代码。
#### 问题 2.1.3 data-access 缺少数据范围过滤P1
- **位置**[data-access.ts#L75](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts#L75) `getTextbooks`、[#L125](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts#L125) `getTextbookById`
- **现象**:查询未结合当前用户身份(年级、班级、学科权限)做过滤,任何能进入路由的用户都能读到全量教材。
- **违反规则**:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
- **原因**:学生端页面虽调用 `getCurrentStudentUser()`,但拿到的 student 信息并未用于过滤教材(如按学生年级筛选)。
- **后果**:跨年级学生可看到非本年级教材;多租户场景下数据越权。
### 2.2 国际化i18n
#### 问题 2.2.1 全模块零 i18n 覆盖P0
- **位置**:模块全部 19 个源文件
- **现象**:项目已接入 next-intl见 [i18n/request.ts](file:///e:/Desktop/CICD/src/i18n/request.ts)),但教材模块**没有任何一处**使用 `useTranslations` / `getTranslations`,所有文案硬编码,且中英文混杂:
- 中文硬编码:`"章节目录"``"知识点"``"图谱"``"请选择一个章节查看知识点。"``"该章节暂无知识点。"``"添加知识点"``"取消"``"删除"``"保存"``"确认删除"``"确定要删除这个知识点吗?此操作无法撤销。"``"创建中..."``"保存中..."``"知识点已创建"``"发生错误"``"删除失败"``"更新失败"``"返回教材列表"` 等([textbook-reader.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx)、[knowledge-point-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-list.tsx)、[knowledge-point-dialogs.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-dialogs.tsx)、[use-knowledge-point-actions.ts](file:///e:/Desktop/CICD/src/modules/textbooks/hooks/use-knowledge-point-actions.ts)、[teacher/textbooks/[id]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/textbooks/[id]/page.tsx)
- 英文硬编码:`"Textbooks"``"Manage your digital curriculum resources and chapters."``"Add Textbook"``"Add New Textbook"``"Create a new digital textbook."``"Save changes"``"Search by title, publisher..."``"All Subjects"``"All Grades"``"Subject"``"Grade"``"Publisher"``"Title"``"Chapters"``"Updated"``"Edit Content"``"Delete"``"Settings"``"Textbook Settings"``"Delete Textbook"``"Add Chapter"``"Add Knowledge Point"``"Knowledge Points"``"No points yet"``"Select a chapter to manage knowledge points"` 等([textbook-filters.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-filters.tsx)、[textbook-form-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-form-dialog.tsx)、[textbook-settings-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-settings-dialog.tsx)、[textbook-card.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-card.tsx)、[knowledge-point-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-panel.tsx)
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
- **原因**:模块开发时未跟进 i18n 改造,文案随写随定。
- **后果**:无法切换语言;同一界面中英混杂,专业度差;后续做国际化需返工全部组件。
### 2.3 类型安全
#### 问题 2.3.1 非空断言与 `as` 断言P1
- **位置**
- [chapter-sidebar-list.tsx#L141](file:///e:/Desktop/CICD/src/modules/textbooks/components/chapter-sidebar-list.tsx#L141)`items={chapter.children!}` —— 已在 `hasChildren` 守卫后仍用 `!`,应改用 narrowing。
- [knowledge-graph.tsx#L105](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx#L105)`positions.get(kp.parentId as string)!` —— `as string` + `!` 双重断言。
- [knowledge-graph.tsx#L106](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx#L106)`positions.get(kp.id)!`
- **违反规则**:项目规则"禁止 `as` 断言(除非从 `unknown` 转换)"、"可选链后禁止跟非空断言 `!`"。
- **后果**:运行时若数据不一致(如 parentId 指向已删除节点),直接抛错而非优雅降级。
#### 问题 2.3.2 `data-access.ts` 使用 `select()` 无类型投影P2
- **位置**[data-access.ts#L413](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts#L413)`db.select().from(chapters)`
- **现象**`select()` 不传参数返回整行,类型推断为全表 schema与模块对外 `Chapter` 类型不完全一致(如 `content` 可空性)。
- **后果**:类型边界模糊,后续 schema 变更可能静默破坏调用方。
### 2.4 错误与边界处理
#### 问题 2.4.1 缺少 React Error BoundaryP1
- **位置**`src/app/(dashboard)/teacher/textbooks/**``src/app/(dashboard)/student/learning/textbooks/**` 均无 `error.tsx`
- **现象**:详情页 `getTextbookById` 返回 `undefined` 时走 `notFound()`,但章节/知识点查询失败、Server Action 抛错时整页崩溃,无降级 UI。
- **违反规则**:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
- **后果**:一次 DB 抖动导致整个阅读器白屏,无法隔离故障域。
#### 问题 2.4.2 删除确认交互不一致P2
- **位置**[textbook-settings-dialog.tsx#L52](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-settings-dialog.tsx#L52)`if (!confirm("Are you sure..."))` 使用浏览器原生 `confirm`
- **现象**:模块内其他删除(章节、知识点)均用 `AlertDialog`,唯独教材删除用 `confirm()`
- **违反规则**:项目规则"组合优先"与 UI 一致性;`confirm()` 阻塞主线程且不可定制样式。
- **后果**:交互体验割裂;移动端 `confirm` 表现不一。
#### 问题 2.4.3 空状态文案与组件不统一P2
- **位置**
- [textbook-reader.tsx#L222](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx#L222):内联 `<div>请选择一个章节查看知识点。</div>`
- [knowledge-point-list.tsx#L32](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-list.tsx#L32):内联 `<div>该章节暂无知识点。</div>`
- [textbook-content-panel.tsx#L67](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx#L67):内联 `<div>请选择一个章节开始阅读。</div>`
- 列表页则用 `EmptyState` 组件
- **后果**同一模块内空状态有三种写法维护成本高a11y 属性缺失。
### 2.5 组件复用与组合
#### 问题 2.5.1 知识点列表/面板存在重复实现P1
- **位置**
- [knowledge-point-list.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-list.tsx)107 行,被 `TextbookReader` 使用)
- [knowledge-point-panel.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-panel.tsx)157 行,未被任何页面引用,疑似旧版遗留)
- **现象**:两个组件职责几乎相同(展示章节知识点 + 删除),`KnowledgePointPanel` 还自带 `router.refresh()`,但实际无调用方。
- **违反规则**:项目规则"最大化复用"。
- **后果**:死代码增加认知负担;修改知识点展示逻辑需同步两处。
#### 问题 2.5.2 创建知识点弹窗存在两套实现P1
- **位置**
- [create-knowledge-point-dialog.tsx](file:///e:/Desktop/CICD/src/modules/textbooks/components/create-knowledge-point-dialog.tsx)(独立弹窗,被 `KnowledgePointPanel` 引用,但 `KnowledgePointPanel` 本身无调用方)
- [knowledge-point-dialogs.tsx#L56-L85](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-dialogs.tsx#L56)(内嵌创建弹窗,被 `TextbookReader` 使用)
- **现象**:两套创建知识点弹窗,文案一中一英,字段一致但实现独立。
- **后果**:同上,双份维护。
#### 问题 2.5.3 学科/年级选项硬编码三处P1
- **位置**
- [textbook-filters.tsx#L43-L66](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-filters.tsx#L43)Select 选项
- [textbook-form-dialog.tsx#L89-L113](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-form-dialog.tsx#L89)Select 选项(且 form 与 settings 的学科列表不一致form 含 Biology/Geographysettings 缺这两项)
- [textbook-settings-dialog.tsx#L106-L112](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-settings-dialog.tsx#L106)Select 选项
- [textbook-card.tsx#L26-L34](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-card.tsx#L26)`subjectColorMap` 学科颜色映射
- **现象**:学科、年级枚举在 4 个文件里各写一份,且**彼此不一致**settings 弹窗的学科列表少了 Biology 和 Geography
- **违反规则**:项目规则"最大化复用……抽象为泛型组件和 hooks"、"配置驱动设计"。
- **后果**:新增学科需改 4 处;当前已出现数据不一致——用户在 form 里能选 Biology但 settings 里看不到,编辑时学科被覆盖。
### 2.6 可访问性a11y
#### 问题 2.6.1 知识图谱 SVG 缺少无障碍属性P1
- **位置**[knowledge-graph.tsx#L142-L158](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx#L142)
- **现象**`<svg>``role="img"`、无 `aria-label`、无 `<title>`;节点用 `<button>` 但无 `aria-label` 描述跳转目标。
- **违反规则**:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
- **后果**:屏幕阅读器用户无法理解图谱内容。
#### 问题 2.6.2 图谱节点不支持键盘导航P2
- **位置**[knowledge-graph.tsx#L159](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx#L159)
- **现象**:节点用绝对定位 `<button>`,但无 `tabIndex` 管理、无方向键导航Tab 顺序混乱。
- **后果**:键盘用户难以在图谱中移动焦点。
### 2.7 可测试性
#### 问题 2.7.1 纯逻辑未导出无法单测P1
- **位置**
- [data-access.ts#L29-L73](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts#L29) `sortChapters` / `buildChapterTree`(模块内未导出)
- [knowledge-graph.tsx#L29-L117](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx#L29) `computeGraphLayout`(模块内未导出)
- [textbook-reader.tsx#L32-L44](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx#L32) `buildChapterIndex`
- **现象**:这些纯函数(树构建、图布局、索引构建)是核心逻辑,但未导出,无法写单测;模块目录下无任何 `__tests__``*.test.ts`
- **违反规则**:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
- **后果**:章节树构建、图谱布局这类容易出 bug 的算法无回归保护。
#### 问题 2.7.2 零测试覆盖P1
- **位置**:整个模块
- **现象**:无单元测试、无集成测试、无 e2e 测试。
- **后果**:重构高风险。
### 2.8 性能
#### 问题 2.8.1 知识点高亮用正则全局替换存在性能与正确性风险P2
- **位置**[textbook-reader.tsx#L153-L165](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-reader.tsx#L153)
- **现象**`processedContent` 对每个知识点名做 `new RegExp(..., "gi")` 全局替换O(n×m) 复杂度;且未处理知识点名互为子串的情况(已按长度降序缓解,但仍可能误伤)。
- **后果**:章节内容长、知识点多时主线程卡顿;高亮可能跨标签边界破坏 Markdown。
#### 问题 2.8.2 `getKnowledgePointsByTextbookId` 一次性拉全量P2
- **位置**[data-access.ts#L357](file:///e:/Desktop/CICD/src/modules/textbooks/data-access.ts#L357)
- **现象**:详情页一次性加载整本教材所有章节的知识点,无分页/懒加载。
- **后果**:大体量教材首屏慢。
### 2.9 安全性
#### 问题 2.9.1 Server Action 未校验资源归属P1
- **位置**[actions.ts](file:///e:/Desktop/CICD/src/modules/textbooks/actions.ts) 全部 Action
- **现象**`updateChapterContentAction(chapterId, content, textbookId)` 仅校验 `TEXTBOOK_UPDATE` 权限,未校验 `chapterId` 是否属于当前用户有权访问的教材。
- **违反规则**:项目规则"Server Action 二次校验"。
- **后果**:教师 A 可通过改 chapterId 篡改教师 B 的章节内容(越权写)。
#### 问题 2.9.2 Markdown 渲染虽用 sanitize但编辑端无 XSS 过滤P2
- **位置**[textbook-content-panel.tsx#L118](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx#L118) 用了 `rehype-sanitize`(✅),但 [RichTextEditor](file:///e:/Desktop/CICD/src/shared/components/ui/rich-text-editor.tsx) 输出未在保存前清洗。
- **后果**:依赖前端 sanitize一旦渲染端配置变更可能被绕过。
---
## 三、行业差距对比
对标国内外主流 K12 教育平台如人教数字教材、ClassIn、Seewo、Khan Academy、好未来"学而思"教材体系)在教材模块的设计,本模块存在以下差距:
### 3.1 内容呈现层
| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 支持富媒体嵌入(图片/音频/视频/公式/交互式 3D 模型) | 仅 Markdown 文本 + `RichTextEditor` | 理科教材无法呈现实验视频、几何图形、化学方程式K12 教学场景严重受限 |
| 公式编辑LaTeX / MathML | 无 | 数学/物理教材无法正确呈现公式 |
| 页面翻阅式阅读(带页码、书签、进度记忆) | 仅滚动 + URL `chapterId` | 学生阅读进度无持久化,无法"续读" |
| 朗读 / TTS 朗读 | 无 | 低年级学生、视障学生体验差 |
| 笔记/划线/高亮/书签 | 仅有"选区创建知识点" | 学生无法在教材上做个人笔记,教师无法布置"精读"任务 |
### 3.2 知识体系层
| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 知识图谱支持缩放/拖拽/力导向布局/关联题目预览 | 静态 SVG 树状布局,无交互(无缩放、无拖拽、无关联题目) | 图谱仅"能看",不能"用",无法支撑知识图谱驱动的个性化学习 |
| 知识点与题目/作业/考试双向关联,支持"知识点掌握度"雷达 | 仅单向"知识点→创建题目"入口 | 无法做学情诊断、薄弱知识点推送 |
| 知识点支持多级层级、跨章节关联、前置/后置依赖 | 仅 `parentId` 树 + `chapterId` 归属 | 无法表达"学习路径",无法做前置知识校验 |
### 3.3 多角色协作层
| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| admin统一教材库 + 多教师协作编辑 + 版本历史 | 仅 teacher 单人编辑,无版本管理 | 多教师同改一本教材会互相覆盖,无回滚能力 |
| parent查看孩子教材进度、笔记 | 完全缺失 parent 角色 | parent 无法了解孩子学习内容 |
| student教材 + 笔记 + 作业联动 | 仅只读阅读 | 学生无法在教材上做标记、无法跳转到对应作业 |
| 教研组:教材模板复用、章节共享 | 无模板/共享机制 | 同学科同年级教材重复建设 |
### 3.4 交互体验层
| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 章节拖拽支持跨级移动 | `reorderChapters` 仅支持同级排序,跨级需先删后建 | 教材结构调整效率低 |
| 全文搜索(章节标题 + 正文 + 知识点) | 仅列表页按 title/subject/grade/publisher 模糊搜索 | 学生无法"在教材里搜概念" |
| 离线下载 / 移动端适配 | 阅读器布局在窄屏下三栏堆叠,未做移动端阅读优化 | 移动端体验差K12 学生主要用平板/手机 |
| 阅读进度条 / 章节完成度 | 无 | 无法量化学习进度 |
### 3.5 数据分析层
| 行业优秀实践 | 本模块现状 | 影响 |
|---|---|---|
| 教材使用统计(阅读时长、热门章节、知识点停留) | 无埋点 | 无法为教研提供数据支撑 |
| 知识点难度标注 / 教师标注重点 | 仅有 `level` 字段但无 UI 录入 | 无法做分层教学 |
---
## 四、改进优先级建议
### P0紧急阻塞多角色上线
1. **解耦跨模块 UI 依赖**:将 `KnowledgePointDialogs` 中对 `CreateQuestionDialog` 的直接 import 改为通过 props 注入render prop 或 children由页面层决定渲染哪个题目创建组件或定义 `QuestionCreator` 接口,由 questions 模块实现并通过 Context 注入。
2. **接入前端权限 Hook**:删除 `canEdit={true}` 硬编码,在 `TextbookReader` 内部调用 `usePermission().hasPermission(Permissions.TEXTBOOK_UPDATE)` 决定编辑按钮可见性;列表页"新增教材"按钮同理用 `TEXTBOOK_CREATE` 控制。
3. **全模块 i18n 改造**:新增 `shared/i18n/messages/{en,zh-CN}/textbooks.json` 命名空间提取所有硬编码文案Server Component 用 `getTranslations`Client Component 用 `useTranslations`;统一中英文混杂问题。
4. **Server Action 资源归属校验**:在 `updateChapterContentAction` / `deleteChapterAction` / `createKnowledgePointAction` 等 Action 内,先校验 `chapterId` 所属 `textbookId` 与传入 `textbookId` 一致,并结合当前用户身份做二次校验。
### P1重要影响正确性与可维护性
1. **data-access 加数据范围过滤**`getTextbooks` 接受 `scope` 参数(年级/班级/学科),学生端按学生年级过滤;`getTextbookById` 校验访问权。
2. **补齐 Error Boundary**:在 `teacher/textbooks/[id]``student/learning/textbooks/[id]` 下新增 `error.tsx``TextbookReader` 内对章节区、知识点区、图谱区分别用 Error Boundary 包裹。
3. **消除重复组件**:删除未使用的 `knowledge-point-panel.tsx``create-knowledge-point-dialog.tsx`;统一知识点列表与创建弹窗为单一实现。
4. **抽取学科/年级配置**:新建 `src/modules/textbooks/constants.ts`,集中导出 `SUBJECTS``GRADES``SUBJECT_COLORS`,供 filters/form/settings/card 复用,消除不一致。
5. **导出纯函数并补单测**:导出 `buildChapterTree` / `sortChapters` / `computeGraphLayout` / `buildChapterIndex`,补 Vitest 单测覆盖空数组、单节点、深层嵌套、循环引用等边界。
6. **修复类型断言**:用类型守卫替换 `!``as`,例如 `chapter.children!` 改为 `hasChildren ? <RecursiveSortableList items={chapter.children} /> : null`
7. **图谱 a11y**svg 加 `role="img"` + `aria-label`;节点加 `aria-label={node.name}`;支持方向键导航。
8. **统一删除确认**`textbook-settings-dialog.tsx``confirm()` 改为 `AlertDialog`,与模块其他删除一致。
### P2优化提升体验与专业度
1. **统一空状态**:内联空状态全部改用 `EmptyState` 组件,补 a11y。
2. **知识点高亮性能优化**:改用一次 AST 遍历(基于 remark 插件)替换正则全局替换,避免跨标签误伤。
3. **知识点懒加载**:详情页仅加载当前章节知识点,切换章节时按需加载。
4. **移动端阅读优化**:窄屏下三栏改为抽屉式(章节侧栏可滑出)。
5. **补全架构图同步**(见第五节)。
6. **埋点接口预留**:在 `data-access``actions` 中预留 `onTextbookView` / `onChapterRead` 钩子,供后续接入监控。
---
## 五、架构图同步说明
本次审计发现 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.5 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中教材模块节点存在以下偏差,需同步修正:
### 5.1 行数统计过期
| 文件 | 图记行数 | 实际行数 |
|------|---------|---------|
| `actions.ts` | 276 | 317 |
| `data-access.ts` | 428 | 514 |
| `types.ts` | 79 | 45 |
| `hooks/use-knowledge-point-actions.ts` | 121 | 121一致 |
| 组件文件数 | 12 | 11 |
### 5.2 导出函数名错误
架构图 §2.5 记录的 Actions 列表含 `getTextbooksAction` / `getTextbookByIdAction` / `getChaptersAction` / `getKnowledgePointsAction`**实际不存在**。读操作直接由 RSC 页面调用 data-access`getTextbooks` / `getTextbookById` / `getChaptersByTextbookId` / `getKnowledgePointsByTextbookId` / `getKnowledgePointsByChapterId`),未包装成 Server Action。实际 Actions 为:
```
createTextbookAction / updateTextbookAction / deleteTextbookAction
createChapterAction / updateChapterContentAction / deleteChapterAction / reorderChaptersAction
createKnowledgePointAction / updateKnowledgePointAction / deleteKnowledgePointAction
```
### 5.3 未记录的跨模块 UI 依赖
架构图标注教材为"标杆模块(无跨模块 DB 访问)",这一结论对 data-access 层成立,但**组件层存在跨模块 UI 依赖**未记录:
- `textbooks/components/knowledge-point-dialogs.tsx``questions/components/create-question-dialog`
应在 004 的依赖关系图与 005 的 `dependencyMatrix` 中补充该 UI 层依赖,并标注为"待解耦P0"。
### 5.4 未记录的跨模块 data-access 调用方
`getKnowledgePointOptions`data-access 导出)被 questions 模块调用架构图已记录§2.4 questions 依赖 textbooks data-access但 005 JSON 中 textbooks 节点的 `exports` 字段未列出该函数。建议补充。
### 5.5 建议的 JSON 节点更新
`005_architecture_data.json``modules.textbooks` 节点建议补充/修正:
```jsonc
{
"textbooks": {
"exports": {
"actions": [
"createTextbookAction", "updateTextbookAction", "deleteTextbookAction",
"createChapterAction", "updateChapterContentAction", "deleteChapterAction",
"reorderChaptersAction",
"createKnowledgePointAction", "updateKnowledgePointAction", "deleteKnowledgePointAction"
],
"dataAccess": [
"getTextbooks", "getTextbookById", "getChaptersByTextbookId",
"getKnowledgePointsByChapterId", "getKnowledgePointsByTextbookId",
"createTextbook", "updateTextbook", "deleteTextbook",
"createChapter", "updateChapterContent", "deleteChapter",
"createKnowledgePoint", "updateKnowledgePoint", "deleteKnowledgePoint",
"reorderChapters", "getTextbooksDashboardStats",
"getKnowledgePointOptions" // 跨模块接口,供 questions 使用
]
},
"uiDeps": [
"questions/components/create-question-dialog // P0 待解耦"
],
"files": {
"actions.ts": 317,
"data-access.ts": 514,
"types.ts": 45,
"schema.ts": 64,
"components": 11
},
"knownIssues": [
"跨模块 UI 依赖 CreateQuestionDialogP0",
"前端权限硬编码 canEditP0",
"全模块零 i18nP0",
"Server Action 未校验资源归属P1",
"data-access 缺数据范围过滤P1",
"缺 Error BoundaryP1",
"知识点列表/弹窗重复实现P1",
"学科/年级选项硬编码且不一致P1",
"纯逻辑未导出零单测P1"
]
}
}
```
---
## 附:重构方案设计要点(不写实现代码)
为满足"完全解耦 / 组合优先 / 国际化就绪 / 最大化复用 / 错误与边界处理 / 可测试性 / 可扩展性 / 企业级补充"八项原则,建议按以下方向重构(详细实现留待后续任务):
### A. 数据服务接口抽象
```ts
// textbooks/services/types.ts
export interface TextbookDataService {
listTextbooks(query?: TextbookQuery): Promise<Textbook[]>
getTextbook(id: string): Promise<Textbook | null>
listChapters(textbookId: string): Promise<Chapter[]>
listKnowledgePoints(textbookId: string): Promise<KnowledgePoint[]>
}
export interface TextbookMutationService {
createTextbook(input: CreateTextbookInput): Promise<ActionState>
updateTextbook(id: string, input: UpdateTextbookInput): Promise<ActionState>
deleteTextbook(id: string): Promise<ActionState>
// ...chapter / knowledgePoint mutations
}
```
通过 `TextbookDataProvider`React Context注入不同角色实现teacher 实现 = 全量 + 可写student 实现 = 按年级过滤 + 只读admin 实现 = 全量 + 可写 + 可分配。
### B. 配置驱动角色渲染
```ts
// textbooks/config/role-config.ts
export const TEXTBOOK_ROLE_CONFIG: Record<Role, TextbookRoleConfig> = {
teacher: { canEdit: true, showStats: true, widgets: ['chapters','knowledge','graph','settings'] },
student: { canEdit: false, showProgress: true, widgets: ['chapters','knowledge','graph','notes'] },
admin: { canEdit: true, showStats: true, showAudit: true, widgets: ['chapters','knowledge','graph','settings','audit'] },
parent: { canEdit: false, showChildProgress: true, widgets: ['chapters','progress'] },
}
```
`TextbookReader` 根据 `useRoleConfig()` 决定渲染哪些 Widget新增角色只改配置。
### C. 组合式 UI
- `TextbookReader` 改为 `children`-based 组合:`<TextbookReader><ChapterSidebar /><ContentPanel /><KnowledgePanel /></TextbookReader>`
- 跨模块的"创建题目"入口改为 render prop`<KnowledgePointList onCreateQuestion={renderQuestionCreator} />`,由页面层注入 questions 模块组件,模块内部不 import questions。
### D. i18n 翻译文件结构示例
```
shared/i18n/messages/
├─ en/textbooks.json
└─ zh-CN/textbooks.json
```
```jsonc
// zh-CN/textbooks.json
{
"list": {
"title": "教材",
"subtitle": "管理数字课程资源与章节",
"add": "新建教材",
"empty": { "withFilters": "没有匹配的教材", "withoutFilters": "暂无教材" }
},
"reader": {
"tabs": { "chapters": "章节目录", "knowledge": "知识点", "graph": "图谱" },
"selectChapter": "请选择一个章节开始阅读",
"emptyKnowledge": "该章节暂无知识点"
},
"dialog": {
"create": { "title": "新建教材", "submit": "保存" },
"settings": { "title": "教材设置", "delete": "删除教材" },
"knowledge": { "create": "添加知识点", "edit": "编辑知识点" }
},
"field": {
"title": "标题", "subject": "学科", "grade": "年级", "publisher": "出版社"
},
"subject": { "Mathematics": "数学", "Physics": "物理", /* ... */ },
"grade": { "Grade 7": "七年级", /* ... */ }
}
```
### E. 错误边界与骨架屏
- 每个独立数据区块(章节树、内容区、知识点区、图谱区)用 `<ErrorBoundary fallback={<ErrorState />}>` 包裹
- 异步加载用 `<Suspense fallback={<TextbookReaderSkeleton />}>`
- 空状态、无权限、网络异常统一用 `EmptyState` / `ForbiddenState` / `ErrorState` 三套标准组件
### F. 可测试性
- 纯逻辑(`buildChapterTree` / `computeGraphLayout` / `sortChapters` / `buildChapterIndex` / `processedContent` 生成器)抽到 `textbooks/utils/` 并导出
- 数据服务接口便于 mock组件测试时注入 stub service
- 补 Vitest 单测 + Playwright e2e列表筛选、章节拖拽、知识点创建三条核心路径
### G. 监控埋点接口
```ts
export interface TextbookAnalytics {
onTextbookOpen(textbookId: string): void
onChapterRead(textbookId: string, chapterId: string, durationMs: number): void
onKnowledgePointClick(kpId: string): void
}
```
通过 Context 注入,默认 no-op后续接入真实监控 SDK。