# 教材(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 表 └─ (client) ├─ → deleteChapterAction / reorderChaptersAction ├─ → updateChapterContentAction ├─ → useKnowledgePointActions → create/update/deleteKnowledgePointAction └─ → ⚠️ 直接 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):内联 `
请选择一个章节查看知识点。
` - [knowledge-point-list.tsx#L32](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-point-list.tsx#L32):内联 `
该章节暂无知识点。
` - [textbook-content-panel.tsx#L67](file:///e:/Desktop/CICD/src/modules/textbooks/components/textbook-content-panel.tsx#L67):内联 `
请选择一个章节开始阅读。
` - 列表页则用 `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) - **现象**:`` 无 `role="img"`、无 `aria-label`、无 ``;节点用 `