Files
Edu/apps/portal-shell
SpecialX 04b7a40bdc feat(portal-shell): 学生域全页面迁移与规范合规修复
- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断)

- 补齐 4 个 MSW mock 缺口,修 diagnostic case 名

- 修 4 处 Tailwind 任意值;新增共享组件与路由
2026-08-31 11:25:21 +08:00
..

portal-shell 模块架构文档

⚠️ 重要2026-07-20:本文件 v2.0 的多处完成度声明与实测不符(详见审计)。portal-shell 前端工作的唯一权威文档已迁移至 ARCHITECTURE.mdv3.0 总纲:现状审计 + 目标架构 + 页面迁移路线图 + 工作规范)。本文件仅其"微内核仪表盘子系统"设计部分继续有效,凡与本文件冲突处以 ARCHITECTURE.md 为准。

版本2.0 日期2026-07-17 状态已落地v2.1 M8-M12 完成 + v1.1 数据抽象与 GraphQL 加固完成 + v2.0 shadcn 标准化 + 三层安全边界 + 流式渲染 + 三级错误处理完成 + P0-P4 全部验证通过typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功 / 206 测试全部通过) 架构范式Modular Monolith + Micro-kernel单 Next.js App Router 容器 + 插件化仪表盘) 关联文档:


目录

  1. 引言与目标
  2. 架构约束
  3. 系统范围与上下文C4 L1
  4. 解决方案策略
  5. 构建块视图C4 L2/L3
  6. 插件目录与分类
  7. 运行时视图(关键场景)
  8. 部署视图
  9. 横切概念
  10. 架构决策记录ADR 索引)
  11. 质量要求与验收
  12. 风险、技术债与演进路线
  13. 数据访问层与 GraphQL 安全栈
  14. v2.0 安全边界与错误处理
  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 19use() 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.0RSC 返回 Promise → React 19 use() 消费 + 三层 Suspense 首屏 HTML 直出 Layout 骨架Config 等数据流式注入
G9 三级错误兜底v2.0Route → Section → Widget 三级 ErrorBoundary 单插件崩溃不污染同 Slot 其他插件;整页崩溃有 Route 兜底
G10 三层安全边界v2.0L1 角色门禁 / 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 图

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-permissionsv2.0 位图头) api-gateway RSC headers() 读取 67 权限点 base36 压缩串
JWT iam 签发 → api-gateway 校验 Apollo Client Authorization: Bearer localStorage + cookie 双通道

3.3 三层安全边界v2.0 新增)

来源:三层安全边界设计 §v2.0 关联:权限位图工具路由权限配置

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. 批量检查 APIbatchCheckRoutePermission(paths, bitmap, role) 用于侧边栏导航批量过滤

路由权限配置示例

// 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 + 运行时 ConfigRegistry 是 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_0001 分钟内去重
  • 检测到配置变化时回调上层 Toast 提示"发现新布局配置,刷新后生效"

5. 构建块视图C4 L2/L3

5.1 Container 图C4 L2

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.tsxv2.0 Route 级错误兜底 Next.js error.tsx + RouteErrorBoundary
app/ shell/loading.tsxv2.0 Route 级加载骨架 整页骨架(顶栏 + 侧栏 + 主区仪表盘骨架)
app/ api/log/route.tsv2.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.tsv2.0 路由权限配置表 + L1/L2 检查 checkRoutePermission / batchCheckRoutePermission / validateRoutePermissionConfigs
shared/lib/ notify.tsv2.0 统一 Toast 封装(禁止业务直接 import sonner notify.info/success/warning/error
shared/lib/ utils.tsv2.0 cn() 工具函数clsx + tailwind-merge shadcn/ui 标准工具
shared/components/ plugin-boundary.tsxv2.0 插件级错误边界 + Suspense + Skeleton 三件套 PluginBoundary / PluginSkeleton5 变体)/ PluginErrorFallback
shared/components/ route-error-boundary.tsxv2.0 Route 级错误边界 Next.js error.tsx 内部使用
shared/components/ section-error-boundary.tsxv2.0 Section 区块级错误边界 DashboardSection 内部使用
shared/components/dashboard/ dashboard-shell.tsx / dashboard-section.tsxv2.0 仪表盘外壳 + 分区组件 仪表盘场景专用
shared/components/layout/ sidebar-provider.tsx / app-sidebar.tsx / site-header.tsxv2.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.tsxv2.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 插件入口,接收 PluginPropsdefault 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 流式渲染 关联:apps/portal-shell/src/app/shell/[[...route]]/page.tsx + apps/portal-shell/src/shell/ClientShell.tsx

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 流式拆分

