# 教材（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-access（RSC），未包装成 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 Boundary（P1）

- **位置**：`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/Geography，settings 缺这两项）
  - [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 依赖 CreateQuestionDialog（P0）",
      "前端权限硬编码 canEdit（P0）",
      "全模块零 i18n（P0）",
      "Server Action 未校验资源归属（P1）",
      "data-access 缺数据范围过滤（P1）",
      "缺 Error Boundary（P1）",
      "知识点列表/弹窗重复实现（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。
