Files
NextEdu/docs/architecture/audit/course-plans-audit-report.md
SpecialX 89b9e181d2 docs(audit): add audit reports for grades, homework, lesson-preparation, messaging, permissions, question-bank, settings, textbooks
- Add grades-audit-report

- Add homework-audit-report and homework-exams-audit-report

- Add lesson-preparation-audit-report-v3 and v4

- Add messaging-audit-report

- Add permissions-audit-report

- Add question-bank-audit-report

- Add settings-profile-audit-report-v3

- Add textbooks-audit-report-v3
2026-07-03 10:23:34 +08:00

263 lines
19 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.
# 课程计划模块审计报告
> 审计日期2026-06-25
> 审计范围:`src/modules/course-plans/` 全部文件 + `src/app/(dashboard)/{admin,teacher}/course-plans/` 全部页面
> 审计依据:`e:\Desktop\CICD\.trae\rules\project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.18、`docs/architecture/005_architecture_data.json` modules.`course-plans`
---
## 一、现有实现概要
### 1.1 文件分布
| 层级 | 文件数 | 主要文件(行数) |
|------|--------|------------------|
| types/schema | 2 | types.ts(97)、schema.ts(180) |
| data-access | 1 | data-access.ts(425) |
| actions | 1 | actions.ts(284) |
| components | 5 | course-plan-list.tsx(160)、course-plan-detail.tsx(243)、course-plan-form.tsx(284)、course-plan-item-editor.tsx(248)、course-plan-progress.tsx(38) |
| 页面 | 6 | admin(4: list/detail/create/edit)、teacher(2: list/detail) |
| i18n | 2 | zh-CN/course-plans.json(15)、en/course-plans.json(15) |
文件行数均在规范范围内(组件 ≤500、actions/data-access ≤800
### 1.2 数据流
```
页面(Server Component)
├─ admin/* → 直接调用 data-access.getCoursePlans / getCoursePlanById无 requirePermission
├─ teacher/* → requirePermission(COURSE_PLAN_READ) → data-access按 teacherId 过滤)
└─ management/grade/dashboard → data-access.getGradeCoursePlanProgress
动态 import classes data-access.getClassesByGradeId
JOIN course_plans + course_plan_items
Client Components
├─ CoursePlanList → usePermission() → 本地筛选
├─ CoursePlanDetail → 直接 import deleteCoursePlanAction
├─ CoursePlanForm → 直接 import create/updateCoursePlanAction
└─ CoursePlanItemEditor → 直接 import item CRUD actions
```
### 1.3 架构图完整性
`docs/architecture/004_architecture_impact_map.md` §2.18 与 `005_architecture_data.json` 已记录该模块的导出函数、文件清单、依赖关系,与实际代码**基本一致**。但存在以下遗漏与不一致:
- `data-access.ts``getSubjectOptions` 函数**未在架构图 exports 中记录**
- `data-access.ts``reorderCoursePlanItems` 函数**未在架构图 exports 中记录**(且无对应 Action / UI属于死代码
- 架构图标注 `getCoursePlansAction`/`getCoursePlanAction``usedBy` 为"待扩展",实际仍无消费方
- 架构图依赖矩阵显示 course-plans → classes/school 为"✅"(通过 data-access但实际 `buildPlanSelect` **直接 JOIN** classes/subjects/users 表,并非通过 data-access 调用——架构图记录与实现不一致
---
## 二、现存问题与原因分析
### 2.1 安全与权限问题P0
#### 问题 1教师详情页未校验计划归属 — 信息泄露漏洞
- **位置**`src/app/(dashboard)/teacher/course-plans/[id]/page.tsx` 第 16-18 行;`data-access.ts` `getCoursePlanById` 第 168-192 行
- **问题**:教师详情页仅调用 `requirePermission(COURSE_PLAN_READ)` 后直接 `getCoursePlanById(id)`**未校验该计划是否属于当前教师**。`getCoursePlanById` 也不接受 `userId` 参数。
- **违反规则**:项目规则 "Parent routes must include permission checks with both parentId and studentId to prevent information leakage"(同理,教师路由也应校验 teacherId 归属);"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"
- **后果**:任何持有 `COURSE_PLAN_READ` 权限的教师,通过枚举/猜测 planId 即可查看全校所有课程计划详情(含其他班级、其他科目的教学进度、大纲、目标),构成信息泄露
#### 问题 2admin 列表页无 requirePermission 调用
- **位置**`src/app/(dashboard)/admin/course-plans/page.tsx` 全文(第 23-49 行)
- **问题**admin 列表页**未调用 `requirePermission()`**,直接调用 `getCoursePlans()` 返回全部数据。对比 `teacher/course-plans/page.tsx` 第 27 行有 `requirePermission` 调用——admin 与 teacher 页面权限处理不一致。
- **违反规则**:项目规则 "所有 Server Action 必须调用 requirePermission() 进行权限校验"(页面层虽非 Action但 data-access 直接被 Server Component 调用时同样需校验);依赖布局层保护属于隐式安全,不符合纵深防御原则
- **后果**若布局层权限配置被误改admin 列表页将完全暴露
#### 问题 3data-access 函数无数据范围DataScope过滤
- **位置**`data-access.ts` `getCoursePlans`(第 144-166 行)、`getCoursePlanById`(第 168-192 行)、`getGradeCoursePlanProgress`(第 335-424 行)
- **问题**:所有查询函数**均不接受 userId / dataScope 参数**,不进行任何归属过滤。`getCoursePlans` 仅靠调用方传入 `teacherId` 参数过滤,但参数可选且可被绕过。
- **违反规则**:项目规则 "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤Server Action 二次校验"
- **后果**:未来新增 parent/student 路由时,若直接复用这些函数将导致越权;当前教师详情页已暴露此问题(见问题 1
### 2.2 国际化严重缺失P0
#### 问题 4组件内大量硬编码文本中英文混杂
- **位置**
- `course-plan-list.tsx`:第 25-47 行 `STATUS_LABEL`/`STATUS_VARIANT`/`FILTER_OPTIONS` 全英文硬编码;第 99/108-112 行 "New Course Plan"/"No course plans"/"There are no course plans yet." 等
- `course-plan-detail.tsx`:第 30-35 行 `STATUS_LABEL` 全中文硬编码("规划中"/"进行中"/"已完成"/"已暂停");第 94/99/106-113/124-134/146-153/161-172/178-183/213 行大量中文硬编码
- `course-plan-form.tsx`:第 98/105/122/139/156/173/186/203/214/225/234/247/257 行全英文硬编码("New Course Plan"/"Class"/"Subject"/"Teacher" 等)
- `course-plan-item-editor.tsx`:第 119/125/136/148/159/172/180/191 行全英文硬编码
- `course-plan-progress.tsx`:第 25/27 行 "Progress"/"hours" 硬编码
- `teacher/course-plans/page.tsx`:第 41-43 行 "My Course Plans"/"View your course teaching plans..." 硬编码
- **违反规则**:项目规则 "所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"
- **后果**
1. 国际化完全不可用——切换语言后课程计划模块仍显示混合中英文
2. 同一模块内 `course-plan-detail.tsx`(中文)与 `course-plan-list.tsx`(英文)状态标签不一致,用户体验割裂
3. 翻译文件 `course-plans.json` 仅含 5 个键title/description/detail/edit/create远不满足组件需要
### 2.3 架构违规问题P1
#### 问题 5跨模块直接 JOIN 其他模块数据库表
- **位置**`data-access.ts` `buildPlanSelect` 第 115-142 行
- **问题**:直接 `leftJoin(classes, ...)``leftJoin(subjects, ...)``leftJoin(users, ...)`,分别查询 classes 模块、school 模块、users 模块拥有的表。架构图却标注为"✅ 通过 data-access"。
- **违反规则**:项目规则 "模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"
- **后果**classes/school/users 模块的表结构变更将直接影响 course-plans 查询;模块未真正解耦,无法独立测试
#### 问题 6缺少 loading.tsx / error.tsx
- **位置**`src/app/(dashboard)/admin/course-plans/``src/app/(dashboard)/teacher/course-plans/` 全部路由
- **问题**6 个页面路由均**无 loading.tsx 和 error.tsx**。
- **违反规则**:项目规则 "All student routes must include loading.tsx and error.tsx for error boundaries"best practice 推广至所有角色路由)
- **后果**:数据加载期间白屏;运行时错误无边界捕获,导致整页崩溃
#### 问题 7使用原生 `<a>` 标签替代 `<Link>`
- **位置**`course-plan-list.tsx` 第 97 行 `<a href={createHref}>`、第 149 行 `<a key={plan.id} href={href}>`
- **违反规则**:项目规则 "Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags"
- **后果**:点击导航触发整页刷新,丢失客户端状态,无预取优化
### 2.4 代码质量问题P1
#### 问题 8使用 `as` 类型断言
- **位置**
- `course-plan-list.tsx` 第 73 行:`setFilter(value as Filter)`
- `course-plan-form.tsx` 第 174 行:`setSemester(v as "1" | "2")`;第 188 行:`setStatus(v as CoursePlanStatus)`
- `teacher/course-plans/page.tsx` 第 19 行:`(v as CoursePlanStatus)`
- **违反规则**:项目规则 "禁止 as 断言(除类型收窄外)"
- **后果**:运行时类型不安全,应使用类型守卫函数(如 admin 页面已实现的 `isValidStatus`
#### 问题 9基于 URL 字符串判断角色的脆弱逻辑
- **位置**
- `course-plan-detail.tsx` 第 63 行:`backHref?.includes("/teacher/") ? "/teacher/course-plans" : "/admin/course-plans"`
- `course-plan-form.tsx` 第 81 行:同样的 `backHref?.includes("/teacher/")` 模式
- **问题**:通过 URL 路径字符串推断用户角色来决定跳转目标,而非通过权限/角色上下文。
- **违反规则**:项目规则 "前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === 'xxx' 硬编码"URL 路径推断属于同类硬编码)
- **后果**:新增 parent/student 路由时跳转逻辑将出错URL 结构调整即破坏功能
#### 问题 10Server Action 入参未经验证
- **位置**`actions.ts` `getCoursePlansAction` 第 135-145 行params 未 Zod 验证)、`getGradeCoursePlanProgressAction` 第 269-284 行gradeId 仅检查非空)
- **违反规则**:项目规则 "输入使用 Zod 验证,验证失败返回结构化错误"
- **后果**:恶意参数可能绕过预期过滤条件
#### 问题 11死代码 — `reorderCoursePlanItems` 无消费方
- **位置**`data-access.ts` 第 290-313 行
- **问题**`reorderCoursePlanItems` 函数存在但无对应 Server Action、无 UI 调用方,架构图也未记录。
- **违反规则**:项目规则 "避免过度工程" + 架构图同步规则
- **后果**:死代码增加维护负担;架构图与实际不一致
### 2.5 错误处理与边界缺失P2
#### 问题 12无 Error Boundary / Suspense / 骨架屏
- **位置**:全部组件和页面
- **问题**:数据区块未用 React Error Boundary 包裹;异步加载无 Suspense + 骨架屏;空数据虽有基础 `EmptyState` 但无操作引导CTA
- **违反规则**:审计要求 "每个独立的数据区块必须用 React Error Boundary 包裹;异步数据使用 React Suspense + 骨架屏"
- **后果**:局部数据错误导致整页不可用;加载体验差
---
## 三、行业差距对比
基于 K12 教育管理系统(如 PowerSchool、Canvas、Schoology、钉钉教育、企业自建校管系统在课程计划/教学进度模块的主流实践,当前差距如下:
| 维度 | 行业优秀实践 | 当前实现 | 影响 |
|------|-------------|---------|------|
| **角色覆盖** | admin/teacher/parent/student 四角色均可查看课程计划(按权限脱敏) | 仅 admin/teacher 有路由parent/student 完全无入口 | 家长无法了解孩子本学期教学安排;学生无法预览学习进度 |
| **进度可视化** | 甘特图/时间轴展示周计划进度,颜色区分已完成/进行中/待开始 | 仅一个简单 Progress 条 + 表格列表 | 管理者难以一目了然掌握全年级教学进度 |
| **数据联动** | 周计划条目关联作业/考试/教材章节,可一键跳转 | `textbookChapter` 仅存文本,无关联跳转 | 教师需手动查找对应教材和作业 |
| **批量操作** | 批量标记完成、批量调整周次、批量复制计划到其他班级 | 无任何批量操作 | 管理员配置多班级计划时重复劳动 |
| **模板复用** | 提供标准课程计划模板,可从模板创建或复制历史计划 | 每次从零创建 | 教师重复录入 |
| **拖拽排序** | 周计划条目支持拖拽调整顺序 | data-access 有 `reorderCoursePlanItems` 但无 UI | 死代码,功能缺失 |
| **导出打印** | 导出 PDF/Excel 教学进度报告 | 无 | 无法线下归档或上报 |
| **空状态 CTA** | 空状态带"创建第一个计划"引导按钮 | 有 EmptyState 但无 CTA 按钮 | 新用户不知如何开始 |
| **骨架屏** | 加载时显示结构化骨架屏 | 无 loading.tsx | 加载白屏 |
| **日历视图** | 月历/周历视图展示教学安排 | 无 | 教师难以对照实际日期安排教学 |
---
## 四、改进优先级建议
### P0 — 安全与国际化(必须立即修复)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P0-1 | 教师详情页信息泄露 | `getCoursePlanById` 增加 `userId` + `dataScope` 参数data-access 层过滤归属;非 admin 仅能查看自己负责的计划 |
| P0-2 | admin 页面无 requirePermission | admin 所有页面补充 `requirePermission(COURSE_PLAN_READ)` |
| P0-3 | data-access 无 DataScope 过滤 | `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数,按 classIds/teacherId 过滤 |
| P0-4 | i18n 严重缺失 | 提取全部硬编码文本到 `course-plans.json`,补全 zh-CN/en 翻译键状态标签、表单字段、按钮、空状态、Toast 消息等) |
### P1 — 架构合规与代码质量
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P1-1 | 跨模块直接 JOIN | 定义 `CoursePlanDataService` 接口抽象 classes/subjects/users 数据依赖,通过组合注入;或先抽取 `getClassNameById`/`getSubjectNameById`/`getTeacherNameById` 轻量 data-access 调用替代 JOIN |
| P1-2 | 缺 loading.tsx/error.tsx | 为 admin 和 teacher 路由补充 loading.tsx骨架屏和 error.tsx错误边界 |
| P1-3 | 原生 `<a>` 标签 | 替换为 Next.js `<Link>` 组件 |
| P1-4 | `as` 断言 | 替换为类型守卫函数(`isValidStatus`/`isValidSemester` |
| P1-5 | URL 路径推断角色 | 改为通过 `usePermission().hasPermission()` 决定跳转基础路径,或由页面 props 传入 `successHref` |
| P1-6 | Action 入参未验证 | `getCoursePlansAction`/`getGradeCoursePlanProgressAction` 增加 Zod schema 验证 |
| P1-7 | 死代码 reorderCoursePlanItems | 删除或补充对应 Action + UI推荐补充拖拽排序 UI |
### P2 — 体验与企业级增强(中长期)
| 编号 | 问题 | 改进方向 |
|------|------|---------|
| P2-1 | 无 Error Boundary / 骨架屏 | 组件级 Error Boundary 包裹数据区块Suspense + 骨架屏 |
| P2-2 | parent/student 无路由 | 新增 parent/student 课程计划只读路由(按孩子班级过滤) |
| P2-3 | 无数据联动 | 周计划条目关联教材章节/作业,支持跳转 |
| P2-4 | 无批量操作 | 批量标记完成、批量复制计划 |
| P2-5 | 无模板复用 | 课程计划模板库,从模板创建 |
| P2-6 | 无导出 | PDF/Excel 导出教学进度报告 |
| P2-7 | 无日历视图 | 月历视图对照实际日期 |
| P2-8 | 监控埋点 | 预留 `trackCoursePlanEvent()` 埋点接口 |
---
## 五、架构图同步说明
本次审计发现架构图需补充/修改以下内容(✅ 已全部完成同步,含 P0/P1/P2 全部实施):
### 004_architecture_impact_map.md §2.18
1. **✅ 已补充导出函数**
- data-access`bulkUpdateItemCompleted``copyCoursePlanToClasses``enrichPlanRows`(名称解析解耦)、`buildScopeCondition`(权限过滤)
- actions`reorderCoursePlanItemsAction``bulkToggleItemsAction``copyCoursePlanAction``getTemplateCandidatesAction`P2-5 新增)、`trackCoursePlanEvent`
- lib`lib/export-utils.ts`P2-6 CSV 导出纯函数)、`lib/calendar-utils.ts`P2-7 日历视图纯函数 + 日期工具)
- types`CoursePlanQueryScope`、类型守卫 `isCoursePlanStatus`/`isCoursePlanSemester`、配置驱动 `ROLE_WIDGET_CONFIG``CalendarEvent`P2-7`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`P2-6
- components新增 `SortableWeekRow`P1-7/P2-3`CoursePlanCalendar`P2-7`TemplatePickerDialog`P2-5
2. **✅ 已修正依赖关系描述**
- 旧描述 "依赖 classes/school合理" → 新描述 "通过动态 import `getClassNamesByIds`/`getSubjectNameMapByIds`/`getUserNamesByIds` 批量解析,不再直接 JOIN"
- `getSubjectOptions` 已移至 school 模块pages 改为从 `@/modules/school/data-access` 导入)
- 新增依赖:`shared/lib/export-utils.ts`P2-6`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`P1-7
3. **✅ 已补充已知问题修复状态**P0-1 至 P1-7 + P2-1 至 P2-8 全部标记为已修复
4. **✅ 已补充页面路由表**10 个路由(含 P2-2 新增 parent/student 4 个路由)+ 权限 + loading/error 状态
5. **✅ 已更新文件清单行数**:反映重构后的实际行数(含新增 lib/ 与 components/ 文件)
### 005_architecture_data.json modules.`course-plans`
1. **✅ 已补充 actions 节点**`reorderCoursePlanItemsAction``bulkToggleItemsAction``copyCoursePlanAction``getTemplateCandidatesAction`P2-5
2. **✅ 已补充 dataAccess 节点**`bulkUpdateItemCompleted``copyCoursePlanToClasses`
3. **✅ 已移除 `getSubjectOptions`**:该函数已从 course-plans 模块删除,改用 school 模块
4. **✅ 已更新 `getCoursePlans`/`getCoursePlanById` 签名**:增加 `scope?: CoursePlanQueryScope` 参数
5. **✅ 已更新依赖关系**:移除 `shared.db.schema.classes/subjects/users`,改为动态 import 对方 data-access
6. **✅ 已补充 schemas**`GetCoursePlansParamsSchema``GradeIdSchema``ReorderItemsSchema``BulkToggleSchema``CopyPlanSchema`
7. **✅ 已补充 types**`CoursePlanQueryScope``GradeCoursePlanProgressItem``GradeCoursePlanProgressResult``isCoursePlanStatus``isCoursePlanSemester``CoursePlanWidgetId``RoleWidgetConfig``ROLE_WIDGET_CONFIG``CalendarEvent`P2-7`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`P2-6
8. **✅ 已更新 components 描述**:反映 P1 + P2 全部修复内容Error Boundary、拖拽、数据联动、导出、日历、模板
9. **✅ 已补充 lib 节点**`export-utils.ts``calendar-utils.ts`P2 新增)
10. **✅ 已补充新增 components**`SortableWeekRow``CoursePlanCalendar``TemplatePickerDialog`
### shared/components/section-error-boundary.tsxP2-1 重构)
- **✅ 已重构**:类组件 + 函数式包装器双层结构
- 函数式包装器自动注入 i18n 文案(`{namespace}.error.boundaryTitle` / `boundaryDescription` / `retry`
- 支持 `fallback` 自定义降级 UI函数形式 `(error, reset) => ReactNode`
- 支持 `onError` 回调(用于埋点/监控AI 模块复用)
- a11y`role="alert"` + `aria-live="assertive"` + 重试按钮 `aria-label`
### shared/lib/export-utils.tsP2-6 新增)
- **✅ 已从 `dashboard/lib/export-utils.ts` 迁移至 shared 层**,供所有模块复用
- 导出:`toCSV``downloadFile``exportCSV``ExportRow``ExportColumn` 类型