# 首次登录引导（Onboarding）重大问题讨论 · v2

> 版本：**v2**（替代 v1，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#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 | 角色选择 | role（student/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 完成跳转硬编码 /dashboard（v2 新增）
- **位置**：[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 的冗余 effect（v2 新增）
- **位置**：[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 Monorepo（turborepo / 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                       # completeOnboardingAction（Server Action + requirePermission）
├─ data-access.ts                   # 仅操作 users.onboardedAt
├─ schema.ts                        # Zod：name/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-4）：jwt 回调 `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-5）：Step 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**：暂不实现家长绑定，由管理员后台预绑定。

### 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。

### Q6：auth.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) | 🟡 | ✅ |
