按 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
375 lines
40 KiB
Markdown
375 lines
40 KiB
Markdown
# 共享路由(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` | 个人资料(缺失) |
|
||
|
||
> 备注 1:portal-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 645,B1 共享)
|
||
- 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 646,B1 共享,契约字段未细化)
|
||
- 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 668,B3)
|
||
- **多角色复用**:教师 / 学生 / 家长
|
||
- **优先级**:**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 669,B3 末,契约完全缺失)
|
||
- 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`(RSC,13 行,渲染 `<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 UI(Button / 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 UI(Card / 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.10(RSC + 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 | 密码修改走 iam,2FA 走 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/*`),无需补齐。
|