Files
Edu/apps/portal-shell/docs/needtodo/shared-NeedTodo.md
SpecialX 062d9e9582 feat(portal-shell): 管理域 §9.4 B5 全量迁移(24 + 21 补充批次共 44 页 + 42 features)
按 ARCHITECTURE.md §9.4 规划口径 + admin-NeedTodo.md §四补充批次完成管理域全量页面迁移:

【§9.4 规划 24 页(B5)】
- users(2) + roles(1) + permissions(1) + audit-logs(4) + invitation-codes(1)
- school(6: redirect/schools/classes/departments/academic-year/grades)
- announcements(1) + files(1) + ai-settings(1) + system(1) + viewports(1)
- students(1) + teachers(1) + organization(1) + plugins(1, config-service)
- 仪表盘已存在(/shell/admin/page.tsx)

【§四补充批次 21 页】
- course-plans(4) + elective(4) + questions(1) + lesson-plans(2) + error-book(1)
- scheduling(3: auto/changes/rules) + attendance(1) + curriculum-map(1)
- announcements 详情/编辑(2) + roles/[id] 详情(1) + users/import(1)

【实现要点】
- 全部使用 ListPageShell + loading/error/empty 三态规范(§11.3 DoD)
- 走 lib/api hooks;未就绪契约走 MSW + @contract-pending 注释(§11.4)
- 文案走 useTranslations(zh-CN + en 两份同步更新)
- 42 个 features/<domain>/transformations.ts 纯函数 + 配套 vitest 单测
- catch 块统一 notify.error;无空 catch;lint:tokens 通过
- 路由全部登记到 route-permissions.ts(39 EXACT + 8 PREFIX)

【验收】
- tsc --noEmit: 0 errors
- ESLint src: 0 errors (4 generated-files warnings, pre-existing)
- lint:tokens: 0 errors
- vitest: 1639/1639 passed (含 23 admin 测试文件 671 用例)
- check:routes: PASS (143 routes, 4 ghost entries pre-existing)
- check:pages: PASS (146 pages)
- check:codegen: PASS
- arch:scan: 24 modules, 8262 symbols

关联:ARCHITECTURE.md §9.4 / §10 P5 / §11.3 DoD / §11.6
2026-07-24 23:07:20 +08:00