// 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 PromisefetchPluginConfig 返回 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

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 失败、组件抛错) PluginErrorFallbackinstanceId + 重试)

错误上报链路

// @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

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 字段):

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_ORDER67 个权限点),运行时由 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 直出骨架

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 静默刷新)

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 自动响应

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 三件套隔离)

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 容器化

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-alpineCI 预拉,见 infra/docker-compose.tools.yml
  • 构建模式output: "standalone"Next.js 内置,自动追踪依赖,产物在 .next/standalone/
  • 多阶段构建depsbuilderrunner
  • 依赖处理
    • 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/healthliveness+ /api/readyreadiness

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.ymlquality-ts job
  • 触发:分支 push / PR质量检查、合并到 main部署
  • 质量门禁pnpm lint + tsc --noEmit + vitest run + next build
  • 部署docker compose up -d --buildno-push 本地构建模式,见 project_rules §15
  • 回滚git revert + pushworkflow_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.csssemantic-*.csstailwind-theme.cssemail-channelmanifest.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.jsTailwind 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={...} />
  • 统一 Toastnotify.info/success/warning/error(禁止业务直接 import { toast } from "sonner"

9.4 可观测性

维度 实现 端点
健康检查 liveness + readiness /api/health + /api/ready
错误日志 console.error + 三级 ErrorBoundary + useErrorReportv2.0 浏览器控制台 + /api/logv2.0 mock
性能 Next.js 内置 Web Vitals可接 OTel Next.js 自动采集
请求追踪 Apollo Client 自动携带 traceparent 经 api-gateway 注入 X-Request-Id
错误上报v2.0 sendBeacon + sessionStorage 节流1 分钟同 digest 去重) /api/logmock/ /api/v1/log(生产,待后端实现)

9.5 国际化MVP

  • locale 管理PluginStore.localezh-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 封装。

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 功能验收

  • 5 种 Layout 模板可切换slot 正确渲染
  • 31 个内置插件全部加载正常7 universal + 4 sidebar + 4 topbar + 4 teacher + 4 student + 2 parent + 6 admin
  • admin 可通过 plugin-manager 插件配置角色插件集合registry / mapping / layout / user 四 Tab
  • admin 可配置角色默认 Layout 模板
  • admin 可配置插件默认 propsJSON 编辑)
  • admin 可重置用户自定义布局
  • admin 改配置 → SWR 静默刷新检测到变化 → Toast 提示用户刷新生效
  • 插件加载失败显示错误兜底不影响其他插件PluginBoundary 三件套v2.0
  • 跨插件状态共享正常class-selector 切换班级 → grades-widget 自动响应)
  • 插件 props 三层合并正确PropsMerger
  • RSC 服务端预取正常:首屏 HTML 直出 Config + initialData
  • useWidgetQuery 自动经 Apollo Client 路由到 apollo-router
  • URL Search Params 可分享、可前进后退
  • config-fetcher 三级降级apollo-router → config-service 直连 → 空默认
  • 流式渲染v2.0RSC 返回 Promise → use() 消费 → 首屏骨架秒出 → Config 流式注入
  • 三级错误边界v2.0Routeerror.tsx→ SectionSectionErrorBoundary→ WidgetPluginBoundary层层兜底
  • 错误上报链路v2.0useErrorReport → sendBeacon → /api/log mock 端点sessionStorage 1 分钟同 digest 去重
  • 三层安全边界v2.0L1 角色门禁 + L2 权限点门禁 + L3 数据范围,路由权限配置表 4 张按优先级匹配
  • 权限位图v2.067 权限点 base36 编解码 + hasPermissionInBitmap / hasAllPermissionsInBitmap / hasAnyPermissionInBitmap
  • PluginManifest requiredPermissionsv2.0:插件级 L2 权限点门禁config-service 三层合并时过滤
  • notify 统一 Toast 封装v2.0:禁止业务直接 import sonnerESLint no-restricted-imports 强制
  • shadcn 标准化v2.0packages/ui-components 13 个文件令牌迁移完成bg-paper→bg-background 等)
  • shadcn/ui 组件库v2.0button / card / badge / skeleton / input / tooltip / sonner / page-header / stat-card / stats-grid / empty-state / filter-bar

