# 首次登录引导(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` 范式明确:**独立路由页面 + `