# 课程计划模块审计报告

> 审计日期：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 即可查看全校所有课程计划详情（含其他班级、其他科目的教学进度、大纲、目标），构成信息泄露

#### 问题 2：admin 列表页无 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 列表页将完全暴露

#### 问题 3：data-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 结构调整即破坏功能

#### 问题 10：Server 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.tsx（P2-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.ts（P2-6 新增）

- **✅ 已从 `dashboard/lib/export-utils.ts` 迁移至 shared 层**，供所有模块复用
- 导出：`toCSV`、`downloadFile`、`exportCSV`、`ExportRow`、`ExportColumn` 类型
