Files
Edu/apps/portal-shell/README.md
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等
- Tailwind v4 + @theme inline,移除 tailwind.config.js
- React 19 use() + Suspense 流式渲染,首屏骨架秒出
- 三级错误边界:Route → Section → Widget 层层兜底
- 错误上报:useErrorReport → sendBeacon → /api/log mock 端点
- 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围
- 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99%
- notify 统一 Toast 封装,禁止业务直接 import sonner
- PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套)

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
2026-07-17 16:10:05 +08:00

1884 lines
134 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 个独立 portalteacher / student / parent / admin+ Module Federation 微前端方案。它承载教师、学生、家长、管理员四类角色的全部教学场景 UI通过 **Micro-kernel + 插件化** 机制实现功能扩展。
- **服务端口**4010HTTP
- **技术栈**v2.0
- **核心**TypeScript 5.6 + Next.js 16 App RouterTurbopack 默认)+ React 19`use()` Hook 流式渲染)
- **样式**Tailwind v4`@import "tailwindcss"` + `@theme inline`,无 `tailwind.config.js`+ shadcn/ui 标准令牌(`--background` / `--foreground` / `--card` / `--primary` 等语义令牌)
- **状态**ZustandUI 状态)+ SWR配置静默刷新+ Apollo ClientGraphQL
- **组件库**shadcn/uiRadix UI + cva + tailwind-merge + clsx
- **字体**Inter 单一字体族(`next/font/google` self-host → `--font-inter` CSS 变量)
- **架构风格**Modular Monolith单体+ Micro-kernel微内核插件
- **部署形态**:单 Docker 容器(`output: "standalone"`
- **上游依赖**api-gatewayJWT 校验 + 反向代理、apollo-routerGraphQL 联邦入口)
- **下游契约**:通过 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 ≤ 300KBgzip |
| 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<T>` |
| 禁 `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["插件化仪表盘<br/>Modular Monolith + Micro-kernel"]
end
subgraph Upstream["上游服务"]
Gateway["api-gateway :8080<br/>JWT 校验 + 反向代理"]
Router["apollo-router :3000<br/>GraphQL 联邦入口"]
Realtime["realtime-gateway :8081<br/>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["路由进入<br/>/shell/*"]
end
subgraph L1["L1 角色门禁"]
L1Check["checkRoutePermission<br/>requiredRoles?"]
end
subgraph L2["L2 权限点门禁"]
L2Check["位图校验<br/>requiredPermissions?"]
end
subgraph L3["L3 数据范围"]
L3Check["DataScope 6 级<br/>config-service 过滤"]
end
subgraph Plugin["插件渲染"]
P["SlotRenderer 信任输入<br/>不再二次过滤"]
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_ORDER67 个权限点)**:开发时通过 `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 契约 ↑
┌──────────────────────────────────────────────────────────┐
│ Widgets31 内置插件,按 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<br/>RootLayout + Inter 字体"]
RootPage["page.tsx<br/>重定向 /shell"]
ShellPage["shell/[[...route]]/page.tsx<br/>RSC 入口 + 流式 Promise"]
ShellError["shell/error.tsx<br/>Route 错误兜底v2.0"]
ShellLoading["shell/loading.tsx<br/>Route 加载骨架v2.0"]
HealthAPI["api/health/route.ts"]
ReadyAPI["api/ready/route.ts"]
LogAPI["api/log/route.ts<br/>错误上报 mock 端点v2.0"]
end
subgraph ShellCore["shell/(微内核)"]
Shell["Shell.tsx"]
ClientShell["ClientShell.tsx<br/>use(configPromise) 流式"]
LayoutMgr["LayoutManager.tsx<br/>5 Layout 模板"]
SlotRend["SlotRenderer.tsx<br/>PluginBoundary 包裹"]
Loader["PluginLoader.tsx<br/>re-export 向后兼容"]
Registry["Registry.tsx<br/>31 插件"]
Lifecycle["PluginLifecycle.ts"]
Store["PluginStore.ts<br/>Zustand"]
Merger["PropsMerger.ts<br/>三层合并"]
end
subgraph Lib["lib/(数据抽象)"]
Apollo["apollo-client.ts<br/>APQ + sha256"]
ConfigFetch["config-fetcher.ts"]
UseConfig["usePluginConfig.ts<br/>SWR 静默刷新"]
UseQuery["useWidgetQuery.ts"]
UseMut["useWidgetMutation.ts"]
Types["types.ts"]
end
subgraph ApiLayer["lib/api/(数据访问层)"]
ApiDomain["<domain>.ts ×7<br/>parent/teacher/admin/student/<br/>universal/sidebar/topbar"]
ApiOps["operations/*.graphql.ts<br/>51 DocumentNode"]
ApiTypes["operations/types.ts<br/>codegen 生成"]
ApiErrors["errors.ts<br/>ApiError 归一化"]
end
subgraph Shared["shared/v2.0 共享层)"]
SharedLib["shared/lib/<br/>route-permissions.ts<br/>notify.ts / utils.ts"]
SharedComp["shared/components/<br/>plugin-boundary.tsx<br/>route-error-boundary.tsx<br/>section-error-boundary.tsx<br/>dashboard/* / layout/* / ui/*"]
SharedHooks["@edu/hooks<br/>use-error-report.ts"]
end
subgraph Providers["providers/"]
ApolloProv["ApolloProvider.tsx"]
AuthProv["AuthProvider.tsx"]
ThemeProv["ThemeI18nProvider.tsx"]
end
subgraph Widgets["widgets/31 内置插件)"]
Universal["universal/<br/>7 插件"]
Sidebar["sidebar/<br/>4 插件"]
Topbar["topbar/<br/>4 插件"]
Teacher["teacher/<br/>4 插件"]
Student["student/<br/>4 插件"]
Parent["parent/<br/>2 插件"]
Admin["admin/<br/>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/** | `<domain>.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` | 主题类名同步到 `<html>` + 简易 i18n | `usePluginStore` theme/locale |
| **widgets/** | `*/index.tsx` | 插件入口,接收 `PluginProps`default export | `PluginProps` 契约 |
| **widgets/** | `*/plugin.manifest.ts` | 插件元数据声明 | `manifestMeta: Omit<PluginManifest, "Component">`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 <Shell config={liveConfig} userId={userId} role={role} />;
}
export function ClientShell(props: ClientShellProps): ReactNode {
return (
<ApolloProvider>
<AuthProvider user={props.user}>
<ThemeI18nProvider>
{/* Suspense 包裹 ShellContentProvider 不被暂停 */}
<Suspense fallback={<RouteSkeleton />}>
<ShellContent {...props} />
</Suspense>
</ThemeI18nProvider>
</AuthProvider>
</ApolloProvider>
);
}
```
**关键约束**
1. **Provider 必须在 Suspense 外**ApolloProvider / AuthProvider / ThemeI18nProvider 不能被 `use()` 暂停,否则子树丢失 Context
2. **RSC 不 await Promise**`fetchPluginConfig` 返回 Promise 直接传给客户端,服务端不阻塞
3. **客户端 `use()` 消费**React 19 的 `use()` Hook 可在 Suspense 边界内消费 Promise
4. **单插件独立 Suspense**:每个插件用 `<PluginBoundary>` 包裹,加载失败或慢不影响其他插件
### 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<br/>app/shell/error.tsx"]
RFallback["整页错误页<br/>含错误 digest + 重试按钮"]
end
subgraph Section["Section 级"]
SEB["SectionErrorBoundary<br/>DashboardSection 内"]
SFallback["区块级错误卡片<br/>显示 Section 标题 + 重试"]
end
subgraph Widget["Widget 级(最细粒度)"]
WEB["PluginBoundary<br/>SlotRenderer 内每个插件"]
WFallback["PluginErrorFallback<br/>显示 instanceId + 重试"]
end
subgraph Report["错误上报链路"]
Hook["useErrorReport<br/>sendBeacon + sessionStorage 节流"]
API["/api/log<br/>mock 端点"]
Store["sessionStorage<br/>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<string, unknown>) => {
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 必须用 `<PluginBoundary>` 包裹Section 必须用 `<SectionErrorBoundary>`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<TProps = Record<string, unknown>> {
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<PluginManifest, "Component"> = {
pluginId: "grades-widget",
version: "0.1.0",
requiredShellVersion: "^1.0.0", // semver rangeShell 启动时校验
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 Schemaadmin 配置面板自动渲染表单
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 | 安装时登记到 DBplugin_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: 每个插件独立 Suspensedynamic import 流式加载
U->>U: 插件用 initialData 作为 SWR fallbackData 渲染
U->>U: 加载完成的插件立即渲染,不影响其他插件
```
**对比 v1.0 CSR 瀑布流**
```
v1.04 层串行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: 切回 TabrevalidateOnFocus<br/>或 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<br/>multi-stage"]
Img["本地镜像<br/>(不推送 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 InterCSS 变量暴露为 `--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`
- **插件容器**`<section className="rounded-xl border bg-card p-4">`
- **插件标题**`text-lg text-foreground`
- **加载态**`<PluginSkeleton variant="card|list|chart|stats|table" />`5 种骨架变体)
- **错误态**`<PluginErrorFallback instanceId={...} onRetry={...} />`
- **统一 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 cookieapi-gateway 配置) |
| 开发模式 | `NEXT_PUBLIC_DEV_MODE=true` 绕过 JWT仅本地开发 |
| **APQ** | `createPersistedQueryLink({ sha256 })` 前端只发 query hashenv `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 拒绝未知 hashentrypoint.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 v42026-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 + initialDataLCP 秒开 |
| **三层 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 inlinev2.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 可配置插件默认 propsJSON 编辑)
- [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**Routeerror.tsx→ SectionSectionErrorBoundary→ WidgetPluginBoundary层层兜底
- [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 requiredPermissionsv2.0**:插件级 L2 权限点门禁config-service 三层合并时过滤
- [x] **notify 统一 Toast 封装v2.0**:禁止业务直接 import sonnerESLint 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.0Next.js 16 Turbopack6 路由生成成功:`/` / `/_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.0ESLint 强制,无硬编码颜色/字体/任意值)
- [x] apollo-router 启用 APQ + PQ Manifest + 深度/成本/批量限制
- [x] 50 个 GraphQL resolver @auth 审计完成35 TS + 15 Python19 个 TS resolver 补齐 @RequirePermission
- [x] arch.db 更新004 文档同步
- [x] **v2.0 README 同步**:本文件 v2.0004 同步更新 ADR-044/045/046/047
- [x] **v2.0 流式渲染验证**:首屏 HTML 直出骨架Config resolve 后流式注入(本地 Docker 验证)
- [ ] Shell 首屏 LCP < 2s需真实环境压测验证
- [ ] 插件加载耗时 < 500msdynamic 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` | 🚧 待补 |
| 单元测试 | 路由权限配置表 + checkRoutePermissionv2.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-originBFF 请求由 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.031 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.0notify 封装单元测试待补** | 低 | 补 4 个方法调用 + ESLint no-restricted-imports 验证 |
| TD-13 | **v2.0useErrorReport 节流逻辑测试待补** | 中 | 补 sessionStorage 1 分钟同 digest 去重 + sendBeacon |
| TD-14 | **v2.0PluginBoundary 三件套测试待补** | 中 | 补 ErrorBoundary + Suspense + Skeleton 5 变体 |
| TD-15 | **v2.0:错误上报端点生产替换** | 中 | 后端实现 /api/v1/log 后,移除 Next.js API Route mock |
| TD-16 | **v2.0TS 子图 AuthMiddleware 覆盖 /graphql** | 中 | 4 个 TS 子图补齐字段级守卫(不依赖 RouterAuthGuard 兜底) |
### 12.3 演进路线
| 阶段 | 内容 | 状态 |
| ----------- | ----------------------------------------------------------------------------------------------- | --------------------- |
| M8 | portal-shell 接入 apollo-routerRSC 预取 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
> 关联 ADRADR-042前端数据访问四层分层、ADR-043PQ 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<br/>只关心渲染]
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<br/>51 个 DocumentNode 常量]
end
subgraph Hook["Hook 层Apollo 封装)"]
H1[useWidgetQuery]
H2[useWidgetMutation]
end
subgraph Codegen["类型生成"]
C[graphql-codegen<br/>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/<domain>.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/<svc>/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<br/>sha256 query → hash]
end
subgraph Router["apollo-router :3000"]
Manifest[pq-manifest.json<br/>hash → query 白名单]
Limits[limits.max_depth=10<br/>max_cost=1000<br/>max_batch_size=5]
Intro[introspection<br/>环境变量控制]
end
subgraph Sub["子图 /graphql"]
Guard[RouterAuthGuard<br/>+ @RequirePermission]
end
APQ -->|只发 hash| Manifest
Manifest -->|未知 hash 拒绝| APQ
Manifest --> Limits
Limits --> Intro
Intro --> Guard
```
**安全机制矩阵**
| 机制 | 位置 | 防御目标 | 配置 |
| ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- |
| APQAutomatic 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 个 resolver35 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["用户请求<br/>/shell/*"] --> L1
L1["L1 角色门禁<br/>route-permissions.ts<br/>requiredRoles?"] -->|通过| L2
L1 -->|拒绝| Deny403["403 Forbidden"]
L2["L2 权限点门禁<br/>位图校验<br/>requiredPermissions?<br/>anyOfPermissions?"] -->|通过| L3
L2 -->|拒绝| Deny403
L3["L3 数据范围<br/>config-service<br/>DataScope 6 级"] -->|过滤| Plugin["插件渲染<br/>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<br/>不 await"]
Promise["返回 Promise"]
end
subgraph Client["客户端"]
Suspense["Suspense 边界"]
Use["use(configPromise)"]
Render["ShellContent 渲染"]
end
subgraph Loading["加载态"]
Skeleton["整页骨架<br/>shell/loading.tsx"]
PluginSk["PluginSkeleton<br/>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<br/>app/shell/error.tsx"]
end
subgraph Section["Section 级"]
SEB["SectionErrorBoundary<br/>DashboardSection 内"]
end
subgraph Widget["Widget 级"]
PB["PluginBoundary<br/>SlotRenderer 内"]
end
subgraph Report["错误上报"]
Hook["useErrorReport"]
Beacon["sendBeacon"]
API["/api/log mock"]
Store["sessionStorage<br/>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` | 错误上报 HooksendBeacon + 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 ComponentNext.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 hashsha256不发明文 query |
| **PQ Manifest** | sha256(query) → query 文本的白名单 JSONapollo-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 | Routeerror.tsx→ SectionSectionErrorBoundary→ WidgetPluginBoundary层层兜底ADR-046 |
| **PluginBoundary**v2.0 | 插件级错误边界 + Suspense + Skeleton 三件套,替代旧 PluginLoaderADR-046 |
| **useErrorReport**v2.0 | 错误上报 HooksendBeacon + 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 sonnerESLint 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-importsnotify 强制)
├─ next.config.js # transpilePackages + 反向代理 + Turbopack
├─ package.json
├─ postcss.config.js
├─ public/
│ └─ pq-manifest.json # PQ Manifest51 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 :4010Turbopack
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 run95 用例)
# 数据层与安全栈
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.02026-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。**