diff --git a/bugs/001_first_login_onboarding.md b/bugs/001_first_login_onboarding.md new file mode 100644 index 0000000..f7a8fb2 --- /dev/null +++ b/bugs/001_first_login_onboarding.md @@ -0,0 +1,258 @@ +# 首次登录引导(Onboarding)重大问题讨论 + +> 创建日期:2026-06-18 +> 状态:**讨论中,待决策** +> 关联架构图:`docs/architecture/004_architecture_impact_map.md` §2.1 shared 层 / §3 已知问题 P2-4 +> 关联代码: +> - [src/shared/components/onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)(312 行) +> - [src/app/api/onboarding/status/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts) +> - [src/app/api/onboarding/complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) +> - [src/app/layout.tsx](file:///e:/Desktop/CICD/src/app/layout.tsx#L41)(全局挂载点) + +--- + +## 一、背景与定位 + +按项目规则"先图后码",先从架构影响地图定位 Onboarding 相关节点: + +- **shared 层**:`components/onboarding-gate.tsx`(312 行)已被架构图标记为 ⚠️ P2-4「业务逻辑泄漏到 shared」 +- **app 层**:`/api/onboarding/status`、`/api/onboarding/complete` 两条路由 +- **数据层**:`users.onboardedAt`([src/shared/db/schema.ts:41](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L41)) +- **被调用模块**:`modules/classes/data-access.ts` 的 `enrollStudentByInvitationCode` + +当前 Onboarding 是一个**全局 Dialog**:在 `app/layout.tsx` 第 41 行无条件挂载 ``,组件内通过 `useEffect` 拉取 `/api/onboarding/status`,若 `required === true` 则弹出不可关闭的 4 步 Dialog。 + +--- + +## 二、现状代码盘点 + +### 2.1 组件层(onboarding-gate.tsx) + +| 步骤 | 标题 | 采集字段 | 备注 | +|------|------|----------|------| +| Step 0 | 角色选择 | role(student/teacher/parent) | admin 只读展示;其他角色用户可下拉**自选** | +| Step 1 | 通用信息 | name / phone / address | 仅校验非空 | +| Step 2 | 角色信息 | classCodes(学生/教师)、teacherSubjects(教师) | 可跳过;家长显示"暂不需要配置" | +| Step 3 | 完成 | — | 调 `/api/onboarding/complete` 后跳 `/dashboard` | + +**角色推断逻辑**(第 90-94 行)——用权限点反推角色: + +```ts +const isAdmin = permissions.includes(Permissions.SETTINGS_ADMIN) +const isTeacher = permissions.includes(Permissions.EXAM_CREATE) +const isStudent = permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE) +const isParent = !permissions.includes(Permissions.EXAM_CREATE) && !permissions.includes(Permissions.HOMEWORK_SUBMIT) && permissions.includes(Permissions.EXAM_READ) +``` + +### 2.2 API 层 + +- `GET /api/onboarding/status`:查 `users.onboardedAt` 是否为空 + 查 `usersToRoles` 推断角色 +- `POST /api/onboarding/complete`:更新 users 表 → 写 usersToRoles → 学生调 `enrollStudentByInvitationCode` → 教师直接 insert `classSubjectTeachers` → 写 `onboardedAt` + +--- + +## 三、重大问题清单(按风险分级) + +### 🔴 P0 级:安全/合规/越权 + +#### P0-1 用户可自选角色(严重越权) +- **位置**:[onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) +- **问题**:Step 0 允许任意登录用户从下拉框选择 `student / teacher / parent` 角色;`complete/route.ts:32-35` 直接信任前端 `body.role` 并写入 `usersToRoles`。 +- **后果**:任何注册用户可自封为 teacher,从而获得 `exam:create`、`homework:grade` 等权限;可自封为 parent 查看他人成绩。**这是 K12 教务系统的合规红线**。 +- **违反规则**:项目规则「Server Action 必须使用 `requirePermission()`」、K12 行业铁律「角色由管理员预分配」。 + +#### P0-2 教师可绑定任意班级+科目 +- **位置**:[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) +- **问题**:教师通过 `classCodes`(6 位邀请码)可把自己写入任意班级的 `classSubjectTeachers`,且 `teacherSubjects` 由前端任意提交,服务端仅做"名称存在性"校验,不校验该教师是否被管理员分配到该班。 +- **后果**:教师可越权查看任意班级学生名单、成绩;可篡改他人班级的任课关系。 +- **违反规则**:项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」——此处 app 层 API 直接 insert `classSubjectTeachers`。 + +#### P0-3 无权限校验、无 Zod、无事务 +- **位置**:[complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 +- **问题**: + - 仅检查 `auth()` 登录态,**未调用 `requirePermission()`** + - 用 `String(body.role ?? "")` 手动解析,**无 Zod**(架构图 005 声称"validation: Zod schema"与实际不符) + - 5 次独立 DB 写入(update users / insert usersToRoles / enrollStudent / insert classSubjectTeachers / update onboardedAt)**无 `db.transaction()`** + - 运行时 `db.insert(roles).values({ name: role })` 创建角色记录(第 66-68 行)——角色应在 seed 时创建,运行时创建属异常路径 +- **后果**:中途失败导致数据不一致(如已绑定角色但 `onboardedAt` 仍为 null,用户被反复弹窗);越权写入。 + +### 🟠 P1 级:架构违规 + +#### P1-1 shared 层反向承载领域逻辑 +- **位置**:[onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 +- **问题**:组件位于 `shared/components/`,但包含角色判断、班级代码、教师科目配置等强领域逻辑,并通过 fetch 调用业务 API。 +- **违反规则**:项目规则「shared 不得反向依赖 @/auth、@/proxy 或任何 modules/*」「shared 是被依赖方」。 +- **架构图标记**:004 文档 §2.1 已标记 P2-4。 + +#### P1-2 app 层 API 直接跨模块写表 +- **位置**:[complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) +- **问题**:`app/api/onboarding/complete/route.ts` 直接 import 并写入 `classes`、`classSubjectTeachers`、`subjects` 表,绕过 `modules/classes` 的 data-access 与权限校验。 +- **违反规则**:项目规则「app 只能调用 modules 的 Server Actions 和 data-access,不直接访问 DB」「modules 之间通过对方 data-access 通信」。 + +#### P1-3 角色推断双源不一致 +- **位置**:[status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) +- **问题**:status API 用 `roles.name` 推断角色(含 `grade_head/teaching_head → teacher` 归一化),组件又用权限点重新推断,两套逻辑可能不一致(如年级组长既有 EXAM_CREATE 又有其他权限,组件推断可能错位)。 + +### 🟡 P2 级:用户体验与可访问性 + +#### P2-1 全局 Dialog 模式缺陷 +- **问题**: + - Dialog 不可关闭(`canClose = !required`),用户被强制锁定 + - 刷新页面丢失步骤状态(step 重置为 0) + - 无独立 URL,无法分享/书签 + - 首屏无骨架屏,`useEffect` 拉取 status 期间会闪烁 + - 依赖 `session?.user?.name` 触发重复请求 +- **对比**:业界主流(Auth.js 官方、Clerk、Vercel 模板)均采用独立路由 `/onboarding` + middleware 重定向。 + +#### P2-2 表单校验粗糙 +- **问题**:电话仅校验非空(无手机号格式校验);姓名无长度限制;地址无长度限制;班级代码无格式预校验。 + +#### P2-3 国际化与可访问性 +- **问题**:中英文混合("Role"、"Select role" 英文,其余中文);Dialog 缺少 `aria-describedby`;进度条无 `aria-valuenow`;表单无 `required` 标记。 + +#### P2-4 进度条与步骤不一致 +- **问题**:admin 跳过 Step 2,但进度条仍渲染 4 段,视觉上 Step 2 永远亮起,造成困惑。 + +--- + +## 四、业界大仓(Monorepo)解决方案引用 + +### 4.1 Auth.js v5 官方推荐 + +- **状态标记**:`users.onboardedAt` 字段 + `jwt`/`session` 回调注入 session;完成时调 `update()` 刷新 token。 +- **强制方式**:**middleware 重定向**到独立 `/onboarding` 路由,而非客户端 Dialog。 + - 在 `middleware.ts` 用 `auth()` 读取 session,若 `user.onboardedAt` 为空且路径不在白名单(`/login`、`/api/auth`、`/onboarding`、静态资源),则 `NextResponse.redirect(new URL('/onboarding', req.url))`。 +- **结论**:客户端 Dialog 仅适合"非阻塞的偏好补全"(如头像、通知偏好);强制 onboarding 应等同未登录处理。 + +### 4.2 商业方案(Clerk / Supabase / Auth0)共性 + +三段式:**metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验**。 + +- **角色等敏感字段放服务端可写的 metadata**(Clerk `privateMetadata` / Auth0 `appMetadata` / Supabase RLS-protected `profiles.role`),**禁止前端自写**。 +- onboarding 完成回调必须由服务端 Action 写入 metadata,前端不能直接改。 +- 未完成 onboarding 时 middleware/Action 层强制重定向。 + +### 4.3 shadcn/ui 生态 + +- 官方无内置 Stepper,但 `examples/forms` 与 `blocks` 范式明确:**独立路由页面 + `
`(react-hook-form + zod)+ 父组件持 step state**。 +- 每步独立 zod schema 做渐进式校验,最后一步汇总写入。 +- 官方 `blocks/login-04` 等登录块均采用独立路由页面,而非全局 Dialog。 + +### 4.4 企业级 K12 教务系统(PowerSchool / Veracross / 国内智慧校园) + +**铁律:角色由管理员预分配,用户不可自选。** + +| 角色 | 首次登录采集字段 | 角色来源 | +|------|------------------|----------| +| 学生 | 学号(预分配不可改)、姓名、性别、出生日期、家长联系方式、紧急联系人 | 管理员批量导入 | +| 教师 | 工号(预分配)、姓名、所教科目、任教班级、办公室、联系电话、学历资质 | 教务处预分配 | +| 家长 | 与学生关系、学生学号(通过学校发放的 **Access ID + Access Password** 绑定)、本人姓名、电话、邮箱 | 学校发放凭证,家长绑定子女 | +| 管理员 | 工号、姓名、职务、管理范围 | 学校 IT 创建 | + +**原因**: +1. **合规**:K12 数据受《个人信息保护法》《未成年人保护法》约束,学生身份必须由学校权威确认。 +2. **安全**:允许自选教师角色 = 任何人可创建考试、查看全班成绩。 +3. **数据一致性**:班级、学号、任课关系是教务核心数据,必须由教务处维护。 + +### 4.5 Monorepo(turborepo / nx)惯例 + +- **turborepo 官方模板**:跨模块"流程型"功能(onboarding、setup-wizard)作为**独立 module**,而非塞进 shared。 +- **nx feature-shell 模式**:onboarding 作为 `feature-onboarding` library,依赖 `data-access-user`、`data-access-class`。 +- **Vercel 自家项目**:`app/(app)/onboarding/[[...step]]/page.tsx` 路由组 + `modules/onboarding/` 模块。 + +--- + +## 五、重构方案建议(待讨论) + +### 5.1 目标架构 + +``` +app/ +├─ (auth)/login/ # 登录页(middleware 白名单) +├─ (onboarding)/onboarding/ # 新增独立路由 +│ └─ page.tsx # 服务端组件,读取 session.onboarded 决定渲染 +└─ middleware.ts # 新增/增强:未 onboarded 时重定向 + +modules/onboarding/ # 新建模块 +├─ actions.ts # completeOnboardingAction(Server Action + requirePermission) +├─ data-access.ts # 仅操作 users.onboardedAt +├─ schema.ts # Zod:name/phone/address/classCodes +├─ types.ts +└─ components/ + ├─ OnboardingStepper.tsx # 客户端 stepper 容器 + ├─ RoleConfirmStep.tsx # 只读展示管理员分配的角色 + ├─ ProfileStep.tsx # 姓名/电话/住址 + └─ BindingStep.tsx # 学生:确认班级;教师:确认任课;家长:绑定子女 + +shared/ +└─ components/onboarding-gate.tsx # 删除 +``` + +### 5.2 关键改动点 + +1. **删除 `shared/components/onboarding-gate.tsx`**,从 `app/layout.tsx` 移除挂载。 +2. **新建 `modules/onboarding/`**,承载所有领域逻辑。 +3. **新建 `app/(onboarding)/onboarding/page.tsx`** 独立路由。 +4. **增强 `middleware.ts`**:读取 session.onboarded,未完成且非白名单路径 → 重定向到 `/onboarding`。 +5. **Auth.js 回调**:在 `jwt`/`session` 回调注入 `onboardedAt`,供 middleware 读取。 +6. **删除 `app/api/onboarding/*/route.ts`**,改为 `modules/onboarding/actions.ts` 的 Server Action。 +7. **角色只读化**:Step 0 改为"角色确认"——只读展示 `usersToRoles` 中的角色,用户不可改。 +8. **班级绑定改造**: + - 学生:仅"确认"管理员预分配的班级,或输入邀请码(服务端校验有效性 + 用途) + - 教师:仅"确认"管理员预分配的任课关系,**移除自填班级代码** + - 家长:输入"子女学号 + 绑定码"绑定子女(参考 PowerSchool Access ID 模式) +9. **事务化**:`completeOnboardingAction` 用 `db.transaction()` 包裹所有写入。 +10. **Zod 校验**:定义 `onboardingSchema`,phone 用 `z.string().regex(/^1\d{10}$/)`。 + +### 5.3 迁移兼容 + +- 已 onboarded 用户(`onboardedAt` 非空)不受影响,middleware 直接放行。 +- 未 onboarded 用户下次登录会被重定向到 `/onboarding`(而非弹 Dialog)。 +- 无需数据迁移,`users.onboardedAt` 字段保留。 + +--- + +## 六、待决策的开放问题 + +请就以下问题给出决策,以便进入实施阶段: + +### Q1:角色分配策略 +- **方案 A**(推荐,符合 K12 铁律):onboarding 中角色完全只读,由管理员通过后台预分配;用户无法在 onboarding 中改变角色。 +- **方案 B**:保留角色选择,但服务端校验"用户已有该角色"才允许选择(即只能从已有角色中选一个主角色)。 +- **方案 C**:暂不改动角色选择,仅修复其他问题。 + +### Q2:教师任课关系绑定 +- **方案 A**(推荐):onboarding 中教师**仅确认**管理员预分配的任课关系,不自填班级代码。 +- **方案 B**:保留自填邀请码,但服务端强校验邀请码用途(teacher-assign)、有效期、使用次数。 +- **方案 C**:完全移除 onboarding 中的班级绑定,统一由管理员后台处理。 + +### Q3:家长绑定子女方式 +- **方案 A**(推荐,PowerSchool 模式):家长输入"子女学号 + 学校发放的 6 位绑定码"。 +- **方案 B**:家长输入"子女学号 + 子女生日"作为验证。 +- **方案 C**:暂不实现家长绑定,由管理员后台预绑定。 + +### Q4:onboarding 路由形态 +- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper(步骤状态用 query param 持久化)。 +- **方案 B**:嵌套路由 `/onboarding/role`、`/onboarding/profile`、`/onboarding/binding`(每步独立 Server Action)。 +- **方案 C**:保留全局 Dialog,仅修复安全与架构问题。 + +### Q5:实施范围 +- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。 +- **方案 B**:先做 P0(安全/越权)+ P1(架构),P2(UX)后续迭代。 +- **方案 C**:仅做 P0 紧急修复,P1/P2 列入 backlog。 + +--- + +## 七、附录:问题与代码位置速查 + +| 问题 | 代码位置 | 风险 | +|------|----------|------| +| 用户自选角色 | [onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) | 🔴 P0 | +| 信任前端 role 写入 | [complete/route.ts:32-35](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L32-L35) | 🔴 P0 | +| 教师绑任意班级 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🔴 P0 | +| 无权限校验/Zod/事务 | [complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 | 🔴 P0 | +| shared 反向承载领域逻辑 | [onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 | 🟠 P1 | +| app 层跨模块写表 | [complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) | 🟠 P1 | +| 角色推断双源不一致 | [status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟠 P1 | +| 全局 Dialog 缺陷 | [app/layout.tsx:41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41) | 🟡 P2 | +| 表单校验粗糙 | [onboarding-gate.tsx:88](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L88) | 🟡 P2 | diff --git a/bugs/admin_bug.md b/bugs/admin_bug.md new file mode 100644 index 0000000..72398f1 --- /dev/null +++ b/bugs/admin_bug.md @@ -0,0 +1,548 @@ +# Admin 前端文件规范核查报告 + +> 核查范围:`src/app/(dashboard)/admin/` 下全部 26 个 `page.tsx` 文件 +> 核查依据: +> - `.trae/rules/project_rules.md`(项目规则) +> - `docs/standards/coding-standards.md`(编码规范 v1.0) +> - `docs/architecture/004_architecture_impact_map.md`(架构影响地图) +> - React / Next.js 16 最佳实践 +> - Web 界面设计规范(WCAG 2.2 AA) +> 核查日期:2026-06-18 + +--- + +## 一、核查概览 + +| 维度 | 文件数 | 通过 | 待改进 | +|------|--------|------|--------| +| 架构分层 | 26 | 24 | 2 | +| TypeScript 规范 | 26 | 4 | 22 | +| 安全与权限 | 26 | 3 | 23 | +| UI 一致性与设计令牌 | 26 | 18 | 8 | +| 错误与加载边界 | 26 | 0 | 26 | +| 代码复用(DRY) | 26 | 0 | 26 | + +**结论**:整体架构清晰、服务端组件使用规范、并行数据获取到位,但在**返回类型标注、权限校验一致性、加载/错误边界、代码复用、UI 文案一致性**方面存在系统性问题,需统一整改。 + +--- + +## 二、问题清单(按严重程度排序) + +### P0 严重问题(必须立即修复) + +#### P0-1 全部 26 个页面缺少 `error.tsx` 与 `loading.tsx` + +**违反规范**: +- 编码规范 §2.3:「每个路由段应提供 `loading.tsx`(骨架屏)和 `error.tsx`(错误边界)」 +- 编码规范 §2.4:「每个路由段都必须提供 `error.tsx`,不得出现未捕获异常导致白屏」 +- 编码规范 §2.4:「`loading.tsx` 必须提供骨架屏或最小可感知的加载状态,不得使用全局 spin 遮罩」 + +**现状**:`src/app/(dashboard)/admin/` 及其所有子路由(`dashboard/`、`announcements/`、`school/*`、`audit-logs/*`、`scheduling/*`、`course-plans/*`、`elective/*`、`attendance/`、`files/`、`users/import/`)均**未提供** `loading.tsx` 和 `error.tsx`。 + +对比:`teacher/`、`student/` 路由组在关键页面已提供 `loading.tsx`(如 `teacher/exams/all/loading.tsx`、`student/dashboard/loading.tsx`),admin 路由组完全缺失。 + +**影响**: +- 数据获取失败时整页白屏,用户体验差 +- 无加载态感知,用户误以为页面卡死 +- 不符合 Next.js 16 App Router 最佳实践(Suspense 流式渲染) + +**修复建议**: +1. 在 `src/app/(dashboard)/admin/` 根目录新增 `error.tsx`(具名导出,客户端组件,含重试按钮) +2. 在 `src/app/(dashboard)/admin/` 根目录新增 `loading.tsx`(骨架屏,匹配各页面布局) +3. 对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)单独提供 `loading.tsx` +4. 对动态路由(`[id]/page.tsx`)单独提供 `error.tsx` 处理 `notFound` 以外的异常 + +--- + +#### P0-2 `attendance/page.tsx` 缺少权限校验 + +**文件**:[src/app/(dashboard)/admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx) + +**违反规范**: +- 项目规则:「Server Action 必须使用 `requirePermission()` 进行权限校验」 +- 编码规范 §8.3:「权限校验:Server Action 必须使用 `requirePermission()`」 + +**现状**:第 26 行仅调用 `getAuthContext()` 获取上下文,**未调用 `requirePermission()`** 验证用户是否有考勤查看权限。 + +```tsx +// 当前代码(第 26 行) +const ctx = await getAuthContext() +``` + +**对比**:同类 admin 页面均做了权限校验: +- `audit-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)` +- `audit-logs/login-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)` +- `audit-logs/data-changes/page.tsx` 第 26 行:`await requirePermission(Permissions.AUDIT_LOG_READ)` +- `files/page.tsx` 第 12 行:`await requirePermission(Permissions.FILE_READ)` + +**影响**:越权风险——无考勤查看权限的用户可直接访问 `/admin/attendance` 查看全校考勤数据。 + +**修复建议**:在 `getAuthContext()` 前增加权限校验: +```tsx +await requirePermission(Permissions.ATTENDANCE_READ) +const ctx = await getAuthContext() +``` + +--- + +### P1 重要问题(应尽快修复) + +#### P1-1 全部 26 个页面组件缺少返回类型标注 + +**违反规范**: +- 编码规范 §4.2:「函数返回值必须显式标注,特别是 `Promise`」 +- 项目规则:「函数返回值必须显式标注」 + +**现状**:所有 `page.tsx` 的默认导出函数均未标注返回类型,例如: + +```tsx +// dashboard/page.tsx +export default async function AdminDashboardPage() { // ❌ 缺少 : Promise + const data = await getAdminDashboardData() + return +} +``` + +**影响**:26 个文件全部不合规,类型推导依赖 TS 隐式推断,不利于代码审查与维护。 + +**修复建议**:统一补充返回类型: +```tsx +export default async function AdminDashboardPage(): Promise { + // ... +} +``` + +涉及文件:admin 目录下全部 26 个 `page.tsx`。 + +--- + +#### P1-2 `getParam` 工具函数在 27 个文件中重复定义 + +**违反规范**: +- 编码规范 §一:「单一职责」「工具函数 ≤ 40 行」 +- DRY 原则 + +**现状**:以下 admin 文件各自重复定义了相同的 `getParam` / `SearchParams` 类型与函数: + +| 文件 | 行号 | +|------|------| +| `announcements/page.tsx` | 8-13 | +| `audit-logs/page.tsx` | 10-15 | +| `audit-logs/login-logs/page.tsx` | 10-15 | +| `audit-logs/data-changes/page.tsx` | 14-19 | +| `scheduling/changes/page.tsx` | 16-21 | +| `course-plans/page.tsx` | 7-12 | +| `elective/page.tsx` | 7-12 | +| `attendance/page.tsx` | 13-18 | +| `school/grades/insights/page.tsx` | 15-22 | + +全项目共 27 个文件重复(含 teacher / student / management 路由组)。 + +**影响**:维护成本高,任何一处逻辑变更需同步修改 27 处。 + +**修复建议**: +1. 在 `src/shared/lib/utils.ts` 新增共享工具: +```tsx +export type SearchParams = { [key: string]: string | string[] | undefined } + +export function getSearchParam(params: SearchParams, key: string): string | undefined { + const v = params[key] + return Array.isArray(v) ? v[0] : v +} +``` +2. 全部页面改为 `import { getSearchParam, type SearchParams } from "@/shared/lib/utils"` +3. 同步更新架构文档 004 / 005 + +--- + +#### P1-3 多个页面使用 `as` 类型断言违反 TypeScript 规范 + +**违反规范**: +- 编码规范 §4.2:「不使用 `as` 断言,除非从 `unknown` 强制转换或在测试中(需注释原因)」 +- 项目规则:「禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)」 + +**现状**:以下文件使用 `as` 进行类型断言而非类型守卫: + +| 文件 | 行号 | 问题代码 | +|------|------|---------| +| `audit-logs/page.tsx` | 28 | `(getParam(params, "status") as AuditLogStatus \| undefined)` | +| `audit-logs/login-logs/page.tsx` | 26-27 | `as LoginLogAction \| undefined`、`as LoginLogStatus \| undefined` | +| `audit-logs/data-changes/page.tsx` | 31 | `as DataChangeAction \| undefined` | +| `attendance/page.tsx` | 39 | `as "present" \| "absent" \| "late" \| "early_leave" \| "excused"` | + +**对比(正确示例)**:以下文件已使用类型守卫,应作为模板推广: +- `announcements/page.tsx` 第 15-16 行:`isValidStatus` 类型守卫 +- `scheduling/changes/page.tsx` 第 23-24 行:`isValidStatus` 类型守卫 +- `course-plans/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫 +- `elective/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫 + +**影响**:运行时无法捕获非法枚举值,类型安全被绕过。 + +**修复建议**:为每个枚举类型补充类型守卫,替换 `as` 断言: +```tsx +const isValidAuditLogStatus = (v?: string): v is AuditLogStatus => + v === "success" || v === "failure" || v === "pending" + +const status = isValidAuditLogStatus(statusParam) ? statusParam : undefined +``` + +--- + +#### P1-4 UI 文案语言不统一(中英文混用) + +**违反规范**: +- 编码规范 §一:「可读性优先」 +- 项目定位为「Next_Edu K12 智慧教务系统」(中文用户) + +**现状**: + +| 文件 | 文案语言 | +|------|---------| +| `users/import/page.tsx` | 中文("批量导入用户"、"返回") | +| `announcements/[id]/page.tsx` | 英文("Edit Announcement") | +| `school/schools/page.tsx` | 英文("Schools"、"Manage schools...") | +| `school/classes/page.tsx` | 英文("Classes"、"Manage classes...") | +| `school/grades/page.tsx` | 英文("Grades"、"Manage grades...") | +| `school/grades/insights/page.tsx` | 英文("Grade Insights"、"Filters") | +| `school/academic-year/page.tsx` | 英文("Academic Year") | +| `school/departments/page.tsx` | 英文("Departments") | +| `audit-logs/page.tsx` | 英文("Audit Logs") | +| `audit-logs/login-logs/page.tsx` | 英文("Login Logs") | +| `audit-logs/data-changes/page.tsx` | 英文("Data Change Logs") | +| `scheduling/auto/page.tsx` | 英文("Auto Schedule") | +| `scheduling/changes/page.tsx` | 英文("Schedule Change Requests") | +| `scheduling/rules/page.tsx` | 英文("Scheduling Rules") | +| `course-plans/page.tsx` | 英文("Course Plans") | +| `course-plans/create/page.tsx` | 英文("New Course Plan") | +| `course-plans/[id]/edit/page.tsx` | 英文("Edit Course Plan") | +| `elective/page.tsx` | 英文("Elective Courses") | +| `elective/create/page.tsx` | 英文("New Elective Course") | +| `elective/[id]/edit/page.tsx` | 英文("Edit Elective Course") | +| `attendance/page.tsx` | 英文("Attendance Overview") | + +**影响**:用户体验割裂,admin 区仅 `users/import` 为中文,其余全英文,与系统定位不符。 + +**修复建议**:统一为中文(与 `users/import/page.tsx` 保持一致),或引入 i18n 方案统一管理。建议优先统一为中文。 + +--- + +### P2 一般问题(建议修复) + +#### P2-1 `school/grades/insights/page.tsx` 使用原生 `` 元素,而项目已提供 `@/shared/components/ui/select.tsx`(shadcn Select)。 + +```tsx +` 样式难以跨浏览器统一 +- 可访问性较弱(缺少 ARIA 属性) + +**修复建议**:替换为 `@/shared/components/ui/select.tsx` 的 `Select` / `SelectTrigger` / `SelectContent` / `SelectItem` 组合。 + +--- + +#### P2-2 `users/import/page.tsx` 使用原生 `` 而非共享组件 + +**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128) + +**现状**:第 93-128 行使用原生 `
` 元素手写表格,而项目已提供 `@/shared/components/ui/table.tsx`(shadcn Table)。 + +**影响**:与 `school/grades/insights/page.tsx` 等使用 shadcn Table 的页面风格不一致。 + +**修复建议**:替换为 `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` 组合。 + +--- + +#### P2-3 Tailwind 任意值违规 + +**违反规范**: +- 编码规范 §6.2:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释说明」 +- 项目规则:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释」 + +**现状**: + +| 文件 | 行号 | 问题类名 | +|------|------|---------| +| `school/grades/insights/page.tsx` | 60 | `md:w-[360px]` | +| `school/grades/insights/page.tsx` | 82, 89, 96 | `h-[360px]` | +| `users/import/page.tsx` | 16 | `h-full flex-1 flex-col`(`flex-1` 合理,但整体布局类应复用) | + +**修复建议**: +- `md:w-[360px]` → 使用设计令牌宽度类(如 `md:w-72` 或 `md:w-80`)或在 globals.css 定义 `--filter-width` 变量 +- `h-[360px]` → 使用 `h-80`(320px)或 `h-96`(384px)等标准档位 + +--- + +#### P2-4 `users/import/page.tsx` 使用硬编码颜色 + +**违反规范**: +- 编码规范 §6.3:「所有视觉设计决策(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值」 + +**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L67) + +**现状**:第 67 行使用 `text-amber-500` 硬编码颜色: +```tsx + +``` + +**修复建议**:使用设计令牌颜色,如 `text-warning`(若存在)或在 globals.css 定义 `--warning` 变量。如暂无 warning 令牌,可使用 `text-primary` 或 `text-muted-foreground` 保持一致。 + +--- + +#### P2-5 `school/grades/insights/page.tsx` 导入顺序违规 + +**违反规范**: +- 编码规范 §4.3:「导入顺序:React → 第三方 → 内部绝对路径 → 相对路径 → 类型导入」 +- 项目规则引用的 ESLint `import/order` 规则 + +**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L1-L11) + +**现状**:第 1-11 行导入顺序混乱,`lucide-react`(第三方库)被放在所有 `@/` 内部导入之后: +```tsx +import Link from "next/link" // next(外部) +import { getGrades } from "@/modules/school/data-access" // 内部 +import { getGradeHomeworkInsights } from "@/modules/classes/data-access" +import { EmptyState } from "@/shared/components/ui/empty-state" +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { Badge } from "@/shared/components/ui/badge" +import { Button } from "@/shared/components/ui/button" +import { Table, TableBody, ... } from "@/shared/components/ui/table" +import { formatDate } from "@/shared/lib/utils" +import { BarChart3 } from "lucide-react" // ❌ 第三方应在前 +``` + +**修复建议**:调整为 `next` → `lucide-react` → `@/` 内部导入,分组间空一行: +```tsx +import Link from "next/link" + +import { BarChart3 } from "lucide-react" + +import { getGrades } from "@/modules/school/data-access" +// ... +``` + +--- + +#### P2-6 `course-plans/[id]/edit/page.tsx` 同模块重复导入 + +**文件**:[src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx#L3-L4) + +**现状**:第 3-4 行从同一模块 `@/modules/course-plans/data-access` 分两行导入: +```tsx +import { getCoursePlanById } from "@/modules/course-plans/data-access" +import { getSubjectOptions } from "@/modules/course-plans/data-access" +``` + +**修复建议**:合并为单行: +```tsx +import { getCoursePlanById, getSubjectOptions } from "@/modules/course-plans/data-access" +``` + +--- + +#### P2-7 `scheduling/*` 页面从 `actions` 而非 `data-access` 获取数据 + +**违反规范**: +- 编码规范 §7.1:「服务端数据获取通过模块的 `data-access.ts` 函数」 +- 架构影响地图:「actions.ts(编排层:权限 + 调用 data-access + revalidate)」 + +**现状**:以下页面从 `@/modules/scheduling/actions` 导入数据查询函数: + +| 文件 | 导入函数 | +|------|---------| +| `scheduling/auto/page.tsx` | `getAdminClassesForScheduling` | +| `scheduling/changes/page.tsx` | `getAdminClassesForScheduling`、`getScheduleChanges` | +| `scheduling/rules/page.tsx` | `getAdminClassesForScheduling`、`getSchedulingRules` | + +**说明**:规范允许 `app/` 调用 Server Actions,但 Server Actions 的职责是「编排:权限 + 调用 data-access + revalidate」,主要用于**变更操作**。纯读取操作应通过 `data-access.ts` 暴露,避免在 Server Component 中触发不必要的 `revalidate` 逻辑。 + +**修复建议**:将 `getAdminClassesForScheduling`、`getScheduleChanges`、`getSchedulingRules` 等纯查询函数迁移到 `scheduling/data-access.ts`,或在 actions 中明确标注其为只读封装。需同步更新架构文档 004 / 005。 + +--- + +## 三、React 性能优化建议(基于最佳实践) + +### R1 利用 Suspense 流式渲染提升首屏感知性能 + +**现状**:所有页面使用 `export const dynamic = "force-dynamic"` 整页动态渲染,数据获取完成前无任何内容呈现。 + +**建议**:对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)拆分为多个 Suspense 边界,优先渲染页面骨架,慢查询部分流式注入: + +```tsx +import { Suspense } from "react" + +export default async function AuditLogsPage(): Promise { + return ( +
+
+ }> + + + }> + + +
+ ) +} +``` + +### R2 `school/grades/insights/page.tsx` 串行查询可优化为并行 + +**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L30-L33) + +**现状**:第 30-33 行先 `await getGrades()` 再条件 `await getGradeHomeworkInsights()`,两次串行查询: +```tsx +const grades = await getGrades() +const selected = gradeId && gradeId !== "all" ? gradeId : "" +const insights = selected ? await getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : null +``` + +**说明**:`insights` 依赖 `selected`(来自 URL 参数,非 `grades` 结果),两者无数据依赖,可并行: +```tsx +const selected = gradeId && gradeId !== "all" ? gradeId : "" +const [grades, insights] = await Promise.all([ + getGrades(), + selected ? getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : Promise.resolve(null), +]) +``` + +### R3 列表页 `classOptions` 映射可下沉至 data-access + +**现状**:`scheduling/auto`、`scheduling/changes`、`scheduling/rules`、`attendance`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit` 等页面均在组件内 `.map()` 转换数据形状: + +```tsx +const classOptions = classes.map((c) => ({ id: c.id, name: c.name, grade: c.grade })) +``` + +**建议**:在对应 `data-access.ts` 提供 `getClassOptions()`、`getStaffOptions()` 等轻量查询函数,仅返回 `{ id, name }` 形状,减少传输数据量与组件层转换逻辑。 + +--- + +## 四、Web 界面设计规范建议(基于 WCAG 2.2 AA) + +### W1 表单 `
` 与语义化标注 + +**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128) + +**现状**:原生 `` 缺少 ``,或替换为 shadcn Table 后通过 `aria-label` 补充。 + +### W3 页面标题层级不统一 + +**现状**: +- 部分页面使用 `