375 lines
40 KiB
Markdown
Raw Permalink 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.
# 共享路由Shared待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\`
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\`
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.1 / §9.2 / §9.3 / §9.5(共享 ~7 页)
> 分析时间2026-07-24
> 分析范围:仅分析,不写代码
---
## 一、页面完成度总览
| 状态 | 路径 | 说明 |
| ---- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | `/login` | 登录页 |
| ✅ | `/shell` | 仪表盘根(按角色分发到 `/shell/{teacher\|student\|parent\|admin}`)。**CICD 参考**无单一分发器——CICD 根 `src/app/page.tsx` 直接 `redirect("/dashboard")`(教师仪表盘);各角色仪表盘分散于 `(dashboard)/dashboard/``(dashboard)/student/dashboard/``(dashboard)/parent/dashboard/``(dashboard)/admin/dashboard/`。portal-shell 实现于 `src/app/shell/[[...route]]/page.tsx`catch-all空路由 → `redirect("/shell/{role}")`+ `src/app/shell/layout.tsx`AppFrame 框架fail-closed 读 `x-user-role` 头) |
| ✅ | `/shell/forbidden` | 403 页(`PUBLIC_ROUTES` 白名单) |
| ❌ | `/shell/notifications` | 通知中心(缺失) |
| ❌ | `/shell/settings` | 设置(缺失) |
| ❌ | `/shell/settings/security` | 安全中心 / 改密码(缺失) |
| ❌ | `/shell/announcements` | 公告列表(缺失) |
| ❌ | `/shell/announcements/[id]` | 公告详情(缺失) |
| ❌ | `/shell/messages` | 消息(缺失) |
| ❌ | `/shell/messages/compose` | 撰写消息(缺失) |
| ❌ | `/shell/messages/[id]` | 消息详情(缺失) |
| ❌ | `/shell/profile` | 个人资料(缺失) |
> 备注 1portal-shell 现存 `widgets/topbar/notification-bell`(顶栏铃铛下拉)与 `widgets/universal/notifications-widget`(仪表盘内嵌通知卡片),但**无独立的通知中心整页**。
> 备注 2`widgets/universal/announcements-widget` 已在 P1-2 下线(数据源迁移到 `adminDashboard` 聚合查询),**无用户端公告列表/详情页**。
> 备注 3`shared/lib/navigation.ts` 与 `shared/lib/route-permissions.ts` **均未登记**上述任一共享路由,按 `route-permissions.ts` §5 fail-closed 规则,未登记的 `/shell/*` 路由会被默认拒绝 → 即便加了 page.tsx 也访问不到,必须同步登记权限配置。
---
## 二、缺失页面详情
### 2.1 `/shell/notifications`(通知中心)
- **CICD 参考**:无独立 `notifications/page.tsx`。CICD 中通知集成在三处:
- `widgets/topbar/notification-bell` 的下拉(顶栏铃铛)
- `messages/page.tsx` 内嵌的 `NotificationListSection`(与消息列表合并展示)
- 通知偏好散落在 `settings` 页 + `notifications/preferences.ts`
- **功能描述**
- 用户视角的"通知中心"整页:分页列出当前用户的站内通知(`message_notifications` 表),支持未读/已读筛选、归档、标记已读、全部已读、按优先级low/normal/high/urgent筛选
- 通知类型包括 `message` / `announcement` / `homework` / `grade` / `diagnostic` 五类
- 通知带 `link` 字段可点击跳转到来源页(如作业/成绩/公告详情)
- 顶栏铃铛仅展示最近 N 条 + 未读角标,整页才是完整列表
- **技术栈**
- Server Component + `Suspense` + `SectionErrorBoundary`(局部失败不影响其他区块)
- `notifications/data-access.ts``getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `archiveNotification` / `getUnreadNotificationCount`(均 `cacheFn` 包装tag=`notifications`TTL=60s
- Server Actions`notifications/actions.ts`384 行,标记已读/归档/批量操作)
- 客户端实时流:`use-notification-stream`SSE+ `use-desktop-notifications`(浏览器桌面通知)
- 表:`message_notifications`(站内通知本体)、`notification_preferences`(偏好)、`notification_logs`(发送日志)
- **ARCHITECTURE.md 契约**:✅ `notifications(userId)`§9.1 line 645B1 共享)
- portal-shell 已有 `GET_NOTIFICATIONS_LIST_DOC` GraphQL document`universal.graphql.ts`),但形状待 P1-7 修正(后端返回平铺数组,前端按 `{items, total}` 解析)
- `useNotifications` hook 已就绪(`lib/api/universal.ts`),可直接复用
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,权限点复用 `MESSAGE_READ`,无独立 `NOTIFICATION_READ`
- **优先级****B1**§9.1 line 645
---
### 2.2 `/shell/settings`(设置)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\settings\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 个人资料编辑表单(姓名/性别/年龄/手机/地址)+ 头像上传
- 通知偏好表单(`smsEnabled` / `emailEnabled` / `pushEnabled` + `homeworkNotifications` / `gradeNotifications` 等业务开关)
- 安全中心入口密码修改、最近登录记录、2FA、当前会话 User-Agent 标记)
- **角色差异化**:通过 `resolveRoleSettingsConfig(roles)` 注入 `descriptionKey` / `backHref` / `generalExtra`(如管理员额外展示学校信息/品牌配置/AI 提供商配置卡片)
- 子页 `/settings/security`:独立密码修改页 + 安全提示
- **技术栈**
- Server Component`requireAuth`,不要求特定权限点)+ `SettingsServiceProvider`Context 注入 `SettingsService` 接口)
- Server Actions`actions-service.ts``updateProfileAction` / `updateNotificationPreferencesAction` / `actions-avatar.ts` / `actions-password.ts` / `actions-security.ts` / `actions-brand.ts` / `actions-system-settings.ts`
- 数据源:`getUserProfile`users 模块)+ `getNotificationPreferences`notifications 模块)
- 角色配置:`modules/settings/config/role-settings-config.tsx`(按角色注入额外卡片)
- 客户端组件:`settings-view` / `profile-settings-form` / `notification-preferences-form` / `password-change-form` / `security-center-card` / `avatar-upload` / `theme-preferences-card` 等 22 个组件
- 工具:`totp.ts`2FA`security-utils.ts`(密码强度校验)
- **ARCHITECTURE.md 契约**§9.1 line 646B1 共享,契约字段未细化)
- portal-shell 当前无 settings 模块、无 `/shell/settings` 路由、无 `navigation.ts` 入口、无 `route-permissions.ts` 登记
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,按 `roles` 动态注入角色专属卡片)
- **优先级****B1**§9.1 line 646
---
### 2.3 `/shell/settings/security`(安全中心子页)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\settings\security\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 独立的密码修改页(`PasswordChangeForm`+ 安全提示卡片
-`settings` 主页的"安全中心卡片"互补,提供专注的改密体验
- **技术栈**
- `requireAuth` + `PasswordChangeForm`(客户端组件,调 `updatePasswordAction`
- 复用 `modules/settings` 模块
- **ARCHITECTURE.md 契约**:🟡 未显式列出,但属于 `settings` 共享子页§9.1 隐含)
- **多角色复用**:全角色
- **优先级**B1跟随 settings
---
### 2.4 `/shell/announcements`(公告列表)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\announcements\page.tsx`
- **功能描述**
- 用户端公告列表(非管理端),分页展示当前用户**可见**的公告school / grade / class 三类,按受众 `gradeIds[]` / `classIds[]` 数组过滤P0-2 多受众修复)
- 支持状态筛选(`published` / `draft` / `archived` / `all`),通过 `searchParams.status` 传递
- `AnnouncementList` + `AnnouncementPagination`(客户端构建分页 URL不传函数 prop
- 点击进入 `/announcements/[id]` 详情
- 管理端在 `/admin/announcements`(不在共享范围)
- **技术栈**
- Server Component + `requirePermission(ANNOUNCEMENT_READ)` + `AnnouncementsServiceProvider`Context
- `announcements/data-access.ts`603 行server-only`getUserAnnouncementsPageData(userId, dataScope, page, pageSize)` 受众过滤 + 分页
- `isAnnouncementVisibleToAudience` 纯函数(单测覆盖三类矩阵)
- 表:`announcements` + `announcement_reads`(已读回执独立表)
- 缓存:`cacheFn` + `invalidateFor` 双路径失效(列表 + 详情)
- **ARCHITECTURE.md 契约**§9.2 line 668"列表"契约未就绪B3
- portal-shell 的 `announcements-widget` 已下线P1-2数据源迁到 `adminDashboard` 聚合,**用户端列表契约完全缺失**
- **多角色复用**:教师 / 学生 / 家长(查看自己受众范围内的公告);管理员走 `/shell/admin/announcements` 管理端,不走共享列表
- **优先级****B3**§9.2 line 668
---
### 2.5 `/shell/announcements/[id]`(公告详情)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\announcements\[id]\page.tsx`
- **功能描述**
- 公告详情页,展示标题/内容/类型/发布时间/作者
- **P0-1 越权修复**`getAnnouncementByIdForUser(id, userId, dataScope)` 在 data-access 层结合受众与 status 过滤,不可见时统一返回 404不暴露公告存在性
- `canManage={false}`(用户端只读,管理操作在 `/admin/announcements/[id]/edit`
- 进入详情后可触发已读回执 `markAnnouncementAsReadAction`
- **技术栈**
- Server Component + `requirePermission(ANNOUNCEMENT_READ)` + `notFound()` 守卫
- `getAnnouncementByIdForUser` 受众过滤查询
- `AnnouncementDetail` 客户端组件 + `AnnouncementsServiceProvider`
- **ARCHITECTURE.md 契约**:🟡 `announcement(id)` 已存在但形状/可见性语义待对齐§9.2 line 668B3
- **多角色复用**:教师 / 学生 / 家长
- **优先级****B3**§9.2 line 668
---
### 2.6 `/shell/messages`(消息列表)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\messages\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- **CICD 把"消息列表"和"通知列表"合并展示在同一页**(消息在上、通知在下,各自独立 `Suspense` + `SectionErrorBoundary`,局部失败互不影响)
- 消息区:`MessageListSection`(按 `dataScope.type === "class_taught"` 决定是否显示"群发"按钮,即教师可群发全班家长)
- 通知区:`NotificationListSection`(调 `getNotifications(userId, {page:1, pageSize:20})`
- 子页 `compose`:撰写新消息 + 草稿列表(`getRecipients` + `getMessageDrafts`
- 子页 `[id]`:消息详情(`getMessageDetailPageData`,含撤回、星标、举报、附件)
- 子页 `group-compose`教师群发fan-out on write
- **技术栈**
- Server Component + `requirePermission(MESSAGE_READ)` + 双 `Suspense` 流式加载
- `messaging/data-access.ts`barrel拆分为 core/bulk/group/templates/reports 5 子文件)
- 表:`messages` / `message_drafts` / `message_templates` / `message_reports` / `user_blocks`
- Server Actions`sendMessageAction` / `recallMessageAction`2 分钟窗口服务端校验,不乐观更新)/ `sendGroupMessageAction`fan-out on write/ `reportMessageAction` / `blockUserAction`
- 乐观更新:`useOptimistic` 仅用于星标切换
- 收件人二次校验P0-1`isReceiverAllowed` 防 UI 绕过越权
- 通知集成:消息发送后调 `notifications/dispatcher.sendNotification` 多通道分发in-app/sms/email/wechat尊重偏好
- **ARCHITECTURE.md 契约**§9.2 line 669B3 末,契约完全缺失)
- portal-shell 当前无 `messaging` 模块、无 `/shell/messages` 路由
- msg 子图仅暴露 `notifications(userId)`,未暴露 messages 相关查询
- **多角色复用**:教师(可群发)/ 学生 / 家长 / 管理员(全角色私信,权限点 `MESSAGE_READ` / `MESSAGE_SEND`
- **优先级****B3 末**§9.2 line 669
---
### 2.7 `/shell/messages/compose` / `/shell/messages/[id]`(消息子页)
- **CICD 参考**
- `e:\desktop\CICD\src\app\(dashboard)\messages\compose\page.tsx` + `loading.tsx`
- `e:\desktop\CICD\src\app\(dashboard)\messages\[id]\page.tsx` + `loading.tsx`
- `e:\desktop\CICD\src\app\(dashboard)\messages\group-compose\page.tsx` + `loading.tsx`
- **功能描述**
- `compose`:撰写表单(收件人选择 + 主题 + 内容 + 附件)+ 草稿列表(`MessageDraftList` 支持恢复编辑/删除)+ 快捷模板选择器(`message-template-picker`
- `[id]`消息详情含撤回2 分钟窗口)、星标、举报、屏蔽、附件预览
- `group-compose`:教师选择班级 → fan-out 群发全班家长(共享 `groupMessageId`
- **技术栈**:同 §2.6
- **ARCHITECTURE.md 契约**§9.2 line 669 隐含B3 末)
- **多角色复用**:教师(含群发)/ 学生 / 家长 / 管理员
- **优先级**B3 末
---
### 2.8 `/shell/profile`(个人资料)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\profile\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 个人资料展示页(只读,编辑入口跳转到 `/settings`
- 头像上传区(`AvatarUpload`,独立 `SectionErrorBoundary`
- 个人信息卡:姓名 / 性别 / 年龄 / 手机 / 地址
- 账户信息卡:邮箱 / 角色 Badge / 注册时间 / Onboarded 时间
- **角色差异化概览**`resolveProfileOverviewType(roles)` 决定底部展示 `ProfileStudentOverview`(学生学业概览)或 `ProfileTeacherOverview`(教师概览),均带 `Suspense` 骨架屏
- **技术栈**
- Server Component + `requireAuth`(无特定权限点)+ `getUserProfile(userId)` + `redirect("/login")` 守卫
- 客户端组件:`AvatarUpload` / `ProfileStudentOverview` / `ProfileTeacherOverview`
- 角色配置:`modules/settings/config/profile-overview-config.ts`
- UI`PageHeader` + `Card` + `Badge` + `Button` + `lucide-react` 图标
- **ARCHITECTURE.md 契约**:🟡 未在 §9 显式列出独立 `/shell/profile` 行,但 §9.5 计数 ~7 页共享 + CICD 有 `profile/page.tsx`,推断属于共享路由隐含项
- portal-shell 当前无 `/shell/profile` 路由、无 settings 模块profile 组件归属 settings 模块)
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,按 `resolveProfileOverviewType` 切换底部概览)
- **优先级**B1/B3跟随 settings建议 B1 一并实现)
---
### 2.9 /register注册页
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\register\page.tsx`RSC13 行,渲染 `<RegisterForm registerAction={registerAction} />`);布局 `e:\desktop\CICD\src\app\(auth)\layout.tsx` 注入品牌配置(`getBrandConfig()`,失败回退默认)
- **功能描述**:账号注册页。表单字段:姓名 / 邮箱 / 密码 / 出生年月日 / 邀请码(可选,提供时覆盖默认 student 角色)。未成年人保护:根据 `birthDate` 计算 `age``< 18` 触发监护人信息区(监护人姓名 / 电话 / 关系下拉)+ 监护人同意复选框。必勾"同意《隐私政策》与《用户协议》"复选框(链接到 `/privacy``/terms``target="_blank"` 新窗打开)。注册成功 → `router.push("/login")` 跳登录页
- **技术栈**RSC + `RegisterForm` 客户端组件(`modules/auth/components/register-form.tsx`+ `registerAction` Server Action`modules/auth/actions`+ `next-intl``useTranslations("auth.register")`+ shadcn UIButton / Input / Label / Checkbox / Select+ `notify` toast + `lucide-react` Loader2
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/register` 路由、无 auth 模块(仅有 `api/auth/login` + `api/auth/logout` 两个 Route Handler注册走 iam 后端但前端页面与 Server Action 完全缺失
- **多角色复用**:公开页(未登录用户),不涉及角色权限
- **优先级**B5 末
---
### 2.10 /privacy隐私政策
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\privacy\page.tsx`RSC纯静态内容
- **功能描述**隐私政策页K12 教育场景7 个章节:引言 / 信息收集说明(账户 / 身份 / 未成年人保护 / 学习数据 / 设备)/ 信息使用说明 / 信息保护措施bcrypt + AES + RBAC + DataScope + HTTPS/ 用户权利(查询 / 更正 / 删除 / 撤回同意 / 可携带)/ Cookie 政策 / 未成年人保护条款(未满 14 周岁须监护人陪同注册)/ 联系方式。底部"返回注册"链接到 `/register`
- **技术栈**RSC + `next/link` + shadcn UICard / CardHeader / CardTitle / CardDescription / CardContent+ `Metadata`SEO+ 纯静态文案(无 DB / 无 Server Action / 无客户端交互)
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/privacy` 路由;属纯静态页,无后端契约依赖,仅需补页面 + i18n 文案
- **多角色复用**:公开页(未登录用户,注册页链接到它)
- **优先级**B5 末
---
### 2.11 /terms服务条款
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\terms\page.tsx`RSC纯静态内容
- **功能描述**用户服务协议页7 个章节:引言 / 服务说明 / 用户注册(真实邮箱 + 未成年人监护人陪同 + 同意隐私政策与本协议)/ 用户行为规范 / 知识产权 / 免责声明AI 生成内容仅供参考,可能存在偏差)/ 服务变更中断与终止 / 法律适用与争议解决(中华人民共和国法律)。底部"返回注册"链接到 `/register`
- **技术栈**:同 §2.10RSC + next/link + shadcn Card + Metadata + 纯静态文案,无 DB / 无 Server Action
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/terms` 路由;纯静态页,无后端契约依赖
- **多角色复用**:公开页(未登录用户,注册页链接到它)
- **优先级**B5 末
---
### 2.12 /onboarding首次登录引导
- **CICD 参考实现**
- 页面:`e:\desktop\CICD\src\app\(onboarding)\onboarding\page.tsx`RSC + auth guard
- 路由组 loading`e:\desktop\CICD\src\app\(onboarding)\loading.tsx`(骨架屏,匹配 stepper 卡片布局)
- 模块:`e:\desktop\CICD\src\modules\onboarding\`data-access / actions / schema / components / hooks / types
- ⚠️ `e:\desktop\CICD\src\app\api\onboarding\status\route.ts`**@deprecated**(返回 410 Gone已迁移到 Server Action `getOnboardingStatusAction`,保留仅为兼容性指示)
- 注:`(onboarding)` 路由组下有 `loading.tsx`(组根级骨架屏)+ `onboarding/page.tsx`(子目录页),并非"无 page.tsx"
- **功能描述**:首次登录引导页。`getAuthContext()` 鉴权(未登录捕获 `PermissionDeniedError``redirect("/login")`)→ 读 DB `users.onboardedAt` 判断是否需要引导(`getOnboardingStatus`DB 为唯一真相源,废弃 `session.user.onboarded` 冗余检查)→ 不需要则 `redirect("/dashboard")` → 需要则渲染 `OnboardingStepper``Suspense` 包裹,支持 `useSearchParams` URL query 持久化步骤)。按角色差异化步骤:
- 通用:更新基础资料(姓名 / 电话 / 住址)
- 学生:通过邀请码绑定班级(`enrollStudentByInvitationCode`
- 教师:通过邀请码绑定任课科目(`enrollTeacherByInvitationCode`P0-3 多选循环绑定,修复 UI 多选但服务端只取第一个的 bug
- 家长:三因子验证绑定子女(邮箱 + 生日 YYYY-MM-DD + 手机后 4 位,组合空间 365×10000=3.65M 防枚举;支持多子女循环绑定 P1-4
- **技术栈**
- RSC + `getAuthContext()``PermissionDeniedError` → redirect /login+ `Suspense` + `OnboardingStepper` 客户端组件
- data-access`modules/onboarding/data-access.ts`server-only`getOnboardingStatus``cacheFn` 300s`users.onboardedAt` + `usersToRoles` + `roles`/ `getUserOnboardedAt`P0-5 幂等检查)/ `markUserOnboarded` / `updateUserProfile` / `bindParentToChild`(三因子验证 + 幂等)
- Server Actions`modules/onboarding/actions.ts``getOnboardingStatusAction` + `completeOnboardingAction`Zod `OnboardingSchema` 校验 / P0-5 服务端幂等已完成直接返回成功 / P1-2 局部失败收集不回滚整事务 / P0-4 审计日志 `onboarding.complete` / audit-P1-8 家长绑定速率限制每小时 5 次防三因子枚举 / `invalidateFor("onboarding.complete")` 缓存失效)
- 跨模块:`modules/classes/data-access``enrollStudentByInvitationCode` / `enrollTeacherByInvitationCode`,含校验)
- 表:`users.onboardedAt` / `usersToRoles` / `roles` / `parentStudentRelations`
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/onboarding` 路由、无 onboarding 模块iam 子图已有 `userProfile`,但 onboarding 状态查询(`onboardedAt`)与家长绑定子女(`parentStudentRelations`)契约需补;班级邀请码绑定依赖 classes 服务
- **多角色复用**:所有新注册的已登录用户(按角色差异化步骤:学生绑定班级码 / 教师绑定任课科目 / 家长绑定子女);管理员一般预分配 onboarded跳过引导
- **优先级**B5 末
---
## 三、按角色复用矩阵
| 共享页面 | 教师 | 学生 | 家长 | 管理员 | 备注 |
| ------------------------------- | ---- | ---- | ---- | ------ | ------------------------------------------------ |
| `/shell/notifications` | ✅ | ✅ | ✅ | ✅ | 全角色,复用 `MESSAGE_READ` 权限点 |
| `/shell/settings` | ✅ | ✅ | ✅ | ✅ | 全角色,`resolveRoleSettingsConfig` 注入角色卡片 |
| `/shell/settings/security` | ✅ | ✅ | ✅ | ✅ | 全角色,密码修改 + 安全提示 |
| `/shell/announcements` | ✅ | ✅ | ✅ | ⚠️ | 管理员走 `/shell/admin/announcements` 管理端 |
| `/shell/announcements/[id]` | ✅ | ✅ | ✅ | ⚠️ | 同上,管理员走管理端 |
| `/shell/messages` | ✅ | ✅ | ✅ | ✅ | 全角色私信;教师额外有群发入口 |
| `/shell/messages/compose` | ✅ | ✅ | ✅ | ✅ | 全角色可发起私信 |
| `/shell/messages/[id]` | ✅ | ✅ | ✅ | ✅ | 全角色可查看自己参与的消息线程 |
| `/shell/messages/group-compose` | ✅ | ❌ | ❌ | ⚠️ | 仅教师(按所教班级)+ 管理员 |
| `/shell/profile` | ✅ | ✅ | ✅ | ✅ | 全角色,底部概览按角色切换 |
---
## 四、契约就绪情况ARCHITECTURE.md §9.5
| 共享页 | 契约状态 | 后端子图 | 备注 |
| ---------------------------------- | -------- | --------- | --------------------------------------------------------------------------------- |
| `/shell/notifications` | ✅ | msg | `notifications(userId)` 已就绪;形状待 P1-7 修正(平铺数组 vs `{items,total}` |
| `/shell/settings` | ✅ | iam / msg | `userProfile` + `notificationPreferences` 已就绪 |
| `/shell/settings/security` | ✅ | iam | 密码修改走 iam2FA 走 iam |
| `/shell/announcements` | ❌ | content | 列表契约缺失,需 content 子图补 `announcements(userScope)` 查询 |
| `/shell/announcements/[id]` | 🟡 | content | `announcement(id)` 存在,可见性语义需对齐 |
| `/shell/messages` | ❌ | msg | messages 查询契约完全缺失,需 msg 子图补 `messages(userId)` / `messageThread(id)` |
| `/shell/messages/compose` / `[id]` | ❌ | msg | 同上,依赖 messages 契约 |
| `/shell/profile` | ✅ | iam | `userProfile` 已就绪;角色概览查询待补(学生/教师) |
> 节奏原则§9.5):契约就绪页先行;❌ 页用 MSW 先上 UI契约工单跟踪后端补齐后切换真实查询。
---
## 五、portal-shell 现状盘点(已具备 / 待补齐)
### 5.1 已具备(可复用)
| 资产 | 路径 | 说明 |
| ----------------- | -------------------------------------------------- | ---------------------------------- |
| 通知铃铛 widget | `widgets/topbar/notification-bell/index.tsx` | 顶栏下拉,调 `useNotificationBell` |
| 通知列表 widget | `widgets/universal/notifications-widget/index.tsx` | 仪表盘内嵌,调 `useNotifications` |
| 通知 GraphQL 文档 | `lib/api/operations/universal.graphql.ts` | `GET_NOTIFICATIONS_LIST_DOC` |
| 通知 hook | `lib/api/universal.ts``useNotifications` | 形状待 P1-7 修正 |
| 顶栏通知 hook | `lib/api/topbar.ts``useNotificationBell` | 用于铃铛 |
| 403 页 | `app/shell/forbidden/page.tsx` | ✅ 已就绪 |
| 角色仪表盘分发 | `app/shell/page.tsx` | ✅ 已就绪(按角色 redirect |
### 5.2 待补齐(缺失)
| 缺失项 | 影响 |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `app/shell/notifications/page.tsx` + `error.tsx` + `loading.tsx` | 通知中心整页 |
| `app/shell/settings/page.tsx` + `error.tsx` + `loading.tsx` | 设置主页 |
| `app/shell/settings/security/page.tsx` + `error.tsx` + `loading.tsx` | 安全中心子页 |
| `app/shell/announcements/page.tsx` + `error.tsx` + `loading.tsx` | 公告列表 |
| `app/shell/announcements/[id]/page.tsx` + `error.tsx` + `loading.tsx` | 公告详情 |
| `app/shell/messages/page.tsx` + `error.tsx` + `loading.tsx` | 消息列表(含通知区) |
| `app/shell/messages/compose/page.tsx` + `loading.tsx` | 撰写消息 |
| `app/shell/messages/[id]/page.tsx` + `loading.tsx` | 消息详情 |
| `app/shell/messages/group-compose/page.tsx` + `loading.tsx` | 教师群发 |
| `app/shell/profile/page.tsx` + `error.tsx` + `loading.tsx` | 个人资料 |
| `modules/notifications/` 整模块 | 通知 data-access / actions / dispatcher / channels / hooks |
| `modules/messaging/` 整模块 | 消息 data-access / actions / components / hooks |
| `modules/announcements/` 整模块(用户端) | 公告 data-access / actions / components |
| `modules/settings/` 整模块 | 设置 components / actions / config / lib |
| `shared/lib/navigation.ts` 入口登记 | 5 个共享路由均未登记到 `NAVIGATION_ITEMS` |
| `shared/lib/route-permissions.ts` 权限登记 | 5 个共享路由均未登记到 `EXACT_ROUTE_PERMISSIONS` / `PREFIX_ROUTE_PERMISSIONS`,按 fail-closed 规则会被默认拒绝 |
| i18n key`messages/zh-CN.json` / `en.json` | `notifications.*` / `settings.*` / `announcements.*` / `messages.*` / `settings.profile.*` 文案键缺失 |
---
## 六、实施建议(按批次与优先级)
### 6.1 B1 优先(契约就绪,可直接接真实查询)
1. **`/shell/notifications`**:复用现有 `useNotifications` hook新增 page + error + loading同步登记 `EXACT_ROUTE_PERMISSIONS``requiredPermissions: ["MESSAGE_READ"]`,不限角色)+ `navigation.ts`(建议放在顶栏 / 通用区分组,但因 `NAVIGATION_ITEMS` 按角色 group需考虑通用入口的 group 归属——或只在顶栏铃铛加"查看全部"链接跳到 `/shell/notifications`
2. **`/shell/settings`** + **`/shell/settings/security`** + **`/shell/profile`**三者强关联profile 复用 settings 模块的 `AvatarUpload` / 角色概览组件),建议一次性补齐;需新建 `modules/settings/` 模块22+ 客户端组件 + Server Actions
### 6.2 B3契约缺失先用 MSW 上 UI
1. **`/shell/announcements`** + **`/shell/announcements/[id]`**:列表契约缺失,需先用 MSW mock `announcements(userScope)` 查询;`announcement(id)` 已存在但可见性语义需对齐
2. **`/shell/messages`** + 子页messages 契约完全缺失,需 msg 子图补 `messages(userId)` / `messageThread(id)` / `sendMessage` mutation 等;建议 B3 末实施
### 6.3 强制同步项(每次新增路由必做)
- `shared/lib/route-permissions.ts`:新增 `EXACT_ROUTE_PERMISSIONS` 条目(否则 fail-closed 拒绝)
- `shared/lib/navigation.ts`:如需侧边栏入口,新增 `NAVIGATION_ITEMS` 条目
- i18n`messages/zh-CN.json` + `messages/en.json` 补对应文案键
- `pnpm run arch:scan` 更新 arch.db按 project_rules §1 强制)
- 若架构设计意图变化,同步更新 `ARCHITECTURE.md` §9
---
## 七、关键风险与约束
1. **fail-closed 风险**`route-permissions.ts` §5 规定 `/shell/**` 下未登记路由默认拒绝。若只加 page.tsx 不登记权限,访问会被 middleware 重定向到 `/shell/forbidden`
2. **通知形状待修正**`notifications(userId)` 后端返回平铺数组,前端按 `{items, total}` 解析P1-7 待修。B1 实施时需同步修正 `universal.graphql.ts``universal.ts` 的类型,或后端补齐 `{items, total}` 包装。
3. **消息撤回不乐观更新**CICD 的 `recallMessageAction` 必须服务端校验 2 分钟窗口(`MESSAGE_RECALL_WINDOW_MS = 2 * 60 * 1000`),禁止 `useOptimistic` 乐观更新会导致超时撤回误成功。B3 实施时必须遵循此约束。
4. **公告可见性越权P0-1**`getAnnouncementByIdForUser` 必须在 data-access 层结合受众 + status 过滤,不可见时统一返回 404不暴露存在性。B3 实施时必须复刻此守卫。
5. **消息收件人二次校验P0-1**Server Action 必须调 `isReceiverAllowed` 校验 `receiverId` 在 sender 的 `DataScope` 内,防 UI 绕过越权。
6. **群发 fan-out on write**:群发时每条收件人写独立行,共享同一 `groupMessageId`,避免读时聚合。
7. **多 AI 模块边界**:按 project_rules §14.2,通知/消息/公告/设置分属不同模块,需协调 AI 分工;`shared-proto` 契约变更由协调 AI 负责。
8. **profile 归属 settings 模块**CICD 中 `profile` 页复用 `modules/settings/components/avatar-upload``profile-{student,teacher}-overview`,实施时不能跳过 settings 模块直接做 profile。
---
## 八、根路径重定向说明(隐含覆盖页)
| 路径 | 实现位置 | 行为 | CICD 对照 |
| --------- | ------------------------------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `/`(根) | `apps/portal-shell/src/app/page.tsx` | `redirect("/shell")` → 再由 `/shell` catch-all 按角色 redirect 到 `/shell/{role}` | CICD `src/app/page.tsx` 直接 `redirect("/dashboard")`(教师仪表盘,无角色分发层) |
> 说明portal-shell 比 CICD 多一层"角色分发"——根 `/` → `/shell` → `/shell/{role}`,因为 portal-shell 是多角色统一入口(教师 / 学生 / 家长 / 管理员共用一个 app而 CICD 默认进入教师仪表盘。此页属"隐含覆盖"(§一 总览未单列),已在 §一 `/shell` 行标注 CICD 参考路径。portal-shell `src/app/page.tsx` 当前已就绪(`redirect("/shell")`portal-shell spec §8.2 路由前缀 `/shell/*`),无需补齐。