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 以包含学生端页面
This commit is contained in:
510
docs/architecture/audit/textbooks-audit-report.md
Normal file
510
docs/architecture/audit/textbooks-audit-report.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# 教材(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。
|
||||
Reference in New Issue
Block a user