Files
NextEdu/docs/feature/001_first_login_onboarding.md
SpecialX 978d9a8309
Some checks failed
Security / deep-security-scan (push) Failing after 20m5s
DR Drill / dr-drill (push) Failing after 1m31s
CI / scheduled-backup (push) Failing after 1m31s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat: 新增备课模块并修复全模块 P0/P1/P2 缺陷
主要变更:

- 新增 lesson-preparation 模块: 备课编辑器、节点编辑、AI 建议、知识点选择、版本历史、作业发布

- 新增 shared 通用组件: charts/question-bank-filters/schedule-list/ui (chip-nav/filter-bar/page-header/stat-card/stat-item)

- 新增 student/admin 端 loading.tsx 与 error.tsx, 优化加载与错误态体验

- 新增 teacher/lesson-plans 页面 (列表/新建/编辑)

- 新增 drizzle 迁移 0002_tiny_lionheart 及 snapshot

- 新增 textbooks/schema.ts 与 exams/utils/normalize-structure.ts

- 修复 Tiptap v3 SSR hydration 崩溃 (rich-text-block immediatelyRender: false)

- 重构多模块 data-access/actions/组件, 修复权限校验与类型规范

- 同步架构文档 004/005 反映新增模块、导出、依赖关系

- 归档 bugs/* 测试报告与 e2e 测试脚本 (admin/parent/student/teacher web_test)
2026-06-22 01:06:16 +08:00

332 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 首次登录引导Onboarding重大问题讨论 · v2
> 版本:**v2**(替代 v12026-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#L41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41)(全局挂载点,未变)
> - [src/auth.ts](file:///e:/Desktop/CICD/src/auth.ts)jwt/session 回调,未注入 onboarded
> - [src/proxy.ts](file:///e:/Desktop/CICD/src/proxy.ts)middleware无 onboarding 拦截)
---
## 、v2 与 v1 的差异说明
经 git 核实(`git log` + `git status` + `git diff`onboarding 相关代码自 v1 审查以来**零改动**
- `onboarding-gate.tsx``api/onboarding/*/route.ts``layout.tsx``auth.ts` 均无修改
- 工作区改动集中在 `proxy.ts`(权限常量替换)、`schema.ts`(新增 lesson_plans 表)等与 onboarding 无关的文件
v2 在 v1 基础上**新增 9 项 v1 遗漏的问题**标为「v2 新增」),其中含 2 项 P0 级越权漏洞。问题编号沿用 v1新增项顺延。
---
## 一、背景与定位
按项目规则"先图后码",从架构影响地图定位 Onboarding 节点:
- **shared 层**`components/onboarding-gate.tsx`312 行)已被架构图标记 ⚠️ P2-4「业务逻辑泄漏到 shared」
- **app 层**`/api/onboarding/status``/api/onboarding/complete` 两条路由
- **数据层**`users.onboardedAt`[schema.ts:41](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L41)
- **被调用模块**`modules/classes/data-access.ts``enrollStudentByInvitationCode`(学生路径);教师路径**绕过** `enrollTeacherByInvitationCode` 直接写表
当前实现:全局 Dialog。`app/layout.tsx` 第 41 行无条件挂载 `<OnboardingGate />`,组件内 `useEffect` 拉取 `/api/onboarding/status``required === true` 时弹出不可关闭的 4 步 Dialog。
---
## 二、现状代码盘点
### 2.1 组件层onboarding-gate.tsx
| 步骤 | 标题 | 采集字段 | 备注 |
|------|------|----------|------|
| Step 0 | 角色选择 | rolestudent/teacher/parent | admin 只读;其他角色用户可下拉**自选** |
| Step 1 | 通用信息 | name / phone / address | 仅校验非空 |
| Step 2 | 角色信息 | classCodes学生/教师、teacherSubjects教师 | 可跳过;家长显示"暂不需要配置" |
| Step 3 | 完成 | — | 调 `/api/onboarding/complete` 后跳 `/dashboard` |
角色推断逻辑([第 90-94 行](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94))用权限点反推角色。
### 2.2 API 层
- `GET /api/onboarding/status`:查 `users.onboardedAt` + 查 `usersToRoles` 推断角色
- `POST /api/onboarding/complete`update users → insert usersToRoles → 学生调 `enrollStudentByInvitationCode`**教师直接 insert `classSubjectTeachers`** → 写 `onboardedAt`
### 2.3 关键表结构v2 补充)
| 表 | 主键 | 影响 |
|----|------|------|
| `usersToRoles` | `(userId, roleId)` 联合主键([schema.ts:118](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L118) | onDuplicateKeyUpdate 无法"替换"角色,只会新增行 → 追加角色 |
| `classSubjectTeachers` | `(classId, subjectId)` 联合主键([schema.ts:364](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L364) | 一个班级一个科目只有一位教师 → onDuplicateKeyUpdate 会**覆盖现有教师** |
---
## 三、重大问题清单(按风险分级)
### 🔴 P0 级:安全/合规/越权
#### P0-1 用户可自选角色(严重越权)
- **位置**[onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201)、[complete/route.ts:32-35](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L32-L35)
- **问题**Step 0 允许任意登录用户自选 student/teacher/parent`complete/route.ts` 直接信任前端 `body.role`
- **后果**:任何注册用户可自封 teacher 获得 `exam:create``homework:grade` 等权限。
- **违反**K12 行业铁律「角色由管理员预分配」、项目规则「Server Action 必须用 `requirePermission()`」。
#### P0-2 教师可绑定任意班级+科目
- **位置**[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
- **问题**:教师通过 `classCodes`6 位邀请码)可把自己写入任意班级的 `classSubjectTeachers``teacherSubjects` 由前端任意提交,服务端仅做"名称存在性"校验。
- **后果**:教师可越权查看任意班级学生名单、成绩。
#### 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 写入无 `db.transaction()`;运行时 `db.insert(roles)` 创建角色记录([第 66-68 行](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L66-L68))属异常路径。
#### P0-4 教师可覆盖现有任课教师v2 新增,严重破坏)
- **位置**[complete/route.ts:124-127](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L124-L127)
- **问题**`classSubjectTeachers` 主键为 `(classId, subjectId)`[schema.ts:364](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L364)一个班级一个科目只有一位教师。onboarding 用 `onDuplicateKeyUpdate({ set: { teacherId: userId, ... } })`**会直接覆盖该班级该科目已有的任课教师**。
- **对比**`modules/classes/data-access.ts``enrollTeacherByInvitationCode`[第 637 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L637))有完整校验 `if (mapping?.teacherId && mapping.teacherId !== tid) throw new Error("Subject already assigned")`,且只认领 `teacherId IS NULL` 的空缺位置([第 657 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L657)。onboarding **绕过了该函数**,直接 insert。
- **后果**:任何自封教师的人可抢占全校任意班级的任课位置,踢掉真实任课教师,篡改任课关系。
- **违反**项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」。
#### P0-5 角色追加越权v2 新增)
- **位置**[complete/route.ts:82-87](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L82-L87)
- **问题**`usersToRoles` 主键为 `(userId, roleId)` 联合主键([schema.ts:118](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L118))。`db.insert(usersToRoles).values({ userId, roleId }).onDuplicateKeyUpdate({ set: { roleId } })` 中,`set roleId` 无意义roleId 已是要插入的值)。当用户已有其他 roleId 时,此操作**新增一行**而非替换——即**追加角色记录**。
- **后果**:学生自选 teacher 角色后,给自己追加一条 teacher 角色行;`auth.ts``resolvePermissions(allRoles)` 会合并所有角色权限([auth.ts:131](file:///e:/Desktop/CICD/src/auth.ts#L131)),学生因此获得 teacher 全部权限。结合 P0-1这是完整的权限提升链。
- **修复方向**onboarding 不应写 `usersToRoles`,角色分配由管理员后台处理。
### 🟠 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/*」。
- **架构图标记**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)
- **问题**:直接 import 并写入 `classes``classSubjectTeachers``subjects` 表,绕过 `modules/classes` 的 data-access 与权限校验。
- **违反**项目规则「app 只能调用 modules 的 Server Actions 和 data-access」「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` 归一化),组件用权限点重新推断,两套逻辑可能不一致。
#### P1-4 auth.ts 未注入 onboarded 状态v2 新增)
- **位置**[auth.ts:122-177](file:///e:/Desktop/CICD/src/auth.ts#L122-L177) jwt/session 回调
- **问题**jwt 回调每次刷新都查 `users.name` + `usersToRoles` + `roles` 三张表([第 143-153 行](file:///e:/Desktop/CICD/src/auth.ts#L143-L153)),但**只读 `name`,未读 `onboardedAt`**token 里永远没有 onboarding 状态。
- **后果链**
1. `proxy.ts`middleware`getToken` 读 token无法判断 onboarded → 无法做重定向拦截
2. `status/route.ts` 必须每次查库判断 `required` → 性能损耗
3. 客户端无法从 `session.user` 读取 onboarded → 必须额外 fetch
4. `onFinish``update()`token 刷新但 onboarded 仍未注入 → 即便有 middleware 也拦不住
- **修复方向**jwt 回调 `columns: { name: true, onboardedAt: true }`,注入 `token.onboarded = !!fresh.onboardedAt`session 回调暴露 `session.user.onboarded`
#### P1-5 onboarding 绕过 classes 模块封装v2 新增)
- **位置**[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
- **问题**`modules/classes/data-access.ts` 已提供 `enrollTeacherByInvitationCode`[第 589 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L589)含「教师身份校验」「科目已分配校验」「只认领空缺位置」等安全逻辑。onboarding **未调用它**,而是直接 insert `classSubjectTeachers`,绕过全部校验。
- **后果**:与 P0-4 叠加,形成完整越权路径。
- **违反**项目规则「modules 之间通过对方 data-access 通信」。
### 🟡 P2 级:用户体验与可访问性
#### P2-1 全局 Dialog 模式缺陷
- 不可关闭(`canClose = !required`);刷新丢步;无独立 URL首屏无骨架屏`useEffect` 拉取期间闪烁。
- **对比**业界主流Auth.js 官方、Clerk、Vercel 模板)均采用独立路由 `/onboarding` + middleware 重定向。
#### P2-2 表单校验粗糙
- 电话仅校验非空(无手机号格式);姓名/地址无长度限制;班级代码无格式预校验。
#### P2-3 国际化与可访问性
- 中英文混合("Role"、"Select role" 英文Dialog 缺 `aria-describedby`;进度条无 `aria-valuenow`
#### P2-4 进度条与步骤不一致
- admin 跳过 Step 2但进度条仍渲染 4 段Step 2 永远亮起。
#### P2-5 完成跳转硬编码 /dashboardv2 新增)
- **位置**[onboarding-gate.tsx:154](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L154)
- **问题**`router.push("/dashboard")` 硬编码,但 [proxy.ts:23-30](file:///e:/Desktop/CICD/src/proxy.ts#L23-L30) 的 `resolveDefaultPath` 按角色返回 `/admin/dashboard``/teacher/dashboard``/student/dashboard``/parent/dashboard`
- **后果**:非 admin 用户完成 onboarding 后跳 `/dashboard`(不存在),被 proxy 权限检查拦截后重定向,体验为"完成→闪跳→再跳"。
#### P2-6 家长角色推断死锁v2 新增)
- **位置**[onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94)
- **问题**
```ts
const isTeacher = permissions.includes(EXAM_CREATE)
const isStudent = permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)
const isParent = !EXAM_CREATE && !HOMEWORK_SUBMIT && permissions.includes(EXAM_READ)
```
- `isTeacher` 先判断且包含 `EXAM_READ`teacher 有 EXAM_READ家长条件 `!EXAM_CREATE && EXAM_READ` 与 teacher 重叠
- 实际角色权限映射中parent 是否有 `EXAM_READ` 存疑;若 parent 无 `EXAM_READ`,则 `isParent` 永远为 false → 家长在 Step 2 看到"暂不需要配置"的分支永远不触发,可能落到空白页
- **后果**家长角色无法被正确识别Step 2 渲染异常。
#### P2-7 学生注册无错误处理v2 新增)
- **位置**[complete/route.ts:89-93](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L89-L93)
- **问题**`enrollStudentByInvitationCode` 会 throw如无效邀请码但无 try/catch。一个无效码导致整个请求 500而前面的 `update users` 已执行(无事务)→ 用户 name/phone 已更新但 `onboardedAt` 仍为 null → 下次登录反复弹窗且数据不一致。
#### P2-8 useEffect 依赖导致重复弹窗v2 新增)
- **位置**[onboarding-gate.tsx:45-68](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L45-L68)
- **问题**useEffect 依赖 `[status, session?.user?.name]`。`auth.ts` jwt 回调每次刷新会重读 `users.name` 并写入 token[auth.ts:158](file:///e:/Desktop/CICD/src/auth.ts#L158)),若 name 变化如管理员改了用户名session.user.name 变化触发 useEffect 重新拉取 status → 可能重复弹窗。
#### P2-9 不可关闭 Dialog 的冗余 effectv2 新增)
- **位置**[onboarding-gate.tsx:70-74](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L70-L74)
- **问题**
```ts
useEffect(() => {
if (!open) return
if (!required) return
setOpen(true) // 冗余open 已为 true
}, [open, required])
```
此 effect 在 open 被 Dialog 的 `onOpenChange` 关闭时强制重开,实现"不可关闭"。但逻辑脆弱:若 required 在异步中变化,可能产生状态竞态。应改为在 `onOpenChange` 中直接判断 `if (!canClose) return`。
---
## 四、业界大仓Monorepo解决方案引用
### 4.1 Auth.js v5 官方推荐
- **状态标记**`users.onboardedAt` + `jwt`/`session` 回调注入;完成时调 `update()` 刷新 token。
- **强制方式****middleware 重定向**到独立 `/onboarding` 路由。在 `proxy.ts`Next.js 16 的 middleware用 `getToken` 读取 `onboarded`,未完成且非白名单路径 → `NextResponse.redirect('/onboarding')`。
- **结论**:客户端 Dialog 仅适合"非阻塞偏好补全";强制 onboarding 应等同未登录处理。
### 4.2 商业方案Clerk / Supabase / Auth0共性
三段式:**metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验**。
- 角色等敏感字段放服务端可写的 metadata**禁止前端自写**。
- onboarding 完成回调必须由服务端 Action 写入,前端不能直接改。
### 4.3 shadcn/ui 生态
- 官方无内置 Stepper但 `examples/forms` 与 `blocks` 范式明确:**独立路由页面 + `<Form>`react-hook-form + zod+ 父组件持 step state**。
- 每步独立 zod schema 渐进式校验,最后一步汇总写入。
### 4.4 企业级 K12 教务系统PowerSchool / Veracross / 国内智慧校园)
**铁律:角色由管理员预分配,用户不可自选。**
| 角色 | 首次登录采集字段 | 角色来源 |
|------|------------------|----------|
| 学生 | 学号(预分配不可改)、姓名、性别、出生日期、家长联系方式、紧急联系人 | 管理员批量导入 |
| 教师 | 工号(预分配)、姓名、所教科目、任教班级、办公室、联系电话、学历资质 | 教务处预分配 |
| 家长 | 与学生关系、学生学号(通过 **Access ID + Access Password** 绑定)、本人姓名、电话、邮箱 | 学校发放凭证,家长绑定子女 |
| 管理员 | 工号、姓名、职务、管理范围 | 学校 IT 创建 |
### 4.5 Monorepoturborepo / nx惯例
- 跨模块"流程型"功能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/ # 登录页proxy 白名单)
├─ (onboarding)/onboarding/ # 新增独立路由
│ └─ page.tsx # 服务端组件,读 session.onboarded 决定渲染
└─ proxy.ts # 增强:未 onboarded 时重定向
modules/onboarding/ # 新建模块
├─ actions.ts # completeOnboardingActionServer Action + requirePermission
├─ data-access.ts # 仅操作 users.onboardedAt
├─ schema.ts # Zodname/phone/address/classCodes
├─ types.ts
└─ components/
├─ OnboardingStepper.tsx
├─ RoleConfirmStep.tsx # 只读展示管理员分配的角色
├─ ProfileStep.tsx # 姓名/电话/住址
└─ BindingStep.tsx # 学生:确认班级;教师:确认任课;家长:绑定子女
shared/
└─ components/onboarding-gate.tsx # 删除
```
### 5.2 关键改动点
1. **auth.ts 回调注入 onboarded**P1-4jwt 回调 `columns: { name: true, onboardedAt: true }``token.onboarded = !!fresh.onboardedAt`session 回调暴露 `session.user.onboarded`。
2. **proxy.ts 增加 onboarding 拦截**:读 `token.onboarded`,未完成且路径不在白名单(`/login`、`/api/auth`、`/onboarding`、静态资源)→ 重定向 `/onboarding`。
3. **删除 `shared/components/onboarding-gate.tsx`**,从 `app/layout.tsx` 移除挂载。
4. **新建 `modules/onboarding/`**,承载所有领域逻辑。
5. **新建 `app/(onboarding)/onboarding/page.tsx`** 独立路由。
6. **删除 `app/api/onboarding/*/route.ts`**,改为 `modules/onboarding/actions.ts` 的 Server Action。
7. **角色只读化**P0-1/P0-5Step 0 改为"角色确认"——只读展示 `usersToRoles` 中的角色,用户不可改;**onboarding 不写 `usersToRoles`**。
8. **班级绑定改造**P0-2/P0-4/P1-5
- 学生:仅"确认"管理员预分配的班级,或输入邀请码(调 `enrollStudentByInvitationCode`
- 教师:**必须调 `enrollTeacherByInvitationCode`**(含"Subject already assigned"校验),禁止直接 insert理想方案是仅"确认"管理员预分配
- 家长:输入"子女学号 + 绑定码"绑定子女(参考 PowerSchool Access ID 模式)
9. **事务化**P0-3/P2-7`completeOnboardingAction` 用 `db.transaction()` 包裹所有写入,`onboardedAt` 在事务最后写入。
10. **Zod 校验**P0-3`onboardingSchema`phone 用 `z.string().regex(/^1\d{10}$/)`name `z.string().min(1).max(50)`address `z.string().max(200).optional()`。
11. **完成跳转修正**P2-5用 `resolveDefaultPath(roles)` 替代硬编码 `/dashboard`。
12. **角色推断统一**P1-3/P2-6删除组件内的权限点反推逻辑统一从 `session.user.roles`auth.ts 已注入)读取。
### 5.3 迁移兼容
- 已 onboarded 用户(`onboardedAt` 非空不受影响proxy 直接放行。
- 未 onboarded 用户下次登录被重定向到 `/onboarding`(而非弹 Dialog
- 无需数据迁移,`users.onboardedAt` 字段保留。
---
## 六、待决策的开放问题
### Q1角色分配策略
- **方案 A**(推荐,符合 K12 铁律onboarding 中角色完全只读,由管理员后台预分配;用户无法改变角色。
- **方案 B**:保留角色选择,但服务端校验"用户已有该角色"才允许(即只能从已有角色中选主角色)。
- **方案 C**:暂不改动角色选择,仅修复其他问题。
### Q2教师任课关系绑定
- **方案 A**推荐onboarding 中教师**仅确认**管理员预分配的任课关系,不自填班级代码。
- **方案 B**:保留自填邀请码,但**必须调 `enrollTeacherByInvitationCode`**(含"Subject already assigned"校验),禁止直接 insert。
- **方案 C**:完全移除 onboarding 中的班级绑定,统一由管理员后台处理。
### Q3家长绑定子女方式
- **方案 A**推荐PowerSchool 模式):家长输入"子女学号 + 学校发放的 6 位绑定码"。
- **方案 B**:家长输入"子女学号 + 子女生日"作为验证。
- **方案 C**:暂不实现家长绑定,由管理员后台预绑定。
### Q4onboarding 路由形态
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper步骤状态用 query param 持久化)。
- **方案 B**:嵌套路由 `/onboarding/role`、`/onboarding/profile`、`/onboarding/binding`(每步独立 Server Action
- **方案 C**:保留全局 Dialog仅修复安全与架构问题。
### Q5实施范围
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
- **方案 B**(推荐):先做 P0安全/越权)+ P1架构P2UX后续迭代。
- **方案 C**:仅做 P0 紧急修复P1/P2 列入 backlog。
### Q6auth.ts jwt 回调性能v2 新增)
jwt 回调每次刷新查 3 张表([auth.ts:143-153](file:///e:/Desktop/CICD/src/auth.ts#L143-L153))。注入 onboarded 可复用此次查库,但是否同步优化为「仅在登录时全量查、刷新时轻量查」?
- **方案 A**:复用现有查库,只加 `onboardedAt` 字段(最小改动)。
- **方案 B**:重构为登录时全量、刷新时只查 `onboardedAt`(优化性能)。
---
## 七、附录:问题与代码位置速查
| 编号 | 问题 | 代码位置 | 风险 | v2 新增 |
|------|------|----------|------|---------|
| P0-1 | 用户自选角色 | [onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) | 🔴 | |
| P0-2 | 教师绑任意班级 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🔴 | |
| P0-3 | 无权限校验/Zod/事务 | [complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 | 🔴 | |
| P0-4 | 教师覆盖现有任课教师 | [complete/route.ts:124-127](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L124-L127) | 🔴 | ✅ |
| P0-5 | 角色追加越权 | [complete/route.ts:82-87](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L82-L87) | 🔴 | ✅ |
| P1-1 | shared 反向承载领域逻辑 | [onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 | 🟠 | |
| P1-2 | app 层跨模块写表 | [complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) | 🟠 | |
| 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) | 🟠 | |
| P1-4 | auth 未注入 onboarded | [auth.ts:143-153](file:///e:/Desktop/CICD/src/auth.ts#L143-L153) | 🟠 | ✅ |
| P1-5 | 绕过 classes 模块封装 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🟠 | ✅ |
| P2-1 | 全局 Dialog 缺陷 | [app/layout.tsx:41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41) | 🟡 | |
| P2-2 | 表单校验粗糙 | [onboarding-gate.tsx:88](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L88) | 🟡 | |
| P2-3 | i18n/a11y | [onboarding-gate.tsx:188-194](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L188-L194) | 🟡 | |
| P2-4 | 进度条与步骤不一致 | [onboarding-gate.tsx:179-184](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L179-L184) | 🟡 | |
| P2-5 | 完成跳转硬编码 /dashboard | [onboarding-gate.tsx:154](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L154) | 🟡 | ✅ |
| P2-6 | 家长角色推断死锁 | [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟡 | ✅ |
| P2-7 | 学生注册无错误处理 | [complete/route.ts:89-93](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L89-L93) | 🟡 | ✅ |
| P2-8 | useEffect 重复弹窗 | [onboarding-gate.tsx:45-68](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L45-L68) | 🟡 | ✅ |
| P2-9 | 冗余不可关闭 effect | [onboarding-gate.tsx:70-74](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L70-L74) | 🟡 | ✅ |