11.2 非功能验收

  • pnpm run lint + pnpm run typecheck 零错误v2.0:含 2 个 auto-generated 文件警告,可忽略)
  • pnpm run build 通过v2.0Next.js 16 Turbopack6 路由生成成功:/ / /_not-found / /api/health / /api/log / /api/ready / /shell/[[...route]]
  • pnpm run test 206/206 通过lib/api 7 domain 55 用例 + 安全栈 10 用例 + Shell/Lifecycle/Context 30 用例 + v2.0 新增组件单元测试 81 用例 + v2.0 E2E 测试 30 用例)
  • 0 处 widget 内联 gql 字面量强制arch:scan 违规检测)
  • 所有新代码遵守 shadcn 标准令牌v2.0ESLint 强制,无硬编码颜色/字体/任意值)
  • 31 个 widget 旧纸感令牌全部迁移到 shadcn 标准P11104 次替换arch:scan 零违规)
  • apollo-router 启用 APQ + PQ Manifest + 深度/成本/批量限制
  • 50 个 GraphQL resolver @auth 审计完成35 TS + 15 Python19 个 TS resolver 补齐 @RequirePermission
  • arch.db 更新004 文档同步
  • v2.0 README 同步:本文件 v2.0004 同步更新 ADR-044/045/046/047
  • v2.0 流式渲染验证:首屏 HTML 直出骨架Config resolve 后流式注入(本地 Docker 验证)
  • v2.0 P2 单元测试5 文件 81 用例(权限位图 24 + 路由权限 26 + notify 12 + useErrorReport 9 + PluginBoundary 10
  • v2.0 P3 生产端点api-gateway POST /api/v1/log + useErrorReport 环境感知端点切换
  • v2.0 P4 E2E 测试3 文件 30 用例(流式渲染 4 + 三级错误边界 6 + 三层安全边界 20
  • Shell 首屏 LCP < 2s需真实环境压测验证
  • 插件加载耗时 < 500msdynamic import 缓存命中后,需真实环境验证)
  • 单元测试覆盖率 ≥ 80%(当前覆盖核心纯函数 + lib/api 全量 + v2.0 新增组件全量admin domain 仅 4 用例待补,插件组件渲染测试待补)
  • E2E 测试tests/e2e/ 真实浏览器场景,含 admin 改配置 → 用户刷新生效、apollo-router 深度限制)
  • 视觉回归测试5 种 layout 截图,待补,含 v2.0 shadcn 标准化对比)
  • 生产部署前 APOLLO_REQUIRE_PQ_MANIFEST=true + APOLLO_ROUTER_INTROSPECTION=false 写入部署 env

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 src/shared/lib/__tests__/permission-bitmap.test.ts 24 用例
单元测试 路由权限配置表 + checkRoutePermissionv2.0 src/shared/lib/__tests__/route-permissions.test.ts 26 用例
单元测试 notify 统一封装v2.0 src/shared/lib/__tests__/notify.test.ts 12 用例
单元测试 useErrorReport 节流逻辑v2.0 src/shared/lib/__tests__/use-error-report.test.ts 9 用例
单元测试 PluginBoundary 三件套v2.0 src/shared/components/__tests__/plugin-boundary.test.tsx 10 用例
E2E 流式渲染React 19 use() + Suspensev2.0 src/__tests__/e2e/streaming.test.tsx 4 用例
E2E 三级错误边界层级v2.0 src/__tests__/e2e/error-boundaries.test.tsx 6 用例
E2E 三层安全边界L1 角色 + L2 权限 + L3 范围v2.0 src/__tests__/e2e/security-boundaries.test.ts 20 用例
单元测试 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 错误上报端点 mockv2.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 标准 完成2026-07-17
v2.0 P2 v2.0 新增组件单元测试补齐(权限位图 / 路由权限 / notify / useErrorReport / PluginBoundary 完成2026-07-17
v2.0 P3 错误上报端点生产替换(后端 /api/v1/log 完成2026-07-17
v2.0 P4 E2E 测试(流式渲染 + 三级错误边界 + 三层安全边界) 完成2026-07-17
P5二期 第三方插件上传 + iframe 沙箱 规划
P6二期 插件市场在线商店 规划
P7二期 canvas 拖拽编辑器 规划
P8二期 next-intl 完整 i18n 规划
P9二期 视觉回归测试自动化 规划

13. 数据访问层与 GraphQL 安全栈

来源:portal-shell 数据抽象与 GraphQL 加固 spec 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

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

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=trueentrypoint.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 中所有 DocumentNodeprint(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

已审计范围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 / msgAuthMiddleware 仅覆盖 REST 路径,未覆盖 /graphql(由 RouterAuthGuard 兜底,仍建议补齐字段级守卫)
  • Python 子图data-ana / ai@RequirePermission 基础设施,需补 Strawberry / Ariadne 中间件

13.6 常用命令

# 类型生成(从 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

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

// 编码:权限点数组 → 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

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

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/uiv2.0 基于 Radix UI + cva + tailwind-merge + clsx 的组件库,对齐 CICD 项目风格ADR-044
Tailwind v4v2.0 Tailwind CSS v4使用 @import "tailwindcss" + @theme inline CSS-first 配置,无 tailwind.config.js
use() Hookv2.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
PluginBoundaryv2.0 插件级错误边界 + Suspense + Skeleton 三件套,替代旧 PluginLoaderADR-046
useErrorReportv2.0 错误上报 HooksendBeacon + sessionStorage 1 分钟同 digest 去重ADR-046
权限位图v2.0 67 权限点 → base36 字符串(~14 字符),压缩 JWT 体积 ≥ 99%ADR-047
PERMISSION_BITMAP_ORDERv2.0 67 个权限点的有序数组,权限位图的唯一合法来源
三层安全边界v2.0 L1 角色门禁 / L2 权限点门禁 / L3 数据范围,路由级 + 插件级 + 数据范围层层过滤ADR-047
路由权限配置表v2.0 4 张表(精确 / 前缀 / 仪表盘 / API按优先级匹配checkRoutePermission 主函数
notifyv2.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常用命令

# 开发
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 架构影响地图 架构设计意图唯一源§11.7 数据访问层 + 安全栈、§16.5 子阶段、v2.0 ADR-044/045/046/047
portal-shell 仪表盘 spec 模块设计源v1.0
portal-shell 数据抽象与 GraphQL 加固 spec 数据访问层 + 安全栈设计源(含 v2.0 三层安全边界 + 流式渲染 + 三级错误处理)
portal-shell 数据抽象与 GraphQL 加固 plan 20-task 实施 plan
GraphQL @auth 审计报告 50 resolver @auth 审计M4
项目规则 强制约束§3.10 设计令牌、§14 多 AI 协作)
UI 设计系统 设计风格v2.0 已迁移到 shadcn 标准)
known-issues §1.11/§2.17 Apollo Router + portal-shell 已知问题速查
local-stack runbook 本地运维手册
端口分配 端口唯一源

本文件是 portal-shell 模块的架构文档v2.02026-07-17遵循 arc42 模板结构 + C4 模型可视化。后续代码变更须按 项目规则 §1 同步更新本文件 + 运行 pnpm run arch:scan 更新 arch.db。v2.0 变更摘要shadcn/ui 标准化 + Tailwind v4 + React 19 use() 流式渲染 + 三级错误边界 + 三层安全边界(权限位图 base36 压缩)+ notify 统一封装 + PluginBoundary 替代 PluginLoader。