441 lines
34 KiB
Markdown
441 lines
34 KiB
Markdown
# 班级(classes)模块审计报告
|
||
|
||
> 审计时间:2026-06-25
|
||
> 审计范围:`src/modules/classes/` 全部 32 个文件 + `src/app/(dashboard)/` 下 11 个相关路由分组
|
||
> 架构图依据:`docs/architecture/004_architecture_impact_map.md` §2.7、`docs/architecture/005_architecture_data.json` modules.classes 节点
|
||
> 审计基线:项目规则(三层架构、权限校验、i18n、TypeScript 严格模式、文件行数、可测试性)
|
||
|
||
## 一、现有实现概要
|
||
|
||
### 1.1 文件分布
|
||
|
||
classes 模块位于 `src/modules/classes/`,共 **32 个文件**:
|
||
|
||
| 层 | 文件数 | 关键文件 |
|
||
|---|---|---|
|
||
| Server Actions | 7 | `actions.ts`(barrel)+ `actions-admin.ts` / `actions-grade.ts` / `actions-teacher.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-shared.ts` |
|
||
| Data-access | 7 | `data-access.ts`(聚合)+ `data-access-admin.ts` / `data-access-teacher.ts` / `data-access-students.ts` / `data-access-stats.ts` / `data-access-schedule.ts` / `data-access-invitations.ts` |
|
||
| Schema/Types | 2 | `schema.ts`(13 个 Zod schema)、`types.ts`(19 个类型) |
|
||
| Components | 21 | `admin-classes-view.tsx` / `grade-classes-view.tsx` / `class-list-table.tsx` / `class-form-dialog.tsx` / `class-delete-dialog.tsx` / `class-list-toolbar.tsx` / `class-form-utils.ts` / `class-error-boundary.tsx` / `class-invitation-manager.tsx` / `class-skeleton.tsx` / `my-classes-grid.tsx` / `schedule-view.tsx` / `schedule-filters.tsx` / `students-filters.tsx` / `students-table.tsx` + `class-detail/` 下 7 个 widget(header/overview-stats/quick-actions/schedule-widget/students-widget/assignments-widget/trends-widget)+ `class-detail/edit-class-dialog.tsx` |
|
||
| Hooks | 2 | `use-class-data.ts`、`use-class-filters.ts` |
|
||
|
||
### 1.2 主要数据流
|
||
|
||
```
|
||
admin/school/classes ─┐
|
||
management/grade/classes ─┤ ─▶ actions-admin / actions-grade
|
||
│ │
|
||
teacher/classes/my ─┤ ▼
|
||
teacher/classes/schedule ─┤ data-access-admin / data-access-teacher
|
||
teacher/classes/students ─┤ │
|
||
│ ▼ (跨模块通过对方 data-access)
|
||
student/learning/courses ─┤ homework/data-access-classes(作业统计)
|
||
student/schedule ─┤ scheduling/data-access-class-schedule(课表写)
|
||
parent/children/[id] ─┘ school/data-access(年级管理权限校验)
|
||
```
|
||
|
||
### 1.3 架构图覆盖完整性评估
|
||
|
||
| 维度 | 004 文档 | 005 JSON | 一致性 |
|
||
|---|---|---|---|
|
||
| 模块职责 | ✅ §2.7 | ✅ modules.classes.description | 一致 |
|
||
| 依赖关系(出向) | ✅ shared/auth/school/homework/scheduling | ✅ dependencyMatrix 5 条边 | 一致 |
|
||
| 被依赖关系(入向) | ⚠️ 列出 10 个,遗漏 elective/error-book/adaptive-practice | ✅ 10 条边覆盖完整 | **不一致** |
|
||
| 导出函数 | ✅ 17 actions + 33 data-access | ⚠️ exports.actions 漏 3 个邀请码 action;exports.dataAccess 漏 6 个工具函数 | **不一致** |
|
||
| 数据库表 | ✅ classes/classSubjectTeachers/classEnrollments/classInvitationCodes | ⚠️ classInvitationCodes 字段未详细登记;classSchedule 的 usedBy 字段未含 scheduling | **部分遗漏** |
|
||
| 权限点 | ✅ 6 个 CLASS_* 权限 | ✅ permissions 常量定义完整 | 一致 |
|
||
| 路由 | ✅ 11 条路由登记 | ✅ routes 节点登记 | 一致 |
|
||
| 文件清单 | ✅ 33 个文件 | ⚠️ modules.classes.files 仅列 21 个,漏 12 个组件文件 | **不一致** |
|
||
|
||
**结论**:架构图总体覆盖较完整,但存在 7 处需同步更新(详见第五章)。
|
||
|
||
## 二、现存问题与原因分析
|
||
|
||
### 2.1 三层架构合规性
|
||
|
||
#### 问题 A1:data-access.ts 与拆分文件 3 对同名函数重复定义(🔴 P0)
|
||
|
||
- **位置**:
|
||
- [data-access.ts:400](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L400) `getTeacherScopeData` ↔ [data-access-teacher.ts:592](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L592)
|
||
- [data-access.ts:428](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L428) `getStudentScopeData` ↔ [data-access-students.ts:309](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L309)
|
||
- [data-access.ts:455](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L455) `getGradeIdsForStudentIds` ↔ [data-access-students.ts:336](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L336)
|
||
- **现状**:`data-access.ts:383-386` 同时 `export * from "./data-access-*"` 与本地 `export const`,ES 模块语义下本地定义优先,子文件同名导出对聚合入口而言是死代码。
|
||
- **违反规则**:架构分层规则「data-access 拆分应避免职责重叠」+ DRY 原则。
|
||
- **后果**:两份实现目前逻辑一致,但任何一方修改都不会自动同步。若消费者直接 `import { getStudentScopeData } from "@/modules/classes/data-access-students"`,会得到另一份实现,是高风险维护陷阱。
|
||
|
||
#### 问题 A2:组件使用绝对路径导入本模块 actions(🟢 P2)
|
||
|
||
- **位置**:[class-invitation-manager.tsx:30-32](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L30)
|
||
- **现状**:`import { createClassInvitationCodeAction } from "@/modules/classes/actions"`,而 `my-classes-grid.tsx`、`admin-classes-view.tsx` 等同模块其他组件使用 `"../actions"` 相对路径。
|
||
- **违反规则**:编码规范一致性。
|
||
- **后果**:模块迁移/重命名成本上升。
|
||
|
||
### 2.2 权限校验
|
||
|
||
#### 问题 B1:listClassInvitationCodesAction 无班级归属校验(🔴 P0 越权漏洞)
|
||
|
||
- **位置**:[actions-invitations.ts:312-351](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L312)
|
||
- **现状**:
|
||
```ts
|
||
export async function listClassInvitationCodesAction(classId: string) {
|
||
await requirePermission(Permissions.CLASS_ENROLL)
|
||
// ❌ 任何拥有 CLASS_ENROLL 权限的用户都可以列出任意 classId 的所有邀请码
|
||
const codes = await listClassInvitationCodes(classId)
|
||
}
|
||
```
|
||
- **违反规则**:安全规范「data-access 查询是否结合当前用户权限过滤(防越权)」+ Server Action 规范。
|
||
- **后果**:教师 A 可通过传入教师 B 的 classId 枚举其邀请码(含 code 字符串),可能引发越权获取加入凭证,破坏邀请码体系的安全性。
|
||
|
||
#### 问题 B2:3 个 schedule action 无班级归属校验(🔴 P0 越权漏洞)
|
||
|
||
- **位置**:[actions-schedule.ts:21-59](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L21)(create)、[61-101](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L61)(update)、[103-122](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L103)
|
||
- **现状**:3 个 action 仅调用 `requirePermission(CLASS_SCHEDULE)` 后直接调用 scheduling 模块 data-access,未校验当前用户对 `classId` 的归属。
|
||
- **违反规则**:安全规范越权防护。
|
||
- **后果**:教师 A 可为教师 B 的班级添加/修改/删除课表项,破坏排课数据完整性。
|
||
|
||
#### 问题 B3:教师 update/delete/enroll action 未在 actions 层做归属校验(🟡 P1)
|
||
|
||
- **位置**:[actions-teacher.ts:79-122](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L79)(update)、[125-146](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L125)(delete)、[actions-invitations.ts:21-49](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L21)、[353-377](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L353)、[383-440](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L383)
|
||
- **现状**:actions 层仅 `requirePermission`,依赖 data-access 内部 `getTeacherIdForMutations()` + `eq(classes.teacherId, teacherId)` 校验。
|
||
- **违反规则**:Server Action 规范「权限校验应在 actions 层完成」。
|
||
- **后果**:管理员(拥有 CLASS_UPDATE 权限)调用时,因 data-access 内部强制按 teacherId 过滤而失败,错误信息为 "Teacher not found" 不友好;如未来重构 data-access 暴露 admin 路径,归属校验会被跳过。
|
||
|
||
#### 问题 B4:data-access 中 7 处硬编码角色名字符串(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [data-access.ts:26](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L26) `eq(roles.name, "teacher")`
|
||
- [data-access-admin.ts:346](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L346)、[425](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L425)
|
||
- [data-access-teacher.ts:125](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L125)、[308](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L308)、[472](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L472)、[545](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L545)
|
||
- **违反规则**:命名规范「常量:UPPER_SNAKE_CASE」+ DRY。
|
||
- **后果**:若角色名变更(中英切换、复数化),需修改 7 处;魔法字符串降低可读性。
|
||
|
||
#### 问题 B5:student 三个页面未调用 requirePermission 显式声明权限点(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [student/learning/courses/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/page.tsx#L19)
|
||
- [student/learning/courses/[classId]/page.tsx:40-41](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/[classId]/page.tsx#L40)
|
||
- [student/schedule/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/schedule/page.tsx#L19)
|
||
- **现状**:仅 `getCurrentStudentUser()` 软校验学生身份,未显式声明所需权限点;`[classId]` 页面在权限不足时返回 `notFound()`,错误语义混淆(404 vs 403)。
|
||
- **违反规则**:Server Action 必须使用 `requirePermission()`。
|
||
- **后果**:权限审计与文档化困难;404/403 错误语义混淆影响用户体验与监控告警。
|
||
|
||
### 2.3 国际化(i18n)
|
||
|
||
#### 问题 C1:class-detail/ 子组件普遍硬编码英文(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [class-assignments-widget.tsx:40,46,64,84,88,101](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-assignments-widget.tsx#L40)
|
||
- [class-overview-stats.tsx:28,33,39,45](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-overview-stats.tsx#L28)
|
||
- [class-quick-actions.tsx:26,32,36](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-quick-actions.tsx#L26)
|
||
- [class-schedule-widget.tsx:17,102,110](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-schedule-widget.tsx#L17)
|
||
- [class-students-widget.tsx:36,41](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-students-widget.tsx#L36)
|
||
- [class-trends-widget.tsx:41-55,145,164,174,182,188,282,288,301,392](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L41)
|
||
- **现状**:8 个详情子组件几乎全部使用硬编码英文字符串。
|
||
- **违反规则**:i18n 强制要求 + 架构图 004:998 已声明"13 个组件全部接入 i18n"——与现状不一致。
|
||
- **后果**:详情页完全无法中文化,与 K12 中文产品定位严重冲突;架构图存在错误声明。
|
||
|
||
#### 问题 C2:schedule-view / schedule-filters / students-filters 硬编码英文(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [schedule-view.tsx:111,117,165,166,180,192,310,343,348,357,369,386,435,448,469,489,505-508](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L111)
|
||
- [schedule-filters.tsx:111,117,164,171,180,192](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L111)
|
||
- [students-filters.tsx:81,110,141,169,173,174,197,204,211](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L81)
|
||
- **违反规则**:i18n 强制要求。
|
||
- **后果**:教师端课表/学生管理页全部英文,破坏产品一致性。
|
||
|
||
#### 问题 C3:所有 actions 返回的 message 为英文硬编码(🟡 P1)
|
||
|
||
- **位置**:全部 7 个 actions 文件
|
||
- **示例**:[actions-admin.ts:39](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L39) `"Class name, grade and teacher are required"` / [actions-admin.ts:59](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L59) `"Class created successfully"` / [actions-invitations.ts:432](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L432) `` `Imported ${imported} students, ${failed} failed` ``
|
||
- **现状**:组件层 `toast.success(res.message)` 直接显示后端字符串。
|
||
- **违反规则**:i18n 强制要求。
|
||
- **后果**:中文用户看到全英文错误提示,体验差。
|
||
|
||
#### 问题 C4:30+ 个 error.tsx 使用硬编码中文文案(🟡 P1)
|
||
|
||
- **位置**:`src/app/(dashboard)/` 下 30 处 error.tsx(含 `admin/school/classes/error.tsx`、`management/grade/classes/error.tsx` 等)
|
||
- **示例**:[admin/school/classes/error.tsx:12-16](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/classes/error.tsx#L12) `title="页面加载失败"` / `description="抱歉,页面加载时发生了意外错误。请稍后重试。"`
|
||
- **违反规则**:i18n 强制要求。
|
||
- **后果**:英文用户在错误页仍看到中文,破坏语言一致性;30+ 处重复字符串维护成本高。
|
||
|
||
#### 问题 C5:DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 业务常量硬编码中文(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [types.ts:46](file:///e:/Desktop/CICD/src/modules/classes/types.ts#L46) `export const DEFAULT_CLASS_SUBJECTS = ["语文", "数学", "英语", "美术", "体育", "科学", "社会", "音乐"] as const`
|
||
- [data-access-students.ts:41](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L41) `const excludeSubjects = ["体育", "音乐", "美术"]`
|
||
- **违反规则**:DRY(两份科目清单)+ i18n(业务规则硬编码字符串)。
|
||
- **后果**:与 `subjects` 表查询重复;不同学校配置无法适配;`excludeSubjects` 未在架构图记录。
|
||
|
||
#### 问题 C6:getSubjectColor 用英文匹配中文科目(🔴 P0 功能 bug)
|
||
|
||
- **位置**:[schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186)
|
||
- **现状**:
|
||
```ts
|
||
const getSubjectColor = (subject: string) => {
|
||
const s = subject.toLowerCase()
|
||
if (s.includes('math')) return 'bg-blue-500/10 ...'
|
||
if (s.includes('physics') || s.includes('science')) return '...'
|
||
if (s.includes('english') || s.includes('lit')) return '...'
|
||
```
|
||
- **问题**:`DEFAULT_CLASS_SUBJECTS` 是中文("数学"、"英语"等),但 `getSubjectColor` 用英文 'math'/'english' 匹配,所有中文科目都会落到 default 分支,颜色视觉分组完全失效。
|
||
- **违反规则**:i18n + 功能正确性。
|
||
- **后果**:课表颜色视觉分组完全失效,不仅是 i18n 问题。
|
||
|
||
### 2.4 错误处理
|
||
|
||
#### 问题 D1:catch 块未记录错误,仅显示 toast(🟡 P1)
|
||
|
||
- **位置**:10 处
|
||
- [my-classes-grid.tsx:75-77](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L75)
|
||
- [schedule-filters.tsx:65-67](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L65)
|
||
- [schedule-view.tsx:113-115](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L113)
|
||
- [students-filters.tsx:73-75](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L73)
|
||
- [admin-classes-view.tsx:52-54](file:///e:/Desktop/CICD/src/modules/classes/components/admin-classes-view.tsx#L52)
|
||
- [grade-classes-view.tsx:42-44](file:///e:/Desktop/CICD/src/modules/classes/components/grade-classes-view.tsx#L42)
|
||
- [class-invitation-manager.tsx:101-103](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L101)、[263-265](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L263)
|
||
- [edit-class-dialog.tsx:55-57](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/edit-class-dialog.tsx#L55)
|
||
- [students-table.tsx:41-43](file:///e:/Desktop/CICD/src/modules/classes/components/students-table.tsx#L41)
|
||
- **现状**:`} catch { toast.error(t("list.failedCreate")) }`
|
||
- **违反规则**:错误处理最佳实践。
|
||
- **后果**:服务端 500、网络错误、权限错误全部显示同一文案,开发者无法从用户截图定位错误;线上排查困难。
|
||
|
||
#### 问题 D2:data-access 内 catch 静默返回空数组(🟡 P1)
|
||
|
||
- **位置**:
|
||
- [data-access-admin.ts:88-124](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L88)(getAdminClasses fallback 合理降级)
|
||
- [data-access-students.ts:159-177](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L159)(getStudentClasses fallback 合理降级)
|
||
- [data-access-teacher.ts:70-73](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L70)(getTeacherClasses 直接返回 `[]`)
|
||
- **问题**:`getTeacherClasses` 失败时返回空数组,会让教师看到"无班级"假象,区分不出"数据库错误"与"无数据"。
|
||
- **违反规则**:错误处理最佳实践。
|
||
- **后果**:教师班级列表假性空数据,难以排查。
|
||
|
||
#### 问题 D3:actions 层错误处理两套风格混用(🟡 P1)
|
||
|
||
- **位置**:
|
||
- 风格 A(手动 try/catch + `PermissionDeniedError`):[actions-admin.ts:63-66](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L63)、[actions-grade.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts)、[actions-invitations.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts)
|
||
- 风格 B(`handleActionError`):[actions-teacher.ts:74-76](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L74)、[actions-schedule.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts)
|
||
- **违反规则**:编码规范一致性。
|
||
- **后果**:权限拒绝场景下风格 A 会 `throw e` 冒泡到 Next.js 错误边界(用户体验差),风格 B 会转为结构化失败;维护成本高。
|
||
|
||
#### 问题 D4:teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx(🟡 P1)
|
||
|
||
- **位置**:`src/app/(dashboard)/teacher/classes/my/[id]/`
|
||
- **现状**:页面内部 4 个 `Promise.all` 并行数据获取,但完全没有 loading.tsx 和 error.tsx。
|
||
- **违反规则**:项目规则「学生路由必须包含 loading.tsx 和 error.tsx」+ 企业级规范。
|
||
- **后果**:无骨架屏感知性能差;任一 data-access 抛错冒泡至上层;`notFound()` 触发时展示默认 404 与站点风格不一致。
|
||
|
||
#### 问题 D5:10 个页面缺 error.tsx(🟡 P1)
|
||
|
||
- **位置**:teacher/classes/my、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule 等
|
||
- **违反规则**:企业级规范要求主要路由必须有 error.tsx。
|
||
- **后果**:错误上下文丢失,错误冒泡至上层。
|
||
|
||
### 2.5 类型安全
|
||
|
||
#### 问题 E1:formData.get 强转 string 应使用类型守卫(🟢 P2)
|
||
|
||
- **位置**:[actions-admin.ts:93](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L93)、[actions-grade.ts:98](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts#L98)
|
||
- **现状**:`formData.get("subjectTeachers") as string | null`
|
||
- **违反规则**:禁止 `as` 断言(除类型收窄外)。
|
||
- **后果**:若前端意外提交 `File` 对象(如通过 FormData.append 上传文件),`parseSubjectTeachers` 会因 `typeof raw !== "string"` 返回 null 而静默丢弃数据。
|
||
|
||
#### 问题 E2:data-access-invitations.ts 中两处 string → union 强转(🟢 P2)
|
||
|
||
- **位置**:[data-access-invitations.ts:274](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L274)、[363](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L363)
|
||
- **现状**:`record.status as ValidationResult["reason"]` / `status: row.status as InvitationCodeStatus`
|
||
- **违反规则**:禁止 `as` 断言。
|
||
- **后果**:DB 中出现意外值(如 "pending")不会触发类型错误,运行时返回错误 reason。
|
||
|
||
#### 问题 E3:class-trends-widget.tsx 中 as string[] 应使用类型守卫(🟢 P2)
|
||
|
||
- **位置**:[class-trends-widget.tsx:129](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L129)
|
||
- **现状**:`Array.from(new Set(assignments.map(a => a.subject).filter(Boolean))) as string[]`
|
||
- **违反规则**:禁止 `as` 断言。
|
||
- **后果**:`filter(Boolean)` 在 TypeScript 中不会收窄类型,跳过空值检查。
|
||
|
||
#### 问题 E4:14 个组件事件处理函数缺 Promise<void> 返回类型(🟢 P2)
|
||
|
||
- **位置**:my-classes-grid.tsx(4 处)、schedule-filters.tsx、schedule-view.tsx(4 处)、students-filters.tsx、students-table.tsx、class-invitation-manager.tsx(3 处)、edit-class-dialog.tsx
|
||
- **违反规则**:函数返回值必须显式标注,特别是 `Promise<T>`。
|
||
- **后果**:若将来函数内部 `return` 一个值(如返回 boolean 表示是否成功),调用方无类型提示。
|
||
|
||
### 2.6 文件大小
|
||
|
||
#### 问题 F1:schedule-view.tsx 527 行超出组件 500 行建议(🟡 P1)
|
||
|
||
- **位置**:[schedule-view.tsx](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx)
|
||
- **现状**:同时承担"周历视图渲染 + 创建对话框 + 编辑对话框 + 删除确认对话框"4 个职责。
|
||
- **违反规则**:React 组件 ≤ 500 行。
|
||
- **后果**:可读性差、修改易引入回归。
|
||
|
||
#### 问题 F2:my-classes-grid.tsx 内含 210 行巨型组件 ClassTicket(🟢 P2)
|
||
|
||
- **位置**:[my-classes-grid.tsx:208-417](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L208)
|
||
- **现状**:单一组件管理 4 段视觉职责(票据左侧信息+邀请码+趋势图+周历嵌入式渲染)。
|
||
- **违反规则**:组件规范。
|
||
- **后果**:调试困难。
|
||
|
||
### 2.7 组件复用性
|
||
|
||
#### 问题 G1:3 个 view 的 CRUD handler 几乎重复(🟢 P2)
|
||
|
||
- **位置**:admin-classes-view.tsx:41-95、grade-classes-view.tsx:31-85、schedule-view.tsx:100-158
|
||
- **现状**:相同的 try/catch + toast + router.refresh 模式重复 9 次(每个 view 3 个 handler)。
|
||
- **违反规则**:DRY。
|
||
- **后果**:错误处理改进(如 D1 加 console.error)需修改 9 处。
|
||
|
||
### 2.8 可测试性
|
||
|
||
#### 问题 H1:纯函数内嵌组件未导出(🟢 P2)
|
||
|
||
- **位置**:
|
||
- [my-classes-grid.tsx:40-48](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L40) `getSeededValue`
|
||
- [my-classes-grid.tsx:268-272](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L268) `performanceChange` 计算
|
||
- [schedule-view.tsx:160-181](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L160) `getPositionStyle`
|
||
- [schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186) `getSubjectColor`
|
||
- **违反规则**:可测试性「纯逻辑是否与 UI 分离」。
|
||
- **后果**:业务计算逻辑(环比变化率、课表块定位、颜色映射)无法独立单测;`getSubjectColor` 还存在 C6 提到的功能 bug,但因内嵌而难以被发现。
|
||
|
||
### 2.9 i18n metadata 缺失
|
||
|
||
#### 问题 I1:8 个 teacher/student 页面缺 generateMetadata(🟢 P2)
|
||
|
||
- **位置**:teacher/classes/my、teacher/classes/my/[id]、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule、parent/children/[studentId]
|
||
- **违反规则**:页面级 metadata.title 应走 i18n。
|
||
- **后果**:浏览器标签栏、社交分享卡片等场景文案不本地化,SEO 友好度差。
|
||
|
||
## 三、行业差距对比
|
||
|
||
参考 Google Classroom、钉钉教育、智学网、ClassDojo、PowerSchool 等 K12 班级管理产品的主流设计模式,对比当前实现差距:
|
||
|
||
### 3.1 班级列表与详情
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 班级卡片信息密度 | Google Classroom 卡片含教师头像、学生数、最近活动时间、未读作业数 | `my-classes-grid.tsx` 含邀请码、提交率趋势、周历嵌入,信息密度高但**未提供"最近活动"时间线** | 缺少"班级最近动态"feed |
|
||
| 班级封面图 | Google Classroom / ClassDojo 支持自定义班级主题图 | 仅色彩区分 | 视觉识别度弱 |
|
||
| 班级详情布局 | 智学网采用"Tab 切换(学生/作业/成绩/课表/设置)+ 顶部 sticky header" | `class-detail/` 采用 widget 网格布局 | widget 平铺在班级数多时滚动疲劳;可考虑 Tab 化 |
|
||
| 班级归档 | Google Classroom 支持"归档班级",归档后只读但保留数据 | 无归档功能 | 学年结束后历史班级污染列表 |
|
||
| 班级复制 | Google Classroom 支持复制班级(含学生/科目配置) | 无 | 新学年建班成本高 |
|
||
|
||
### 3.2 学生管理
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 学生加入方式 | 邀请码 + 邮件 + 批量导入 + 班级链接 | 已实现邀请码 + 邮件 + 批量导入 | 缺少"班级加入链接"(点击即加入) |
|
||
| 学生列表筛选 | 智学网支持按科目成绩、出勤率、活跃度多维筛选 | `students-filters.tsx` 仅按班级 + 状态 | 缺少按学业表现筛选 |
|
||
| 学生卡片信息 | Google Classroom 显示学生头像、最近提交、整体进度 | `students-table.tsx` 显示头像+科目成绩 | 缺少"最近提交/整体进度"时间维度 |
|
||
| 学生迁移 | 钉钉教育支持"批量迁班"(学年升级时) | 无 | 学年升级时手动逐个调整 |
|
||
| 学生邀请码状态可视化 | ClassDojo 显示邀请码扫描情况(已加入/待加入) | `class-invitation-manager.tsx` 显示邀请码列表 | 缺少"已扫码未加入"中间状态 |
|
||
|
||
### 3.3 课表
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 课表视图 | 智学网周课表 + 日课表 + 月课表三视图切换 | `schedule-view.tsx` 仅周课表 | 缺少日/月视图 |
|
||
| 课表冲突检测 | PowerSchool 在添加时自动检测教室/教师/时段冲突 | 依赖 scheduling 模块外部检测 | 当前 actions-schedule 未触发检测(B2) |
|
||
| 课表颜色编码 | 钉钉教育按科目自动配色 | `getSubjectColor` 中文失效(C6) | **功能 bug,必须修复** |
|
||
| 课表导出/打印 | 智学网支持导出 PDF/Excel、打印 | 无 | 教师打印课表需求未满足 |
|
||
| 课表提醒 | Google Classroom 课前 5 分钟推送提醒 | 无 | 缺少课表提醒集成 |
|
||
|
||
### 3.4 多角色协作
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 家长视角班级信息 | ClassDojo 家长看到班级公告、教师动态、孩子表现 | parent 仅看到孩子课表 + 班级概览 | 缺少"班级动态 feed" |
|
||
| 学生视角班级首页 | Google Classroom 学生首页是"待办作业流" | student/learning/courses 是班级列表 | 缺少"班级作业待办流"聚合视图 |
|
||
| 跨班级协作 | 钉钉教育支持"年级主任一键查看所有班级对比" | management/grade 已有 insights | ✅ 已实现,对比图较完善 |
|
||
| 班级消息 | Google Classroom 班级内消息流 | messaging 模块支持 `class_members` scope | ✅ 已通过 messaging 模块实现 |
|
||
|
||
### 3.5 数据洞察
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 班级健康度评分 | PowerSchool 综合出勤+成绩+参与度给出班级健康分 | `class-trends-widget.tsx` 仅展示提交率/平均分 | 缺少综合健康度评分 |
|
||
| 早期预警 | ClassDojo 识别"低参与度学生"自动预警 | 无 | 缺少学生风险预警 |
|
||
| 班级对比 | 智学网支持同年级班级多维度对比 | `management/grade/insights` 已有 | ✅ |
|
||
| 趋势同比环比 | PowerSchool 提供周/月/学期同比 | `class-trends-widget.tsx` 仅"Latest"指标 | 缺少时间维度对比 |
|
||
|
||
### 3.6 可访问性与性能
|
||
|
||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||
|---|---|---|---|
|
||
| 键盘导航 | WCAG 2.1 AA 要求所有交互可键盘操作 | 班级列表/课表键盘可访问但无 focus-visible 样式优化 | a11y 待加强 |
|
||
| 流式渲染 | React 18+ Suspense 流式渲染 | 全部 RSC 同步获取,无 Suspense 边界 | 班级详情 4 个并行查询可流式 |
|
||
| 骨架屏精确度 | 骨架屏应反映实际布局 | `class-skeleton.tsx` 5 个 skeleton 布局匹配 | ✅ 良好 |
|
||
|
||
## 四、改进优先级建议
|
||
|
||
### P0 — 安全/功能阻断(立即修复)
|
||
|
||
| # | 问题 | 改进方向 |
|
||
|---|---|---|
|
||
| P0-1 | A1:data-access.ts 与拆分文件 3 对同名函数重复定义 | 删除 data-access.ts 中的本地定义,统一从拆分文件导出(保持 barrel 入口兼容) |
|
||
| P0-2 | B1:listClassInvitationCodesAction 无归属校验 | 在 actions 层调用 `verifyTeacherOwnsClass(classId, ctx.userId)`(admin scope 跳过) |
|
||
| P0-3 | B2:3 个 schedule action 无归属校验 | 同 P0-2,调用 `verifyTeacherOwnsClass` |
|
||
| P0-4 | C6:getSubjectColor 中文科目不匹配 | 改用科目 ID 或类型守卫匹配;纯函数抽出到 `schedule-utils.ts` 并补单测 |
|
||
| P0-5 | B3:教师 update/delete/enroll action 未在 actions 层做归属校验 | actions 层显式判断 `hasAdminScope(ctx)` 否则校验 `classes.teacherId === ctx.userId` |
|
||
|
||
### P1 — 重要合规性(本期实施)
|
||
|
||
| # | 问题 | 改进方向 |
|
||
|---|---|---|
|
||
| P1-1 | C1:class-detail/ 8 子组件硬编码英文 | 全部接入 `useTranslations("classes.detail.*")` |
|
||
| P1-2 | C2:schedule-view / schedule-filters / students-filters 硬编码英文 | 接入 i18n |
|
||
| P1-3 | C3:所有 actions message 英文硬编码 | 在 actions 层使用 `getTranslations()` 或返回错误码由组件层翻译 |
|
||
| P1-4 | C4:30+ error.tsx 硬编码中文 | 抽取 `shared/components/ErrorState` + i18n key `common.error.boundary.*` |
|
||
| P1-5 | C5:DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 硬编码 | 改为从 `subjects` 表查询;统一单一来源 |
|
||
| P1-6 | B4:data-access 中 7 处 roles.name 硬编码 | 抽出 `ROLE_TEACHER` 常量 |
|
||
| P1-7 | B5:student 三个页面未调用 requirePermission | 补 `requirePermission(Permissions.HOMEWORK_SUBMIT)` 或新增 STUDENT_READ 权限 |
|
||
| P1-8 | D1:10 处 catch 块未记录错误 | 改为 `catch (error) { console.error("[classes] xxx:", error); toast.error(...) }` |
|
||
| P1-9 | D2:getTeacherClasses 静默返回空数组 | 区分"DB 错误"与"无数据",DB 错误抛出 |
|
||
| P1-10 | D3:actions 层错误处理两套风格 | 统一为 `handleActionError` |
|
||
| P1-11 | D4:teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx | 新增 |
|
||
| P1-12 | D5:10 个页面缺 error.tsx | 补齐 |
|
||
| P1-13 | F1:schedule-view.tsx 527 行超限 | 抽出 3 个对话框子组件 |
|
||
|
||
### P2 — 工程优化(中长期)
|
||
|
||
| # | 问题 | 改进方向 |
|
||
|---|---|---|
|
||
| P2-1 | E1/E2/E3:5 处 as 断言 | 改用类型守卫 |
|
||
| P2-2 | E4:14 个事件处理函数缺 Promise<void> | 补返回类型 |
|
||
| P2-3 | G1:3 个 view CRUD handler 重复 | 抽 `useClassFormHandlers` hook |
|
||
| P2-4 | H1:纯函数内嵌组件 | 抽到 `class-stats-utils.ts` / `schedule-utils.ts` 并补单测 |
|
||
| P2-5 | F2:my-classes-grid ClassTicket 210 行 | 拆分为 `ClassTicketHeader` / `ClassTicketInvitation` / `ClassTicketTrend` |
|
||
| P2-6 | A2:class-invitation-manager 绝对路径 | 改为相对路径 |
|
||
| P2-7 | I1:8 个页面缺 generateMetadata | 补齐 |
|
||
| P2-8 | 行业差距:班级归档/复制 | 中长期产品规划 |
|
||
| P2-9 | 行业差距:班级健康度评分/早期预警 | 中长期产品规划 |
|
||
| P2-10 | 行业差距:课表多视图/导出/提醒 | 中长期产品规划 |
|
||
|
||
### 重构设计原则(强制满足)
|
||
|
||
为达成"完全解耦 + 组合优先 + 国际化就绪 + 最大化复用 + 错误边界 + 可测试 + 可扩展 + 企业级补充"八项原则,本次实施遵循:
|
||
|
||
1. **完全解耦**:classes 模块内部组件不直接 import 其他业务模块(exams/grades/homework/scheduling)的 actions/data-access;跨模块数据通过本模块 data-access 暴露的接口调用(已基本达成,仅需修复 P0-2/P0-3 的越权问题)。
|
||
2. **组合优先**:错误处理抽 `useClassFormHandlers` hook;纯逻辑抽 `class-stats-utils.ts`、`schedule-utils.ts`;详情 widget 通过配置驱动(`ClassDetailWidgetConfig`)。
|
||
3. **国际化就绪**:所有修复项必须使用 i18n key;新增翻译键写入 `messages/{locale}/classes.json` 的 `detail.*` / `schedule.*` / `students.*` 命名空间。
|
||
4. **最大化复用**:admin/grade/teacher/student 共用 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `useClassData` / `useClassFilters` / `useClassFormHandlers`。
|
||
5. **错误与边界**:每个详情 widget 独立用 `ClassErrorBoundary` 包裹;RSC 数据获取用 React Suspense + 骨架屏;空数据/无权限/网络异常状态明确处理。
|
||
6. **可测试性**:纯函数导出至 `*.ts` 文件,便于 vitest 单测;接口类型显式标注。
|
||
7. **可扩展性**:角色差异通过 `hasAdminScope` / `hasTeacherScope` 抽象,新增角色只改 actions-shared.ts。
|
||
8. **企业级补充**:补 a11y(focus-visible、ARIA)、性能(Suspense 流式)、安全(actions 层归属校验)、监控(埋点接口预留 `trackClassEvent` 工具函数)。
|
||
|
||
## 五、架构图同步说明
|
||
|
||
本次审计发现架构图存在以下遗漏或不一致,需在实施修复时同步更新:
|
||
|
||
| # | 位置 | 问题 | 修复动作 |
|
||
|---|---|---|---|
|
||
| 1 | 005 `modules.classes.files` | 仅列 21 个文件,漏 schema.ts/types.ts/12 个组件文件 | 补全至 33 个文件 |
|
||
| 2 | 005 `modules.classes.exports.actions` | 漏 `createClassInvitationCodeAction` / `revokeClassInvitationCodesAction` / `listClassInvitationCodesAction` 3 个 v3 邀请码 action | 补全至 20 个 |
|
||
| 3 | 005 `modules.classes.exports.dataAccess` | 漏 `compareClassLike` / `isDuplicateInvitationCodeError` / `generateUniqueInvitationCode` / `getAccessibleClassIdsForTeacher` / `getSessionTeacherId` / `getTeacherSubjectIdsForClass` 6 个工具函数 | 补全 |
|
||
| 4 | 005 `dbTables` | `classInvitationCodes` 表字段未详细登记 | 补全字段定义 |
|
||
| 5 | 005 `dbTables` | `classSchedule` 的 `usedBy` 字段未含 scheduling | 补充 `["classes", "scheduling"]` |
|
||
| 6 | 005 `knownIssues.P0-1` | 仍记录原始问题状态,未标注「已修复」 | 更新 status 字段 |
|
||
| 7 | 004 §2.7「被依赖」 | 遗漏 `elective` / `error-book` / `adaptive-practice` 三个模块 | 补全至 13 个 |
|
||
| 8 | 005 `dependencyMatrix` | `exams → classes` 与 `homework → classes` 边未明确登记 | 补全边定义 |
|
||
| 9 | 004 §2.7 已知问题 | P0-2/P0-3/P0-4/P0-5 等本次新增修复 | 同步新增修复记录 |
|
||
| 10 | 004 §2.7 文件清单 | 修复后行数变化(如 schedule-view.tsx 拆分) | 更新行数 |
|