` 作为页面主标题(如 `school/schools`、`audit-logs`) +- `users/import/page.tsx` 也使用 `

` +- 但页面布局中未见统一的 `

` 主标题层级 + +**建议**:确认 `(dashboard)/layout.tsx` 是否提供 `

` 或页面 `
` 的 accessible name,若无,建议各页面统一使用 `

` 作为页面主标题,`

` 用于区块标题,保持标题层级连贯。 + +### W4 交互式筛选器缺少 `aria-live` 反馈 + +**文件**:`school/grades/insights/page.tsx`、`attendance/page.tsx`、`audit-logs/*` + +**现状**:筛选器提交后表格数据刷新,但屏幕阅读器用户无法感知数据已更新。 + +**违反**:WCAG 2.2 SC 4.1.3(状态消息)。 + +**建议**:在表格容器添加 `aria-live="polite"` 或使用项目已有的 `useAriaLive` Hook 通知「已加载 N 条记录」。 + +### W5 `EmptyState` 组件使用一致但图标语义可优化 + +**现状**:`scheduling/*`、`attendance`、`school/grades/insights` 均使用 `EmptyState` 组件,图标统一使用 `ClipboardList` / `BarChart3`,体验一致(优点)。 + +**建议**:`BarChart3` 用于「无数据」与「选择年级」两种语义略显混淆,建议「等待操作」类空状态使用 `MousePointerClick` 或 `Filter` 图标区分。 + +--- + +## 五、优秀实践(已符合规范,应保持) + +1. **服务端组件默认化**:全部 26 个页面均为 async 服务端组件,未滥用 `"use client"`,符合 §5.2。 +2. **并行数据获取**:`announcements/page.tsx`、`audit-logs/*`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit`、`school/grades`、`scheduling/rules` 等均使用 `Promise.all` 并行查询,性能良好。 +3. **类型守卫正确使用**:`announcements/page.tsx`、`scheduling/changes/page.tsx`、`course-plans/page.tsx`、`elective/page.tsx` 使用 `isValidStatus` 类型守卫,是 `as` 断言的正确替代方案。 +4. **404 处理**:`announcements/[id]/page.tsx`、`course-plans/[id]/page.tsx`、`course-plans/[id]/edit/page.tsx`、`elective/[id]/edit/page.tsx` 使用 `notFound()` 处理资源不存在场景。 +5. **权限校验到位**:`audit-logs/*`(3 个文件)、`files/page.tsx` 正确调用 `requirePermission()`。 +6. **模块化组合**:页面仅负责数据获取与组合,UI 逻辑下沉至 `modules/*/components/`,符合三层架构。 +7. **`force-dynamic` 标注**:需要实时数据的页面均显式声明 `export const dynamic = "force-dynamic"`。 +8. **`metadata` 导出**:`users/import/page.tsx` 正确导出 `metadata` 用于 SEO(建议其他页面补充)。 + +--- + +## 六、修复优先级与建议执行顺序 + +| 优先级 | 问题编号 | 建议执行顺序 | 影响范围 | +|--------|---------|-------------|---------| +| P0 | P0-2 | 立即修复 attendance 权限 | 1 文件 | +| P0 | P0-1 | 补充 error.tsx / loading.tsx | 新增 ~6 文件 | +| P1 | P1-1 | 补充返回类型标注 | 26 文件 | +| P1 | P1-2 | 抽取共享 getSearchParam | 27 文件 | +| P1 | P1-3 | 替换 as 断言为类型守卫 | 4 文件 | +| P1 | P1-4 | 统一 UI 文案语言 | ~20 文件 | +| P2 | P2-1 ~ P2-7 | 逐步整改 | 单文件级 | +| R1 ~ R3 | 性能优化 | 迭代优化 | 关键页面 | +| W1 ~ W5 | 可访问性 | 迭代优化 | 关键页面 | + +--- + +## 七、附:文件清单与合规状态 + +| 文件 | P0 | P1 | P2 | 备注 | +|------|----|----|----|----| +| `dashboard/page.tsx` | - | 缺返回类型 | - | 整体合规 | +| `announcements/page.tsx` | - | 缺返回类型、getParam 重复 | - | 类型守卫正确 | +| `announcements/[id]/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `users/import/page.tsx` | - | 缺返回类型 | 原生 table、硬编码颜色 | 文案为中文(正确) | +| `school/page.tsx` | - | 缺返回类型 | - | 仅 redirect | +| `school/schools/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `school/classes/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `school/grades/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `school/grades/insights/page.tsx` | - | 缺返回类型、英文文案 | 原生 select、任意值、导入顺序、label 未关联 | 问题最多 | +| `school/academic-year/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `school/departments/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `audit-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 | +| `audit-logs/login-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 | +| `audit-logs/data-changes/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 | +| `scheduling/auto/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - | +| `scheduling/changes/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | 从 actions 取数 | 类型守卫正确 | +| `scheduling/rules/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - | +| `course-plans/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 | +| `course-plans/create/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `course-plans/[id]/page.tsx` | - | 缺返回类型 | - | - | +| `course-plans/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | 重复导入 | - | +| `elective/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 | +| `elective/create/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `elective/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | - | - | +| `attendance/page.tsx` | **缺权限校验** | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 最高优先级 | +| `files/page.tsx` | - | 缺返回类型 | - | 权限校验正确、整体合规 | + +--- + +> 报告生成完毕。建议按「六、修复优先级」顺序整改,每完成一批次后运行 `npm run lint` 与 `npx tsc --noEmit` 验证,并同步更新架构文档 004 / 005。 diff --git a/bugs/back_bug.md b/bugs/back_bug.md new file mode 100644 index 0000000..64235e8 --- /dev/null +++ b/bugs/back_bug.md @@ -0,0 +1,1434 @@ +# 后端模块规范核查报告 + +> 核查日期:2026-06-18 +> 核查范围:`src/modules/` 下所有后端 `.ts` 文件(actions / data-access / schema / types / services 等非 components、非 hooks 文件) +> 核查依据: +> - `.trae/rules/project_rules.md` 项目规则 +> - `docs/standards/coding-standards.md` 编码规范 +> - `docs/architecture/004_architecture_impact_map.md` 架构影响地图 +> - Vercel React Best Practices 性能优化规则(react-best-practices 技能) +> +> 严重程度定义: +> - **P0**:严重违规,破坏架构分层 / 安全漏洞 / 数据正确性问题,必须立即修复 +> - **P1**:明确违规,影响可维护性 / 类型安全 / 性能,应尽快修复 +> - **P2**:轻微违规或代码异味,可在迭代中优化 + +--- + +## 目录 + +- [一、汇总统计](#一汇总统计) +- [二、P0 严重问题清单(立即修复)](#二p0-严重问题清单立即修复) +- [三、按模块详细核查](#三按模块详细核查) + - [3.1 exams(考试模块)](#31-exams考试模块) + - [3.2 homework(作业模块)](#32-homework作业模块) + - [3.3 questions(题库模块)](#33-questions题库模块) + - [3.4 grades(成绩模块)](#34-grades成绩模块) + - [3.5 textbooks(教材模块)](#35-textbooks教材模块) + - [3.6 classes(班级模块)](#36-classes班级模块) + - [3.7 school(学校模块)](#37-school学校模块) + - [3.8 scheduling(排课模块)](#38-scheduling排课模块) + - [3.9 attendance(考勤模块)](#39-attendance考勤模块) + - [3.10 course-plans(教学计划模块)](#310-course-plans教学计划模块) + - [3.11 users(用户模块)](#311-users用户模块) + - [3.12 messaging(消息模块)](#312-messaging消息模块) + - [3.13 notifications(通知模块)](#313-notifications通知模块) + - [3.14 parent(家长模块)](#314-parent家长模块) + - [3.15 audit(审计模块)](#315-audit审计模块) + - [3.16 elective(选修课模块)](#316-elective选修课模块) + - [3.17 proctoring(监考模块)](#317-proctoring监考模块) + - [3.18 diagnostic(诊断模块)](#318-diagnostic诊断模块) + - [3.19 dashboard(仪表盘模块)](#319-dashboard仪表盘模块) + - [3.20 files(文件模块)](#320-files文件模块) + - [3.21 announcements(公告模块)](#321-announcements公告模块) + - [3.22 settings(设置模块)](#322-settings设置模块) + - [3.23 layout(布局模块)](#323-layout布局模块) +- [四、跨模块共性问题](#四跨模块共性问题) +- [五、性能优化专项(react-best-practices)](#五性能优化专项react-best-practices) +- [六、优先修复路线图](#六优先修复路线图) + +--- + +## 一、汇总统计 + +### 1.1 按严重程度 + +| 严重程度 | 数量 | 典型问题 | +|---------|------|---------| +| **P0** | 14 | 跨模块直查/直写 DB 表、循环依赖、`z.any()`、N+1 查询、未返回 ActionState、无 Zod 验证、硬编码弱密码 | +| **P1** | 60+ | `as` 断言滥用、`requireAuth` 替代 `requirePermission`、缺少 `import type`、动态 import、无事务、除零 bug、导出数据截断 | +| **P2** | 100+ | 非空断言 `!`、未用 `React.cache()`、串行查询未并行化、重复 filter、静默吞异常、缺返回类型标注、文档不同步 | + +### 1.2 按问题类别 + +| 问题类别 | 涉及文件数 | 关键发现 | +|---------|-----------|---------| +| 架构违规 | 25+ | 跨模块直查 DB 是最普遍问题;messaging↔notifications 循环依赖;actions 层直接 DB 操作 | +| TS 规范 | 30+ | `as` 断言大量存在;`z.any()`;`import type` 未用;非空断言 `!`;隐式 `any[]` | +| Server Action 规范 | 12+ | `requireAuth` 替代 `requirePermission`;未返回 `ActionState`;无 Zod 验证;缺 `revalidatePath` | +| 性能 | 20+ | N+1 查询;动态 import;未用 `cache()`;串行 await 循环;重复 filter 遍历 | +| 安全 | 2 | 硬编码弱密码 "123456";`as` 断言掩盖类型不兼容 | +| 行数 | 1 | `exams/ai-pipeline.ts` 912 行(超 800 建议,未超 1000 硬上限) | + +### 1.3 模块问题分布 + +| 模块 | P0 | P1 | P2 | 总体评价 | +|------|----|----|-----|---------| +| exams | 3 | 5 | 6 | 跨模块直查/直写严重 | +| homework | 0 | 8 | 5 | 跨模块直查普遍 | +| questions | 2 | 2 | 3 | `z.any()` + 未返回 ActionState | +| grades | 1 | 6 | 7 | N+1 查询 + 跨模块直查 | +| textbooks | 2 | 2 | 4 | 无 Zod 验证 + `as` 断言 | +| classes | 3 | 8 | 6 | 耦合最严重,混入多模块逻辑 | +| school | 1 | 1 | 2 | actions 层直接 DB 操作 | +| scheduling | 0 | 4 | 3 | 缺返回类型标注 | +| attendance | 0 | 1 | 0 | 整体规范 | +| course-plans | 0 | 3 | 2 | 缺 revalidatePath | +| users | 1 | 3 | 4 | 硬编码弱密码 + 无事务 | +| messaging | 1 | 6 | 6 | 循环依赖 + requireAuth | +| notifications | 3 | 3 | 8 | 循环依赖 + as 断言类型不兼容 | +| parent | 0 | 2 | 2 | 跨模块直查 | +| audit | 0 | 4 | 6 | 导出数据截断 | +| elective | 0 | 3 | 5 | 无事务 + 串行更新 | +| proctoring | 1 | 6 | 3 | actions 层直接 DB 操作 | +| diagnostic | 0 | 2 | 5 | 跨模块直查 + 性能问题 | +| dashboard | 0 | 0 | 0 | 标杆模块 | +| files | 0 | 2 | 3 | 隐式 any[] + 非空断言 | +| announcements | 0 | 1 | 5 | as 断言 + 错误吞没 | +| settings | 0 | 3 | 9 | 缺 data-access 层 + 无 Zod | +| layout | 0 | 0 | 2 | 类型松散 | + +--- + +## 二、P0 严重问题清单(立即修复) + +### P0-1:exams 模块直写 questions 表 + +- **文件**:`src/modules/exams/data-access.ts` +- **行号**:2, 318 +- **问题**:`persistAiGeneratedExamDraft` 直接 `db.insert(questions)` 写入 questions 表(属 questions 模块),破坏模块封装 +- **修复**:改为调用 `questions/data-access.createQuestionWithRelations()` 或在 questions 模块暴露批量插入接口 + +### P0-2:exams 模块直查 classes 表 + +- **文件**:`src/modules/exams/data-access.ts` +- **行号**:2, 72-80, 156-163, 357-364 +- **问题**:`getExams`/`getExamById`/`getExamsDashboardStats` 直接查询 `classes` 表获取 gradeId +- **修复**:在 classes 模块暴露 `getGradeIdsByClassIds(classIds)` 接口 + +### P0-3:questions/schema.ts 使用 `z.any()` + +- **文件**:`src/modules/questions/schema.ts` +- **行号**:6 +- **问题**:`content: z.any()` 违反"禁止 any"规则 +- **修复**:改为 `z.unknown()` + +### P0-4:questions/actions.ts 未返回 ActionState + +- **文件**:`src/modules/questions/actions.ts` +- **行号**:154, 166-175 +- **问题**:`getQuestionsAction` 和 `getKnowledgePointOptionsAction` 直接返回原始结果,未包装为 `ActionState`,错误时 throw 而非返回失败状态 +- **修复**:改为 `Promise>` 返回 `{ success: true, data: result }` + +### P0-5:textbooks 模块无 Zod 验证 + 大量 `as` 断言 + +- **文件**:`src/modules/textbooks/actions.ts` +- **行号**:全文件,特别 54-58, 92-98, 154, 219-221, 259-261 +- **问题**:缺少 `schema.ts` 文件,所有 Action 均无 Zod 验证,使用手动 `if (!rawData.title)` 检查;14 处 `formData.get("title") as string` 断言 +- **修复**:新建 `schema.ts`,为每个 Action 定义 Zod schema;改用 `typeof` 守卫或 `getStringValue()` 辅助函数 + +### P0-6:grades 模块 N+1 查询 + +- **文件**:`src/modules/grades/data-access-analytics.ts` +- **行号**:142-185 +- **问题**:`getClassComparison` 中 `for (const cls of classRows) { ... await db.select(...) }` 循环内串行查询,N+1 问题 +- **修复**:用 `inArray` 一次性查询所有班级的成绩,再在内存分组 + +### P0-7:classes 模块跨模块直接查询 DB 表 + +- **文件**:`src/modules/classes/data-access-stats.ts`、`src/modules/classes/data-access-students.ts` +- **行号**:data-access-stats.ts: 11-18;data-access-students.ts: 10-14 +- **问题**:直接导入并查询 `homeworkAssignments`、`homeworkSubmissions`、`homeworkAssignmentTargets`、`homeworkAssignmentQuestions`、`exams` 等其他模块的表 +- **修复**:通过 homework 模块的 data-access 暴露的函数获取数据 + +### P0-8:school 模块 actions 层直接操作 DB + +- **文件**:`src/modules/school/actions.ts` +- **行号**:7-8, 26-30, 53-59, 73, 96-108, 133-147, 161, 182-186, 211-217, 233, 261-268, 294-303, 317 +- **问题**:actions 层直接导入 `db` 和所有 schema,所有 mutation 直接操作 DB,未下沉到 data-access 层 +- **修复**:在 `data-access.ts` 中新增 `createDepartment`、`updateDepartment`、`deleteDepartment`、`createSchool` 等函数 + +### P0-9:proctoring 模块 actions 层直接查询 DB + +- **文件**:`src/modules/proctoring/actions.ts` +- **行号**:11-13, 79-84 +- **问题**:actions.ts(编排层)直接导入 `db`、`examSubmissions` 并执行 DB 查询,违反三层架构 +- **修复**:将 submission 归属校验逻辑移至 `data-access.ts` + +### P0-10:messaging↔notifications 循环依赖 + +- **文件**:`src/modules/messaging/data-access.ts`、`src/modules/notifications/data-access.ts`、`src/modules/notifications/channels/in-app-channel.ts` +- **行号**:messaging/data-access.ts: 192-203;notifications/data-access.ts: 20-21;in-app-channel.ts: 50 +- **问题**:messaging 写 `messageNotifications` 表,notifications 反向依赖 messaging,形成循环依赖 +- **修复**:将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块 + +### P0-11:notifications/in-app-channel.ts 非法 as 断言 + +- **文件**:`src/modules/notifications/channels/in-app-channel.ts` +- **行号**:54 +- **问题**:`payload.type as "message" | "announcement" | "homework" | "grade"` 源类型 `"info"|"warning"|"error"|"success"` 与目标类型不兼容,as 断言非法 +- **修复**:重新设计类型映射函数,或统一 messaging/notifications 的 type 定义 + +### P0-12:users 模块硬编码弱密码 + +- **文件**:`src/modules/users/user-service.ts` +- **行号**:13 +- **问题**:`const DEFAULT_PASSWORD = "123456"` 硬编码弱密码,批量导入用户使用极弱默认密码 +- **修复**:改为随机生成密码或要求首次登录强制修改密码,至少使用 `crypto.randomBytes` 生成 + +### P0-13:users/actions.ts updateUserProfile 绕过权限校验 + +- **文件**:`src/modules/users/actions.ts` +- **行号**:29-51 +- **问题**:`updateUserProfile` 使用 `requireAuth()` 而非 `requirePermission()`,且在 actions 层直接执行 `db.update(users)`,返回 `Promise` 而非 `ActionState` +- **修复**:改为 `requirePermission(Permissions.USER_MANAGE)` 或新增 `USER_PROFILE_UPDATE` 权限点;DB 操作下沉到 data-access;返回 `ActionState` + +### P0-14:scheduling/data-access.ts 缺返回类型标注 + +- **文件**:`src/modules/scheduling/data-access.ts` +- **行号**:239, 246, 255, 262 +- **问题**:`getAdminClassesForScheduling()`、`getTeachersForScheduling()`、`getClassroomsForScheduling()`、`getClassSubjectsForScheduling()` 缺少返回类型标注,违反"函数返回值必须显式标注,特别是 Promise" +- **修复**:添加返回类型 `: Promise>` 等 + +--- + +## 三、按模块详细核查 + +### 3.1 exams(考试模块) + +#### `src/modules/exams/actions.ts`(767 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 270 | TS规范 | P1 | `formData.get("questionsJson") as string \| null` — 使用 `as` 断言,非从 unknown 转换 | 改用 `typeof` 守卫:`const raw = formData.get("questionsJson"); const rawQuestions = typeof raw === "string" ? raw : null` | +| 349-351 | TS规范 | P1 | 三处 `as string \| null` 断言(`questionsJson`/`aiQuestionsJson`/`structureJson`) | 统一使用 `getStringValue()` 辅助函数 | +| 4 | TS规范 | P2 | `import { ActionState }` — 类型未使用 `import type` | 改为 `import type { ActionState }` | +| 564-565 | Server Action规范 | P2 | `JSON.parse(rawQuestions)` 直接 parse 后传入 Zod,parse 异常未捕获 | 用 try-catch 包裹或先 `as unknown` 再 safeParse | +| 全文件 | 行数 | P2 | 实际 767 行,架构文档记录为 691 行,文档与代码不同步 | 同步更新 004 和 005 架构文档 | + +**合规项**:所有 10 个 Action 均调用 `requirePermission()` ✓;均使用 Zod 验证 ✓;均返回 `ActionState` ✓;均使用 `revalidatePath` ✓。 + +#### `src/modules/exams/ai-pipeline.ts`(912 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 全文件 | 行数 | P2 | 912 行,超出 800 行建议(架构文档记录 857 行,已不同步)。混合 4 类职责 | 按职责拆分为 `ai-json-parser.ts`、`ai-prompts.ts`、`ai-requests.ts`、`ai-preview-builder.ts` | +| 464 | TS规范 | P2 | `draft.sections!.forEach` — 非空断言 | 改用 `if (draft.sections) { draft.sections.forEach(...) }` | +| 657, 665, 666, 671 | TS规范 | P2 | 多处 `aiParsed.sections!.flatMap` / `aiParsed.sections!.map` — 非空断言 | 同上,用条件守卫替代 | +| 505, 508, 545, 879 | TS规范 | P2 | `as Record` — 虽然从 unknown 转换允许,但缺少注释说明原因 | 添加注释 `// safe: validated by isRecord` | +| 557-558 | 代码质量 | P2 | `catch {}` 空捕获块,静默吞掉错误 | 至少记录 `console.warn` | +| 503-524 | 代码质量 | P2 | `normalizeQuestionCandidate` 嵌套定义在 `parseQuestionDetail` 内部,每次调用都重新创建闭包 | 提取为模块级函数 | + +**合规项**:使用 `mapWithConcurrency` 实现并发控制 ✓;使用 `Promise.all` ✓;`env.AI_MODEL` 服务端环境变量无 `NEXT_PUBLIC_` 前缀 ✓。 + +#### `src/modules/exams/data-access.ts`(524 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 2, 318 | 架构违规 | **P0** | `persistAiGeneratedExamDraft` 直接 `db.insert(questions)` 写入 questions 表 | 改为调用 `questions/data-access.createQuestionWithRelations()` | +| 2, 72-80, 156-163, 357-364 | 架构违规 | **P0** | `getExams`/`getExamById`/`getExamsDashboardStats` 直接查询 `classes` 表 | 在 classes 模块暴露 `getGradeIdsByClassIds(classIds)` 接口 | +| 2, 206-226, 509-524 | 架构违规 | P1 | `resolveSubjectGradeNames`/`getExamSubjects`/`getExamGrades` 直接查询 `subjects`/`grades` 表 | 在 school 模块暴露 `getSubjectOptions()`/`getGradeOptions()` 接口 | +| 76, 160, 361 | TS规范 | P1 | `teacherGradeIds.map(g => g.gradeId).filter(Boolean) as string[]` — `as` 断言 | 使用类型守卫:`.filter((id): id is string => Boolean(id))` | +| 108, 172 | TS规范 | P2 | `(exam.status as ExamStatus)` — `as` 断言从 string 转换 | 使用 `z.enum()` 验证或类型守卫函数 | +| 182 | TS规范 | P2 | `exam.structure as unknown` — 转换到 unknown 允许,但无注释 | 添加注释说明 | + +**合规项**:使用 `cache()` 包装查询函数 ✓;使用 `Promise.all` 并行查询 ✓;使用 `Map` 做 O(1) 查找 ✓。 + +#### `src/modules/exams/types.ts`(31 行) + +无违规问题。类型定义规范,接口命名 PascalCase 无 `I` 前缀 ✓。 + +--- + +### 3.2 homework(作业模块) + +#### `src/modules/homework/actions.ts`(282 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 247 | TS规范 | P1 | `formData.get("answersJson") as string \| null` — `as` 断言 | 改用 `typeof` 守卫 | +| 54 | TS规范 | P2 | `JSON.parse(targetStudentIdsJson) as unknown` — 转换到 unknown 允许,但无注释 | 添加注释 | + +**合规项**:所有 5 个 Action 均调用 `requirePermission()` ✓;使用 Zod 验证 ✓;返回 `ActionState` ✓;使用 `revalidatePath` ✓;使用 `Set` 做查找 ✓。 + +#### `src/modules/homework/data-access.ts`(681 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 8-20, 101-105, 162-166, 283-287, 337-340, 518-519 | 架构违规 | P1 | 直接查询 `exams` 表(5 处),属 exams 模块 | 在 exams 模块暴露 `getExamIdsByGradeIds(gradeIds)` 接口 | +| 8-20, 66-77, 86-95, 149-158, 242-254, 275-281, 347-351 | 架构违规 | P1 | 直接查询 `classEnrollments` 表(多处),属 classes 模块 | 在 classes 模块暴露 `getStudentIdsByClassId(classId)` / `getStudentIdsByClassIds(classIds)` 接口 | +| 8-20, 515-519 | 架构违规 | P1 | 直接查询 `subjects` 表,属 school 模块 | 通过 school data-access 获取 | +| 8-20, 480-486 | 架构违规 | P1 | 直接查询 `users`/`roles`/`usersToRoles` 表,属 users 模块 | 在 users 模块暴露 `getUserWithRole(userId, roleName)` 接口 | +| 475-490 | 架构违规 | P2 | `getDemoStudentUser` 使用 `auth()` 而非 `getAuthContext()`,绕过 auth-guard 标准模式 | 改用 `getAuthContext()` 获取 userId | +| 39 | TS规范 | P1 | `return v as HomeworkQuestionContent` — 从 `Record` 断言 | 使用类型守卫或 `z.safeParse()` 验证 | +| 124, 226, 314, 392, 467, 645 | TS规范 | P2 | 多处 `(status as HomeworkAssignmentStatus)` — `as` 断言 | 使用类型守卫函数或 Zod 验证 | +| 451-455 | 性能 | P2 | `getHomeworkSubmissionDetails` 获取全部 submissions 仅为计算 prev/next 导航 | 改用 SQL `LAG`/`LEAD` 窗口函数或仅查询相邻 ID | + +**合规项**:使用 `cache()` ✓;使用 `Map`/`Set` 聚合 ✓;`"server-only"` 导入 ✓。 + +#### `src/modules/homework/data-access-write.ts`(317 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 8-17, 70-76, 125-130 | 架构违规 | P1 | 直接查询 `classes` 表(`getClassTeacherById`) | 在 classes 模块暴露 `getClassTeacherById(classId)` 接口 | +| 8-17, 132-143 | 架构违规 | P1 | 直接查询 `classEnrollments` 表(`getActiveClassStudentIdsForHomework`) | 在 classes 模块暴露 `getActiveStudentIdsByClassId(classId)` 接口 | +| 8-17, 107-116 | 架构违规 | P1 | 直接查询 `classSubjectTeachers` 表 | 在 classes 模块暴露 `getTeacherSubjectIdsByClass(classId, teacherId)` 接口 | +| 8-17, 81-88 | 架构违规 | P1 | 直接查询 `exams` 表(`getExamWithQuestionsForHomework`) | 在 exams 模块暴露 `getExamForHomeworkCreation(examId)` 接口 | +| 305-311 | 性能 | P2 | `gradeHomeworkAnswers` 中 `for (const ans of answers) { await db.update(...) }` — 循环内串行 await,未使用事务 | 用 `db.transaction` 包裹,或用 `Promise.all` 并行化无依赖更新 | + +**合规项**:`"server-only"` 导入 ✓;所有函数显式标注返回类型 ✓;无 `as`/`any` ✓。 + +#### `src/modules/homework/schema.ts`(29 行) + +无违规问题。Zod 验证完整 ✓。 + +#### `src/modules/homework/stats-service.ts`(483 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 8-16, 313-323, 320-323 | 架构违规 | P1 | 直接查询 `classEnrollments` 表 | 通过 classes data-access 获取 | +| 8-16, 437-441 | 架构违规 | P1 | 直接查询 `classes` 表 | 通过 classes data-access 获取 | +| 8-16, 425-429, 443-447 | 架构违规 | P1 | 直接查询 `exams` 表 | 通过 exams data-access 获取 | +| 8-16, 366-369 | 架构违规 | P1 | 直接查询 `users` 表 | 在 users 模块暴露 `getUserNamesByIds(ids)` 接口 | +| 220 | TS规范 | P2 | `(assignment.status as HomeworkAssignmentStatus)` — `as` 断言 | 使用类型守卫 | +| 346 | 性能 | P2 | `limit: 5000` 查询班级所有 graded submissions,大班级可能性能问题 | 考虑分批查询或 SQL 聚合 | + +**合规项**:使用 `cache()` ✓;使用 `Promise.all` 并行查询 ✓;使用 `Map` 聚合 ✓。 + +#### `src/modules/homework/types.ts`(205 行) + +无违规问题。类型定义规范 ✓。 + +--- + +### 3.3 questions(题库模块) + +#### `src/modules/questions/actions.ts`(176 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 154 | Server Action规范 | **P0** | `getQuestionsAction` 未返回 `ActionState`,直接返回 `getQuestions(params)` 的原始结果 `{ data, meta }`,错误时 throw | 改为 `Promise>` 返回 `{ success: true, data: result }` | +| 166-175 | Server Action规范 | **P0** | `getKnowledgePointOptionsAction` 未返回 `ActionState`,直接返回 `KnowledgePointOption[]` | 同上,包装为 ActionState | +| 154 | TS规范 | P1 | `getQuestionsAction` 缺少显式返回类型标注 | 添加 `: Promise>` | +| 7 | TS规范 | P2 | `import { ActionState }` — 类型导入未用 `import type` | 改为 `import type { ActionState }` | +| 158-163, 170-175 | 代码质量 | P2 | catch 块两个分支都 `throw e`,是死代码 | 简化为单个 `throw e` 或返回失败 ActionState | +| 20 | 命名规范 | P2 | 函数名 `createNestedQuestion` 与其他模块的 `createXxxAction` 命名模式不一致 | 改为 `createQuestionAction` | + +**合规项**:所有 Action 调用 `requirePermission()` ✓;create/update/delete 使用 Zod 验证 ✓;写操作使用 `revalidatePath` ✓。 + +#### `src/modules/questions/data-access.ts`(299 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 4, 266-298 | 架构违规 | P1 | `getKnowledgePointOptions` 直接查询 `knowledgePoints`/`chapters`/`textbooks` 表,属 textbooks 模块 | 在 textbooks 模块暴露 `getKnowledgePointOptions()` 接口 | +| 108 | 代码质量 | P2 | 局部变量名 `knowledgePoints` 与 import 的 `knowledgePoints` 表名冲突 | 重命名为 `kpRows` 或 `kps` | +| 151-184, 231-242 | 性能 | P2 | `insertQuestionWithRelations` / `deleteQuestionRecursive` 递归内串行 await | 对于删除可改为先收集所有 ID 再批量删除 | + +**合规项**:使用 `cache()` ✓;`"server-only"` 导入 ✓;无 `as`/`any` ✓;显式返回类型 ✓。 + +#### `src/modules/questions/schema.ts`(18 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6 | TS规范 | **P0** | `content: z.any()` — 使用 `any`,违反"禁止 any"规则 | 改为 `z.unknown()` | + +#### `src/modules/questions/types.ts`(34 行) + +无违规问题。`content: unknown` 正确使用 unknown ✓。 + +--- + +### 3.4 grades(成绩模块) + +#### `src/modules/grades/actions.ts`(312 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 93 | 代码质量 | P2 | `JSON.parse(recordsJson)` 未用 try-catch 包裹,parse 失败会 500 | 用 try-catch 或先 `as unknown` 再 safeParse | + +**合规项**:所有 Action 调用 `requirePermission()` ✓;使用 Zod 验证 ✓;返回 `ActionState` ✓;使用 `revalidatePath` ✓;scope 二次校验 ✓。 + +#### `src/modules/grades/actions-analytics.ts`(133 行) + +**合规项**:所有 Action 调用 `requirePermission()` ✓;返回 `ActionState` ✓;scope 二次校验 ✓。无违规问题。 + +#### `src/modules/grades/data-access.ts`(419 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6-12, 100, 382-386 | 架构违规 | P1 | 直接查询 `classes` 表 | 通过 classes data-access 获取 | +| 6-12, 365-368, 408-411 | 架构违规 | P1 | 直接查询 `classEnrollments` 表 | 通过 classes data-access 获取 | +| 6-12, 102, 276, 280 | 架构违规 | P1 | 直接查询 `subjects` 表 | 通过 school data-access 获取 | +| 6-12, 100, 109-112, 269, 342 | 架构违规 | P1 | 直接查询 `users` 表 | 通过 users data-access 获取 | +| 148, 172 | 性能 | P1 | `const { createId } = await import("@paralleldrive/cuid2")` — 函数内动态 import,每次调用都有开销 | 改为文件顶部静态 `import { createId } from "@paralleldrive/cuid2"` | +| 249 | 代码质量/Bug | P1 | `const ratio = scores[i] / fullScores[i]` — `fullScores[i]` 可能为 0,导致 `Infinity`/`NaN` | 添加 `if (fullScores[i] <= 0) continue` 守卫 | +| 全文件 | 性能 | P2 | 查询函数未使用 `React.cache()` 包装 | 用 `cache()` 包装查询函数 | +| 248-252 | 性能 | P2 | `for` 循环内两次独立 `if` 判断遍历同一数组 | 合并为单次循环同时计算 passCount 和 excellentCount | + +**合规项**:`"server-only"` 导入 ✓;使用 `Map`/`Set` ✓;无 `as`/`any` ✓;显式返回类型 ✓。 + +#### `src/modules/grades/data-access-analytics.ts`(293 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6-10, 79, 125-128 | 架构违规 | P1 | 直接查询 `classes` 表 | 通过 classes data-access 获取 | +| 6-10, 80, 211 | 架构违规 | P1 | 直接查询 `subjects` 表 | 通过 school data-access 获取 | +| 22, 27, 32 | 代码质量 | P2 | `toNumber`/`normalize`/`buildScopeClassFilter` 在 data-access.ts 和 data-access-ranking.ts 中重复定义 | 提取到 `grades/utils.ts` | +| 142-185 | 性能 | **P0** | `getClassComparison` 中 `for (const cls of classRows) { ... await db.select(...) }` — 循环内串行查询,N+1 问题 | 用 `inArray` 一次性查询所有班级的成绩,再在内存分组 | +| 133-136 | 性能 | P2 | `classRows.filter((c) => scope.classIds.includes(c.id))` — `includes` 是 O(n) 查找 | 将 `scope.classIds` 转为 `Set` 后用 `.has()` | +| 180-181, 238-239 | 性能 | P2 | `normalized.filter((s) => s >= 60).length` 和 `normalized.filter((s) => s >= 85).length` — 两次 filter 遍历同一数组 | 合并为单次 `reduce` 同时统计两个计数 | +| 全文件 | 性能 | P2 | 查询函数未使用 `React.cache()` | 用 `cache()` 包装 | + +**合规项**:`"server-only"` ✓;无 `as`/`any` ✓;显式返回类型 ✓。 + +#### `src/modules/grades/data-access-ranking.ts`(121 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6-10, 44-53 | 架构违规 | P1 | 直接查询 `classEnrollments` 表 | 通过 classes data-access 获取 | +| 6-10, 37-41 | 架构违规 | P1 | 直接查询 `users` 表 | 通过 users data-access 获取 | +| 17, 22 | 代码质量 | P2 | `toNumber`/`normalize` 重复定义 | 提取到共享 utils | +| 100, 102 | 性能 | P2 | `sorted.findIndex(...)` 和 `sorted.find(...)` 两次 O(n) 查找同一元素 | 合并为一次遍历 | +| 全文件 | 性能 | P2 | 查询函数未使用 `React.cache()` | 用 `cache()` 包装 | + +#### `src/modules/grades/export.ts`(214 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6-11, 116-120 | 架构违规 | P1 | 直接查询 `classes` 表 | 通过 classes data-access 获取 | +| 6-11, 124-132 | 架构违规 | P1 | 直接查询 `subjects` 表 | 通过 school data-access 获取 | +| 6-11, 135-144 | 架构违规 | P1 | 直接查询 `users` 表 | 通过 users data-access 获取 | +| 154 | TS规范 | P2 | `subjMap.get(r.studentId)!` — 非空断言 | 改用 `const subjMap = scoreMap.get(r.studentId); if (!subjMap) continue;` | + +#### `src/modules/grades/schema.ts`(52 行)& `types.ts`(176 行) + +无违规问题。Zod 验证完整,类型定义规范 ✓。 + +--- + +### 3.5 textbooks(教材模块) + +#### `src/modules/textbooks/actions.ts`(276 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 全文件 | Server Action规范 | **P0** | **缺少 `schema.ts` 文件**,所有 Action 均无 Zod 验证,使用手动 `if (!rawData.title)` 检查 | 新建 `schema.ts`,为每个 Action 定义 Zod schema | +| 41-45 | 架构违规 | P1 | 本地定义 `export type ActionState`,未使用 `@/shared/types/action-state` 的统一类型 | 删除本地定义,改用 `import type { ActionState } from "@/shared/types/action-state"` | +| 18 | TS规范 | P1 | `import { CreateTextbookInput, UpdateTextbookInput }` — 类型导入未用 `import type` | 改为 `import type { ... }` | +| 54-58, 92-98, 154, 219-221, 259-261 | TS规范 | **P0** | 多处 `formData.get("title") as string` 等 `as` 断言(共 14 处) | 改用 `typeof` 守卫或 `getStringValue()` 辅助函数 | +| 20, 47 | 代码质量 | P2 | `// ... existing code ...` 残留注释 | 删除 | +| 164 | 代码质量 | P2 | `order: 0 // Default order` 魔法数字 | 提取为常量 `const DEFAULT_CHAPTER_ORDER = 0` | + +**合规项**:所有 Action 调用 `requirePermission()` ✓;使用 `revalidatePath` ✓。 + +#### `src/modules/textbooks/data-access.ts`(444 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 42 | TS规范 | P2 | `byId.get(pid)!.children.push(ch)` — 非空断言 | 改用 `const parent = byId.get(pid); if (parent) parent.children.push(ch)` | +| 51 | TS规范 | P1 | `sortRecursive(n.children as Array)` — `as` 断言 | 使用类型守卫或在 `Chapter` 类型中明确 `children` 字段 | +| 71 | TS规范 | P2 | `or(...)!` — 对 `or()` 返回值使用非空断言 | 改用条件判断 | +| 110, 128 | 代码质量 | P2 | `getTextbookById` 返回 `Textbook \| undefined`,与其他模块返回 `null` 的模式不一致 | 统一返回 `Textbook \| null` | +| 368, 381 | 代码质量 | P2 | `createKnowledgePoint`/`updateKnowledgePoint` 参数使用内联类型 | 将输入类型移至 `types.ts` | + +**合规项**:使用 `cache()` ✓;使用 `Promise.all` ✓;`"server-only"` ✓;无 `any` ✓;显式返回类型 ✓。这是架构文档中的"标杆模块",无跨模块 DB 访问 ✓。 + +#### `src/modules/textbooks/types.ts`(83 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 1-3 | 代码质量 | P2 | 残留注释 `// In a real app, we would infer these from the schema...` | 删除过时注释 | + +--- + +### 3.6 classes(班级模块) + +#### `src/modules/classes/actions.ts`(765 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 8-9 | 架构违规 | **P0** | actions 层直接导入 `db` 和 `grades, classes` schema 并执行 DB 查询 | 将权限校验查询下沉到 `data-access-admin.ts` | +| 48-53, 109-111, 140-142, 175-183, 369-377, 518-533, 636-644 | Server Action规范 | P1 | 输入验证使用手动 `typeof` 检查而非 Zod,classes 模块完全没有 schema.ts 文件 | 新建 `schema.ts`,定义 Zod schema | +| 538 | TS规范 | P1 | `weekdayNum as 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` — `as` 断言 | 用类型守卫 `isValidWeekday(weekdayNum): weekdayNum is 1\|2\|3\|4\|5\|6\|7` 替换 | +| 582 | TS规范 | P1 | `weekdayNum as 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| undefined` — 同上 | 同上 | +| 287, 706 | TS规范 | P2 | `JSON.parse(subjectTeachers) as unknown` 后续用 `as { subject?: unknown }` | 用 Zod schema 解析 | + +#### `src/modules/classes/data-access.ts`(656 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 26-27, 182-206 | 架构违规 | **P0** | `getTeacherClasses` 中调用 `getClassHomeworkInsights`(homework 逻辑)和 `getClassSchedule`(scheduling 逻辑) | 将 homework insights 聚合逻辑移至 homework 模块 | +| 47 | TS规范 | P1 | `isDuplicateInvitationCodeError` 箭头函数缺少返回类型标注 | 添加 `: boolean` | +| 54 | TS规范 | P1 | `generateInvitationCode` 箭头函数缺少返回类型标注 | 添加 `: string` | +| 87 | TS规范 | P1 | `normalizeSortText` 箭头函数缺少返回类型标注 | 添加 `: string` | +| 89 | TS规范 | P1 | `parseFirstInt` 箭头函数缺少返回类型标注 | 添加 `: number \| null` | +| 94 | TS规范 | P1 | `compareGradeLabel` 箭头函数缺少返回类型标注 | 添加 `: number` | +| 101-104 | TS规范 | P1 | `compareClassLike` 箭头函数缺少返回类型标注 | 添加 `: number` | +| 240 | TS规范 | P1 | `r.subject as ClassSubject` — `as` 断言 | 用类型守卫 `isClassSubject(r.subject)` 过滤 | +| 266 | TS规范 | P1 | `r.name as ClassSubject` — 同上 | 同上 | +| 287 | TS规范 | P2 | `idByName.get(name)!` — 非空断言 | 用 `const id = idByName.get(name); if (!id) return null` 显式处理 | +| 297-299 | 代码质量 | P1 | `throw new Error("Failed to create class")` 后有 `return id` 不可达代码 | 删除 `return id` | +| 561 | TS规范 | P1 | `r.name as ClassSubject` — `as` 断言 | 用类型守卫替换 | +| 567 | TS规范 | P2 | `idByName.get(name)!` — 非空断言 | 显式判空处理 | +| 120-127 | 性能 | P2 | `getAccessibleClassIdsForTeacher` 中两个独立 DB 查询串行执行 | 用 `Promise.all` 并行化 | + +#### `src/modules/classes/data-access-admin.ts`(441 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 82-117, 237-239 | 代码质量 | P1 | try-catch 吞掉错误返回空数组或 fallback 查询 | 移除 try-catch 或记录错误日志后 rethrow | +| 135 | TS规范 | P1 | `r.subject as ClassSubject` — `as` 断言 | 用类型守卫替换 | +| 259 | TS规范 | P1 | `r.subject as ClassSubject` — 同上 | 同上 | +| 303 | TS规范 | P1 | `getManagedGrades` 缺少返回类型标注 | 添加 `: Promise` | +| 349 | TS规范 | P1 | `r.name as ClassSubject` — `as` 断言 | 用类型守卫替换 | +| 369 | TS规范 | P2 | `idByName.get(name)!` — 非空断言 | 显式判空 | +| 380-382 | 代码质量 | P1 | `throw new Error("Failed to create class")` 后有 `return id` 不可达代码 | 删除 `return id` | + +#### `src/modules/classes/data-access-schedule.ts`(230 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 7-11, 33-48 | 架构违规 | P1 | 直接导入并查询 `classSchedule` 表(该表归属 scheduling 模块) | 将读取也委托给 scheduling 模块的 data-access | +| 54 | TS规范 | P1 | `r.weekday as StudentScheduleItem["weekday"]` — `as` 断言 | 用类型守卫或 Zod 校验 weekday 范围 | +| 93 | TS规范 | P1 | `r.weekday as ClassScheduleItem["weekday"]` — 同上 | 同上 | + +#### `src/modules/classes/data-access-stats.ts`(604 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 11-18 | 架构违规 | **P0** | 直接导入并查询 `homeworkAssignmentQuestions`、`homeworkAssignmentTargets`、`homeworkAssignments`、`homeworkSubmissions`、`exams` 等 homework/exams 模块的 DB 表 | 通过 homework 模块的 data-access 暴露的函数获取数据 | +| 250 | TS规范 | P1 | `(s.status ?? "started") as string` — `as` 断言 | `s.status` 已是 string 类型,无需断言 | +| 261 | TS规范 | P1 | `(a.status as string) ?? "draft"` — `as` 断言 | 同上 | +| 512 | TS规范 | P1 | `(s.status ?? "started") as string` — 同上 | 同上 | +| 523 | TS规范 | P1 | `(a.status as string) ?? "draft"` — 同上 | 同上 | +| 130-151, 175-191 | 性能 | P2 | 多个早期返回中重复构造相同的返回对象结构 | 提取 `buildEmptyInsights(classRow, studentCounts)` 工厂函数 | + +#### `src/modules/classes/data-access-students.ts`(280 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 10-14 | 架构违规 | **P0** | 直接导入并查询 `homeworkAssignmentTargets`、`homeworkAssignments`、`homeworkSubmissions`、`exams` 等 homework/exams 模块的 DB 表 | 通过 homework 模块 data-access 获取数据 | +| 102 | TS规范 | P2 | `studentScores.get(s.studentId)!` — 非空断言 | 显式判空处理 | +| 168-204 | 代码质量 | P2 | `getStudentClasses` 中 try-catch fallback 查询使用 `sql\`NULL\`.as(...)` 模式,吞掉错误 | 移除 try-catch | + +#### `src/modules/classes/types.ts`(201 行) + +基本符合规范,无违规问题。 + +--- + +### 3.7 school(学校模块) + +#### `src/modules/school/actions.ts`(325 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 7-8, 26-30, 53-59, 73, 96-108, 133-147, 161, 182-186, 211-217, 233, 261-268, 294-303, 317 | 架构违规 | **P0** | actions 层直接导入 `db` 和所有 schema,所有 mutation 直接操作 DB,完全没有下沉到 data-access 层 | 在 `data-access.ts` 中新增 `createDepartment`、`updateDepartment`、`deleteDepartment`、`createSchool` 等函数 | +| 188, 219, 235 | 性能 | P2 | `await logAudit(...)` 阻塞响应,审计日志写入是非关键路径 | 使用 Next.js `after()` 函数将 `logAudit` 改为非阻塞执行 | + +#### `src/modules/school/data-access.ts`(186 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 13, 27, 44, 59, 112, 133 | 代码质量 | P2 | 所有函数都用 try-catch 包裹并返回空数组 `[]` 吞掉错误 | 移除 try-catch 或记录错误日志后 rethrow | + +#### `src/modules/school/schema.ts`(51 行)& `types.ts`(42 行) + +符合规范。 + +--- + +### 3.8 scheduling(排课模块) + +#### `src/modules/scheduling/actions.ts`(300 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 10-11, 112-116 | 架构违规 | **P0** | actions 层直接导入 `db` 和 `users` schema,在 `autoScheduleAction` 中直接查询 `users` 表获取教师信息 | 在 data-access.ts 中新增 `getTeachersByIds(teacherIds: string[])` 函数 | +| 115 | TS规范 | P1 | `teacherIds[0]!` — 非空断言 | 用条件表达式 `eq(users.id, teacherIds[0] ?? "")` | + +#### `src/modules/scheduling/auto-scheduler.ts`(310 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 144-145 | TS规范 | P1 | `schedule[i]!`、`schedule[j]!` — 非空断言 | 用局部变量 + 显式判空 | +| 全文件 | 行数 | P2 | 工具函数规范要求 ≤ 40 行,此文件 310 行 | 拆分为 `auto-scheduler/` 目录下的多个文件 | + +#### `src/modules/scheduling/data-access.ts`(368 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 239 | TS规范 | **P0** | `getAdminClassesForScheduling()` 缺少返回类型标注 | 添加返回类型 | +| 246 | TS规范 | **P0** | `getTeachersForScheduling()` 缺少返回类型标注 | 添加返回类型 | +| 255 | TS规范 | **P0** | `getClassroomsForScheduling()` 缺少返回类型标注 | 添加返回类型 | +| 262 | TS规范 | **P0** | `getClassSubjectsForScheduling(classId: string)` 缺少返回类型标注 | 添加返回类型 | +| 113-114 | 代码质量 | P2 | `requesterName: users.name` 与 `originalTeacherName: users.name` 选中同一列,`requesterName` 在 map 中被 `userMap.get()` 覆盖,select 中的字段是死代码 | 删除 select 中的 `requesterName: users.name` | +| 140 | TS规范 | P1 | `userIds[0]!` — 非空断言 | 显式判空 | +| 221-222 | TS规范 | P1 | `rows[i]!`、`rows[j]!` — 非空断言 | 用局部变量 + 显式判空 | + +#### `src/modules/scheduling/schema.ts`(81 行)& `types.ts`(124 行) + +符合规范。 + +--- + +### 3.9 attendance(考勤模块) + +#### `src/modules/attendance/actions.ts`(271 行) + +基本符合规范。所有 action 调用了 `requirePermission`、使用 Zod 验证、返回 `ActionState`、使用 `revalidatePath`。 + +#### `src/modules/attendance/data-access.ts`(271 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 195 | TS规范 | P1 | `const update: Record = { updatedAt: new Date() }` — 使用宽泛的 `Record` 丢失类型安全 | 使用 `Partial` 类型 | + +#### `src/modules/attendance/data-access-stats.ts`(145 行)& `schema.ts`(43 行)& `types.ts`(103 行) + +符合规范。 + +--- + +### 3.10 course-plans(教学计划模块) + +#### `src/modules/course-plans/actions.ts`(265 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 236-246 | Server Action规范 | P1 | `deleteCoursePlanItemAction` 未调用 `revalidatePath` | 添加 `revalidatePlanPaths()` 调用 | +| 248-264 | Server Action规范 | P1 | `toggleCoursePlanItemCompletedAction` 未调用 `revalidatePath` | 同上 | + +#### `src/modules/course-plans/data-access.ts`(320 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 149 | TS规范 | P1 | `params.status as CoursePlanStatus` — `as` 断言 | 用类型守卫或 Zod 校验 | +| 158, 183 | 代码质量 | P2 | `getCoursePlans` 和 `getCoursePlanById` 用 try-catch 返回空数组/null 吞掉错误 | 移除 try-catch 或记录日志后 rethrow | +| 300-305 | 性能 | P2 | `reorderCoursePlanItems` 中循环内串行 `await db.update()`,N 次 DB 往返 | 用 `db.transaction` 批量更新,或用 `Promise.all` 并行化 | + +#### `src/modules/course-plans/schema.ts`(148 行)& `types.ts`(60 行) + +符合规范。 + +--- + +### 3.11 users(用户模块) + +#### `src/modules/users/actions.ts`(151 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 29-51 | Server Action规范 | **P0** | `updateUserProfile` 使用 `requireAuth()` 而非 `requirePermission()` | 改为 `requirePermission(Permissions.USER_MANAGE)` 或新增 `USER_PROFILE_UPDATE` 权限点 | +| 29-51 | 架构违规 | P1 | `updateUserProfile` 在 actions 层直接执行 `db.update(users)` | 将写操作下沉到 `data-access.ts` | +| 29-51 | Server Action规范 | P1 | `updateUserProfile` 返回 `Promise` 而非 `ActionState` | 改为返回 `Promise>` | +| 29 | TS规范 | P2 | `updateUserProfile(data: UpdateUserProfileInput)` 缺少显式返回类型标注 | 标注为 `Promise>` | +| 29-51 | Server Action规范 | P2 | `updateUserProfile` 输入未使用 Zod 验证 | 新增 Zod schema | +| 76-125 | Server Action规范 | P2 | `importUsersAction` 输入验证使用手动逻辑,未使用 Zod | 将 `parseUserImportData` 改用 Zod 实现 | + +#### `src/modules/users/class-registration.ts`(27 行) + +合规,无违规问题。正确通过 `classes/data-access.enrollStudentByInvitationCode` 跨模块通信。 + +#### `src/modules/users/data-access.ts`(152 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 24 | 命名规范 | P2 | `const rolePriority` 常量未使用 UPPER_SNAKE_CASE | 重命名为 `ROLE_PRIORITY` | +| 26-31 | 架构违规 | P2 | `normalizeRoleName` 与 `shared/lib/role-utils.ts` 的 `normalizeRole` 重复 | 改为 `import { normalizeRole } from "@/shared/lib/role-utils"` 复用 | +| 26 | TS规范 | P2 | `normalizeRoleName` 缺少显式返回类型标注 | 标注为 `(value: string) => string` | +| 33 | TS规范 | P2 | `resolvePrimaryRole` 缺少显式返回类型标注 | 标注为 `(roleNames: string[]) => string` | + +#### `src/modules/users/import-export.ts`(176 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 98 | TS规范 | P2 | `const conditions = []` 隐式推断为 `any[]`,违反"禁止 any"规则 | 标注类型 `const conditions: ReturnType[] = []` | +| 100-114 | 性能 | P2 | `exportUsersToExcel` 中 role 查询 → userIds 查询 → users 查询为串行 | 可接受(有依赖关系),但建议合并为子查询减少往返 | + +#### `src/modules/users/user-service.ts`(99 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 13 | 安全规范 | **P0** | `const DEFAULT_PASSWORD = "123456"` 硬编码弱密码 | 改为随机生成密码或要求首次登录强制修改密码 | +| 15-19 | 架构违规 | P2 | `normalizeBcryptHash` 与 `shared/lib/bcrypt-utils.ts` 重复 | 改为 `import { normalizeBcryptHash } from "@/shared/lib/bcrypt-utils"` 复用 | +| 15 | TS规范 | P2 | `normalizeBcryptHash` 缺少显式返回类型标注 | 标注为 `(value: string) => string` | +| 30-92 | 架构违规 | P1 | `batchImportUsers` 注释标注"批量导入用户(事务)",但实际未使用 `db.transaction` 包裹 | 用 `await db.transaction(async (tx) => { ... })` 包裹 | +| 50-92 | 性能 | P2 | 循环内串行 `await db.insert`,N 条记录需 N 次 DB 往返 | 可批量插入用户和角色关联 | + +--- + +### 3.12 messaging(消息模块) + +#### `src/modules/messaging/actions.ts`(247 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 163 | Server Action规范 | P1 | `getNotificationsAction` 使用 `requireAuth()` 而非 `requirePermission()` | 改为 `requirePermission(Permissions.MESSAGE_READ)` | +| 177 | Server Action规范 | P1 | `markNotificationAsReadAction` 使用 `requireAuth()` 而非 `requirePermission()` | 同上 | +| 190 | Server Action规范 | P1 | `markAllNotificationsAsReadAction` 使用 `requireAuth()` 而非 `requirePermission()` | 同上 | +| 203 | Server Action规范 | P1 | `getNotificationPreferencesAction` 使用 `requireAuth()` 而非 `requirePermission()` | 同上 | +| 218 | Server Action规范 | P1 | `updateNotificationPreferencesAction` 使用 `requireAuth()` 而非 `requirePermission()` | 同上 | +| 213-247 | Server Action规范 | P2 | `updateNotificationPreferencesAction` 未使用 Zod 验证 | 新增 Zod schema 校验 8 个布尔字段 | +| 87, 101, 129 | Server Action规范 | P2 | `markMessageAsReadAction`/`deleteMessageAction`/`getMessageDetailAction` 参数 `messageId` 未使用 Zod 验证 | 新增 `z.string().min(1)` 校验 messageId | + +#### `src/modules/messaging/data-access.ts`(252 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 12-14 | 架构违规 | P1 | 导入 `classEnrollments, classes`,`getRecipients`(227-251 行)直接 JOIN 跨模块表 | 通过 `classes/data-access` 暴露 `getStudentsByClassIds(classIds)` 接口 | +| 94 | TS规范 | P2 | `const conds = []` 隐式 `any[]` | 标注 drizzle 条件类型 | +| 97 | TS规范 | P2 | `or(...)!` 使用非空断言 | 改为显式处理 null | +| 117 | TS规范 | P2 | `or(...)!` 使用非空断言 | 同上 | +| 163 | TS规范 | P2 | `or(...)!` 使用非空断言 | 同上 | +| 63 | TS规范 | P2 | `mapMessage` 缺少显式返回类型标注 | 标注返回类型 | +| 77 | TS规范 | P2 | `mapNotification` 缺少显式返回类型标注 | 标注返回类型 | +| 80 | TS规范 | P2 | `r.type as NotificationType` 使用 as 断言 | 使用类型守卫 | +| 74, 85 | TS规范 | P2 | `toIso(r.createdAt) as string` 使用 as 断言 | 处理 null 情况或修改 toIso 返回类型 | +| 192-203 | 架构违规 | **P0** | `createNotification` 写 `messageNotifications` 表,但 notifications 模块反向调用此函数,形成循环依赖 | 将 `messageNotifications` 表所有权移交 notifications 模块 | + +#### `src/modules/messaging/notification-preferences.ts`(166 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 14 | TS规范 | P2 | `toIso` 缺少显式返回类型标注 | 标注为 `(d: Date) => string` | +| 16 | TS规范 | P2 | `mapRow` 缺少显式返回类型标注 | 标注返回类型 | + +#### `src/modules/messaging/schema.ts`(18 行)& `types.ts`(108 行) + +合规,无违规问题。 + +--- + +### 3.13 notifications(通知模块) + +#### `src/modules/notifications/actions.ts`(119 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 14-17 | 架构违规 | P1 | actions 层直接导入 `db`、`classEnrollments`、`classes` | 将 DB 查询下沉到 `data-access.ts` | +| 83-96 | 架构违规 | P1 | `sendClassNotificationAction` 直接查询 `classes` 和 `classEnrollments` 表 | 通过 `classes/data-access` 暴露查询接口 | +| 30-52 | Server Action规范 | P2 | `sendNotificationAction` 参数 `payload` 未使用 Zod 验证 | 新增 `NotificationPayloadSchema` | +| 62-119 | Server Action规范 | P2 | `sendClassNotificationAction` 参数 `classId`/`payload` 未使用 Zod 验证 | 新增 Zod schema 校验 | + +#### `src/modules/notifications/data-access.ts`(86 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 20 | 架构违规 | **P0** | `import { getNotificationPreferences } from "@/modules/messaging/notification-preferences"` 反向依赖 messaging 模块 | 将 `notificationPreferences` 表所有权移交 notifications 模块 | +| 21 | 架构违规 | **P0** | `import type { NotificationPreferences } from "@/modules/messaging/types"` 反向依赖 messaging 模块 | 类型定义应迁移到 notifications 模块 | +| 71-77 | 架构违规 | P2 | `logNotificationSend` 仅 `console.info`,无 DB 持久化 | 新增 `notification_logs` 表并写入发送结果 | + +#### `src/modules/notifications/dispatcher.ts`(152 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 44 | TS规范 | P2 | `getSenders` 缺少显式返回类型标注 | 标注为 `() => SenderRegistry` | +| 59 | TS规范 | P2 | `selectChannels` 缺少显式返回类型标注 | 标注返回类型 | + +#### `src/modules/notifications/external-sdk.d.ts`(47 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 12, 20, 25, 33, 42, 43 | TS规范 | P2 | 多处使用 `any`(`const _default: any`、`config: any`、`options: any`) | 安装实际 SDK 后用其自带类型覆盖;或使用 `unknown` | + +#### `src/modules/notifications/index.ts`(38 行)& `types.ts`(70 行)& `channels/types.ts`(38 行) + +合规,无违规问题。 + +#### `src/modules/notifications/channels/email-channel.ts`(183 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 29 | TS规范 | P2 | `getEmailConfig` 缺少显式返回类型标注 | 标注返回类型 | +| 45 | TS规范 | P2 | `getTypeColor` 缺少显式返回类型标注 | 标注为 `(type: NotificationPayload["type"]) => string` | +| 60 | TS规范 | P2 | `buildHtmlContent` 缺少显式返回类型标注 | 标注为 `(payload: NotificationPayload) => string` | +| 79 | TS规范 | P2 | `escapeHtml` 缺少显式返回类型标注 | 标注为 `(text: string) => string` | + +#### `src/modules/notifications/channels/in-app-channel.ts`(88 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 50 | 架构违规 | **P0** | `await import("@/modules/messaging/data-access")` 动态 import messaging 模块,运行时形成循环 | 将 `messageNotifications` 表所有权移交 notifications | +| 54 | TS规范 | P1 | `payload.type as "message" \| "announcement" \| "homework" \| "grade"` 使用 as 断言,且源类型与目标类型不兼容,as 断言非法 | 重新设计类型映射函数 | + +#### `src/modules/notifications/channels/sms-channel.ts`(236 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 30 | TS规范 | P2 | `getSmsConfig` 缺少显式返回类型标注 | 标注返回类型 | +| 32 | TS规范 | P2 | `(process.env.SMS_PROVIDER ?? "mock") as "aliyun" \| "tencent" \| "mock"` 使用 as 断言 | 使用类型守卫或 Zod 校验环境变量 | +| 44 | TS规范 | P2 | `buildTemplateParams` 缺少显式返回类型标注 | 标注返回类型 | + +#### `src/modules/notifications/channels/wechat-channel.ts`(208 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 40 | TS规范 | P2 | `getWechatConfig` 缺少显式返回类型标注 | 标注返回类型 | +| 99 | TS规范 | P2 | `buildTemplateData` 缺少显式返回类型标注 | 标注返回类型 | + +--- + +### 3.14 parent(家长模块) + +#### `src/modules/parent/data-access.ts`(234 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 59-105 | 架构违规 | P1 | `getChildBasicInfo` 直接查询 `users`、`grades`、`classEnrollments`、`classes` 表 | 通过 `users/data-access.getUserProfile`、`classes/data-access` 等接口查询 | +| 58 | TS规范 | P2 | `getChildBasicInfo` 缺少显式返回类型标注 | 标注返回类型 | +| 30 | TS规范 | P2 | `(day === 0 ? 7 : day) as 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` 使用 as 断言 | 使用类型守卫 `function isWeekday(n: number): n is 1\|2\|3\|4\|5\|6\|7` | +| 58-117 | 性能 | P2 | `getChildBasicInfo` 串行查询 student → grade → enrollment → class | 使用 JOIN 合并为单次查询,或用 Promise.all 并行化 | + +#### `src/modules/parent/types.ts`(57 行) + +合规,无违规问题。 + +--- + +### 3.15 audit(审计模块) + +#### `src/modules/audit/actions.ts`(212 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 6, 70-100, 120-146, 166-192 | 架构违规 | P2 | Excel 导出逻辑内联在 actions 层 | 新建 `audit/export.ts`,将 Excel 构建逻辑迁移 | +| 63-205 | 架构违规 | P2 | 三个导出 Action 结构高度重复 | 抽取通用 `buildExcelSheet(name, columns, rows)` 辅助函数 | +| 207-212 | 架构违规 | P2 | `formatDateForFile` 工具函数定义在 actions.ts 中 | 迁移到 `shared/lib/utils.ts` 或 `audit/utils.ts` | +| 24-61 | Server Action规范 | P2 | `getDataChangeLogsAction` 参数 `params` 未使用 Zod 验证 | 新增 `DataChangeLogQueryParamsSchema` | +| 63-205 | Server Action规范 | P2 | 三个导出 Action 参数未使用 Zod 验证 | 新增对应 Zod schema | + +#### `src/modules/audit/data-access.ts`(260 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 18 | TS规范 | P2 | `toIso` 缺少显式返回类型标注 | 标注为 `(d: Date) => string` | +| 23 | TS规范 | P2 | `clampPageSize` 缺少显式返回类型标注 | 标注为 `(size?: number) => number` | +| 28 | TS规范 | P2 | `clampPage` 缺少显式返回类型标注 | 标注为 `(page?: number) => number` | +| 40, 95, 158 | TS规范 | P2 | `const conditions = []` 隐式 `any[]` | 标注 drizzle 条件类型 | +| 75 | TS规范 | P2 | `r.status as "success" \| "failure"` 使用 as 断言 | 使用类型守卫 | +| 122 | TS规范 | P2 | `r.action as "signin" \| "signout" \| "signup"` 使用 as 断言 | 使用类型守卫 | +| 123 | TS规范 | P2 | `r.status as "success" \| "failure"` 使用 as 断言 | 使用类型守卫 | +| 186 | TS规范 | P2 | `r.action as "create" \| "update" \| "delete"` 使用 as 断言 | 使用类型守卫 | +| 50-86, 104-137, 168-202 | 安全/质量 | P2 | `getAuditLogs`/`getLoginLogs`/`getDataChangeLogs` 使用 try-catch 吞错误返回空数组 | 至少 `console.error` 记录错误 | +| 235-240 | 逻辑错误 | P1 | `getAuditLogsForExport` 注释标注 "no pagination cap",但实际调用 `getAuditLogs({ page: 1, pageSize: 100 })`,最多只返回 100 条,导出数据不完整 | 实现真正的无分页查询,或循环分页拉取全部数据 | +| 245-250 | 逻辑错误 | P1 | `getLoginLogsForExport` 同上问题,最多 100 条 | 同上 | +| 255-260 | 逻辑错误 | P1 | `getDataChangeLogsForExport` 同上问题,最多 100 条 | 同上 | + +#### `src/modules/audit/types.ts`(91 行) + +合规,无违规问题。 + +--- + +### 3.16 elective(选修课模块) + +#### `src/modules/elective/data-access.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 117 | TS规范 | P1 | `params.status as ElectiveCourseStatus` 使用 `as` 断言 | 直接使用 `params.status`,无需断言 | +| 78 | TS规范 | P2 | `buildCourseSelect = () =>` 箭头函数缺少显式返回类型标注 | 添加返回类型标注 | +| 136-138, 150-152 | 代码质量 | P2 | `catch { return [] / null }` 静默吞掉异常 | 至少 `console.error` 记录错误 | + +#### `src/modules/elective/data-access-selections.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 102 | TS规范 | P1 | `r.status as CourseSelectionStatus` 使用 `as` 断言 | 应通过类型守卫或直接赋值 | +| 114 | TS规范 | P1 | `r.courseStatus as ElectiveCourseStatus \| null` 同上 | 同上 | +| 59, 117 | TS规范 | P2 | `buildCourseSelect` 和 `selectionDetailSelect` 箭头函数缺少显式返回类型 | 添加返回类型标注 | +| 7-14 | 架构违规 | P1 | 直接导入并查询 `classes`、`classEnrollments` 表 | 通过 `@/modules/classes/data-access` 暴露的函数获取 | +| 全文件 | 性能 | P2 | 均未用 `React.cache()` 包裹 | 对读函数用 `cache()` 包裹 | + +#### `src/modules/elective/data-access-operations.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 18-34 | 性能 | P2 | `runLottery` 中先查 course 再查 selections,两次独立查询串行 `await` | 用 `Promise.all` 并行 | +| 46-71 | 性能 | P1 | `for` 循环内逐条 `await db.update()`,N 名学生产生 N 次 DB 往返 | 拆分为两组 ID,用 `inArray` 执行 2 次批量 UPDATE | +| 46-76 | 安全/数据完整性 | P1 | `runLottery` 多步更新未包裹事务 | 使用 `db.transaction()` 包裹整个摇号流程 | +| 86-112 | 性能 | P2 | `selectCourse` 中查 course 和查 existing selection 两次独立查询串行 | 用 `Promise.all` 并行 | +| 158-182 | 性能 | P2 | `dropCourse` 中查 existing 和查 course 串行 | 并行化独立查询 | + +#### `src/modules/elective/schema.ts`(20-24 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 20-24 | TS规范 | P2 | `emptyToNull` 和 `optionalStringToNull` 箭头函数缺少显式返回类型 | 添加返回类型 | + +#### `src/modules/elective/types.ts` + +未发现违规问题。 + +#### `src/modules/elective/actions.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 278-294 | Server Action规范 | P2 | `getStudentSelectionsAction(studentId: string)` 直接接收参数,未经 Zod 校验 | 对 `studentId` 用 Zod 校验 | + +--- + +### 3.17 proctoring(监考模块) + +#### `src/modules/proctoring/actions.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 11-13, 79-84 | 架构违规 | **P0** | actions.ts(编排层)直接导入 `db`、`examSubmissions` 并执行 DB 查询 | 将 submission 归属校验逻辑移至 `data-access.ts` | +| 3 | TS规范 | P1 | `import { ActionState }` 应为 `import type { ActionState }` | 改为 `import type { ActionState }` | +| 39 | TS规范 | P1 | `as z.ZodType` 使用 `as` 断言 | 直接使用 `z.enum([...])` 推导 | +| 63 | Server Action规范 | P1 | `recordProctoringEventAction` 使用 `requireAuth()` 而非 `requirePermission()` | 使用 `requirePermission(Permissions.EXAM_SUBMIT)` | +| 全文件 | Server Action规范 | P2 | `recordProctoringEventAction` 未调用 `revalidatePath` | 添加 `revalidatePath` 刷新监考面板相关路径 | + +#### `src/modules/proctoring/data-access.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 127 | TS规范 | P1 | `row.event.eventType as ProctoringEventType` 使用 `as` 断言 | drizzle enum 列类型已是字面量联合,应直接匹配 | +| 152 | TS规范 | P1 | `row.eventType as ProctoringEventType` 同上 | 同上 | +| 208 | TS规范 | P1 | `stat.eventType as ProctoringEventType` 同上 | 同上 | +| 290 | TS规范 | P1 | `row.eventType as ProctoringEventType` 同上 | 同上 | +| 313 | TS规范 | P1 | `(row.submission.status ?? null) as StudentProctoringStatus["submissionStatus"]` 使用 `as` 断言 | 用类型守卫函数校验 status 值 | +| 378 | TS规范 | P1 | `row.event.eventType as ProctoringEventType` 同上 | 同上 | +| 5-9 | 架构违规 | P1 | 直接导入 `exams`、`examSubmissions` 表 | 通过 `@/modules/exams/data-access` 暴露的函数获取 | +| 188-193 | 性能 | P2 | `getExamProctoringSummary` 中对 `submissions` 数组调用两次 `.filter()` | 用单次 `for` 循环同时统计两个计数 | +| 178-223 | 性能 | P2 | `getExamProctoringSummary` 中 submissions 查询、eventStats 查询、studentEventCounts 查询三者相互独立,却串行 `await` | 用 `Promise.all` 并行执行 | + +#### `src/modules/proctoring/types.ts` + +未发现违规问题。 + +--- + +### 3.18 diagnostic(诊断模块) + +#### `src/modules/diagnostic/actions.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 26-33, 54-61, 82-85, 105-108 | Server Action规范 | P1 | `generateStudentReportAction`、`generateClassReportAction`、`publishReportAction`、`deleteReportAction` 均使用手动 `typeof` + `length` 校验,未使用 Zod schema | 新建 `schema.ts`,定义 Zod schema | +| 121-133, 136-148 | Server Action规范 | P2 | `getDiagnosticReportsAction(params)` 和 `getDiagnosticReportByIdAction(id)` 直接接收参数,未经 Zod 校验 | 对入参用 Zod 校验 | +| 123, 138 | TS规范 | P2 | `Promise>>>` 类型表达式过于复杂 | 在 types.ts 中定义具名返回类型并导入 | + +#### `src/modules/diagnostic/data-access.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 9, 13 | 架构违规 | P1 | 直接导入 `examSubmissions`、`submissionAnswers`、`questionsToKnowledgePoints` 表 | 通过对应模块的 data-access 函数获取数据 | +| 116 | 性能 | P2 | `answers.find((a) => a.questionId === link.questionId)` 在 `for` 循环内调用,O(n×m) 复杂度 | 先用 `new Map(answers.map(a => [a.questionId, a]))` 构建 Map | +| 125-146 | 性能 | P2 | `for` 循环内逐条 `await db.insert().onDuplicateKeyUpdate()`,N 个知识点产生 N 次 DB 往返 | 用 `Promise.all` 并行化,或构建批量 upsert | +| 80-81 | 性能 | P2 | `allMastery.filter(m => m.masteryLevel >= 80)` 和 `allMastery.filter(m => m.masteryLevel < 60)` 两次遍历同一数组 | 合并为单次循环 | +| 全文件 | 性能 | P2 | 所有读函数均未用 `React.cache()` 包裹 | 用 `cache()` 包裹读函数 | + +#### `src/modules/diagnostic/data-access-reports.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 29-31 | TS规范 | P1 | `(r.strengths as string[] \| null)`、`(r.weaknesses as string[] \| null)`、`(r.recommendations as string[] \| null)` 三处 `as` 断言 | 编写类型守卫 `isStringArray(v: unknown): v is string[]` | +| 59, 103 | 性能/规范 | P2 | `const { createId } = await import("@paralleldrive/cuid2")` 在函数内部动态导入 | 改为顶层静态导入 | +| 201-202 | 代码质量 | P2 | `void round2` — 为抑制未使用警告而保留无用函数 | 直接删除 `round2` 函数定义及 `void round2` | +| 172-180 | 性能 | P2 | `getDiagnosticReportById` 中先查 report,再单独查 generator 名称,两次串行查询 | 将 generator 查询合并到主查询的 JOIN 中 | +| 全文件 | 性能 | P2 | `getDiagnosticReports`、`getDiagnosticReportById` 未用 `React.cache()` 包裹 | 用 `cache()` 包裹 | +| 107 | 代码质量 | P2 | `studentId: generatedBy` 班级报告将生成者 ID 存入 studentId 字段 | 考虑将 studentId 改为可空,或使用单独的 classId 字段 | + +#### `src/modules/diagnostic/types.ts` + +未发现违规问题。 + +--- + +### 3.19 dashboard(仪表盘模块) + +#### `src/modules/dashboard/data-access.ts` & `types.ts` + +**无违规问题**。正确使用 `Promise.all` 并行获取多模块数据,正确使用 `cache()`,正确通过各模块 data-access 通信。是除 textbooks 外的另一个标杆模块。 + +--- + +### 3.20 files(文件模块) + +#### `src/modules/files/data-access.ts` + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 190 | TS规范 | P1 | `const conditions = []` 缺少类型标注,TypeScript 推断为 `never[]` 或 `any[]` | 改为 `const conditions: SQL[] = []` | +| 204 | TS规范 | P1 | `or(...)!` 使用非空断言 | 显式处理 undefined | +| 160-170 | 性能 | P2 | `deleteFileAttachments` 回退逻辑中 `for` 循环内逐条 `await db.delete()` | 用 `Promise.allSettled` 并行删除 | +| 53-55, 70-72, 95-97, 114-116, 131-133, 143-145, 219-221, 248-250, 264-266 | 代码质量 | P2 | 大量 `catch { return null / [] / false }` 静默吞掉异常,共 9 处 | 至少 `console.error` 记录错误信息 | +| 全文件 | 性能 | P2 | 所有读函数均未用 `React.cache()` 包裹 | 用 `cache()` 包裹读函数 | + +#### `src/modules/files/types.ts` + +未发现违规问题。 + +--- + +### 3.21 announcements(公告模块) + +#### `src/modules/announcements/data-access.ts`(186 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 46-47 | TS规范 | P1 | `toIso(row.createdAt) as string` — `as` 断言掩盖潜在 null | 让 `toIso` 对非空入参返回 `string`,拆分为两个函数 | +| 59, 62 | TS规范 | P2 | `params.status as AnnouncementStatus` — 冗余 `as` 断言 | 直接使用收窄后的变量 | +| 88-90, 118-120 | 性能/可靠性 | P2 | `catch {}` 静默吞掉所有异常 | 至少记录错误日志 | +| 25-26 | 命名/DRY | P2 | `mapRow` 参数中内联定义类型,与 types.ts 重复 | 导入并复用 types.ts 中的类型 | + +#### `src/modules/announcements/actions.ts`(231 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 217-231 | Server Action规范 | P2 | `getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission(Permissions.ANNOUNCEMENT_READ)` | 改为 `requirePermission(Permissions.ANNOUNCEMENT_READ)` | +| 73-79, 137-143, 159-165, 185-191, 208-214, 224-230 | 性能/可维护性 | P2 | 6 个 Action 的 try/catch 错误处理块完全相同 | 提取共享错误处理函数 `handleActionError(e)` | + +#### `src/modules/announcements/schema.ts`(45 行)& `types.ts`(50 行) + +符合规范。 + +--- + +### 3.22 settings(设置模块) + +#### `src/modules/settings/actions.ts`(205 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 8, 64-76, 95-100, 104-113, 123-139, 154-169 | 架构违规 | P1 | 模块无 `data-access.ts` 文件,actions.ts 直接 `import { db }` 并执行 DB 查询 | 新建 `data-access.ts`,将所有 DB 操作下沉 | +| 62-77 | Server Action规范 | P2 | `getAiProviderSummaries` 返回 `Promise` 而非 `ActionState` | 移至 data-access.ts 或包装为 ActionState | +| 38-46 | 架构违规 | P2 | `AiProviderSummary` 类型定义在 actions.ts 中 | 新建 `types.ts`,将类型移入 | +| 48-51 | TS规范 | P2 | `ensureUser` 是 async 箭头函数,未显式标注返回类型 | 添加 `: Promise<{ id: string }>` | +| 53-60 | TS规范 | P2 | `normalizeBaseUrl` 未显式标注返回类型 | 添加 `: string \| null` | +| 95-113 | 性能 | P2 | `upsertAiProviderAction` 的更新分支中,`count()` 查询与 `select` 查询是两个独立查询,串行执行 | 用 `Promise.all` 并行化 | +| 120, 152 | 命名规范 | P2 | `nextIsDefault`、`makeDefault` 布尔变量前缀不规范 | 改为 `isNextDefault`、`shouldMakeDefault` | + +#### `src/modules/settings/actions-password.ts`(113 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 7, 58-62, 78-81, 83-87, 89-105 | 架构违规 | P1 | actions-password.ts 直接 `import { db }` 并执行 DB 操作,无 data-access.ts | 将 DB 操作下沉至 `data-access.ts` | +| 39-51 | Server Action规范 | P1 | 输入通过 `String(formData.get(...))` 手动读取,未使用 Zod schema 验证 | 定义 Zod schema 并 safeParse | +| 30 | Server Action规范 | P2 | `changePasswordAction` 使用 `requireAuth()` 而非 `requirePermission()` | 新增 `PASSWORD_SELF_CHANGE` 权限点并赋给所有角色 | +| 58-87 | 性能 | P2 | 查询 user 和查询 passwordSecurity 是两个独立表的查询,当前串行执行 | 重构为 `Promise.all` 并行获取 | +| 14-18 | TS规范 | P2 | `normalizeBcryptHash` 未显式标注返回类型 | 添加 `: string` | + +--- + +### 3.23 layout(布局模块) + +#### `src/modules/layout/config/navigation.ts`(317 行) + +| 行号范围 | 问题类别 | 严重程度 | 问题描述 | 改进建议 | +|---------|---------|---------|---------|---------| +| 30-31 | TS规范 | P2 | `NavItem.permission` 和子项的 `permission` 均为 `string` 类型,但项目已有 `Permission` 类型 | 导入 Permission 类型并使用 | +| 34 | 架构违规 | P2 | `export type Role = "admin" \| "teacher" \| "student" \| "parent"` 定义在 config 文件中 | 考虑将 Role 类型提升至 `@/shared/types` | + +--- + +## 四、跨模块共性问题 + +### 4.1 架构违规(P0/P1,最高优先级) + +#### 4.1.1 跨模块直接查询 DB 表(最普遍问题) + +| 调用方模块 | 被查模块表 | 严重程度 | +|-----------|-----------|---------| +| exams | questions(直写)、classes、subjects、grades | P0/P1 | +| homework | exams(5 处)、classes、classEnrollments、subjects、users | P1 | +| questions | knowledgePoints、chapters、textbooks | P1 | +| grades | classes、classEnrollments、subjects、users | P1 | +| classes | homeworkAssignments、homeworkSubmissions、exams、classSchedule | P0 | +| school | (actions 层直接 DB 操作) | P0 | +| scheduling | users(actions 层) | P0 | +| messaging | classEnrollments、classes | P1 | +| notifications | classes、classEnrollments、messaging(循环) | P0/P1 | +| parent | users、grades、classEnrollments、classes | P1 | +| proctoring | exams、examSubmissions | P0/P1 | +| diagnostic | examSubmissions、submissionAnswers、questionsToKnowledgePoints | P1 | +| elective | classes、classEnrollments | P1 | + +**统一修复方案**:在各模块 data-access 暴露跨模块查询接口,消除直查。 + +#### 4.1.2 messaging↔notifications 循环依赖 + +- `messaging/data-access.ts` 写 `messageNotifications` 表 +- `notifications/data-access.ts` 反向 import messaging 的 `notification-preferences` 和 `types` +- `notifications/channels/in-app-channel.ts` 动态 import messaging 的 `data-access` + +**修复方案**:将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块。 + +#### 4.1.3 actions 层直接操作 DB + +| 模块 | 文件 | 问题 | +|------|------|------| +| school | actions.ts | 所有 mutation 直接操作 DB | +| scheduling | actions.ts | 直接查询 users 表 | +| proctoring | actions.ts | 直接查询 examSubmissions 表 | +| classes | actions.ts | 直接查询 grades、classes 表做权限校验 | +| settings | actions.ts、actions-password.ts | 无 data-access.ts,直接 DB 操作 | +| users | actions.ts | updateUserProfile 直接 db.update | + +**统一修复方案**:DB 操作必须下沉到 data-access.ts。 + +### 4.2 TypeScript 规范违规(P1) + +#### 4.2.1 `as` 断言滥用(非从 unknown 转换) + +| 模块 | 文件 | 行号 | +|------|------|------| +| exams | data-access.ts | 76, 160, 361, 108, 172 | +| exams | actions.ts | 270, 349-351 | +| homework | data-access.ts | 39, 124, 226, 314, 392, 467, 645 | +| homework | stats-service.ts | 220 | +| textbooks | actions.ts | 54-58, 92-98, 154, 219-221, 259-261(14 处) | +| textbooks | data-access.ts | 51 | +| classes | data-access.ts | 240, 266, 561 | +| classes | data-access-admin.ts | 135, 259, 349 | +| classes | data-access-schedule.ts | 54, 93 | +| classes | data-access-stats.ts | 250, 261, 512, 523 | +| classes | actions.ts | 538, 582 | +| course-plans | data-access.ts | 149 | +| messaging | data-access.ts | 80, 74, 85 | +| notifications | channels/in-app-channel.ts | 54(非法) | +| notifications | channels/sms-channel.ts | 32 | +| audit | data-access.ts | 75, 122, 123, 186 | +| elective | data-access.ts | 117 | +| elective | data-access-selections.ts | 102, 114 | +| proctoring | data-access.ts | 127, 152, 208, 290, 313, 378 | +| proctoring | actions.ts | 39 | +| diagnostic | data-access-reports.ts | 29-31 | +| parent | data-access.ts | 30 | +| announcements | data-access.ts | 46-47, 59, 62 | +| attendance | data-access.ts | 195(Record) | + +**统一修复方案**:用类型守卫函数或 Zod 校验替换 `as` 断言。 + +#### 4.2.2 非空断言 `!` + +| 模块 | 文件 | 行号 | +|------|------|------| +| exams | ai-pipeline.ts | 464, 657, 665, 666, 671 | +| homework | export.ts | 154 | +| textbooks | data-access.ts | 42, 71 | +| classes | data-access.ts | 287, 567 | +| classes | data-access-admin.ts | 369 | +| classes | data-access-students.ts | 102 | +| scheduling | data-access.ts | 140, 221, 222 | +| scheduling | actions.ts | 115 | +| scheduling | auto-scheduler.ts | 144, 145 | +| messaging | data-access.ts | 97, 117, 163 | +| files | data-access.ts | 204 | + +**统一修复方案**:用显式判空处理替换非空断言。 + +#### 4.2.3 函数返回值未显式标注 + +| 模块 | 文件 | 函数 | +|------|------|------| +| classes | data-access.ts | isDuplicateInvitationCodeError, generateInvitationCode, normalizeSortText, parseFirstInt, compareGradeLabel, compareClassLike | +| classes | data-access-admin.ts | getManagedGrades | +| scheduling | data-access.ts | getAdminClassesForScheduling, getTeachersForScheduling, getClassroomsForScheduling, getClassSubjectsForScheduling | +| users | data-access.ts | normalizeRoleName, resolvePrimaryRole | +| users | user-service.ts | normalizeBcryptHash | +| messaging | data-access.ts | mapMessage, mapNotification | +| messaging | notification-preferences.ts | toIso, mapRow | +| notifications | dispatcher.ts | getSenders, selectChannels | +| notifications | channels/email-channel.ts | getEmailConfig, getTypeColor, buildHtmlContent, escapeHtml | +| notifications | channels/sms-channel.ts | getSmsConfig, buildTemplateParams | +| notifications | channels/wechat-channel.ts | getWechatConfig, buildTemplateData | +| audit | data-access.ts | toIso, clampPageSize, clampPage | +| parent | data-access.ts | getChildBasicInfo | +| announcements | — | — | +| settings | actions.ts | ensureUser, normalizeBaseUrl | +| settings | actions-password.ts | normalizeBcryptHash | +| elective | data-access.ts | buildCourseSelect | +| elective | data-access-selections.ts | buildCourseSelect, selectionDetailSelect | +| elective | schema.ts | emptyToNull, optionalStringToNull | +| questions | actions.ts | getQuestionsAction | + +**统一修复方案**:所有函数(特别是箭头函数)必须显式标注返回类型。 + +#### 4.2.4 `import type` 未使用 + +| 模块 | 文件 | 行号 | +|------|------|------| +| exams | actions.ts | 4 | +| questions | actions.ts | 7 | +| textbooks | actions.ts | 18 | +| proctoring | actions.ts | 3 | + +**统一修复方案**:仅用于类型的导入必须使用 `import type`。 + +### 4.3 Server Action 规范违规 + +#### 4.3.1 `requireAuth()` 替代 `requirePermission()` + +| 模块 | 文件 | Action | +|------|------|--------| +| users | actions.ts | updateUserProfile | +| messaging | actions.ts | getNotificationsAction, markNotificationAsReadAction, markAllNotificationsAsReadAction, getNotificationPreferencesAction, updateNotificationPreferencesAction | +| proctoring | actions.ts | recordProctoringEventAction | +| announcements | actions.ts | getAnnouncementsAction | +| settings | actions-password.ts | changePasswordAction | + +**统一修复方案**:改为 `requirePermission(Permissions.XXX)`。 + +#### 4.3.2 缺少 Zod 验证 + +| 模块 | 文件 | 问题 | +|------|------|------| +| textbooks | actions.ts | 完全无 schema.ts,所有 Action 无 Zod 验证 | +| classes | actions.ts | 完全无 schema.ts,使用手动 typeof 检查 | +| diagnostic | actions.ts | 4 个 Action 使用手动 typeof 校验 | +| settings | actions-password.ts | 输入手动读取,无 Zod | +| messaging | actions.ts | 多个 Action 参数未 Zod 校验 | +| notifications | actions.ts | 多个 Action 参数未 Zod 校验 | +| audit | actions.ts | 多个 Action 参数未 Zod 校验 | +| users | actions.ts | updateUserProfile 无 Zod | +| elective | actions.ts | getStudentSelectionsAction 无 Zod | + +**统一修复方案**:新建/补充 schema.ts,所有 Action 输入用 Zod 验证。 + +#### 4.3.3 缺少 `revalidatePath` + +| 模块 | 文件 | Action | +|------|------|--------| +| course-plans | actions.ts | deleteCoursePlanItemAction, toggleCoursePlanItemCompletedAction | +| proctoring | actions.ts | recordProctoringEventAction | + +#### 4.3.4 未返回 `ActionState` + +| 模块 | 文件 | Action | +|------|------|--------| +| questions | actions.ts | getQuestionsAction, getKnowledgePointOptionsAction | +| users | actions.ts | updateUserProfile(返回 Promise) | +| settings | actions.ts | getAiProviderSummaries(返回原始数组) | + +### 4.4 重复代码 + +| 重复内容 | 出现位置 | 修复方案 | +|---------|---------|---------| +| `normalizeRoleName` | users/data-access.ts、shared/lib/role-utils.ts | 复用 shared | +| `normalizeBcryptHash` | users/user-service.ts、settings/actions-password.ts、shared/lib/bcrypt-utils.ts | 复用 shared | +| `toNumber`/`normalize`/`buildScopeClassFilter` | grades/data-access.ts、data-access-analytics.ts、data-access-ranking.ts | 提取到 grades/utils.ts | +| `serializeDate` | scheduling/data-access.ts、attendance/data-access.ts、attendance/data-access-stats.ts | 提取到 shared | +| `toIso` | announcements/data-access.ts、messaging/data-access.ts、messaging/notification-preferences.ts、audit/data-access.ts | 提取到 shared | +| try/catch 错误处理块 | announcements/actions.ts(6 处) | 提取 `handleActionError(e)` | +| Excel 导出逻辑 | audit/actions.ts(3 处) | 抽取 `buildExcelSheet` | + +### 4.5 错误吞没(静默 catch) + +| 模块 | 文件 | 处数 | +|------|------|------| +| school | data-access.ts | 6 | +| classes | data-access-admin.ts、data-access-students.ts | 多处 | +| course-plans | data-access.ts | 2 | +| announcements | data-access.ts | 2 | +| files | data-access.ts | 9 | +| audit | data-access.ts | 3 | +| elective | data-access.ts | 2 | + +**统一修复方案**:至少 `console.error` 记录错误,或向上抛出由调用方处理。 + +### 4.6 不可达代码 + +| 模块 | 文件 | 行号 | +|------|------|------| +| classes | data-access.ts | 297-299(throw 后 return id) | +| classes | data-access-admin.ts | 380-382(throw 后 return id) | +| questions | actions.ts | 158-163, 170-175(catch 块两分支都 throw e) | + +--- + +## 五、性能优化专项(react-best-practices) + +依据 Vercel React Best Practices 规则集,识别以下性能优化机会: + +### 5.1 CRITICAL:消除瀑布流(async-*) + +#### 5.1.1 `async-parallel`:独立操作用 Promise.all() + +| 模块 | 文件 | 行号 | 问题 | 修复 | +|------|------|------|------|------| +| classes | data-access.ts | 120-127 | `getAccessibleClassIdsForTeacher` 两个独立 DB 查询串行 | `Promise.all` | +| settings | actions.ts | 95-113 | `upsertAiProviderAction` count 与 select 串行 | `Promise.all` | +| settings | actions-password.ts | 58-87 | user 与 passwordSecurity 查询串行 | `Promise.all` | +| elective | data-access-operations.ts | 18-34 | `runLottery` course 与 selections 串行 | `Promise.all` | +| elective | data-access-operations.ts | 86-112 | `selectCourse` course 与 existing 串行 | `Promise.all` | +| elective | data-access-operations.ts | 158-182 | `dropCourse` existing 与 course 串行 | `Promise.all` | +| proctoring | data-access.ts | 178-223 | `getExamProctoringSummary` 三个独立查询串行 | `Promise.all` | +| diagnostic | data-access-reports.ts | 172-180 | `getDiagnosticReportById` report 与 generator 串行 | 合并 JOIN 或 `Promise.all` | +| parent | data-access.ts | 58-117 | `getChildBasicInfo` 4 个串行查询 | JOIN 或 `Promise.all` | + +#### 5.1.2 `async-defer-await`:将 await 移到实际使用的分支 + +无明显违规,多数文件已正确处理。 + +### 5.2 HIGH:服务端性能(server-*) + +#### 5.2.1 `server-cache-react`:用 React.cache() 做请求内去重 + +| 模块 | 文件 | 缺失 cache() 的函数 | +|------|------|---------------------| +| grades | data-access.ts | getGradeRecords, getGradeRecordById, getClassGradeStats | +| grades | data-access-analytics.ts | getClassComparison, getGradeTrends | +| grades | data-access-ranking.ts | getClassRanking, getStudentRanking | +| elective | data-access-selections.ts | getCourseSelections, getStudentSelections, getStudentGradeId, getAvailableCoursesForStudent | +| diagnostic | data-access.ts | getStudentMastery, getStudentMasterySummary, getClassMasterySummary, getKnowledgePointStats | +| diagnostic | data-access-reports.ts | getDiagnosticReports, getDiagnosticReportById | +| files | data-access.ts | getFileAttachment, getFileAttachmentsByTarget, getFileAttachmentsByUploader, getAllFileAttachments, getFileAttachmentsWithFilters, getFileStats, getFileAttachmentsByIds | +| proctoring | data-access.ts | 多个查询函数 | + +**标杆**:exams、homework、questions、textbooks、dashboard、announcements 已正确使用 `cache()` ✓。 + +#### 5.2.2 `server-after-nonblocking`:用 after() 做非阻塞操作 + +| 模块 | 文件 | 行号 | 问题 | +|------|------|------|------| +| school | actions.ts | 188, 219, 235 | `await logAudit(...)` 阻塞响应 | + +**修复**:使用 Next.js `after()` 将审计日志改为非阻塞。 + +#### 5.2.3 `server-serialization`:最小化传给客户端组件的数据 + +需结合前端组件审查,本次后端核查未深入。 + +### 5.3 MEDIUM:JavaScript 性能(js-*) + +#### 5.3.1 `js-set-map-lookups`:用 Set/Map 做 O(1) 查找 + +| 模块 | 文件 | 行号 | 问题 | +|------|------|------|------| +| grades | data-access-analytics.ts | 133-136 | `scope.classIds.includes(c.id)` — O(n) 查找 | +| diagnostic | data-access.ts | 116 | `answers.find(...)` 在 for 循环内 — O(n×m) | + +**修复**:将数组转为 Set/Map 后用 `.has()` / `.get()`。 + +#### 5.3.2 `js-combine-iterations`:合并多次 filter/map 为一次循环 + +| 模块 | 文件 | 行号 | 问题 | +|------|------|------|------| +| grades | data-access.ts | 248-252 | 两次 if 判断遍历同一数组 | +| grades | data-access-analytics.ts | 180-181, 238-239 | 两次 filter 遍历同一数组 | +| grades | data-access-ranking.ts | 100, 102 | findIndex 和 find 查找同一元素 | +| diagnostic | data-access.ts | 80-81 | 两次 filter 遍历同一数组 | +| proctoring | data-access.ts | 188-193 | 两次 filter 统计 submissions | + +#### 5.3.3 `js-early-exit`:函数早返回 + +多数文件已正确处理。 + +#### 5.3.4 动态 import 改静态 import + +| 模块 | 文件 | 行号 | 问题 | +|------|------|------|------| +| grades | data-access.ts | 148, 172 | `await import("@paralleldrive/cuid2")` 函数内动态导入 | +| diagnostic | data-access-reports.ts | 59, 103 | 同上 | + +**修复**:改为文件顶部静态 `import { createId } from "@paralleldrive/cuid2"`。 + +### 5.4 性能问题汇总(按影响排序) + +| 优先级 | 问题 | 影响范围 | +|--------|------|---------| +| **CRITICAL** | grades N+1 查询(getClassComparison) | 大班级成绩对比性能差 | +| **HIGH** | 循环内串行 await db.update/insert(homework/data-access-write.ts、elective/data-access-operations.ts、diagnostic/data-access.ts、course-plans/data-access.ts) | 批量操作慢 | +| **HIGH** | 大量读函数未用 React.cache()(grades、elective、diagnostic、files、proctoring) | 请求内重复查询 | +| **MEDIUM** | 独立查询未并行化(9 处) | 响应时间累加 | +| **MEDIUM** | 动态 import(3 处) | 每次调用有开销 | +| **LOW** | 重复 filter 遍历(5 处) | 微小性能损失 | +| **LOW** | O(n) 查找代替 O(1)(2 处) | 微小性能损失 | + +--- + +## 六、优先修复路线图 + +### 第一阶段:P0 严重问题(立即修复) + +1. **exams/data-access.ts**:`persistAiGeneratedExamDraft` 改为调用 questions data-access +2. **exams/data-access.ts**:`getExams`/`getExamById` 改为通过 classes data-access 获取 gradeId +3. **questions/schema.ts**:`z.any()` → `z.unknown()` +4. **questions/actions.ts**:`getQuestionsAction` 和 `getKnowledgePointOptionsAction` 包装为 ActionState +5. **textbooks/actions.ts**:新建 schema.ts,添加 Zod 验证;清理 14 处 `as` 断言 +6. **grades/data-access-analytics.ts**:`getClassComparison` N+1 查询重构为 `inArray` 批量查询 +7. **classes/data-access-stats.ts、data-access-students.ts**:移除对 homework/exams 表的直接查询 +8. **school/actions.ts**:所有 DB 操作下沉到 data-access.ts +9. **proctoring/actions.ts**:DB 查询下沉到 data-access.ts +10. **messaging↔notifications**:将 `messageNotifications` 和 `notificationPreferences` 表所有权移交 notifications 模块 +11. **notifications/channels/in-app-channel.ts:54**:修复非法 as 断言 +12. **users/user-service.ts:13**:移除硬编码弱密码 "123456" +13. **users/actions.ts**:`updateUserProfile` 整改(权限+下沉+返回类型) +14. **scheduling/data-access.ts**:4 个函数补充返回类型标注 + +### 第二阶段:P1 重要问题(尽快修复) + +1. **跨模块直查 DB**:在各模块 data-access 暴露跨模块查询接口,消除直查(涉及 exams、homework、questions、grades、classes、messaging、notifications、parent、proctoring、diagnostic、elective) +2. **`as` 断言清理**:全局替换为类型守卫(涉及 20+ 文件,60+ 处) +3. **`requireAuth` → `requirePermission`**:users、messaging、proctoring、announcements、settings 共 8 处 +4. **`import type` 修正**:exams、questions、textbooks、proctoring 共 4 处 +5. **动态 import 改静态**:grades、diagnostic 共 3 处 +6. **grades/data-access.ts:249**:除零 bug 守卫 +7. **audit/data-access.ts**:导出函数数据截断问题(3 处) +8. **elective/data-access-operations.ts**:`runLottery` 加事务包裹 + 批量更新 +9. **diagnostic/actions.ts**:新建 schema.ts,用 Zod 替代手动校验 +10. **classes 模块**:新建 schema.ts,用 Zod 替换 actions.ts 中的手动校验 +11. **course-plans/actions.ts**:补齐 revalidatePath 调用(2 处) +12. **homework/data-access-write.ts:305-311**:`gradeHomeworkAnswers` 加事务包裹 +13. **users/user-service.ts**:`batchImportUsers` 加事务包裹 +14. **不可达代码清理**:classes/data-access.ts:297-299、classes/data-access-admin.ts:380-382 + +### 第三阶段:P2 优化(迭代修复) + +1. **React.cache() 包装**:grades、elective、diagnostic、files、proctoring 读函数添加 cache() +2. **Promise.all 并行化**:9 处独立查询串行执行 +3. **`after()` 非阻塞审计日志**:school/actions.ts(3 处) +4. **错误吞没清理**:school、classes、course-plans、announcements、files、audit、elective 共 30+ 处静默 catch +5. **非空断言 `!` 清理**:20+ 处替换为显式判空 +6. **函数返回类型补齐**:30+ 个箭头函数 +7. **重复代码提取**:normalizeRoleName、normalizeBcryptHash、toIso、serializeDate、toNumber/normalize、handleActionError、buildExcelSheet +8. **合并 filter 遍历**:5 处 +9. **Set/Map 查找优化**:2 处 +10. **架构文档同步**:exams/actions.ts 行数、settings 导出函数列表、P2-11 状态、announcements 依赖关系 +11. **exams/ai-pipeline.ts 拆分**:912 行按职责拆分为 4 个文件 +12. **layout/navigation.ts**:permission 字段改用 Permission 类型 +13. **notifications/external-sdk.d.ts**:安装实际 SDK 后用其自带类型覆盖 any + +--- + +## 附录:核查方法说明 + +### 核查流程 + +1. **架构图优先**:先阅读 `docs/architecture/004_architecture_impact_map.md`,理解模块依赖关系和已知问题 +2. **并行核查**:启动 5 个并行搜索代理,分别核查核心教学、教学管理、用户沟通、扩展功能、其他模块 +3. **逐文件审查**:每个代理读取所有后端 `.ts` 文件,对照项目规范逐条核查 +4. **性能规则应用**:应用 Vercel React Best Practices 的 65 条规则,识别性能优化机会 +5. **结果汇总**:合并 5 个代理的核查结果,按严重程度和模块分类整理 + +### 核查依据 + +- `.trae/rules/project_rules.md`:项目规则(架构分层、TS规范、命名、组件、Server Action、Tailwind、安全、提交) +- `docs/standards/coding-standards.md`:编码规范(详细规范) +- `docs/architecture/004_architecture_impact_map.md`:架构影响地图(模块清单、依赖关系、已知问题) +- `docs/architecture/005_architecture_data.json`:AI 友好格式的结构化数据 +- Vercel React Best Practices:65 条性能优化规则(8 个类别) + +### 未核查范围 + +- `src/modules/*/components/` 下的前端组件文件 +- `src/modules/*/hooks/` 下的 Hook 文件 +- `src/shared/` 下的基础设施文件 +- `src/app/` 下的路由层文件 +- 测试文件(`*.test.ts`) + +如需核查上述范围,请另行指派任务。 diff --git a/bugs/others_bug.md b/bugs/others_bug.md new file mode 100644 index 0000000..81e0f0b --- /dev/null +++ b/bugs/others_bug.md @@ -0,0 +1,960 @@ +# `src/app/(dashboard)/{announcements,dashboard,management,messages,profile,settings}` 规范核查报告 + +> 核查日期:2026-06-18 +> 核查范围:`src/app/(dashboard)/` 下的 announcements、dashboard、management、messages、profile、settings 子路由及其直接依赖的模块组件 +> 依据文档: +> - [项目规则](../.trae/rules/project_rules.md) +> - [编码规范](../docs/standards/coding-standards.md) +> - [架构影响地图 004](../docs/architecture/004_architecture_impact_map.md) +> - [架构数据 005](../docs/architecture/005_architecture_data.json) +> 应用技能:`vercel-react-best-practices`、`web-design-guidelines`(`web-artifacts-builder` 加载失败,界面优化建议已合并至 web-design-guidelines 章节) + +--- + +## 一、核查文件清单 + +| 文件 | 行数 | 类型 | 用途 | +|------|------|------|------| +| [announcements/page.tsx](../src/app/(dashboard)/announcements/page.tsx) | 20 | RSC 页面 | 公告列表(普通用户) | +| [dashboard/page.tsx](../src/app/(dashboard)/dashboard/page.tsx) | 18 | RSC 页面 | 角色路由分发 | +| [management/grade/classes/page.tsx](../src/app/(dashboard)/management/grade/classes/page.tsx) | 31 | RSC 页面 | 年级班级管理 | +| [management/grade/insights/page.tsx](../src/app/(dashboard)/management/grade/insights/page.tsx) | 243 | RSC 页面 | 年级作业洞察 | +| [messages/page.tsx](../src/app/(dashboard)/messages/page.tsx) | 31 | RSC 页面 | 消息+通知列表 | +| [messages/[id]/page.tsx](../src/app/(dashboard)/messages/[id]/page.tsx) | 30 | RSC 页面 | 消息详情 | +| [messages/compose/page.tsx](../src/app/(dashboard)/messages/compose/page.tsx) | 34 | RSC 页面 | 撰写消息 | +| [profile/page.tsx](../src/app/(dashboard)/profile/page.tsx) | 305 | RSC 页面 | 个人资料(学生/教师视图) | +| [settings/page.tsx](../src/app/(dashboard)/settings/page.tsx) | 32 | RSC 页面 | 设置入口(按角色分发) | +| [settings/security/page.tsx](../src/app/(dashboard)/settings/security/page.tsx) | 50 | RSC 页面 | 安全设置 | +| [layout.tsx](../src/app/(dashboard)/layout.tsx) | 21 | RSC 布局 | Dashboard 通用布局 | +| [error.tsx](../src/app/(dashboard)/error.tsx) | 22 | 客户端组件 | 错误边界 | +| [not-found.tsx](../src/app/(dashboard)/not-found.tsx) | 23 | RSC 组件 | 404 页面 | +| [modules/announcements/components/announcement-list.tsx](../src/modules/announcements/components/announcement-list.tsx) | 108 | 客户端组件 | 公告列表(含筛选) | +| [modules/announcements/components/announcement-card.tsx](../src/modules/announcements/components/announcement-card.tsx) | 79 | 客户端组件 | 公告卡片 | +| [modules/announcements/components/announcement-detail.tsx](../src/modules/announcements/components/announcement-detail.tsx) | 206 | 客户端组件 | 公告详情 | +| [modules/messaging/components/message-list.tsx](../src/modules/messaging/components/message-list.tsx) | 117 | 客户端组件 | 消息列表 | +| [modules/messaging/components/message-detail.tsx](../src/modules/messaging/components/message-detail.tsx) | 153 | 客户端组件 | 消息详情 | +| [modules/messaging/components/message-compose.tsx](../src/modules/messaging/components/message-compose.tsx) | 146 | 客户端组件 | 撰写消息表单 | +| [modules/messaging/components/notification-list.tsx](../src/modules/messaging/components/notification-list.tsx) | 141 | 客户端组件 | 通知列表 | +| [modules/settings/components/admin-settings-view.tsx](../src/modules/settings/components/admin-settings-view.tsx) | 129 | 客户端组件 | 管理员设置视图 | +| [modules/settings/components/teacher-settings-view.tsx](../src/modules/settings/components/teacher-settings-view.tsx) | 132 | 客户端组件 | 教师设置视图 | +| [modules/settings/components/student-settings-view.tsx](../src/modules/settings/components/student-settings-view.tsx) | 120 | 客户端组件 | 学生设置视图 | +| [modules/settings/components/password-change-form.tsx](../src/modules/settings/components/password-change-form.tsx) | 180 | 客户端组件 | 修改密码表单 | +| [modules/settings/components/profile-settings-form.tsx](../src/modules/settings/components/profile-settings-form.tsx) | 198 | 客户端组件 | 资料编辑表单 | +| [modules/settings/components/notification-preferences-form.tsx](../src/modules/settings/components/notification-preferences-form.tsx) | 260 | 客户端组件 | 通知偏好表单 | +| [modules/settings/components/theme-preferences-card.tsx](../src/modules/settings/components/theme-preferences-card.tsx) | 60 | 客户端组件 | 主题偏好 | +| [modules/settings/components/ai-provider-settings-card.tsx](../src/modules/settings/components/ai-provider-settings-card.tsx) | 405 | 客户端组件 | AI Provider 配置 | +| [modules/classes/components/grade-classes-view.tsx](../src/modules/classes/components/grade-classes-view.tsx) | 455 | 客户端组件 | 年级班级管理视图 | + +--- + +## 二、违规问题清单 + +### 2.1 [announcements/page.tsx](../src/app/(dashboard)/announcements/page.tsx) — 严重度:高 + +#### BUG-A01:缺少权限校验(违反 Server Action 规范) +- **位置**:`src/app/(dashboard)/announcements/page.tsx:6-7` +- **问题**:页面直接调用 `getAnnouncements({ status: "published" })`,未通过 `requirePermission()` 或 `requireAuth()` 进行任何权限校验 +- **规范依据**:项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」;架构文档 004 已记录此问题(P2-12) +- **影响**:未登录用户可直接访问 `/announcements` 路由获取公告数据,存在信息泄露风险 +- **改进建议**: + ```typescript + import { requirePermission } from "@/shared/lib/auth-guard" + import { Permissions } from "@/shared/types/permissions" + + export default async function AnnouncementsPage() { + await requirePermission(Permissions.ANNOUNCEMENT_READ) + const announcements = await getAnnouncements({ status: "published" }) + // ... + } + ``` + +#### BUG-A02:缺少 `metadata` 导出 +- **位置**:`src/app/(dashboard)/announcements/page.tsx` +- **问题**:未导出 `metadata`,浏览器标签页无标题 +- **规范依据**:Web Interface Guidelines — Metadata & SEO +- **改进建议**:补充 `export const metadata = { title: "Announcements" }` + +--- + +### 2.2 [dashboard/page.tsx](../src/app/(dashboard)/dashboard/page.tsx) — 严重度:中 + +#### BUG-D01:使用权限反推角色(硬编码反模式) +- **位置**:`src/app/(dashboard)/dashboard/page.tsx:14-16` +- **问题**:使用 `permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)` 反推学生身份,应使用 `hasRole("student")` +- **规范依据**:项目规则「前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`」;架构文档 004 已标记此为 P2 问题 +- **影响**:当学生被授予 `EXAM_CREATE` 权限(如助教)时会被错误路由到教师页面 +- **改进建议**:服务端应使用 `session.user.roles` 判断 + ```typescript + const roles = session.user.roles ?? [] + if (roles.includes("admin")) redirect("/admin/dashboard") + if (roles.includes("student")) redirect("/student/dashboard") + if (roles.includes("parent")) redirect("/parent/dashboard") + redirect("/teacher/dashboard") + ``` + +#### BUG-D02:多重 `redirect` 调用难以维护 +- **位置**:`src/app/(dashboard)/dashboard/page.tsx:14-17` +- **问题**:4 个连续 `if + redirect` 缺乏优先级文档说明,新增角色时易遗漏 +- **改进建议**:抽取为 `resolveDefaultPath(roles)` 单一函数(`proxy.ts` 已有类似实现),保持单一职责 + +--- + +### 2.3 [management/grade/classes/page.tsx](../src/app/(dashboard)/management/grade/classes/page.tsx) — 严重度:高 + +#### BUG-M01:缺少权限校验 +- **位置**:`src/app/(dashboard)/management/grade/classes/page.tsx:7-15` +- **问题**:仅调用 `auth()` 获取 session,未调用 `requirePermission()` 校验 `CLASS_MANAGE` 权限 +- **规范依据**:项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」 +- **影响**:无 `CLASS_MANAGE` 权限的用户可访问页面并获取教师列表、年级数据 +- **改进建议**: + ```typescript + import { requirePermission } from "@/shared/lib/auth-guard" + import { Permissions } from "@/shared/types/permissions" + + export default async function GradeClassesPage() { + const ctx = await requirePermission(Permissions.CLASS_MANAGE) + const userId = ctx.userId + // ... + } + ``` + +#### BUG-M02:`userId` 兜底为空字符串存在隐患 +- **位置**:`src/app/(dashboard)/management/grade/classes/page.tsx:9` +- **问题**:`const userId = session?.user?.id ?? ""` 在未登录时返回空字符串,下游 `getGradeManagedClasses("")` 会查询无意义数据 +- **改进建议**:未登录应直接 `redirect("/login")`,不应继续执行 + +--- + +### 2.4 [management/grade/insights/page.tsx](../src/app/(dashboard)/management/grade/insights/page.tsx) — 严重度:高 + +#### BUG-MI01:缺少权限校验 +- **位置**:`src/app/(dashboard)/management/grade/insights/page.tsx:25-34` +- **问题**:页面直接调用 `getTeacherIdForMutations()` 和 `getGradesForStaff()`,未调用 `requirePermission()` +- **规范依据**:项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」 +- **改进建议**:增加 `requirePermission(Permissions.HOMEWORK_READ)` 或对应年级负责人权限校验 + +#### BUG-MI02:使用原生 `` 元素,与项目其他页面使用的 shadcn `Select` 组件风格不一致 +- **规范依据**:Web Interface Guidelines — Consistency;项目组件规范 +- **影响**:视觉风格不统一,无障碍特性差异,主题切换时原生 select 样式无法跟随 +- **改进建议**:替换为 shadcn `Select` 组件 + +#### BUG-MI03:`

` 描述表格用途,屏幕阅读器无法快速理解表格主题。 + +**修复建议**:增加 `模板字段说明