Files
NextEdu/docs/architecture/audit/archive/course-plans-audit-report.md

19 KiB
Raw Blame History

课程计划模块审计报告

审计日期2026-06-25 审计范围:src/modules/course-plans/ 全部文件 + src/app/(dashboard)/{admin,teacher}/course-plans/ 全部页面 审计依据:e:\Desktop\CICD\.trae\rules\project_rules.mddocs/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.tsgetSubjectOptions 函数未在架构图 exports 中记录
  • data-access.tsreorderCoursePlanItems 函数未在架构图 exports 中记录(且无对应 Action / UI属于死代码
  • 架构图标注 getCoursePlansAction/getCoursePlanActionusedBy 为"待扩展",实际仍无消费方
  • 架构图依赖矩阵显示 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 推广至所有角色路由)
  • 后果:数据加载期间白屏;运行时错误无边界捕获,导致整页崩溃
  • 位置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-accessbulkUpdateItemCompletedcopyCoursePlanToClassesenrichPlanRows(名称解析解耦)、buildScopeCondition(权限过滤)
    • actionsreorderCoursePlanItemsActionbulkToggleItemsActioncopyCoursePlanActiongetTemplateCandidatesActionP2-5 新增)、trackCoursePlanEvent
    • liblib/export-utils.tsP2-6 CSV 导出纯函数)、lib/calendar-utils.tsP2-7 日历视图纯函数 + 日期工具)
    • typesCoursePlanQueryScope、类型守卫 isCoursePlanStatus/isCoursePlanSemester、配置驱动 ROLE_WIDGET_CONFIGCalendarEventP2-7CoursePlanExportColumnKey/CoursePlanColumnLabelsP2-6
    • components新增 SortableWeekRowP1-7/P2-3CoursePlanCalendarP2-7TemplatePickerDialogP2-5
  2. 已修正依赖关系描述
    • 旧描述 "依赖 classes/school合理" → 新描述 "通过动态 import getClassNamesByIds/getSubjectNameMapByIds/getUserNamesByIds 批量解析,不再直接 JOIN"
    • getSubjectOptions 已移至 school 模块pages 改为从 @/modules/school/data-access 导入)
    • 新增依赖:shared/lib/export-utils.tsP2-6@dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilitiesP1-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 节点reorderCoursePlanItemsActionbulkToggleItemsActioncopyCoursePlanActiongetTemplateCandidatesActionP2-5
  2. 已补充 dataAccess 节点bulkUpdateItemCompletedcopyCoursePlanToClasses
  3. 已移除 getSubjectOptions:该函数已从 course-plans 模块删除,改用 school 模块
  4. 已更新 getCoursePlans/getCoursePlanById 签名:增加 scope?: CoursePlanQueryScope 参数
  5. 已更新依赖关系:移除 shared.db.schema.classes/subjects/users,改为动态 import 对方 data-access
  6. 已补充 schemasGetCoursePlansParamsSchemaGradeIdSchemaReorderItemsSchemaBulkToggleSchemaCopyPlanSchema
  7. 已补充 typesCoursePlanQueryScopeGradeCoursePlanProgressItemGradeCoursePlanProgressResultisCoursePlanStatusisCoursePlanSemesterCoursePlanWidgetIdRoleWidgetConfigROLE_WIDGET_CONFIGCalendarEventP2-7CoursePlanExportColumnKey/CoursePlanColumnLabelsP2-6
  8. 已更新 components 描述:反映 P1 + P2 全部修复内容Error Boundary、拖拽、数据联动、导出、日历、模板
  9. 已补充 lib 节点export-utils.tscalendar-utils.tsP2 新增)
  10. 已补充新增 componentsSortableWeekRowCoursePlanCalendarTemplatePickerDialog

shared/components/section-error-boundary.tsxP2-1 重构)

  • 已重构:类组件 + 函数式包装器双层结构
  • 函数式包装器自动注入 i18n 文案({namespace}.error.boundaryTitle / boundaryDescription / retry
  • 支持 fallback 自定义降级 UI函数形式 (error, reset) => ReactNode
  • 支持 onError 回调(用于埋点/监控AI 模块复用)
  • a11yrole="alert" + aria-live="assertive" + 重试按钮 aria-label

shared/lib/export-utils.tsP2-6 新增)

  • 已从 dashboard/lib/export-utils.ts 迁移至 shared 层,供所有模块复用
  • 导出:toCSVdownloadFileexportCSVExportRowExportColumn 类型