19 KiB
19 KiB
课程计划模块审计报告
审计日期: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.jsonmodules.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.tsgetCoursePlanById第 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.tsgetCoursePlans(第 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),提取翻译键"
- 后果:
- 国际化完全不可用——切换语言后课程计划模块仍显示混合中英文
- 同一模块内
course-plan-detail.tsx(中文)与course-plan-list.tsx(英文)状态标签不一致,用户体验割裂 - 翻译文件
course-plans.json仅含 5 个键(title/description/detail/edit/create),远不满足组件需要
2.3 架构违规问题(P1)
问题 5:跨模块直接 JOIN 其他模块数据库表
- 位置:
data-access.tsbuildPlanSelect第 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.tsgetCoursePlansAction第 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
- ✅ 已补充导出函数:
- 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)
- data-access:
- ✅ 已修正依赖关系描述:
- 旧描述 "依赖 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)
- 旧描述 "依赖 classes/school(合理)" → 新描述 "通过动态 import
- ✅ 已补充已知问题修复状态:P0-1 至 P1-7 + P2-1 至 P2-8 全部标记为已修复
- ✅ 已补充页面路由表:10 个路由(含 P2-2 新增 parent/student 4 个路由)+ 权限 + loading/error 状态
- ✅ 已更新文件清单行数:反映重构后的实际行数(含新增 lib/ 与 components/ 文件)
005_architecture_data.json modules.course-plans
- ✅ 已补充 actions 节点:
reorderCoursePlanItemsAction、bulkToggleItemsAction、copyCoursePlanAction、getTemplateCandidatesAction(P2-5) - ✅ 已补充 dataAccess 节点:
bulkUpdateItemCompleted、copyCoursePlanToClasses - ✅ 已移除
getSubjectOptions:该函数已从 course-plans 模块删除,改用 school 模块 - ✅ 已更新
getCoursePlans/getCoursePlanById签名:增加scope?: CoursePlanQueryScope参数 - ✅ 已更新依赖关系:移除
shared.db.schema.classes/subjects/users,改为动态 import 对方 data-access - ✅ 已补充 schemas:
GetCoursePlansParamsSchema、GradeIdSchema、ReorderItemsSchema、BulkToggleSchema、CopyPlanSchema - ✅ 已补充 types:
CoursePlanQueryScope、GradeCoursePlanProgressItem、GradeCoursePlanProgressResult、isCoursePlanStatus、isCoursePlanSemester、CoursePlanWidgetId、RoleWidgetConfig、ROLE_WIDGET_CONFIG、CalendarEvent(P2-7)、CoursePlanExportColumnKey/CoursePlanColumnLabels(P2-6) - ✅ 已更新 components 描述:反映 P1 + P2 全部修复内容(Error Boundary、拖拽、数据联动、导出、日历、模板)
- ✅ 已补充 lib 节点:
export-utils.ts、calendar-utils.ts(P2 新增) - ✅ 已补充新增 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类型