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
主要变更: - 新增 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)
332 lines
25 KiB
Markdown
332 lines
25 KiB
Markdown
# 首次登录引导(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) | 🟡 | ✅ |
|