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
This commit is contained in:
SpecialX
2026-07-24 23:07:20 +08:00
parent 5a9f652943
commit 062d9e9582
394 changed files with 60468 additions and 118 deletions

View File

@@ -0,0 +1,374 @@
# 共享路由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/*`),无需补齐。