# portal-shell 模块架构文档 > 版本:2.0 > 日期:2026-07-17 > 状态:已落地(v2.1 M8-M12 完成 + v1.1 数据抽象与 GraphQL 加固完成 + v2.0 shadcn 标准化 + 三层安全边界 + 流式渲染 + 三级错误处理完成 + P0 全部验证通过:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功) > 架构范式:Modular Monolith + Micro-kernel(单 Next.js App Router 容器 + 插件化仪表盘) > 关联文档: > > - [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md) §1.2/§3/§4/§11.7/§16 > - [portal-shell 仪表盘 spec](../../docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md) v2.1 > - [portal-shell 数据抽象与 GraphQL 加固 spec](../../docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) v1.0 > - [项目规则](../../.trae/rules/project_rules.md) §3.10 设计令牌、§14 多 AI 协作 > - [UI 设计系统](../../docs/standards/ui-design-system.md) > - [known-issues §1.11/§2.17](../../docs/troubleshooting/known-issues.md) > - [GraphQL @auth 审计报告](../../docs/security/graphql-auth-audit-2026-07.md) --- ## 目录 1. [引言与目标](#1-引言与目标) 2. [架构约束](#2-架构约束) 3. [系统范围与上下文(C4 L1)](#3-系统范围与上下文c4-l1) 4. [解决方案策略](#4-解决方案策略) 5. [构建块视图(C4 L2/L3)](#5-构建块视图c4-l2l3) 6. [插件目录与分类](#6-插件目录与分类) 7. [运行时视图(关键场景)](#7-运行时视图关键场景) 8. [部署视图](#8-部署视图) 9. [横切概念](#9-横切概念) 10. [架构决策记录(ADR 索引)](#10-架构决策记录adr-索引) 11. [质量要求与验收](#11-质量要求与验收) 12. [风险、技术债与演进路线](#12-风险技术债与演进路线) 13. [数据访问层与 GraphQL 安全栈](#13-数据访问层与-graphql-安全栈) 14. [v2.0 安全边界与错误处理](#14-v20-安全边界与错误处理) 15. [术语表](#15-术语表) --- ## 1. 引言与目标 ### 1.1 模块定位 portal-shell 是 Edu 平台 v2.1 架构重设计后的**唯一前端入口**,以单 Next.js App Router 容器替代 v1.0 的 4 个独立 portal(teacher / student / parent / admin)+ Module Federation 微前端方案。它承载教师、学生、家长、管理员四类角色的全部教学场景 UI,通过 **Micro-kernel + 插件化** 机制实现功能扩展。 - **服务端口**:4010(HTTP) - **技术栈**(v2.0): - **核心**:TypeScript 5.6 + Next.js 16 App Router(Turbopack 默认)+ React 19(含 `use()` Hook 流式渲染) - **样式**:Tailwind v4(`@import "tailwindcss"` + `@theme inline`,无 `tailwind.config.js`)+ shadcn/ui 标准令牌(`--background` / `--foreground` / `--card` / `--primary` 等语义令牌) - **状态**:Zustand(UI 状态)+ SWR(配置静默刷新)+ Apollo Client(GraphQL) - **组件库**:shadcn/ui(Radix UI + cva + tailwind-merge + clsx) - **字体**:Inter 单一字体族(`next/font/google` self-host → `--font-inter` CSS 变量) - **架构风格**:Modular Monolith(单体)+ Micro-kernel(微内核插件) - **部署形态**:单 Docker 容器(`output: "standalone"`) - **上游依赖**:api-gateway(JWT 校验 + 反向代理)、apollo-router(GraphQL 联邦入口) - **下游契约**:通过 apollo-router 查询 7 个业务子图(iam / config-service / core-edu / content / msg / data-ana / ai) ### 1.2 设计目标 | # | 目标 | 衡量标准 | | --- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | G1 | **单服务部署**:一个 Dockerfile、一个容器,无 MF 远程加载 | 容器数 = 1;无运行时远程 bundle | | G2 | **配置驱动可见性**:admin 改配置 → 用户刷新生效,无需重新部署 | 配置变更感知延迟 ≤ 5 分钟(SWR refreshInterval) | | G3 | **首屏秒开**:RSC 服务端预取 Config + initialData,消除 CSR 瀑布流 | LCP < 2s(本地 Docker) | | G4 | **插件强隔离**:插件间禁止直接 import,仅通过 URL/Zustand 共享状态 | ESLint `no-restricted-imports` 强制;arch:scan 违规检测 | | G5 | **设计系统一致**:所有插件使用 shadcn 标准令牌,禁硬编码颜色/字体/字号 | ESLint `no-restricted-syntax` + `design-tokens/no-hardcoded-fonts` 零违规 | | G6 | **按需加载**:dynamic import 懒加载,首屏只加载可见 slot 插件 | 首屏 JS bundle ≤ 300KB(gzip) | | G7 | **跨角色复用**:universal 插件按 role 渲染不同视图,一套代码服务多角色 | universal 插件复用率 100% | | G8 | **流式渲染(v2.0)**:RSC 返回 Promise → React 19 `use()` 消费 + 三层 Suspense | 首屏 HTML 直出 Layout 骨架,Config 等数据流式注入 | | G9 | **三级错误兜底(v2.0)**:Route → Section → Widget 三级 ErrorBoundary | 单插件崩溃不污染同 Slot 其他插件;整页崩溃有 Route 兜底 | | G10 | **三层安全边界(v2.0)**:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 | 路由级 + 插件级 + 数据范围三级过滤,权限位图压缩 JWT 体积 ≥ 99% | | G11 | **shadcn 标准化(v2.0)**:废弃纸感令牌(paper/ink/accent),统一 bg-card/text-foreground 等 | 所有新代码 100% 使用 shadcn 标准令牌;旧 widget 令牌迁移在 P1 完成 | ### 1.3 非目标 - 不重写后端微服务、BFF、网关 - 不实现插件拖拽编辑器(canvas 模板预留接口,MVP 不做拖拽) - MVP 不实现第三方插件上传(二期,预留 iframe + postMessage 沙箱方案) - 不实现插件市场在线商店(二期仅支持本地 ZIP 上传) --- ## 2. 架构约束 ### 2.1 全局约束(来自项目规则) | 约束 | 来源 | portal-shell 落地方式 | | --------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- | | 禁硬编码颜色(`#hex`) | project_rules §3.10 | ESLint `no-restricted-syntax` + shadcn Tailwind 类(`bg-*` / `text-*`) | | 禁硬编码字体(`'Inter'`) | project_rules §3.10 | `next/font/google` self-host → `--font-inter` CSS 变量 | | 禁 Tailwind 任意值(`w-[Npx]`) | project_rules §3.10 | 映射到 Tailwind 默认阶梯(`p-4` / `gap-2` / `rounded-xl` 等) | | 函数返回值显式标注 | project_rules §3.4 | 所有插件 `React.ReactElement` / `Promise` | | 禁 `any` / `as` 断言 | project_rules §3.4 | `unknown` + 类型守卫;测试外不写 `as` | | Controller 权限装饰器 | project_rules §3.8 | portal-shell 无 Controller,权限由 api-gateway 注入 `x-user-role` | | ESM `.js` 后缀导入 | project_rules §3.4 | `next.config.js` webpack `extensionAlias` 映射 | | Tailwind v4 + shadcn 标准(v2.0) | project_rules §3.10 | `@import "tailwindcss"` + `@theme inline`,无 `tailwind.config.js`,使用 shadcn 语义令牌 | ### 2.2 模块边界(多 AI 协作) - **只修改**:`apps/portal-shell/` 目录 - **只读引用**: - `packages/shared-ts/src/contracts/`(plugin.ts / layout.ts / plugin-store.ts / plugin-context.ts) - `packages/ui-components/`、`packages/ui-tokens/`、`packages/hooks/` - `packages/shared-proto/proto/`(仅查询字段命名对齐) - **跨模块契约**:通过 apollo-router GraphQL 子图(不直接调 gRPC,不直接访问业务服务 DB) - **禁止**: - 直接 import 其他 `apps/*` 或 `services/*` 的源码 - 直接访问 MySQL / Redis / Kafka - 在插件间直接 import(必须通过 URL Search Params 或 Zustand Store 共享状态) ### 2.3 通信约束 | 调用方 → 被调用方 | 协议 | 场景 | | ------------------------------- | -------------- | ------------------------------------------------------------- | | portal-shell → api-gateway | HTTP 反向代理 | `/api/v1/*` 透传(JWT 校验 + 注入 `x-user-id`/`x-user-role`) | | portal-shell → apollo-router | HTTP / GraphQL | 所有业务查询与配置查询(M8 验收点) | | portal-shell → realtime-gateway | SSE | 通知推送(默认) | | portal-shell → realtime-gateway | WebSocket | 监考双向场景(仅此场景用 WS) | **禁止**:portal-shell 直连业务服务 gRPC 或 DB。 --- ## 3. 系统范围与上下文(C4 L1) ### 3.1 Context 图 ```mermaid graph TB subgraph Users["用户"] Teacher["教师"] Student["学生"] Parent["家长"] Admin["管理员"] end subgraph PortalShell["portal-shell :4010"] Shell["插件化仪表盘
Modular Monolith + Micro-kernel"] end subgraph Upstream["上游服务"] Gateway["api-gateway :8080
JWT 校验 + 反向代理"] Router["apollo-router :3000
GraphQL 联邦入口"] Realtime["realtime-gateway :8081
SSE 推送"] end subgraph Subgraphs["业务子图(经 Router 聚合)"] IAM["iam"] Config["config-service"] CoreEdu["core-edu"] Content["content"] Msg["msg"] DataAna["data-ana"] AI["ai"] end Teacher --> Shell Student --> Shell Parent --> Shell Admin --> Shell Shell -->|HTTP /api/v1/*| Gateway Shell -->|GraphQL 查询| Router Shell -->|SSE 通知| Realtime Gateway -->|JWT 校验 + 注入头| Shell Router --> IAM Router --> Config Router --> CoreEdu Router --> Content Router --> Msg Router --> DataAna Router --> AI ``` ### 3.2 外部契约 | 契约 | 提供方 | 消费方式 | 说明 | | -------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------- | ---------------------------- | | `pluginConfig(userId, role)` | config-service 子图 | RSC 服务端查询 + SWR 客户端静默刷新 | 三层合并后的插件配置 | | `pluginRegistry` / `rolePluginMapping` / `layoutTemplates` / `roleLayoutDefault` | config-service 子图 | admin 配置面板(plugin-manager 插件) | admin CRUD | | `resetUserLayoutOverride(userId)` | config-service 子图 | admin 重置用户布局 | mutation | | 业务查询(grades / homework / schedule 等) | core-edu / content / msg 等子图 | 各 widget 插件 `useWidgetQuery` | 经 apollo-router 自动路由 | | `x-user-id` / `x-user-role` 请求头 | api-gateway | RSC `headers()` 读取 | JWT 校验后注入 | | `x-user-permissions`(v2.0 位图头) | api-gateway | RSC `headers()` 读取 | 67 权限点 base36 压缩串 | | JWT | iam 签发 → api-gateway 校验 | Apollo Client `Authorization: Bearer` | localStorage + cookie 双通道 | ### 3.3 三层安全边界(v2.0 新增) > 来源:[三层安全边界设计](../../docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) §v2.0 > 关联:[权限位图工具](../../packages/shared-ts/src/permission-bitmap.ts)、[路由权限配置](../../apps/portal-shell/src/shared/lib/route-permissions.ts) ```mermaid graph LR subgraph Request["用户请求"] R["路由进入
/shell/*"] end subgraph L1["L1 角色门禁"] L1Check["checkRoutePermission
requiredRoles?"] end subgraph L2["L2 权限点门禁"] L2Check["位图校验
requiredPermissions?"] end subgraph L3["L3 数据范围"] L3Check["DataScope 6 级
config-service 过滤"] end subgraph Plugin["插件渲染"] P["SlotRenderer 信任输入
不再二次过滤"] end R --> L1Check L1Check -->|角色通过| L2Check L1Check -->|角色拒绝| Deny["403 Forbidden"] L2Check -->|权限通过| L3Check L2Check -->|权限拒绝| Deny L3Check -->|数据范围过滤| Plugin L3Check -->|无可见数据| Empty["空状态"] ``` **三层职责矩阵**: | 层 | 位置 | 实现机制 | 触发时机 | 失败行为 | | ----------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------- | | **L1 角色门禁** | `src/shared/lib/route-permissions.ts` | `EXACT_ROUTE_PERMISSIONS` / `PREFIX_ROUTE_PERMISSIONS` 等四张表按优先级匹配,`requiredRoles?: Role[]` | 路由进入(middleware 或 RSC) | 重定向 403 | | **L2 权限点门禁** | `src/shared/lib/route-permissions.ts` + `@edu/shared-ts/permission-bitmap` | `requiredPermissions?: string[]`(AND 语义)/ `anyOfPermissions?: string[]`(OR 语义),从 JWT 头读取 base36 位图解码 | 路由进入 + 插件 Manifest 校验 | 重定向 403 或不渲染该插件 | | **L3 数据范围** | config-service GraphQL 子图 | DataScope 6 级(school / grade / class / subject / student / self),三层合并时过滤可见插件集 | RSC 服务端拉取 Config | 不渲染无可见数据的插件 | **关键约束**: 1. **路由权限配置集中管理**:4 张表(精确路由 / 前缀路由 / 仪表盘路由 / API 路由)按优先级匹配,新增路由必须在表中登记 2. **L2 权限点必须来自 PERMISSION_BITMAP_ORDER(67 个权限点)**:开发时通过 `validateRoutePermissionConfigs()` 校验合法性 3. **SlotRenderer 信任输入**:L1/L2/L3 三层过滤在 RSC 服务端完成,客户端 SlotRenderer 不再二次过滤(性能优化) 4. **权限位图压缩 JWT 体积**:67 权限点 → base36 字符串(~14 字符)替代 JSON 数组(~600 字符),体积减少 ≥ 99% 5. **批量检查 API**:`batchCheckRoutePermission(paths, bitmap, role)` 用于侧边栏导航批量过滤 **路由权限配置示例**: ```typescript // src/shared/lib/route-permissions.ts export const EXACT_ROUTE_PERMISSIONS: RoutePermissionConfig[] = [ { path: "/shell/admin/users", requiredRoles: ["admin"], requiredPermissions: ["user.read"], // AND 语义:必须同时拥有 }, { path: "/shell/teacher/grades", requiredRoles: ["teacher", "admin"], anyOfPermissions: ["grade.read", "grade.write"], // OR 语义:拥有其一即可 }, ]; export const PREFIX_ROUTE_PERMISSIONS: RoutePermissionConfig[] = [ { path: "/shell/admin/", requiredRoles: ["admin"], }, ]; ``` --- ## 4. 解决方案策略 ### 4.1 核心策略:Micro-kernel + Plugin Registry ``` ┌──────────────────────────────────────────────────────────┐ │ Shell(微内核,纯宿主,无业务逻辑) │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │ │ LayoutMgr │ │ SlotRend │ │ PluginLoader │ │ │ │ 5 模板 │→ │ 按slot分发 │→ │ dynamic import │ │ │ └────────────┘ └────────────┘ │ + ErrorBoundary │ │ │ └────────────────────┘ │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │ │ Registry │ │ PropsMerge │ │ PluginStore │ │ │ │ 31 插件 │ │ 三层合并 │ │ Zustand UI 状态 │ │ │ └────────────┘ └────────────┘ └────────────────────┘ │ └──────────────────────────────────────────────────────────┘ ↑ PluginProps 契约 ↑ ┌──────────────────────────────────────────────────────────┐ │ Widgets(31 内置插件,按 7 分类组织) │ │ universal / sidebar / topbar / teacher / student / ... │ └──────────────────────────────────────────────────────────┘ ``` **关键决策**: 1. **Shell 是纯宿主**:不含任何业务逻辑,只负责 Layout 框架、Config 下发、Registry 查表、Loader 挂载 2. **插件即卡片**:所有功能单元统一为"插件",无"卡片"与"插件"之分 3. **编译时 Registry + 运行时 Config**:Registry 是 `plugin_id → dynamic import` 静态映射(编译时登记),Config 是"当前用户在哪些 slot 渲染哪些插件"的运行时配置(DB 三层合并) 4. **RSC 服务端预取**:Shell 在 Server Component 中完成 Config 拉取 + initialData 预取,随 HTML 直出 5. **dynamic import 懒加载**:插件按需加载,首屏只加载可见 slot 的插件 6. **强隔离**:插件间禁止直接 import,通过 URL Search Params(可分享)+ Zustand(纯 UI)共享状态 ### 4.2 数据流分层 ``` URL Search Params ← 可分享、可前进后退的全局上下文(classId/childId/termId/view) Zustand Store ← 纯 UI、不可分享(theme/locale/sidebarCollapsed) 插件内部 useState ← 插件私有状态(表单临时值、弹窗开关) PluginProps.props ← 三层合并后的配置 props(系统默认 < 角色默认 < 用户调整) PluginProps.initialData ← RSC 服务端预取的初始数据(SWR fallbackData) ``` ### 4.3 配置刷新策略 抛弃 Kafka + WebSocket 推送链路(对低频布局变更属于过度设计),改用 **SWR 静默后台刷新**: - `revalidateOnFocus: true`:用户切回 Tab 时静默刷新 - `refreshInterval: 300_000`:每 5 分钟轮询 - `dedupingInterval: 60_000`:1 分钟内去重 - 检测到配置变化时回调上层 Toast 提示"发现新布局配置,刷新后生效" --- ## 5. 构建块视图(C4 L2/L3) ### 5.1 Container 图(C4 L2) ```mermaid graph TB subgraph App["apps/portal-shell(单 Next.js 容器)"] subgraph AppRouter["app/(Next.js App Router)"] Layout["layout.tsx
RootLayout + Inter 字体"] RootPage["page.tsx
重定向 /shell"] ShellPage["shell/[[...route]]/page.tsx
RSC 入口 + 流式 Promise"] ShellError["shell/error.tsx
Route 错误兜底(v2.0)"] ShellLoading["shell/loading.tsx
Route 加载骨架(v2.0)"] HealthAPI["api/health/route.ts"] ReadyAPI["api/ready/route.ts"] LogAPI["api/log/route.ts
错误上报 mock 端点(v2.0)"] end subgraph ShellCore["shell/(微内核)"] Shell["Shell.tsx"] ClientShell["ClientShell.tsx
use(configPromise) 流式"] LayoutMgr["LayoutManager.tsx
5 Layout 模板"] SlotRend["SlotRenderer.tsx
PluginBoundary 包裹"] Loader["PluginLoader.tsx
re-export 向后兼容"] Registry["Registry.tsx
31 插件"] Lifecycle["PluginLifecycle.ts"] Store["PluginStore.ts
Zustand"] Merger["PropsMerger.ts
三层合并"] end subgraph Lib["lib/(数据抽象)"] Apollo["apollo-client.ts
APQ + sha256"] ConfigFetch["config-fetcher.ts"] UseConfig["usePluginConfig.ts
SWR 静默刷新"] UseQuery["useWidgetQuery.ts"] UseMut["useWidgetMutation.ts"] Types["types.ts"] end subgraph ApiLayer["lib/api/(数据访问层)"] ApiDomain[".ts ×7
parent/teacher/admin/student/
universal/sidebar/topbar"] ApiOps["operations/*.graphql.ts
51 DocumentNode"] ApiTypes["operations/types.ts
codegen 生成"] ApiErrors["errors.ts
ApiError 归一化"] end subgraph Shared["shared/(v2.0 共享层)"] SharedLib["shared/lib/
route-permissions.ts
notify.ts / utils.ts"] SharedComp["shared/components/
plugin-boundary.tsx
route-error-boundary.tsx
section-error-boundary.tsx
dashboard/* / layout/* / ui/*"] SharedHooks["@edu/hooks
use-error-report.ts"] end subgraph Providers["providers/"] ApolloProv["ApolloProvider.tsx"] AuthProv["AuthProvider.tsx"] ThemeProv["ThemeI18nProvider.tsx"] end subgraph Widgets["widgets/(31 内置插件)"] Universal["universal/
7 插件"] Sidebar["sidebar/
4 插件"] Topbar["topbar/
4 插件"] Teacher["teacher/
4 插件"] Student["student/
4 插件"] Parent["parent/
2 插件"] Admin["admin/
6 插件"] end end ShellPage -->|RSC 调用| ConfigFetch ShellPage -->|configPromise| ClientShell ClientShell -->|use()| Shell Shell --> LayoutMgr LayoutMgr --> SlotRend SlotRend --> SharedComp SlotRend --> Registry SharedComp --> Widgets ClientShell --> UseConfig UseConfig --> Apollo Widgets --> ApiDomain ApiDomain --> ApiOps ApiDomain --> UseQuery ApiDomain --> UseMut ApiOps --> ApiTypes UseQuery --> Apollo UseMut --> Apollo SharedComp -.->|onError| LogAPI SharedLib -.->|权限校验| SharedLib ``` ### 5.2 组件职责矩阵 | 层 | 文件 | 职责 | 关键契约 | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------- | | **app/** | `shell/[[...route]]/page.tsx` | RSC 入口,服务端拉 Config + 流式注入 Promise | `fetchPluginConfig(userId, role)`(v2.0 不 await,直接传 Promise) | | **app/** | `shell/error.tsx`(v2.0) | Route 级错误兜底 | Next.js `error.tsx` + `RouteErrorBoundary` | | **app/** | `shell/loading.tsx`(v2.0) | Route 级加载骨架 | 整页骨架(顶栏 + 侧栏 + 主区仪表盘骨架) | | **app/** | `api/log/route.ts`(v2.0) | 错误上报 mock 端点 | POST `sendBeacon` 数据,开发态日志输出 | | **app/** | `layout.tsx` | RootLayout,挂载 `next/font/google` Inter 字体变量 | `--font-inter` CSS 变量 | | **app/** | `page.tsx` | 根路径重定向到 `/shell` | `redirect("/shell")` | | **shell/** | `Shell.tsx` | 微内核入口,选择 LayoutManager 模板 | `config.activeLayout.layoutId` | | **shell/** | `ClientShell.tsx` | 客户端入口,`use(configPromise)` 流式 + SWR 刷新 | `ShellContent` + `LegacyShell` 双模式(v2.0) | | **shell/** | `LayoutManager.tsx` | 5 种 Layout 模板渲染器 | classic / focus / split / triple / canvas | | **shell/** | `SlotRenderer.tsx` | 按 slot 过滤+排序+查表+PluginBoundary 包裹 | `PluginPlacement[]`(v2.0 信任输入,不再二次过滤权限) | | **shell/** | `PluginLoader.tsx` | re-export 文件,向后兼容(v2.0 已迁移到 PluginBoundary) | `PluginSkeleton` 5 变体 | | **shell/** | `Registry.tsx` | 编译时登记 31 内置插件 | `plugin_id → { Component, metadata }` | | **shell/** | `PluginLifecycle.ts` | 版本兼容性 + 激活路径 + 可渲染判断 | `checkVersionCompatibility` / `isPluginRenderable` | | **shell/** | `PluginStore.ts` | Zustand 全局 UI 状态 | theme / locale / sidebarCollapsed | | **shell/** | `PropsMerger.ts` | 三层 props 深合并 | `mergeProps(sys, role, user)` | | **shared/lib/** | `route-permissions.ts`(v2.0) | 路由权限配置表 + L1/L2 检查 | `checkRoutePermission` / `batchCheckRoutePermission` / `validateRoutePermissionConfigs` | | **shared/lib/** | `notify.ts`(v2.0) | 统一 Toast 封装(禁止业务直接 import sonner) | `notify.info/success/warning/error` | | **shared/lib/** | `utils.ts`(v2.0) | `cn()` 工具函数(clsx + tailwind-merge) | shadcn/ui 标准工具 | | **shared/components/** | `plugin-boundary.tsx`(v2.0) | 插件级错误边界 + Suspense + Skeleton 三件套 | `PluginBoundary` / `PluginSkeleton`(5 变体)/ `PluginErrorFallback` | | **shared/components/** | `route-error-boundary.tsx`(v2.0) | Route 级错误边界 | Next.js `error.tsx` 内部使用 | | **shared/components/** | `section-error-boundary.tsx`(v2.0) | Section 区块级错误边界 | DashboardSection 内部使用 | | **shared/components/dashboard/** | `dashboard-shell.tsx` / `dashboard-section.tsx`(v2.0) | 仪表盘外壳 + 分区组件 | 仪表盘场景专用 | | **shared/components/layout/** | `sidebar-provider.tsx` / `app-sidebar.tsx` / `site-header.tsx`(v2.0) | 布局组件 | SidebarContext + 桌面折叠 + 移动 Sheet | | **shared/components/ui/** | `button.tsx` / `card.tsx` / `badge.tsx` / `skeleton.tsx` / `input.tsx` / `tooltip.tsx` / `sonner.tsx` / `page-header.tsx` / `stat-card.tsx` / `stats-grid.tsx` / `empty-state.tsx` / `filter-bar.tsx`(v2.0) | shadcn/ui 组件库 | 基于 Radix UI + cva + tailwind-merge | | **lib/** | `apollo-client.ts` | Apollo Client 单例(客户端)+ 工厂(服务端)+ APQ 链 | `getApolloClient()` / `createApolloClient()`,`createPersistedQueryLink({ sha256 })` | | **lib/** | `config-fetcher.ts` | RSC 服务端拉 Config,含三级降级 | apollo-router → config-service 直连 → 空默认 | | **lib/** | `usePluginConfig.ts` | SWR 静默刷新配置 + 变更检测回调 | `revalidateOnFocus` + `refreshInterval: 300_000` | | **lib/** | `useWidgetQuery.ts` | 统一 GraphQL 查询 Hook,支持 fallbackData | `useQuery` + `skip` + `pollInterval` | | **lib/** | `useWidgetMutation.ts` | 统一 GraphQL 变更 Hook | `useMutation` + `errorPolicy: "all"` | | **lib/** | `types.ts` | 共享类型契约 | `PluginProps` / `PluginManifest` / `PluginConfigResponse` | | **lib/api/** | `.ts ×7` | 语义化函数 API 层 | `useMyChildren()` / `saveLessonPlan()` 等,禁止 widget 直接写 gql | | **lib/api/** | `operations/*.graphql.ts` | gql DocumentNode 集中存放 | 51 个 query/mutation 常量 | | **lib/api/** | `operations/types.ts` | codegen 生成的 TS 类型 | 从 7 子图 schema.graphql 生成 | | **lib/api/** | `errors.ts` | ApiError 归一化 | `toApiError(graphQLErrors)` 统一错误模型 | | **providers/** | `ApolloProvider.tsx` | 注入 Apollo Client 单例 | `getApolloClient(readToken)` | | **providers/** | `AuthProvider.tsx` | 提供 `useAuth()`,从 RSC props 下发用户身份 | `AuthUser` context | | **providers/** | `ThemeI18nProvider.tsx` | 主题类名同步到 `` + 简易 i18n | `usePluginStore` theme/locale | | **widgets/** | `*/index.tsx` | 插件入口,接收 `PluginProps`,default export | `PluginProps` 契约 | | **widgets/** | `*/plugin.manifest.ts` | 插件元数据声明 | `manifestMeta: Omit`(v2.0 含 `requiredPermissions?`) | ### 5.3 Layout 模板清单 | layoutId | 显示名 | 布局 | 可用 slots | 适用场景 | | --------- | -------- | ------------------------------------------------- | ---------------------------- | -------------- | | `classic` | 经典三栏 | TopBar + SideNav + Main | top / side / main | 默认(仪表盘) | | `focus` | 聚焦 | TopBar + 全宽 Main | top / main | 备课/编辑器 | | `split` | 双栏 | TopBar + 左右等分 Main | top / main-left / main-right | 对比/批改 | | `triple` | 三栏内容 | TopBar + SideNav + Main + RightRail | top / side / main / right | 数据分析 | | `canvas` | 自由画布 | TopBar + 自由摆放(MVP 按 grid 排列,不实现拖拽) | top / canvas-grid | 个性化 | ### 5.4 三层配置模型 **优先级**:用户覆盖 > 角色模板 > 系统默认 ``` Layer 1: 系统默认(plugin_registry 表) ← 开发者在 plugin.manifest.ts 声明 defaultProps ← 构建时由 sync-builtin-plugins.ts 同步到 DB Layer 2: 角色模板(role_plugin_mapping 表) ← admin 通过 plugin-manager 插件配置角色可用插件集 + 角色级 props Layer 3: 用户覆盖(user_layout_override 表) ← 用户切换 Layout、调整插件位置、隐藏插件、调整 props ← 仅限角色可用集内操作 ``` **Props 合并算法**(`PropsMerger.mergeProps`): - 普通对象递归合并(深合并) - 数组、原始值后者覆盖前者 - `null` / `undefined` 跳过 ### 5.5 流式渲染(v2.0 新增) > 来源:[React 19 use() + Suspense 流式渲染](https://react.dev/reference/react/use) > 关联:`apps/portal-shell/src/app/shell/[[...route]]/page.tsx` + `apps/portal-shell/src/shell/ClientShell.tsx` ```mermaid sequenceDiagram participant U as 用户浏览器 participant N as Next.js RSC participant R as apollo-router participant C as config-service U->>N: GET /shell N->>N: headers() 读取 x-user-id / x-user-role / x-user-permissions N->>R: query pluginConfig(userId, role)【不 await】 Note over N: RSC 返回 Promise,立即返回 HTML 骨架 N-->>U: HTML 流式输出(layout 骨架 + Suspense 占位) Note over U: 浏览器开始渲染骨架,不阻塞 R->>C: 路由到 config-service C-->>R: 三层合并后的 PluginConfigResponse R-->>N: Config Promise resolve N->>N: use(configPromise) 解析 N-->>U: 流式注入 Config 后的内容(SlotRenderer + PluginBoundary) Note over U: 插件按 dynamic import 流式加载(PluginBoundary 内 Suspense) U->>U: 每个插件独立 Suspense,加载完成后逐个渲染 ``` **三层 Suspense 边界**: | 层 | 位置 | Suspense fallback | 触发场景 | | ---------- | ----------------------------- | ------------------------------- | ---------------------------- | | **Route** | `shell/[[...route]]/page.tsx` | `shell/loading.tsx` 整页骨架 | Config Promise 未 resolve | | **Slot** | `LayoutManager.tsx` 内 | Slot 级骨架(按 slot 类型推导) | 整个 slot 数据未就绪 | | **Widget** | `PluginBoundary` 内 | `PluginSkeleton` 5 变体 | 单插件 dynamic import 未完成 | **ClientShell 流式拆分**: ```typescript // apps/portal-shell/src/shell/ClientShell.tsx function ShellContent({ configPromise, userId, role }: ShellContentProps): ReactNode { // use() 在 Suspense 边界内消费 Promise,自动暂停直到 resolve const resolvedConfig = use(configPromise); // SWR 后续静默刷新 const { config: liveConfig } = usePluginConfig({ initialConfig: resolvedConfig, userId, role, onChanged: () => { notify.info("发现新布局配置,刷新后生效"); }, }); return ; } export function ClientShell(props: ClientShellProps): ReactNode { return ( {/* Suspense 包裹 ShellContent,Provider 不被暂停 */} }> ); } ``` **关键约束**: 1. **Provider 必须在 Suspense 外**:ApolloProvider / AuthProvider / ThemeI18nProvider 不能被 `use()` 暂停,否则子树丢失 Context 2. **RSC 不 await Promise**:`fetchPluginConfig` 返回 Promise 直接传给客户端,服务端不阻塞 3. **客户端 `use()` 消费**:React 19 的 `use()` Hook 可在 Suspense 边界内消费 Promise 4. **单插件独立 Suspense**:每个插件用 `` 包裹,加载失败或慢不影响其他插件 ### 5.6 三级错误处理(v2.0 新增) > 关联:`shared/components/route-error-boundary.tsx` / `section-error-boundary.tsx` / `plugin-boundary.tsx` + `@edu/hooks/use-error-report.ts` ```mermaid graph TB subgraph Route["Route 级(最高优先级)"] REB["RouteErrorBoundary
app/shell/error.tsx"] RFallback["整页错误页
含错误 digest + 重试按钮"] end subgraph Section["Section 级"] SEB["SectionErrorBoundary
DashboardSection 内"] SFallback["区块级错误卡片
显示 Section 标题 + 重试"] end subgraph Widget["Widget 级(最细粒度)"] WEB["PluginBoundary
SlotRenderer 内每个插件"] WFallback["PluginErrorFallback
显示 instanceId + 重试"] end subgraph Report["错误上报链路"] Hook["useErrorReport
sendBeacon + sessionStorage 节流"] API["/api/log
mock 端点"] Store["sessionStorage
1 分钟同 digest 去重"] end REB --> RFallback SEB --> SFallback WEB --> WFallback RFallback -.->|onError| Hook SFallback -.->|onError| Hook WFallback -.->|onError| Hook Hook --> Store Hook -->|sendBeacon| API ``` **三级职责矩阵**: | 级别 | 组件 | 位置 | 触发场景 | Fallback | | ----------- | ---------------------- | ------------------------- | ------------------------------------------- | ------------------------------------------ | | **Route** | `RouteErrorBoundary` | `app/shell/error.tsx` | 整页崩溃(Shell 渲染失败、Provider 错误) | 整页错误页 + digest + 重试按钮 | | **Section** | `SectionErrorBoundary` | `DashboardSection` 内 | 区块级崩溃(Slot 渲染失败、聚合数据错误) | 区块级错误卡片(含 Section 标题) | | **Widget** | `PluginBoundary` | `SlotRenderer` 内每个插件 | 单插件崩溃(dynamic import 失败、组件抛错) | `PluginErrorFallback`(instanceId + 重试) | **错误上报链路**: ```typescript // @edu/hooks/use-error-report.ts export function useErrorReport() { return useCallback((error: Error, context?: Record) => { const digest = computeDigest(error); // sha256(message + stack) const key = `err:${digest}`; // 1. sessionStorage 节流:1 分钟内同 digest 不重复上报 if (sessionStorage.getItem(key)) return; sessionStorage.setItem(key, String(Date.now() + 60_000)); // 2. sendBeacon 异步上报(页面卸载也能发出) navigator.sendBeacon( "/api/log", JSON.stringify({ level: "error", message: error.message, stack: error.stack, digest, url: location.href, timestamp: Date.now(), context, }), ); }, []); } ``` **关键约束**: 1. **三级边界不可跳过**:每个 widget 必须用 `` 包裹,Section 必须用 ``,Route 必须有 `error.tsx` 2. **错误上报节流**:`sessionStorage` 1 分钟同 digest 去重,避免崩溃循环刷爆日志端点 3. **sendBeacon 优先**:页面卸载时也能发出请求,不阻塞 unload 4. **digest 唯一标识**:基于 `message + stack` 的 sha256,便于后端聚合相同错误 5. **mock 端点**:`/api/log` 当前为 Next.js API Route,仅开发态日志输出,生产由后端 `/api/v1/log` 替换 --- ## 6. 插件目录与分类 ### 6.1 内置插件全览(31 个) | 分类 | 数量 | 插入 slot | 跨角色 | 插件清单 | | ----------- | ---- | --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `universal` | 7 | main-* | 是 | grades-widget / homework-widget / schedule-widget / attendance-widget / exams-widget / notifications-widget / announcements-widget | | `sidebar` | 4 | side | 部分 | class-selector / child-selector / term-switcher / quick-actions | | `topbar` | 4 | top | 是 | notification-bell / user-menu / global-search / locale-switcher | | `teacher` | 4 | main | 否 | lesson-plan-editor / question-bank / textbook-manager / scheduling-rules | | `student` | 4 | main | 否 | error-book / learning-path / elective-selector / ai-tutor | | `parent` | 2 | main | 否 | child-overview / leave-approval | | `admin` | 6 | main | 否 | user-management / rbac-manager / plugin-manager / school-settings / audit-logs / invitation-codes | ### 6.2 插件契约(PluginProps) ```typescript interface PluginProps> { instanceId: string; // 插件实例 ID(同插件多实例时区分) role: "admin" | "teacher" | "student" | "parent"; user: { id: string; name: string; email: string; dataScope: string; // IAM DataScope 6 级 }; slot: { name: string; // 'main' | 'side' | 'top' | 'main-left' | ... layoutId: string; // 'classic' | 'focus' | ... size?: { colSpan: number; rowSpan: number }; }; props: TProps; // 三层合并后的最终 props initialData?: unknown; // RSC 服务端预取的初始数据 } ``` ### 6.3 插件 Manifest 契约 每个插件通过 `plugin.manifest.ts` 声明元数据(v2.0 新增 `requiredPermissions` 字段): ```typescript export const manifestMeta: Omit = { pluginId: "grades-widget", version: "0.1.0", requiredShellVersion: "^1.0.0", // semver range,Shell 启动时校验 metadata: { displayName: "成绩", description: "查看班级成绩", category: "universal", requiredRoles: ["teacher", "student", "parent"], requiredPermissions: ["grade.read"], // v2.0 新增:L2 权限点门禁(AND 语义) defaultSlot: "main", defaultSize: { colSpan: 2, rowSpan: 1 }, defaultProps: { limit: 20 }, propsSchema: { // JSON Schema,admin 配置面板自动渲染表单 type: "object", properties: { limit: { type: "number", description: "显示条数" }, }, }, }, }; ``` **v2.0 `requiredPermissions` 字段说明**: - **空数组或 undefined**:仅 L1 角色门禁生效 - **非空数组**:用户必须同时拥有所有权限点(**AND 语义**) - **权限点必须来自 `PERMISSION_BITMAP_ORDER`**(67 个权限点),运行时由 `isValidPermission` 校验 - **配合路由权限表**:路由级 L2 由 `route-permissions.ts` 检查,插件级 L2 由 Manifest 检查(config-service 三层合并时过滤) ### 6.4 插件生命周期 | 阶段 | 内置插件 | 第三方插件(二期) | | ------------- | ------------------------------ | ------------------------------------- | | `registered` | 编译时登记到 Registry | 安装时登记到 DB(plugin_packages 表) | | `enabled` | admin 通过 config-service 启用 | admin 启用 | | `loaded` | dynamic import 加载 | iframe + postMessage 沙箱加载 | | `active` | 渲染并挂载 | 渲染并挂载 | | `disabled` | admin 禁用,不渲染 | admin 禁用,不渲染 | | `uninstalled` | 不可卸载(内置) | admin 卸载,删除包 | --- ## 7. 运行时视图(关键场景) ### 7.1 场景一:首屏加载(RSC 服务端预取 + 流式渲染) **目标**:消除 CSR 瀑布流,实现仪表盘"秒开",首屏 HTML 直出骨架 ```mermaid sequenceDiagram participant U as 用户浏览器 participant N as Next.js RSC participant R as apollo-router participant C as config-service participant S as 业务子图 U->>N: GET /shell N->>N: headers() 读取 x-user-id / x-user-role / x-user-permissions N->>R: query pluginConfig(userId, role)【不 await,返回 Promise】 Note over N: RSC 立即返回 HTML 骨架 + Suspense 占位 N-->>U: HTML 流式输出(layout 骨架) Note over U: 浏览器开始渲染骨架,不阻塞 R->>C: 路由到 config-service 子图 C-->>R: 三层合并后的 PluginConfigResponse R-->>N: Config Promise resolve N->>N: PropsMerger 解析 propsJson / sizeJson N-->>U: 流式注入 SlotRenderer + PluginBoundary Note over U: 每个插件独立 Suspense,dynamic import 流式加载 U->>U: 插件用 initialData 作为 SWR fallbackData 渲染 U->>U: 加载完成的插件立即渲染,不影响其他插件 ``` **对比 v1.0 CSR 瀑布流**: ``` v1.0(4 层串行,LCP 差): HTML 骨架 → 水合 → 拉 Config → import 插件 → 插件拉数据 → 渲染 总耗时 = SSR + 水合 + Config RTT + import RTT + BFF RTT v2.0 RSC 流式渲染(首屏秒开 + 流式注入): 服务端:RSC 返回 Promise → 立即输出 HTML 骨架 → Config resolve 后流式注入 客户端:水合骨架 → use(configPromise) 解析 → dynamic import → 用 initialData 渲染 总耗时 = max(骨架渲染, Config RTT) + 水合 + import RTT 收益:用户提前看到骨架,感知性能大幅提升 ``` ### 7.2 场景二:配置变更生效(SWR 静默刷新) ```mermaid sequenceDiagram participant A as Admin participant PM as plugin-manager 插件 participant R as apollo-router participant C as config-service participant U as 用户浏览器 participant SWR as SWR 缓存 A->>PM: 修改角色插件映射 PM->>R: mutation updateRolePluginMapping R->>C: 路由到 config-service C-->>R: 更新成功 R-->>PM: 返回结果 Note over U,SWR: 用户侧(异步感知) U->>U: 切回 Tab(revalidateOnFocus)
或 5 分钟轮询触发 SWR->>R: query pluginConfig(userId, role) R->>C: 路由到 config-service C-->>R: 新配置 R-->>SWR: 返回新配置 SWR->>SWR: hasConfigChanged(prev, next) 检测变化 SWR->>U: 回调 onChanged → Toast 提示 U->>U: 用户点击"刷新" → window.location.reload() U->>N: 重新走 RSC 预取流程,加载新配置 ``` ### 7.3 场景三:跨插件状态共享(URL 驱动) **示例**:class-selector 切换班级 → grades-widget 自动响应 ```mermaid sequenceDiagram participant CS as class-selector 插件 participant URL as URL Search Params participant GW as grades-widget 插件 participant R as apollo-router CS->>CS: 用户选择班级 "三年二班" CS->>URL: router.push('?classId=cls-123') Note over URL: URL 变更触发 React 重渲染 URL->>GW: useSearchParams() 返回新 classId GW->>GW: useWidgetQuery(GET_GRADES, { classId }) GW->>R: query grades(classId: "cls-123") R-->>GW: 返回成绩数据 GW->>GW: 重新渲染表格 ``` **收益**: - 无 EventBus 发布订阅黑盒 - URL 可分享(用户可复制链接定位特定班级视图) - 浏览器前进后退天然支持 - React DevTools 可追踪状态变更 ### 7.4 场景四:插件加载失败(PluginBoundary 三件套隔离) ```mermaid graph TB A[SlotRenderer 渲染插件] --> B[PluginBoundary 包裹] B --> C{dynamic import + 渲染成功?} C -- 是 --> D[组件渲染] C -- 否 --> E[ErrorBoundary 捕获] E --> F[PluginErrorFallback 显示] F --> G[显示错误图标 + instanceId] F --> H[重试按钮] H --> I[重置 hasError=false 重新加载] E -.->|onError| J[useErrorReport 上报] J --> K[sendBeacon → /api/log] L[同 Slot 其他插件] -.->|不受影响| M[正常渲染] N[其他 Slot] -.->|不受影响| O[正常渲染] P[整页] -.->|Route 兜底| Q[RouteErrorBoundary] ``` **关键约束**: - 单个插件失败**只影响自身**,同 Slot 其他插件 / 其他 Slot / 整页均不受影响 - 三级错误边界(Route → Section → Widget)层层兜底,最坏情况整页崩溃也有 `app/shell/error.tsx` 兜底 - 错误自动上报到 `/api/log`(开发态 mock)/ `/api/v1/log`(生产态,待后端实现) - `sessionStorage` 节流避免崩溃循环刷爆日志端点 --- ## 8. 部署视图 ### 8.1 容器化 ```mermaid graph LR subgraph Build["构建阶段"] Src["源码 apps/portal-shell/"] Docker["Dockerfile
multi-stage"] Img["本地镜像
(不推送 registry)"] end subgraph Runtime["运行时(Docker Compose)"] Container["portal-shell 容器 :4010"] Next["Next.js standalone server"] end subgraph Deps["依赖容器"] Gateway["api-gateway :8080"] Router["apollo-router :3000"] Realtime["realtime-gateway :8081"] end Src --> Docker Docker --> Img Img --> Container Container --> Next Next -->|HTTP /api/v1/*| Gateway Next -->|GraphQL| Router Next -->|SSE| Realtime ``` ### 8.2 Dockerfile 要点 - **基础镜像**:`node:22-alpine`(CI 预拉,见 `infra/docker-compose.tools.yml`) - **构建模式**:`output: "standalone"`(Next.js 内置,自动追踪依赖,产物在 `.next/standalone/`) - **多阶段构建**:`deps` → `builder` → `runner` - **依赖处理**: - `COPY tsconfig.base.json*` 确保 TS 类型解析 - `transpilePackages: ["@edu/ui-components", "@edu/ui-tokens", "@edu/hooks", "@edu/shared-ts"]` 编译 workspace 包 - `pnpm install --no-frozen-lockfile` 解决 workspace 符号链接问题 - **端口**:4010 - **健康检查**:`/api/health`(liveness)+ `/api/ready`(readiness) ### 8.3 环境变量 | 变量 | 用途 | 默认值 | | ------------------------------- | ------------------------------------------------------- | ------------------------------- | | `NEXT_PUBLIC_APOLLO_ROUTER_URL` | apollo-router GraphQL 端点(客户端) | `http://localhost:3000/graphql` | | `APOLLO_ROUTER_URL` | apollo-router GraphQL 端点(服务端 RSC) | 同上 | | `API_GATEWAY_URL` | api-gateway 反向代理目标 | `http://localhost:8080` | | `CONFIG_SERVICE_URL` | 开发态降级:直连 config-service GraphQL(生产不应设置) | 未设置 | | `NEXT_PUBLIC_DEV_MODE` | 开发模式:绕过 JWT,接受 `dev-token` + 预定义角色 | `false` | ### 8.4 CI/CD - **流水线**:`.github/workflows/ci.yml` 的 `quality-ts` job - **触发**:分支 push / PR(质量检查)、合并到 main(部署) - **质量门禁**:`pnpm lint` + `tsc --noEmit` + `vitest run` + `next build` - **部署**:`docker compose up -d --build`(no-push 本地构建模式,见 project_rules §15) - **回滚**:`git revert + push` 或 `workflow_dispatch` 指定 `commit_sha` --- ## 9. 横切概念 ### 9.1 设计令牌(shadcn 标准化,v2.0) **三层令牌模型**(project_rules §3.10): | Layer | 用途 | 位置 | 业务代码引用 | | ----------------- | ---------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------- | | L1 Primitive | 原始色板/字号/间距/阴影 | `packages/ui-tokens/src/primitive.css` | ❌ 禁止直接引用 | | L2 Semantic | shadcn 标准语义令牌(light/dark) | `packages/ui-tokens/src/semantic-light.css` / `semantic-dark.css` | ✅ 唯一引用入口(`hsl(var(--*))`) | | L3 Tailwind Theme | `@theme inline` 暴露为 Tailwind 类 | `packages/ui-tokens/src/tailwind-theme.css` | ✅ `bg-background` / `bg-card` / `text-foreground` / `border` / `rounded-xl` 等 | **shadcn 标准令牌清单**(v2.0 废弃纸感命名 paper/ink/accent/rule): | 语义令牌 | Light 值 | Dark 值 | Tailwind 类 | 用途 | | ---------------------- | ---------------- | ---------------- | ------------------------------------- | ----------------------------- | | `--background` | `0 0% 100%` | `240 10% 3.9%` | `bg-background` | 页面背景 | | `--foreground` | `240 10% 3.9%` | `0 0% 98%` | `text-foreground` | 主文本 | | `--card` | `0 0% 100%` | `240 10% 3.9%` | `bg-card` | 卡片背景 | | `--card-foreground` | `240 10% 3.9%` | `0 0% 98%` | `text-card-foreground` | 卡片内文本 | | `--primary` | `240 5.9% 10%` | `0 0% 98%` | `bg-primary` / `text-primary` | 主要按钮/强调 | | `--primary-foreground` | `0 0% 98%` | `240 5.9% 10%` | `text-primary-foreground` | 主要按钮上文本 | | `--muted` | `240 4.8% 95.9%` | `240 3.7% 15.9%` | `bg-muted` | 静默背景(旧 bg-subtle) | | `--muted-foreground` | `240 3.8% 46.1%` | `240 5% 64.9%` | `text-muted-foreground` | 静默文本(旧 text-ink-muted) | | `--border` | `240 5.9% 90%` | `240 3.7% 15.9%` | `border` | 边框(旧 border-rule) | | `--destructive` | `0 84.2% 60.2%` | `0 62.8% 30.6%` | `bg-destructive` / `text-destructive` | 危险/错误 | | `--radius` | `0.5rem` | `0.5rem` | `rounded-xl` / `rounded-md` | 圆角(旧 rounded-card) | **令牌迁移映射表(v1.x → v2.0)**: | v1.x 纸感令牌 | v2.0 shadcn 标准 | | -------------------- | ------------------------- | | `bg-paper` | `bg-background` | | `bg-surface` | `bg-card` | | `bg-subtle` | `bg-muted` | | `bg-accent` | `bg-primary` | | `bg-accent-hover` | `bg-primary/90` | | `text-ink` | `text-foreground` | | `text-ink-muted` | `text-muted-foreground` | | `text-ink-on-accent` | `text-primary-foreground` | | `text-ink-onAccent` | `text-primary-foreground` | | `border-rule` | `border` | | `rounded-card` | `rounded-xl` | | `rounded-button` | `rounded-md` | | `p-md` / `gap-md` | `p-4` / `gap-4` | | `p-sm` / `gap-sm` | `p-2` / `gap-2` | | `p-lg` | `p-6` | | `space-y-md` | `space-y-4` | | `text-small` | `text-sm` | | `text-tiny` | `text-xs` | **ESLint 强制约束**: - `no-restricted-syntax`:禁止 `#hex` 字面量 - `design-tokens/no-hardcoded-fonts`:禁止 `'Inter'` 字面量 - 白名单:`primitive.css`、`semantic-*.css`、`tailwind-theme.css`、`email-channel`、`manifest.ts` **portal-shell v2.0 落地**: - 字体通过 `next/font/google` self-host Inter,CSS 变量暴露为 `--font-inter` - 所有新代码使用 shadcn 标准 Tailwind 类(`bg-background` / `text-foreground` / `bg-card` / `border` / `rounded-xl` 等) - 无 `tailwind.config.js`(Tailwind v4 使用 `@theme inline` 替代) - 31 个 widget 仍使用旧纸感令牌(P1 阶段批量迁移) ### 9.2 字体策略(v2.0 简化) | 字体族 | 用途 | CSS 变量 | Tailwind 类 | | ------ | ----------------------- | -------------- | ----------- | | Inter | UI 文本 + 主标题 + 等宽 | `--font-inter` | `font-sans` | > v2.0 简化字体策略:仅使用 Inter 单一字体族(对齐 CICD 项目风格),废弃 Fraunces / JetBrains Mono 双字体方案。如需等宽,使用 Tailwind 默认 `font-mono`。 ### 9.3 shadcn 标准化设计风格(v2.0) 参考 CICD 项目风格 + shadcn/ui 官方规范: - **背景**:`bg-background` / `bg-card` / `bg-muted` 三层语义 - **圆角**:`rounded-xl`(卡片)/ `rounded-md`(按钮)/ `rounded-full`(头像) - **间距**:Tailwind 默认阶梯(`p-2` / `p-4` / `p-6` / `gap-2` / `gap-4`) - **插件容器**:`
` - **插件标题**:`text-lg text-foreground` - **加载态**:``(5 种骨架变体) - **错误态**:`` - **统一 Toast**:`notify.info/success/warning/error`(禁止业务直接 `import { toast } from "sonner"`) ### 9.4 可观测性 | 维度 | 实现 | 端点 | | -------------------- | --------------------------------------------------------------- | ----------------------------------------------------- | | 健康检查 | liveness + readiness | `/api/health` + `/api/ready` | | 错误日志 | `console.error` + 三级 ErrorBoundary + `useErrorReport`(v2.0) | 浏览器控制台 + `/api/log`(v2.0 mock) | | 性能 | Next.js 内置 Web Vitals(可接 OTel) | Next.js 自动采集 | | 请求追踪 | Apollo Client 自动携带 traceparent | 经 api-gateway 注入 `X-Request-Id` | | **错误上报(v2.0)** | `sendBeacon` + `sessionStorage` 节流(1 分钟同 digest 去重) | `/api/log`(mock)/ `/api/v1/log`(生产,待后端实现) | ### 9.5 国际化(MVP) - **locale 管理**:`PluginStore.locale`(`zh-CN` / `en`) - **locale 切换**:`locale-switcher` 插件(topbar) - **翻译函数**:`useT()` 从 `ThemeI18nProvider` 导出 - **翻译范围**:MVP 仅覆盖 Shell 框架文案(toggleSidebar / layout / empty),插件文案暂硬编码中文(二期接 next-intl) ### 9.6 安全 | 维度 | 实现 | | --------------------------------- | -------------------------------------------------------------------------------------------------------- | | JWT 注入 | Apollo Client `setContext` 从 localStorage 读取,注入 `Authorization: Bearer` | | Cookie | `credentials: "include"` 携带 httpOnly cookie | | XSS 防护 | React 自动转义 + 禁止 `dangerouslySetInnerHTML` | | CSRF | 依赖 SameSite=Strict cookie(api-gateway 配置) | | 开发模式 | `NEXT_PUBLIC_DEV_MODE=true` 绕过 JWT,仅本地开发 | | **APQ** | `createPersistedQueryLink({ sha256 })` 前端只发 query hash,env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭 | | **PQ Manifest** | `public/pq-manifest.json` 51 query 的 `sha256 → query` 白名单,由 `scripts/generate-pq-manifest.ts` 生成 | | **Router 强制 manifest** | `APOLLO_REQUIRE_PQ_MANIFEST=true` 时 router 拒绝未知 hash,entrypoint.sh 启动前校验文件存在性 | | **深度/成本/批量限制** | apollo-router `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5` | | **Introspection 控制** | `APOLLO_ROUTER_INTROSPECTION=false` 生产关闭 schema 内省 | | **Router-Authorization** | 子图 `RouterAuthGuard` 校验 header,拒绝非 Router 的直接 GraphQL 请求 | | **Resolver 字段级权限** | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点 | | **权限位图(v2.0)** | 67 权限点 → base36 字符串(~14 字符),JWT 头 `x-user-permissions` 注入,体积减少 ≥ 99% | | **三层安全边界(v2.0)** | L1 角色门禁 / L2 权限点门禁 / L3 数据范围,详见 §3.3 | | **路由权限配置表(v2.0)** | 4 张表(精确 / 前缀 / 仪表盘 / API)按优先级匹配,`checkRoutePermission` 主函数 | | **PluginManifest 权限点(v2.0)** | `metadata.requiredPermissions?: string[]`(AND 语义),config-service 三层合并时过滤 | | **错误上报节流(v2.0)** | `sessionStorage` 1 分钟同 digest 去重,避免崩溃循环刷爆日志端点 | ### 9.7 统一 Toast 封装(v2.0 新增) > 关联:`apps/portal-shell/src/shared/lib/notify.ts` **强制规则**:业务代码**禁止**直接 `import { toast } from "sonner"`,必须统一走 `notify` 封装。 ```typescript import { notify } from "@/shared/lib/notify"; // ✅ 正确:统一封装 notify.success("保存成功"); notify.error("保存失败", { description: error.message }); notify.info("发现新布局配置,刷新后生效"); notify.warning("该操作不可撤销"); // ❌ 禁止:直接 import sonner import { toast } from "sonner"; // ESLint no-restricted-imports 拦截 toast.success("保存成功"); ``` **封装收益**: 1. 统一 Toast 样式与位置(Toaster 在 RootLayout 挂载一次) 2. 便于后续替换底层库(sonner → react-hot-toast 或自研) 3. 集中添加埋点 / 错误上报 / 国际化等横切逻辑 4. ESLint `no-restricted-imports` 强制约束 --- ## 10. 架构决策记录(ADR 索引) portal-shell 的关键架构决策记录在 004 文档的 ADR 章节,此处为索引: | ADR | 决策 | 状态 | 关联 | | ------- | ------------------------------------------------------- | --------- | ------- | | ADR-033 | portal-shell 单容器 Modular Monolith 替代 4 portal + MF | ✅ 已落地 | M8-M10 | | ADR-023 | apollo-router 替代 3 BFF 手写聚合 | ✅ 已落地 | M2/M9 | | ADR-026 | config-service 从 iam 拆分,三层配置合并 | ✅ 已落地 | M3 | | ADR-029 | SSE 优先替代 WebSocket 单向推送 | ✅ 已落地 | M7 | | ADR-034 | Redis Pub/Sub 作为推送背板 | ✅ 已落地 | M7 | | ADR-036 | Router-Authorization 信任凭证 | ✅ 已落地 | M1 | | ADR-041 | ScopeToken 优化大规模 ID 列表 | ✅ 已落地 | M4 | | ADR-042 | portal-shell 前端数据访问四层分层(2026-07-17) | ✅ 已落地 | v1.1 M1 | | ADR-043 | PQ Manifest + APQ 安全加固(2026-07-17) | ✅ 已落地 | v1.1 M3 | | ADR-044 | shadcn/ui 标准化 + Tailwind v4(2026-07-17) | ✅ 已落地 | v2.0 | | ADR-045 | React 19 use() + Suspense 流式渲染(2026-07-17) | ✅ 已落地 | v2.0 | | ADR-046 | 三级错误边界 + 错误上报链路(2026-07-17) | ✅ 已落地 | v2.0 | | ADR-047 | 权限位图 base36 压缩 + 三层安全边界(2026-07-17) | ✅ 已落地 | v2.0 | ### 10.1 模块级决策(未单独编号) | 决策 | 理由 | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------- | | **dynamic import 替代 Module Federation** | 单体部署,无运行时远程加载,研发与运维复杂度最低 | | **URL Search Params + Zustand 替代 EventBus** | 数据流向清晰、支持 React DevTools、URL 可分享、浏览器前进后退天然支持 | | **SWR 静默刷新替代 Kafka+WebSocket 推送** | 低频布局变更无需实时推送,SWR 5 分钟轮询 + 切回 Tab 触发足够 | | **RSC 服务端预取替代客户端拉取** | 消除 4 层 CSR 瀑布流,HTML 直出 Config + initialData,LCP 秒开 | | **三层 props 合并(系统 < 角色 < 用户)** | 配置驱动可见性,admin 改配置无需重新部署 | | **编译时 Registry + 运行时 Config 分离** | Registry 是静态映射(plugin_id → lazy 组件),Config 是动态配置(DB 三层合并) | | **ErrorBoundary 隔离单插件失败** | 单个插件加载/渲染失败不影响其他插件 | | **MVP 不实现第三方插件沙箱** | 单体应用直接 script 注入不信任代码等于交出主站权限,二期用 iframe + postMessage | | **4 层数据访问分层** | Widget 内联 gql 字面量暴露 schema、难审计、难重构;抽取到 lib/api/ 四层架构集中管理(ADR-042) | | **APQ + PQ Manifest** | 前端只发 query hash 防止 schema 探测;Router 白名单 manifest 拒绝未知 hash 任意查询(ADR-043) | | **Router 深度/成本/批量限制** | 防止深度嵌套 / 高成本 / 批量查询 DoS,配置 max_depth=10 / max_cost=1000 / max_batch_size=5 | | **Resolver 字段级 @RequirePermission** | 防止越权访问字段,每个 Resolver 必须声明权限点;50 resolver 审计后补齐 19 个 TS 守卫 | | **shadcn/ui 标准化替代纸感令牌(v2.0)** | 对齐 CICD 项目风格,统一生态,降低 UI 维护成本,废弃 paper/ink/accent/rule 命名(ADR-044) | | **Tailwind v4 + @theme inline(v2.0)** | 移除 tailwind.config.js,使用 CSS-first 配置,对齐 shadcn/ui 官方推荐(ADR-044) | | **React 19 use() + Suspense 流式渲染(v2.0)** | RSC 返回 Promise → 客户端 use() 消费,首屏骨架秒出,数据流式注入(ADR-045) | | **三级错误边界(v2.0)** | Route → Section → Widget 层层兜底,单插件崩溃不污染整页,最坏情况整页有 error.tsx 兜底(ADR-046) | | **sendBeacon + sessionStorage 节流(v2.0)** | 页面卸载也能上报,1 分钟同 digest 去重避免崩溃循环刷爆日志端点(ADR-046) | | **权限位图 base36 压缩(v2.0)** | 67 权限点 → ~14 字符 base36 字符串,替代 JSON 数组(~600 字符),JWT 体积减少 ≥ 99%(ADR-047) | | **三层安全边界(v2.0)** | L1 角色门禁 / L2 权限点门禁 / L3 数据范围,路由级 + 插件级 + 数据范围层层过滤(ADR-047) | | **notify 统一 Toast 封装(v2.0)** | 禁止业务直接 import sonner,统一封装便于替换底层库与添加横切逻辑(ESLint 强制) | | **Provider 在 Suspense 外(v2.0)** | use() 暂停子树时 Provider 不能被暂停,否则 Context 丢失,ClientShell 拆分 ShellContent | | **SlotRenderer 信任输入(v2.0)** | L1/L2/L3 三层过滤在 RSC 服务端完成,客户端 SlotRenderer 不再二次过滤(性能优化) | --- ## 11. 质量要求与验收 ### 11.1 功能验收 - [x] 5 种 Layout 模板可切换,slot 正确渲染 - [x] 31 个内置插件全部加载正常(7 universal + 4 sidebar + 4 topbar + 4 teacher + 4 student + 2 parent + 6 admin) - [x] admin 可通过 plugin-manager 插件配置角色插件集合(registry / mapping / layout / user 四 Tab) - [x] admin 可配置角色默认 Layout 模板 - [x] admin 可配置插件默认 props(JSON 编辑) - [x] admin 可重置用户自定义布局 - [x] admin 改配置 → SWR 静默刷新检测到变化 → Toast 提示用户刷新生效 - [x] 插件加载失败显示错误兜底,不影响其他插件(PluginBoundary 三件套,v2.0) - [x] 跨插件状态共享正常(class-selector 切换班级 → grades-widget 自动响应) - [x] 插件 props 三层合并正确(PropsMerger) - [x] RSC 服务端预取正常:首屏 HTML 直出 Config + initialData - [x] useWidgetQuery 自动经 Apollo Client 路由到 apollo-router - [x] URL Search Params 可分享、可前进后退 - [x] config-fetcher 三级降级:apollo-router → config-service 直连 → 空默认 - [x] **流式渲染(v2.0)**:RSC 返回 Promise → use() 消费 → 首屏骨架秒出 → Config 流式注入 - [x] **三级错误边界(v2.0)**:Route(error.tsx)→ Section(SectionErrorBoundary)→ Widget(PluginBoundary)层层兜底 - [x] **错误上报链路(v2.0)**:useErrorReport → sendBeacon → /api/log mock 端点,sessionStorage 1 分钟同 digest 去重 - [x] **三层安全边界(v2.0)**:L1 角色门禁 + L2 权限点门禁 + L3 数据范围,路由权限配置表 4 张按优先级匹配 - [x] **权限位图(v2.0)**:67 权限点 base36 编解码 + hasPermissionInBitmap / hasAllPermissionsInBitmap / hasAnyPermissionInBitmap - [x] **PluginManifest requiredPermissions(v2.0)**:插件级 L2 权限点门禁,config-service 三层合并时过滤 - [x] **notify 统一 Toast 封装(v2.0)**:禁止业务直接 import sonner,ESLint no-restricted-imports 强制 - [x] **shadcn 标准化(v2.0)**:packages/ui-components 13 个文件令牌迁移完成(bg-paper→bg-background 等) - [x] **shadcn/ui 组件库(v2.0)**:button / card / badge / skeleton / input / tooltip / sonner / page-header / stat-card / stats-grid / empty-state / filter-bar ### 11.2 非功能验收 - [x] `pnpm run lint` + `pnpm run typecheck` 零错误(v2.0:含 2 个 auto-generated 文件警告,可忽略) - [x] `pnpm run build` 通过(v2.0:Next.js 16 Turbopack,6 路由生成成功:`/` / `/_not-found` / `/api/health` / `/api/log` / `/api/ready` / `/shell/[[...route]]`) - [x] `pnpm run test` 95/95 通过(lib/api 7 domain 55 用例 + 安全栈 10 用例 + Shell/Lifecycle/Context 30 用例) - [x] 0 处 widget 内联 gql 字面量(强制,arch:scan 违规检测) - [x] 所有新代码遵守 shadcn 标准令牌(v2.0:ESLint 强制,无硬编码颜色/字体/任意值) - [x] apollo-router 启用 APQ + PQ Manifest + 深度/成本/批量限制 - [x] 50 个 GraphQL resolver @auth 审计完成(35 TS + 15 Python),19 个 TS resolver 补齐 @RequirePermission - [x] arch.db 更新,004 文档同步 - [x] **v2.0 README 同步**:本文件 v2.0,004 同步更新 ADR-044/045/046/047 - [x] **v2.0 流式渲染验证**:首屏 HTML 直出骨架,Config resolve 后流式注入(本地 Docker 验证) - [ ] Shell 首屏 LCP < 2s(需真实环境压测验证) - [ ] 插件加载耗时 < 500ms(dynamic import 缓存命中后,需真实环境验证) - [ ] 单元测试覆盖率 ≥ 80%(当前覆盖核心纯函数 + lib/api 全量,admin domain 仅 4 用例待补,插件组件测试待补,v2.0 新增组件测试待补) - [ ] E2E 测试(tests/e2e/portal-shell.spec.ts,待补,含 v2.0 流式渲染 + 三级错误边界场景) - [ ] 视觉回归测试(5 种 layout 截图,待补,含 v2.0 shadcn 标准化对比) - [ ] 生产部署前 APOLLO_REQUIRE_PQ_MANIFEST=true + APOLLO_ROUTER_INTROSPECTION=false 写入部署 env - [ ] 31 个 widget 旧纸感令牌批量迁移到 shadcn 标准(P1 阶段,arch:scan 违规检测) ### 11.3 测试矩阵 | 测试类型 | 范围 | 文件 | 状态 | | -------- | --------------------------------------------- | ---------------------------------------------------------- | ----------------- | | 单元测试 | PluginLifecycle 纯函数 | `src/shell/__tests__/PluginLifecycle.test.ts` | ✅ 12 用例 | | 单元测试 | Registry 插件注册 | `src/shell/__tests__/Registry.test.ts` | ✅ 6 用例 | | 单元测试 | plugin-context URL 上下文 | `src/lib/__tests__/plugin-context.test.ts` | ✅ 12 用例 | | 单元测试 | lib/api universal domain | `src/lib/api/__tests__/universal.test.ts` | ✅ 6 用例 | | 单元测试 | lib/api sidebar domain | `src/lib/api/__tests__/sidebar.test.tsx` | ✅ 9 用例 | | 单元测试 | lib/api topbar domain | `src/lib/api/__tests__/topbar.test.tsx` | ✅ 9 用例 | | 单元测试 | lib/api teacher domain | `src/lib/api/__tests__/teacher.test.tsx` | ✅ 7 用例 | | 单元测试 | lib/api student domain | `src/lib/api/__tests__/student.test.tsx` | ✅ 9 用例 | | 单元测试 | lib/api parent domain | `src/lib/api/__tests__/parent.test.tsx` | ✅ 11 用例 | | 单元测试 | lib/api admin domain | `src/lib/api/__tests__/admin.test.tsx` | ✅ 4 用例(待补) | | 单元测试 | PQ Manifest + APQ + 深度限制 | `src/lib/api/__tests__/security.test.ts` | ✅ 10 用例 | | 单元测试 | 权限位图 base36 编解码(v2.0) | `packages/shared-ts/__tests__/permission-bitmap.test.ts` | 🚧 待补 | | 单元测试 | 路由权限配置表 + checkRoutePermission(v2.0) | `src/shared/lib/__tests__/route-permissions.test.ts` | 🚧 待补 | | 单元测试 | notify 统一封装(v2.0) | `src/shared/lib/__tests__/notify.test.ts` | 🚧 待补 | | 单元测试 | useErrorReport 节流逻辑(v2.0) | `packages/hooks/__tests__/use-error-report.test.ts` | 🚧 待补 | | 单元测试 | PluginBoundary 三件套(v2.0) | `src/shared/components/__tests__/plugin-boundary.test.tsx` | 🚧 待补 | | 单元测试 | PropsMerger 三层合并 | 待补 | 🚧 | | 单元测试 | 各插件组件渲染 | 待补 | 🚧 | | E2E | 登录 → 加载 layout → 渲染插件 → 切换 layout | `tests/e2e/portal-shell.spec.ts` | ⏳ | | E2E | admin 改配置 → 用户刷新生效 | `tests/e2e/plugin-config.spec.ts` | ⏳ | | E2E | apollo-router 拒绝 11 层嵌套查询 | `tests/e2e/graphql-depth-limit.spec.ts` | ⏳ | | E2E | apollo-router 拒绝未知 PQ hash | `tests/e2e/graphql-pq-manifest.spec.ts` | ⏳ | | E2E | 流式渲染 + 三级错误边界(v2.0) | `tests/e2e/streaming-and-error-boundary.spec.ts` | ⏳(v2.0) | | E2E | 三层安全边界 + 权限位图(v2.0) | `tests/e2e/security-boundary.spec.ts` | ⏳(v2.0) | | 视觉回归 | 5 种 layout 截图对比 | `tests/visual/portal-shell.spec.ts` | ⏳ | | 视觉回归 | shadcn 标准化对比(v2.0) | `tests/visual/shadcn-migration.spec.ts` | ⏳(v2.0) | --- ## 12. 风险、技术债与演进路线 ### 12.1 风险与缓解 | 风险 | 影响 | 缓解措施 | | --------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- | | 插件数量增长导致首屏 bundle 过大 | 首屏加载慢 | dynamic import 按需加载 + IntersectionObserver 滚动加载 + 首屏只加载可见 slot | | 单体架构插件间隐式耦合 | 维护困难 | ESLint 禁止跨 widgets 目录 import + arch:scan 违规检测 + 强制 URL/Zustand 共享状态 | | 单角色专属功能受 props 契约约束 | 复杂功能实现受限 | PluginProps 设计灵活(initialData + props 任意 JSON);复杂功能在插件内部自行组织 | | config-service 配置查询压力 | RSC 每次请求查询多表 | 复用 config-service Redis 缓存 + RSC `cache()` 去重 + SWR 客户端轮询自然刷新 | | 插件 props 三层合并逻辑复杂 | props 不一致 | PropsMerger 集中实现 + 单元测试覆盖(待补) | | admin 配置面板 propsSchema 复杂 | 表单体验差 | MVP 用 JSON textarea 编辑,二期接 react-jsonschema-form | | 二期第三方插件沙箱 | 安全风险 | iframe + postMessage(最安全),禁止 same-origin,BFF 请求由 Shell 代理 | | **v2.0 流式渲染 Provider 暂停**(v2.0) | Context 丢失导致子树崩溃 | 严格约束 Provider 必须在 Suspense 外,ClientShell 拆分 ShellContent 隔离 use() | | **v2.0 权限位图 BigInt 兼容性**(v2.0) | 旧浏览器不支持 BigInt | 目标浏览器为现代浏览器(Chrome 67+ / Firefox 68+ / Safari 14+),不兼容 IE | | **v2.0 错误上报端点 mock**(v2.0) | 生产环境无后端端点 | 当前 `/api/log` 为 Next.js API Route mock,生产由后端 `/api/v1/log` 替换 | | **v2.0 旧令牌混用**(v2.0) | 31 widget 仍用纸感令牌 | P1 阶段批量迁移,arch:scan 违规检测,新代码强制 shadcn 标准 | ### 12.2 技术债 | # | 技术债 | 优先级 | 计划 | | ----- | ---------------------------------------------------------------------------- | ------ | --------------------------------------------------------- | | TD-1 | PluginLifecycle 版本校验仅 major,未引入 semver 库 | 低 | MVP 够用,二期按需引入 `semver` | | TD-2 | ThemeI18nProvider 仅覆盖 Shell 框架文案,插件文案硬编码中文 | 中 | 二期接 next-intl,提取到 messages/{en,zh-CN}.json | | TD-3 | PropsMerger 单元测试待补 | 中 | 补充深合并 + 数组覆盖 + null 跳过用例 | | TD-4 | 插件组件单元测试待补(仅 Registry 与 Lifecycle 有测试) | 中 | 补充各插件渲染 + loading + error 用例 | | TD-5 | E2E 测试与视觉回归测试待补 | 高 | 接 Playwright + 5 种 layout 截图 | | TD-6 | `prefetchPluginData` 服务端并发预取未实现(仅拉 Config,未预取 initialData) | 中 | RSC 中按 pluginId 分发预取,传入 fallbackData | | TD-7 | canvas layout 仅按 grid 排列,未实现拖拽 | 低 | MVP 预留接口,二期按需实现 | | TD-8 | 第三方插件沙箱未实现 | 低 | 二期 iframe + postMessage | | TD-9 | **v2.0:31 widget 旧纸感令牌批量迁移** | 高 | P1 阶段,arch:scan 违规检测,bg-paper→bg-background 等 | | TD-10 | **v2.0:权限位图单元测试待补** | 中 | 补 base36 编解码 + hasAll/hasAny + isValidPermission | | TD-11 | **v2.0:路由权限配置表单元测试待补** | 中 | 补 4 张表匹配 + checkRoutePermission + batch + validate | | TD-12 | **v2.0:notify 封装单元测试待补** | 低 | 补 4 个方法调用 + ESLint no-restricted-imports 验证 | | TD-13 | **v2.0:useErrorReport 节流逻辑测试待补** | 中 | 补 sessionStorage 1 分钟同 digest 去重 + sendBeacon | | TD-14 | **v2.0:PluginBoundary 三件套测试待补** | 中 | 补 ErrorBoundary + Suspense + Skeleton 5 变体 | | TD-15 | **v2.0:错误上报端点生产替换** | 中 | 后端实现 /api/v1/log 后,移除 Next.js API Route mock | | TD-16 | **v2.0:TS 子图 AuthMiddleware 覆盖 /graphql** | 中 | 4 个 TS 子图补齐字段级守卫(不依赖 RouterAuthGuard 兜底) | ### 12.3 演进路线 | 阶段 | 内容 | 状态 | | ----------- | ----------------------------------------------------------------------------------------------- | --------------------- | | M8 | portal-shell 接入 apollo-router(RSC 预取 Config) | ✅ 完成 | | M9 | 旧 BFF 下线(teacher/student/parent-bff) | ✅ 完成 | | M10 | 旧 portal 下线(teacher/student/parent/admin-portal) | ✅ 完成 | | v1.1 M1 | lib/api 四层架构 + 31 widget 迁移 | ✅ 完成(2026-07-17) | | v1.1 M3 | GraphQL 安全加固(APQ + PQ Manifest + router limits) | ✅ 完成(2026-07-17) | | v1.1 M4 | Resolver @RequirePermission 审计 + 补齐 | ✅ 完成(2026-07-17) | | **v2.0 P0** | **shadcn 标准化 + 三层安全 + 流式渲染 + 三级错误处理** | ✅ 完成(2026-07-17) | | v1.1 FU-1 | 4 个 TS 子图 AuthMiddleware 覆盖 /graphql 路径 | ⏳ Follow-up | | v1.1 FU-2 | Python 子图(data-ana/ai)补 @RequirePermission 基础设施 | ⏳ Follow-up | | v1.1 FU-3 | admin domain 测试用例补齐(当前仅 4 用例) | ⏳ Follow-up | | **v2.0 P1** | **31 widget 旧纸感令牌批量迁移到 shadcn 标准** | ⏳ 规划 | | **v2.0 P2** | **v2.0 新增组件单元测试补齐**(权限位图 / 路由权限 / notify / useErrorReport / PluginBoundary) | ⏳ 规划 | | **v2.0 P3** | **错误上报端点生产替换**(后端 /api/v1/log) | ⏳ 规划 | | **v2.0 P4** | **E2E 测试**(流式渲染 + 三级错误边界 + 三层安全边界) | ⏳ 规划 | | P5(二期) | 第三方插件上传 + iframe 沙箱 | ⏳ 规划 | | P6(二期) | 插件市场在线商店 | ⏳ 规划 | | P7(二期) | canvas 拖拽编辑器 | ⏳ 规划 | | P8(二期) | next-intl 完整 i18n | ⏳ 规划 | | P9(二期) | 视觉回归测试自动化 | ⏳ 规划 | --- ## 13. 数据访问层与 GraphQL 安全栈 > 来源:[portal-shell 数据抽象与 GraphQL 加固 spec](../../docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) v1.0 > 关联 ADR:ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固) > 关联 004:§11.7 portal-shell 前端数据访问层 + GraphQL 安全栈 ### 13.1 问题背景 v1.0 portal-shell 的 widget 直接内联 `gql\`...\`` 字面量查询,存在三个核心问题: 1. **Schema 泄露**:前端 bundle 包含明文 GraphQL 查询,攻击者可通过 DevTools 构造任意查询探测 schema 2. **查询碎片化**:31 个 widget 各自维护 gql 字符串,难审计、难重构、难统一优化 3. **缺抽象**:widget 直接 import Apollo hooks,业务逻辑与传输层耦合 ### 13.2 解决方案:4 层数据访问分层(ADR-042) ```mermaid graph TD subgraph Widget["Widget 层(UI)"] W[widgets/*.tsx
只关心渲染] end subgraph API["API 层(语义化函数)"] A1[lib/api/parent.ts] A2[lib/api/teacher.ts] A3[lib/api/admin.ts] A4[lib/api/student.ts] A5[lib/api/universal.ts] A6[lib/api/sidebar.ts] A7[lib/api/topbar.ts] end subgraph Ops["Operations 层(gql 文档集中)"] O[lib/api/operations/*.graphql.ts
51 个 DocumentNode 常量] end subgraph Hook["Hook 层(Apollo 封装)"] H1[useWidgetQuery] H2[useWidgetMutation] end subgraph Codegen["类型生成"] C[graphql-codegen
7 子图 schema.graphql → types.ts] end W --> A1 & A2 & A3 & A4 & A5 & A6 & A7 A1 & A2 & A3 & A4 & A5 & A6 & A7 --> O A1 & A2 & A3 & A4 & A5 & A6 & A7 --> H1 & H2 O --> C ``` **层职责矩阵**: | 层 | 位置 | 职责 | 禁止 | | ---------- | --------------------------------------------- | ------------------------------------------------------- | ----------------------------------------- | | Widget | `src/widgets/*.tsx` | UI 渲染、用户交互 | 内联 gql 字面量、直接 import Apollo hooks | | API | `src/lib/api/.ts` | 语义化函数(`useMyChildren()` / `saveLessonPlan()` 等) | 写 gql 字符串、直接操作 Apollo cache | | Operations | `src/lib/api/operations/*.graphql.ts` | gql DocumentNode 常量集中存放(51 个 query/mutation) | 包含业务逻辑 | | Hook | `src/lib/useWidgetQuery/useWidgetMutation.ts` | Apollo useQuery/useMutation 封装 + ApiError 归一化 | 引用具体 domain | **7 个 domain API 文件**: | 文件 | Widget 数 | 主要场景 | | -------------- | --------- | ---------------------------------------------------------------------------------- | | `universal.ts` | 7 | 公告 / 通知 / 课表 / 作业 / 考试 / 成绩 / 考勤(多角色复用) | | `sidebar.ts` | 3 | 当前用户 / 我的班级 / 学期列表(侧边栏) | | `topbar.ts` | 2 | 搜索 / 通知铃铛(顶栏,`useNotificationBell` 限 N 条) | | `teacher.ts` | 5 | 教材 / 教案 / 题目 / 课表规则 / 教案保存 / 课表更新 | | `student.ts` | 5 | AI Tutor / 选修课 / 错题本 / 学习路径 / 错题掌握标记 | | `parent.ts` | 3 | 我的子女 / 请假审批 / 请假驳回 | | `admin.ts` | 11 | 用户 / 角色 / 权限 / 学校 / 插件注册 / 角色插件映射 / 布局模板 / 邀请码 / 审计日志 | **类型生成(graphql-codegen)**: - 配置:`apps/portal-shell/codegen.yml` - schema 来源:7 个子图的 `services//src/graphql/generated/schema.graphql` - 生成产物:`src/lib/api/operations/types.ts`(仅类型,无运行时代码) - 关键配置:`skipDocumentsValidation: true`(避免子图未启动时 codegen 失败) - schema 归一化:`scripts/normalize-schema.ts` 移除 federation 指令(`@key` / `@requires` / `@extends`)防止 codegen 误解析 ### 13.3 GraphQL 安全栈(ADR-043) ```mermaid graph LR subgraph FE["portal-shell(前端)"] APQ[createPersistedQueryLink
sha256 query → hash] end subgraph Router["apollo-router :3000"] Manifest[pq-manifest.json
hash → query 白名单] Limits[limits.max_depth=10
max_cost=1000
max_batch_size=5] Intro[introspection
环境变量控制] end subgraph Sub["子图 /graphql"] Guard[RouterAuthGuard
+ @RequirePermission] end APQ -->|只发 hash| Manifest Manifest -->|未知 hash 拒绝| APQ Manifest --> Limits Limits --> Intro Intro --> Guard ``` **安全机制矩阵**: | 机制 | 位置 | 防御目标 | 配置 | | ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- | | APQ(Automatic Persisted Queries) | `apps/portal-shell/src/lib/apollo-client.ts` | 前端只发 query hash,不发明文 query | `createPersistedQueryLink({ sha256 })`,env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭 | | PQ Manifest | `apps/portal-shell/public/pq-manifest.json` | Router 仅解析白名单 hash,拒绝未知 hash 任意查询 | 51 个 query 的 `sha256 → query` 映射,由 `scripts/generate-pq-manifest.ts` 生成 | | Router 强制 manifest | `infra/apollo-router/router.yaml` | 生产模式(`require_manifest: true`)拒绝未注册查询 | env `APOLLO_REQUIRE_PQ_MANIFEST=true`,entrypoint.sh 启动前检查文件存在性 | | 深度限制 | `router.yaml` `limits.max_depth=10` | 防止深度嵌套查询 DoS | 11 层嵌套被 router 拒绝(`QUERY_DEPTH_EXCEEDED`) | | 成本限制 | `router.yaml` `limits.max_cost=1000` | 防止高成本查询 DoS | 按字段复杂度评分累加 | | 批量限制 | `router.yaml` `limits.max_batch_size=5` | 防止批量查询 DoS | 单次请求最多 5 个 query | | Introspection 控制 | `router.yaml` `supergraph.introspection` | 生产关闭 introspection 防止 schema 泄露 | env `APOLLO_ROUTER_INTROSPECTION=false`(开发默认 true) | | Router-Authorization 信任 | 子图 `RouterAuthGuard`(见 §5.5) | 防止绕过 Router 直接访问子图 | 子图校验 `Router-Authorization` header,拒绝非 Router 请求(ADR-036) | | Resolver 字段级权限 | 子图 `@RequirePermission()` 装饰器 | 防止越权访问字段 | 每个 Resolver 必须声明权限点(见 §3.8) | ### 13.4 PQ Manifest 生成流程 1. `pnpm --filter @edu/portal-shell run codegen` → 从 7 子图 schema 生成 TS 类型 2. `pnpm --filter @edu/portal-shell run generate-pq-manifest` → 遍历 `lib/api/operations/index.ts` 中所有 DocumentNode,`print(doc)` 后 `sha256(query)` 生成映射,写入 `public/pq-manifest.json` 3. `prebuild` 钩子自动串联 codegen + generate-pq-manifest 4. Docker compose 挂载 `pq-manifest.json` 到 apollo-router `/etc/apollo-router/pq-manifest.json:ro` 5. apollo-router entrypoint.sh 启动前校验 manifest 存在性(`require_manifest=true` 时缺失即 exit 1) ### 13.5 Resolver @RequirePermission 审计(M4) > 详见:[docs/security/graphql-auth-audit-2026-07.md](../../docs/security/graphql-auth-audit-2026-07.md) **已审计范围**:50 个 resolver(35 TS + 15 Python) | 状态 | 数量 | 说明 | | ------------------------- | ---- | -------------------------------------------------------------------------------------- | | 已有 `@RequirePermission` | 37 | 18 原有 + 19 M4 补齐 | | 缺失守卫 | 13 | 4 个 TS(`/graphql` 路径未覆盖,由 RouterAuthGuard 兜底)+ 9 个 Python(待补基础设施) | **Follow-up(不阻断 M3 验收)**: - 4 个 TS 子图(iam / core-edu / content / msg)的 `AuthMiddleware` 仅覆盖 REST 路径,未覆盖 `/graphql`(由 `RouterAuthGuard` 兜底,仍建议补齐字段级守卫) - Python 子图(data-ana / ai)缺 `@RequirePermission` 基础设施,需补 Strawberry / Ariadne 中间件 ### 13.6 常用命令 ```bash # 类型生成(从 7 子图 schema 生成 TS 类型) pnpm --filter @edu/portal-shell run codegen # PQ Manifest 生成 pnpm --filter @edu/portal-shell run generate-pq-manifest # 生产构建(prebuild 自动串联 codegen + generate-pq-manifest) pnpm --filter @edu/portal-shell run build # 验证 0 处内联 gql 字面量 pnpm --filter @edu/portal-shell run lint # 安全栈测试 npx vitest run src/lib/api/__tests__/security.test.ts ``` --- ## 14. v2.0 安全边界与错误处理 > 本章汇总 v2.0 新增的安全边界、流式渲染、错误处理机制,详细设计见各章节交叉引用。 ### 14.1 三层安全边界(详见 §3.3) ```mermaid graph TD User["用户请求
/shell/*"] --> L1 L1["L1 角色门禁
route-permissions.ts
requiredRoles?"] -->|通过| L2 L1 -->|拒绝| Deny403["403 Forbidden"] L2["L2 权限点门禁
位图校验
requiredPermissions?
anyOfPermissions?"] -->|通过| L3 L2 -->|拒绝| Deny403 L3["L3 数据范围
config-service
DataScope 6 级"] -->|过滤| Plugin["插件渲染
SlotRenderer 信任输入"] L3 -->|无可见数据| Empty["空状态"] ``` **关键文件**: | 文件 | 职责 | | ------------------------------------------------------- | ------------------------------------------------ | | `packages/shared-ts/src/permission-bitmap.ts` | 67 权限点定义 + base36 编解码 + 校验 | | `apps/portal-shell/src/shared/lib/route-permissions.ts` | 4 张路由权限配置表 + checkRoutePermission | | `packages/shared-ts/src/contracts/plugin.ts` | PluginManifest.metadata.requiredPermissions 字段 | **权限位图 API**: ```typescript // 编码:权限点数组 → base36 字符串 const bitmap = encodePermissionsBitmap(["user.read", "grade.write"]); // → "1a2b3c..." // 解码:base36 字符串 → 权限点数组 const permissions = decodePermissionsBitmap(bitmap); // → ["user.read", "grade.write"] // 校验 hasPermissionInBitmap(bitmap, "user.read"); // → true hasAllPermissionsInBitmap(bitmap, ["user.read", "grade.write"]); // → true hasAnyPermissionInBitmap(bitmap, ["user.read", "user.delete"]); // → true isValidPermission("user.read"); // → true(必须在 PERMISSION_BITMAP_ORDER 中) ``` ### 14.2 流式渲染(详见 §5.5) ```mermaid graph LR subgraph RSC["RSC 服务端"] Fetch["fetchPluginConfig
不 await"] Promise["返回 Promise"] end subgraph Client["客户端"] Suspense["Suspense 边界"] Use["use(configPromise)"] Render["ShellContent 渲染"] end subgraph Loading["加载态"] Skeleton["整页骨架
shell/loading.tsx"] PluginSk["PluginSkeleton
5 变体"] end Fetch --> Promise Promise --> Suspense Suspense -->|Promise 未 resolve| Skeleton Suspense -->|Promise resolve| Use Use --> Render Render -->|单插件加载| PluginSk Render -->|插件加载完成| Done["渲染完成"] ``` **关键文件**: | 文件 | 职责 | | ------------------------------------------------------------- | ------------------------------------ | | `apps/portal-shell/src/app/shell/[[...route]]/page.tsx` | RSC 入口,返回 Promise 不 await | | `apps/portal-shell/src/shell/ClientShell.tsx` | use() 消费 + Provider 在 Suspense 外 | | `apps/portal-shell/src/app/shell/loading.tsx` | Route 级加载骨架 | | `apps/portal-shell/src/shared/components/plugin-boundary.tsx` | Widget 级 Suspense + Skeleton | ### 14.3 三级错误处理(详见 §5.6) ```mermaid graph TB subgraph Route["Route 级"] REB["RouteErrorBoundary
app/shell/error.tsx"] end subgraph Section["Section 级"] SEB["SectionErrorBoundary
DashboardSection 内"] end subgraph Widget["Widget 级"] PB["PluginBoundary
SlotRenderer 内"] end subgraph Report["错误上报"] Hook["useErrorReport"] Beacon["sendBeacon"] API["/api/log mock"] Store["sessionStorage
1 分钟去重"] end REB --> SEB --> PB REB -.->|onError| Hook SEB -.->|onError| Hook PB -.->|onError| Hook Hook --> Store Hook --> Beacon --> API ``` **关键文件**: | 文件 | 职责 | | -------------------------------------------------------------------- | ------------------------------------------------- | | `apps/portal-shell/src/app/shell/error.tsx` | Route 级 Next.js error.tsx | | `apps/portal-shell/src/shared/components/route-error-boundary.tsx` | Route 错误边界组件 | | `apps/portal-shell/src/shared/components/section-error-boundary.tsx` | Section 错误边界组件 | | `apps/portal-shell/src/shared/components/plugin-boundary.tsx` | Widget 错误边界 + Suspense + Skeleton 三件套 | | `packages/hooks/src/use-error-report.ts` | 错误上报 Hook(sendBeacon + sessionStorage 节流) | | `apps/portal-shell/src/app/api/log/route.ts` | 错误上报 mock 端点 | **5 种骨架变体**: | 变体 | 用途 | 适用场景 | | ------- | -------- | ------------------ | | `card` | 卡片骨架 | 通用卡片插件 | | `list` | 列表骨架 | 通知/公告/作业列表 | | `chart` | 图表骨架 | 数据分析图表 | | `stats` | 统计骨架 | 数字统计卡片 | | `table` | 表格骨架 | 成绩/考勤表格 | ### 14.4 v2.0 关键决策汇总 | 决策 | 理由 | 关联 ADR | | ------------------------------------ | ---------------------------------------- | ---------- | | shadcn/ui 标准化替代纸感令牌 | 对齐 CICD 项目风格,统一生态 | ADR-044 | | Tailwind v4 + @theme inline | CSS-first 配置,对齐 shadcn 官方推荐 | ADR-044 | | React 19 use() + Suspense 流式渲染 | 首屏骨架秒出,数据流式注入 | ADR-045 | | 三级错误边界(Route/Section/Widget) | 层层兜底,单插件崩溃不污染整页 | ADR-046 | | sendBeacon + sessionStorage 节流 | 页面卸载也能上报,避免崩溃循环 | ADR-046 | | 权限位图 base36 压缩 | 67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% | ADR-047 | | 三层安全边界(L1/L2/L3) | 路由级 + 插件级 + 数据范围层层过滤 | ADR-047 | | notify 统一 Toast 封装 | 禁止业务直接 import sonner,便于替换 | 模块级决策 | | Provider 在 Suspense 外 | use() 暂停子树时 Context 不丢失 | 模块级决策 | | SlotRenderer 信任输入 | L1/L2/L3 服务端过滤,客户端不再二次过滤 | 模块级决策 | ### 14.5 v2.0 文件清单 ``` apps/portal-shell/src/ ├─ app/ │ ├─ api/log/route.ts # v2.0 错误上报 mock 端点 │ ├─ shell/error.tsx # v2.0 Route 级错误兜底 │ └─ shell/loading.tsx # v2.0 Route 级加载骨架 ├─ shared/ │ ├─ lib/ │ │ ├─ route-permissions.ts # v2.0 路由权限配置表(4 张表 + checkRoutePermission) │ │ ├─ notify.ts # v2.0 统一 Toast 封装 │ │ └─ utils.ts # v2.0 cn() 工具函数 │ └─ components/ │ ├─ plugin-boundary.tsx # v2.0 插件级错误边界 + Suspense + Skeleton │ ├─ route-error-boundary.tsx # v2.0 Route 级错误边界 │ ├─ section-error-boundary.tsx # v2.0 Section 级错误边界 │ ├─ dashboard/ │ │ ├─ dashboard-shell.tsx # v2.0 仪表盘外壳 │ │ └─ dashboard-section.tsx # v2.0 仪表盘分区 │ ├─ layout/ │ │ ├─ sidebar-provider.tsx # v2.0 侧边栏状态 │ │ ├─ app-sidebar.tsx # v2.0 应用侧边栏 │ │ └─ site-header.tsx # v2.0 顶部头部 │ └─ ui/ │ ├─ button.tsx / card.tsx / badge.tsx # v2.0 shadcn/ui 组件 │ ├─ skeleton.tsx / input.tsx / tooltip.tsx │ ├─ sonner.tsx # v2.0 Toaster │ ├─ page-header.tsx / stat-card.tsx │ ├─ stats-grid.tsx / empty-state.tsx │ └─ filter-bar.tsx packages/shared-ts/src/ ├─ permission-bitmap.ts # v2.0 权限位图工具(67 权限点 + base36) └─ contracts/plugin.ts # v2.0 PluginManifest.metadata.requiredPermissions packages/hooks/src/ └─ use-error-report.ts # v2.0 错误上报 Hook packages/ui-components/src/ # v2.0 13 个文件令牌迁移完成 ├─ plugin-error-fallback.tsx / plugin-skeleton.tsx ├─ plugin-card.tsx / slot-placeholder.tsx / status-badge.tsx ├─ props-config-form.tsx / form.tsx / modal.tsx ├─ data-table.tsx / filter-bar.tsx / chart.tsx ├─ calendar.tsx / rich-text-editor.tsx packages/ui-tokens/src/ ├─ primitive.css # v2.0 zinc/stone/indigo 色板 ├─ semantic-light.css / semantic-dark.css # v2.0 shadcn 标准语义令牌 └─ tailwind-theme.css # v2.0 @theme inline ``` --- ## 15. 术语表 | 术语 | 定义 | | ----------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Shell** | 微内核宿主,渲染 Layout 框架 + Slots + PluginBoundary,不含业务逻辑 | | **Registry** | 编译时登记的插件清单,`plugin_id → dynamic import 组件` 静态映射 | | **Config** | 运行时配置 JSON,决定当前用户在哪些 Slots 渲染哪些插件(DB 三层合并) | | **PluginProps** | 插件契约,Shell 与插件之间的唯一交互接口 | | **Slot** | Layout 模板预定义的插件放置区域(top / side / main / main-left / main-right / right / canvas-grid) | | **Layout 模板** | 5 种内置布局(classic / focus / split / triple / canvas) | | **三层配置** | 系统默认(plugin_registry) < 角色模板(role_plugin_mapping) < 用户覆盖(user_layout_override) | | **三层 props 合并** | 系统默认 defaultProps < 角色默认 widgetProps < 用户调整 plugin_placements[].props | | **RSC** | React Server Component,Next.js App Router 的服务端组件,可异步获取数据 | | **dynamic import** | `next/dynamic` 懒加载,`ssr: false` 仅客户端渲染 | | **SWR** | stale-while-revalidate 数据请求库,支持静默后台刷新 | | **Zustand** | React 全局状态管理库,替代 EventBus | | **apollo-router** | Apollo Federation 聚合层,替代 3 BFF 手写聚合 | | **config-service** | 从 iam 拆分的配置服务,管理插件配置 + 布局 + 用户偏好 | | **DataScope** | IAM 6 级数据范围(school / grade / class / subject / student / self) | | **ScopeToken** | 大规模 ID 列表的轻量令牌(Redis 存储),替代 GraphQL 联邦全数组传递 | | **Modular Monolith** | 单体应用 + 模块化组织,介于单进程与微服务之间 | | **Micro-kernel** | 微内核架构,核心仅含基础框架,功能以插件形式扩展 | | **APQ** | Automatic Persisted Queries,前端只发 query hash(sha256),不发明文 query | | **PQ Manifest** | sha256(query) → query 文本的白名单 JSON,apollo-router 据此解析未知 hash | | **4 层数据访问分层** | Widget → API → Operations → Hook,废弃 widget 内联 gql 字面量(ADR-042) | | **@RequirePermission** | NestJS GraphQL Resolver 字段级权限装饰器,每个 Resolver 必须声明权限点 | | **RouterAuthGuard** | 子图 Guard,校验 `Router-Authorization` header,拒绝非 Router 的直接 GraphQL 请求 | | **shadcn/ui**(v2.0) | 基于 Radix UI + cva + tailwind-merge + clsx 的组件库,对齐 CICD 项目风格(ADR-044) | | **Tailwind v4**(v2.0) | Tailwind CSS v4,使用 `@import "tailwindcss"` + `@theme inline` CSS-first 配置,无 tailwind.config.js | | **use() Hook**(v2.0) | React 19 新 Hook,在 Suspense 边界内消费 Promise,实现流式渲染(ADR-045) | | **流式渲染**(v2.0) | RSC 返回 Promise → 客户端 use() 消费 → 首屏骨架秒出 → Config resolve 后流式注入(ADR-045) | | **三级错误边界**(v2.0) | Route(error.tsx)→ Section(SectionErrorBoundary)→ Widget(PluginBoundary)层层兜底(ADR-046) | | **PluginBoundary**(v2.0) | 插件级错误边界 + Suspense + Skeleton 三件套,替代旧 PluginLoader(ADR-046) | | **useErrorReport**(v2.0) | 错误上报 Hook,sendBeacon + sessionStorage 1 分钟同 digest 去重(ADR-046) | | **权限位图**(v2.0) | 67 权限点 → base36 字符串(~14 字符),压缩 JWT 体积 ≥ 99%(ADR-047) | | **PERMISSION_BITMAP_ORDER**(v2.0) | 67 个权限点的有序数组,权限位图的唯一合法来源 | | **三层安全边界**(v2.0) | L1 角色门禁 / L2 权限点门禁 / L3 数据范围,路由级 + 插件级 + 数据范围层层过滤(ADR-047) | | **路由权限配置表**(v2.0) | 4 张表(精确 / 前缀 / 仪表盘 / API)按优先级匹配,`checkRoutePermission` 主函数 | | **notify**(v2.0) | 统一 Toast 封装,禁止业务直接 import sonner,ESLint no-restricted-imports 强制 | | **cn()**(v2.0) | shadcn/ui 标准工具函数(clsx + tailwind-merge),管理条件类名 | | **SlotRenderer 信任输入**(v2.0) | L1/L2/L3 三层过滤在 RSC 服务端完成,客户端 SlotRenderer 不再二次过滤(性能优化) | --- ## 附录 A:文件清单 ``` apps/portal-shell/ ├─ src/ │ ├─ app/ │ │ ├─ api/ │ │ │ ├─ health/route.ts # liveness │ │ │ ├─ log/route.ts # v2.0 错误上报 mock 端点 │ │ │ └─ ready/route.ts # readiness │ │ ├─ shell/ │ │ │ ├─ [[...route]]/page.tsx # RSC 入口(v2.0 流式 Promise) │ │ │ ├─ error.tsx # v2.0 Route 级错误兜底 │ │ │ └─ loading.tsx # v2.0 Route 级加载骨架 │ │ ├─ globals.css # v2.0 全局样式 + Tailwind v4 + shadcn 令牌 │ │ ├─ layout.tsx # RootLayout + Inter 字体(v2.0) │ │ └─ page.tsx # 重定向 /shell │ ├─ lib/ │ │ ├─ __tests__/plugin-context.test.ts │ │ ├─ api/ # 数据访问层 │ │ │ ├─ __tests__/ │ │ │ │ ├─ admin.test.tsx # 4 用例(待补) │ │ │ │ ├─ parent.test.tsx # 11 用例 │ │ │ │ ├─ security.test.ts # 10 用例(PQ Manifest + APQ + 深度限制) │ │ │ │ ├─ sidebar.test.tsx # 9 用例 │ │ │ │ ├─ student.test.tsx # 9 用例 │ │ │ │ ├─ teacher.test.tsx # 7 用例 │ │ │ │ ├─ topbar.test.tsx # 9 用例 │ │ │ │ └─ universal.test.ts # 6 用例 │ │ │ ├─ operations/ │ │ │ │ ├─ *.graphql.ts # 51 个 DocumentNode 常量 │ │ │ │ ├─ index.ts # barrel │ │ │ │ └─ types.ts # codegen 生成 │ │ │ ├─ admin.ts # 11 widget API │ │ │ ├─ errors.ts # ApiError 归一化 │ │ │ ├─ internal.ts # 内部工具 │ │ │ ├─ parent.ts # 3 widget API │ │ │ ├─ sidebar.ts # 3 widget API │ │ │ ├─ student.ts # 5 widget API │ │ │ ├─ teacher.ts # 5 widget API │ │ │ ├─ topbar.ts # 2 widget API │ │ │ ├─ types.ts # 共享类型 │ │ │ └─ universal.ts # 7 widget API │ │ ├─ apollo-client.ts # Apollo Client 工厂 + 单例 + APQ 链 │ │ ├─ config-fetcher.ts # RSC 服务端拉 Config(三级降级) │ │ ├─ types.ts # 共享类型契约(v2.0 含 requiredPermissions) │ │ ├─ usePluginConfig.ts # SWR 静默刷新 │ │ ├─ useWidgetQuery.ts # 统一查询 Hook │ │ └─ useWidgetMutation.ts # 统一变更 Hook │ ├─ providers/ │ │ ├─ ApolloProvider.tsx │ │ ├─ AuthProvider.tsx │ │ └─ ThemeI18nProvider.tsx │ ├─ shared/ # v2.0 共享层 │ │ ├─ lib/ │ │ │ ├─ route-permissions.ts # v2.0 路由权限配置表(4 张表 + checkRoutePermission) │ │ │ ├─ notify.ts # v2.0 统一 Toast 封装 │ │ │ └─ utils.ts # v2.0 cn() 工具函数 │ │ └─ components/ │ │ ├─ plugin-boundary.tsx # v2.0 插件级错误边界 + Suspense + Skeleton │ │ ├─ route-error-boundary.tsx # v2.0 Route 级错误边界 │ │ ├─ section-error-boundary.tsx # v2.0 Section 级错误边界 │ │ ├─ dashboard/ │ │ │ ├─ dashboard-shell.tsx # v2.0 仪表盘外壳 │ │ │ └─ dashboard-section.tsx # v2.0 仪表盘分区 │ │ ├─ layout/ │ │ │ ├─ sidebar-provider.tsx # v2.0 侧边栏状态 │ │ │ ├─ app-sidebar.tsx # v2.0 应用侧边栏 │ │ │ └─ site-header.tsx # v2.0 顶部头部 │ │ └─ ui/ # v2.0 shadcn/ui 组件库 │ │ ├─ button.tsx / card.tsx / badge.tsx │ │ ├─ skeleton.tsx / input.tsx / tooltip.tsx │ │ ├─ sonner.tsx # Toaster │ │ ├─ page-header.tsx / stat-card.tsx │ │ ├─ stats-grid.tsx / empty-state.tsx │ │ └─ filter-bar.tsx │ ├─ shell/ │ │ ├─ __tests__/ │ │ │ ├─ PluginLifecycle.test.ts # 12 用例 │ │ │ └─ Registry.test.ts # 6 用例 │ │ ├─ ClientShell.tsx # v2.0 use(configPromise) 流式 + ShellContent 拆分 │ │ ├─ LayoutManager.tsx # 5 Layout 模板(v2.0 令牌迁移) │ │ ├─ PluginLifecycle.ts # 生命周期管理 │ │ ├─ PluginLoader.tsx # v2.0 re-export(向后兼容) │ │ ├─ PluginStore.ts # Zustand 全局状态 │ │ ├─ PropsMerger.ts # 三层合并 │ │ ├─ Registry.tsx # 31 插件注册表 │ │ ├─ Shell.tsx # 微内核入口 │ │ └─ SlotRenderer.tsx # v2.0 PluginBoundary 包裹 + 信任输入 │ ├─ styles/ │ │ └─ tokens.css # 设计令牌映射(旧,待 P1 移除) │ └─ widgets/ │ ├─ admin/ # 6 插件 │ │ ├─ audit-logs/ │ │ ├─ invitation-codes/ │ │ ├─ plugin-manager/ │ │ ├─ rbac-manager/ │ │ ├─ school-settings/ │ │ └─ user-management/ │ ├─ parent/ # 2 插件 │ │ ├─ child-overview/ │ │ └─ leave-approval/ │ ├─ sidebar/ # 4 插件 │ │ ├─ child-selector/ │ │ ├─ class-selector/ │ │ ├─ quick-actions/ │ │ └─ term-switcher/ │ ├─ student/ # 4 插件 │ │ ├─ ai-tutor/ │ │ ├─ elective-selector/ │ │ ├─ error-book/ │ │ └─ learning-path/ │ ├─ teacher/ # 4 插件 │ │ ├─ lesson-plan-editor/ │ │ ├─ question-bank/ │ │ ├─ scheduling-rules/ │ │ └─ textbook-manager/ │ ├─ topbar/ # 4 插件 │ │ ├─ global-search/ │ │ ├─ locale-switcher/ │ │ ├─ notification-bell/ │ │ └─ user-menu/ │ └─ universal/ # 7 插件 │ ├─ announcements-widget/ │ ├─ attendance-widget/ │ ├─ exams-widget/ │ ├─ grades-widget/ │ ├─ homework-widget/ │ ├─ notifications-widget/ │ └─ schedule-widget/ ├─ .env.example ├─ components.json # v2.0 shadcn CLI 配置 ├─ codegen.yml # graphql-codegen 配置 ├─ Dockerfile # standalone 构建 ├─ eslint.config.js # v2.0 含 no-restricted-imports(notify 强制) ├─ next.config.js # transpilePackages + 反向代理 + Turbopack ├─ package.json ├─ postcss.config.js ├─ public/ │ └─ pq-manifest.json # PQ Manifest(51 query hash → query 白名单) ├─ scripts/ │ ├─ generate-pq-manifest.ts # PQ Manifest 生成脚本 │ └─ normalize-schema.ts # schema 归一化(移除 federation 指令) ├─ tsconfig.json # paths 别名(v2.0 含 @edu/shared-ts/permission-bitmap) ├─ vitest.config.ts # jsdom + 别名 └─ README.md # 本文件 ``` **关联包文件清单**: ``` packages/ ├─ shared-ts/src/ │ ├─ permission-bitmap.ts # v2.0 权限位图工具(67 权限点 + base36) │ └─ contracts/plugin.ts # v2.0 PluginManifest.metadata.requiredPermissions ├─ hooks/src/ │ └─ use-error-report.ts # v2.0 错误上报 Hook ├─ ui-components/src/ # v2.0 13 个文件令牌迁移完成 │ ├─ plugin-error-fallback.tsx │ ├─ plugin-skeleton.tsx │ ├─ plugin-card.tsx │ ├─ slot-placeholder.tsx │ ├─ status-badge.tsx │ ├─ props-config-form.tsx │ ├─ form.tsx │ ├─ modal.tsx │ ├─ data-table.tsx │ ├─ filter-bar.tsx │ ├─ chart.tsx │ ├─ calendar.tsx │ └─ rich-text-editor.tsx └─ ui-tokens/src/ ├─ primitive.css # v2.0 zinc/stone/indigo 色板 ├─ semantic-light.css # v2.0 shadcn 标准语义令牌(light) ├─ semantic-dark.css # v2.0 shadcn 标准语义令牌(dark) └─ tailwind-theme.css # v2.0 @theme inline ``` ## 附录 B:常用命令 ```bash # 开发 pnpm --filter @edu/portal-shell run dev # 启动 dev server :4010(Turbopack) pnpm --filter @edu/portal-shell run build # 生产构建(prebuild 自动 codegen + generate-pq-manifest) pnpm --filter @edu/portal-shell run start # 生产启动 # 质量校验 pnpm --filter @edu/portal-shell run lint # ESLint(含 v2.0 no-restricted-imports: notify 强制) pnpm --filter @edu/portal-shell run lint:tokens # 设计令牌专项 pnpm --filter @edu/portal-shell run typecheck # tsc --noEmit pnpm --filter @edu/portal-shell run test # vitest run(95 用例) # 数据层与安全栈 pnpm --filter @edu/portal-shell run codegen # graphql-codegen 生成 TS 类型 pnpm --filter @edu/portal-shell run generate-pq-manifest # 生成 public/pq-manifest.json npx vitest run src/lib/api/__tests__/security.test.ts # 安全栈测试(10 用例) # v2.0 验证(待补测试文件) # npx vitest run packages/shared-ts/__tests__/permission-bitmap.test.ts # 权限位图(待补) # npx vitest run src/shared/lib/__tests__/route-permissions.test.ts # 路由权限(待补) # npx vitest run src/shared/lib/__tests__/notify.test.ts # notify 封装(待补) # npx vitest run packages/hooks/__tests__/use-error-report.test.ts # 错误上报(待补) # npx vitest run src/shared/components/__tests__/plugin-boundary.test.tsx # PluginBoundary(待补) # 架构扫描 pnpm run arch:scan # 更新 arch.db pnpm run arch:query -- module-deps # 查模块依赖 pnpm run arch:query -- violations # 查架构违规(含旧纸感令牌检测) ``` ## 附录 C:关联文档索引 | 文档 | 用途 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md) | 架构设计意图唯一源(§11.7 数据访问层 + 安全栈、§16.5 子阶段、v2.0 ADR-044/045/046/047) | | [portal-shell 仪表盘 spec](../../docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md) | 模块设计源(v1.0) | | [portal-shell 数据抽象与 GraphQL 加固 spec](../../docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) | 数据访问层 + 安全栈设计源(含 v2.0 三层安全边界 + 流式渲染 + 三级错误处理) | | [portal-shell 数据抽象与 GraphQL 加固 plan](../../docs/superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md) | 20-task 实施 plan | | [GraphQL @auth 审计报告](../../docs/security/graphql-auth-audit-2026-07.md) | 50 resolver @auth 审计(M4) | | [项目规则](../../.trae/rules/project_rules.md) | 强制约束(§3.10 设计令牌、§14 多 AI 协作) | | [UI 设计系统](../../docs/standards/ui-design-system.md) | 设计风格(v2.0 已迁移到 shadcn 标准) | | [known-issues §1.11/§2.17](../../docs/troubleshooting/known-issues.md) | Apollo Router + portal-shell 已知问题速查 | | [local-stack runbook](../../docs/runbooks/local-stack.md) | 本地运维手册 | | [端口分配](../../infra/port-allocation.md) | 端口唯一源 | --- > **本文件是 portal-shell 模块的架构文档(v2.0,2026-07-17),遵循 arc42 模板结构 + C4 模型可视化。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan` 更新 arch.db。v2.0 变更摘要:shadcn/ui 标准化 + Tailwind v4 + React 19 use() 流式渲染 + 三级错误边界 + 三层安全边界(权限位图 base36 压缩)+ notify 统一封装 + PluginBoundary 替代 PluginLoader。**