263 lines
19 KiB
Markdown
263 lines
19 KiB
Markdown
# 课程计划模块审计报告
|
||
|
||
> 审计日期: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` 类型
|