Files
Edu/apps/portal-shell
SpecialX f7e52b5b7f docs(portal-shell): update README to v1.1 with data layer and GraphQL hardening
- 版本 1.0 -> 1.1,日期 2026-07-17
- 新增 §13 数据访问层与 GraphQL 安全栈(6 子节)
- 更新 §5/§9.6/§10/§11/§12/附录 A/B/C
- 修正 004 §16.5 测试数(admin 31->4,sidebar 5->9)
- arch:scan 通过(TS 20 模块/4803 符号)
2026-07-17 13:47:40 +08:00
..

portal-shell 模块架构文档

版本1.1 日期2026-07-17 状态已落地v2.1 M8-M12 完成 + 2026-07-17 数据抽象与 GraphQL 加固 M1-M4 完成) 架构范式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 安全栈v1.1 新增)
  14. 术语表

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
  • 技术栈TypeScript 5.6 + Next.js 14 App Router + React 18 + Tailwind + Zustand + SWR + Apollo Client
  • 架构风格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 设计系统一致:所有插件使用 @edu/ui-tokens,禁硬编码颜色/字体/字号 ESLint no-restricted-syntax + design-tokens/no-hardcoded-fonts 零违规
G6 按需加载dynamic import 懒加载,首屏只加载可见 slot 插件 首屏 JS bundle ≤ 300KBgzip
G7 跨角色复用universal 插件按 role 渲染不同视图,一套代码服务多角色 universal 插件复用率 100%

1.3 非目标

  • 不重写后端微服务、BFF、网关
  • 不实现插件拖拽编辑器canvas 模板预留接口MVP 不做拖拽)
  • MVP 不实现第三方插件上传(二期,预留 iframe + postMessage 沙箱方案)
  • 不实现插件市场在线商店(二期仅支持本地 ZIP 上传)

2. 架构约束

2.1 全局约束(来自项目规则)

约束 来源 portal-shell 落地方式
禁硬编码颜色(#hex project_rules §3.10 ESLint no-restricted-syntax + Tailwind bg-*
禁硬编码字体('Inter' project_rules §3.10 next/font/google + CSS 变量 --font-family-*
禁 Tailwind 任意值(w-[Npx] project_rules §3.10 映射到 --space-* 或 Tailwind 默认阶梯
函数返回值显式标注 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 映射

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 校验后注入
JWT iam 签发 → api-gateway 校验 Apollo Client Authorization: Bearer localStorage + cookie 双通道

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 + 字体"]
            RootPage["page.tsx<br/>重定向 /shell"]
            ShellPage["shell/[[...route]]/page.tsx<br/>RSC 入口"]
            HealthAPI["api/health/route.ts"]
            ReadyAPI["api/ready/route.ts"]
        end

        subgraph ShellCore["shell/(微内核)"]
            Shell["Shell.tsx"]
            ClientShell["ClientShell.tsx"]
            LayoutMgr["LayoutManager.tsx<br/>5 Layout 模板"]
            SlotRend["SlotRenderer.tsx"]
            Loader["PluginLoader.tsx<br/>+ ErrorBoundary"]
            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/v1.1 数据访问层)"]
            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 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 -->|props| ClientShell
    ClientShell --> Shell
    Shell --> LayoutMgr
    LayoutMgr --> SlotRend
    SlotRend --> Registry
    SlotRend --> Loader
    Loader --> Widgets
    ClientShell --> UseConfig
    UseConfig --> Apollo
    Widgets --> ApiDomain
    ApiDomain --> ApiOps
    ApiDomain --> UseQuery
    ApiDomain --> UseMut
    ApiOps --> ApiTypes
    UseQuery --> Apollo
    UseMut --> Apollo

5.2 组件职责矩阵

文件 职责 关键契约
app/ shell/[[...route]]/page.tsx RSC 入口,服务端拉 Config + 预取 initialData fetchPluginConfig(userId, role)
app/ layout.tsx RootLayout挂载 next/font/google 字体变量 --font-inter / --font-fraunces / --font-jetbrains-mono
app/ page.tsx 根路径重定向到 /shell redirect("/shell")
shell/ Shell.tsx 微内核入口,选择 LayoutManager 模板 config.activeLayout.layoutId
shell/ ClientShell.tsx 客户端入口,挂载 Providers + SWR 配置刷新 usePluginConfig(initialConfig)
shell/ LayoutManager.tsx 5 种 Layout 模板渲染器 classic / focus / split / triple / canvas
shell/ SlotRenderer.tsx 按 slot 过滤+排序+查表+注入 PluginProps PluginPlacement[]
shell/ PluginLoader.tsx dynamic loading 骨架 + ErrorBoundary 隔离 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)
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 层v1.1 useMyChildren() / saveLessonPlan() 等,禁止 widget 直接写 gql
lib/api/ operations/*.graphql.ts gql DocumentNode 集中存放v1.1 51 个 query/mutation 常量
lib/api/ operations/types.ts codegen 生成的 TS 类型v1.1 从 7 子图 schema.graphql 生成
lib/api/ errors.ts ApiError 归一化v1.1 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">

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 跳过

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 声明元数据:

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"],
    defaultSlot: "main",
    defaultSize: { colSpan: 2, rowSpan: 1 },
    defaultProps: { limit: 20 },
    propsSchema: {
      // JSON Schemaadmin 配置面板自动渲染表单
      type: "object",
      properties: {
        limit: { type: "number", description: "显示条数" },
      },
    },
  },
};

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 瀑布流,实现仪表盘"秒开"

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
    N->>R: query pluginConfig(userId, role)
    R->>C: 路由到 config-service 子图
    C-->>R: 三层合并后的 PluginConfigResponse
    R-->>N: ConfigactiveLayout + plugins[]
    N->>N: PropsMerger 解析 propsJson / sizeJson
    N->>R: 并发预取各插件 initialDataPromise.all
    R->>S: 路由到对应子图
    S-->>R: 插件初始数据
    R-->>N: initialData[]
    N-->>U: HTML 直出(含 Config + initialData
    U->>U: 水合 → dynamic import 插件组件
    U->>U: 插件用 initialData 作为 SWR fallbackData 渲染

对比 v2.0 CSR 瀑布流

v2.04 层串行LCP 差):
  HTML 骨架 → 水合 → 拉 Config → import 插件 → 插件拉数据 → 渲染
  总耗时 = SSR + 水合 + Config RTT + import RTT + BFF RTT

v2.1 RSC 预取2 层并行LCP 秒开):
  服务端RSC 拉 Config + 并发预取 initialData → HTML 直出
  客户端:水合 → dynamic import → 用 initialData 渲染
  总耗时 = max(Config RTT, BFF 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 场景四插件加载失败ErrorBoundary 隔离)

graph TB
    A[SlotRenderer 渲染插件] --> B{dynamic import 成功?}
    B -- 是 --> C[组件渲染]
    B -- 否 --> D[PluginErrorBoundary 捕获]
    D --> E[PluginErrorFallback 显示]
    E --> F[显示错误图标 + instanceId]
    E --> G[重试按钮]
    G --> H[重置 hasError=false 重新加载]

    I[其他插件] -.->|不受影响| J[正常渲染]

关键约束单个插件失败不影响其他插件ErrorBoundary 隔离作用域。


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 设计令牌(强制)

三层令牌模型project_rules §3.10

Layer 用途 位置 业务代码引用
L1 Primitive 原始色板/字号/间距/阴影 packages/ui-tokens/src/primitive.css 禁止直接引用
L2 Semantic 语义令牌light/dark packages/ui-tokens/src/semantic-*.css 唯一引用入口(hsl(var(--*))
L3 Tailwind Theme 暴露为 Tailwind 类 packages/ui-tokens/src/tailwind-theme.css bg-* / text-* / font-*

ESLint 强制约束

  • no-restricted-syntax:禁止 #hex 字面量
  • design-tokens/no-hardcoded-fonts:禁止 'Inter' / 'Fraunces' 字面量
  • 白名单:primitive.cssemail-channelmanifest.ts

portal-shell 落地

  • 字体通过 next/font/google self-hostCSS 变量暴露为 --font-inter / --font-fraunces / --font-jetbrains-mono
  • 所有插件使用 Tailwind 类(bg-surface / text-ink / border-rule / rounded-card 等)
  • tailwind.config.js 映射 semantic 令牌到 Tailwind 类

9.2 字体策略

字体族 用途 CSS 变量 Tailwind 类
Inter UI 文本sans-serif --font-family-sans font-sans
Fraunces 主标题/正文serif --font-family-serif font-serif
JetBrains Mono 代码/等宽 --font-family-mono font-mono

9.3 纸感编辑器设计风格

参考 docs/standards/ui-design-system.md

  • 背景:纸感 hsl(var(--paper)) / hsl(var(--surface))
  • 圆角var(--radius-card) / var(--radius-button)
  • 间距var(--space-xs) ~ var(--space-xl)
  • 插件容器<section className="rounded-card border border-rule bg-surface p-md">
  • 插件标题text-heading-3 text-ink
  • 加载态<PluginSkeleton variant="card|list|chart|stats|table" />
  • 错误态<PluginErrorFallback instanceId={...} onRetry={...} />

9.4 可观测性

维度 实现 端点
健康检查 liveness + readiness /api/health + /api/ready
错误日志 console.error + PluginErrorBoundary componentDidCatch 浏览器控制台
性能 Next.js 内置 Web Vitals可接 OTel Next.js 自动采集
请求追踪 Apollo Client 自动携带 traceparent 经 api-gateway 注入 X-Request-Id

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仅本地开发
APQv1.1 createPersistedQueryLink({ sha256 }) 前端只发 query hashenv NEXT_PUBLIC_APOLLO_APQ=false 关闭
PQ Manifestv1.1 public/pq-manifest.json 51 query 的 sha256 → query 白名单,由 scripts/generate-pq-manifest.ts 生成
Router 强制 manifestv1.1 APOLLO_REQUIRE_PQ_MANIFEST=true 时 router 拒绝未知 hashentrypoint.sh 启动前校验文件存在性
深度/成本/批量限制v1.1 apollo-router limits.max_depth=10 / max_cost=1000 / max_batch_size=5
Introspection 控制v1.1 APOLLO_ROUTER_INTROSPECTION=false 生产关闭 schema 内省
Router-Authorizationv1.1 子图 RouterAuthGuard 校验 header拒绝非 Router 的直接 GraphQL 请求
Resolver 字段级权限v1.1 每个 GraphQL Resolver 必须用 @RequirePermission('perm') 声明权限点

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

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 层数据访问分层v1.1 Widget 内联 gql 字面量暴露 schema、难审计、难重构抽取到 lib/api/ 四层架构集中管理ADR-042
APQ + PQ Manifestv1.1 前端只发 query hash 防止 schema 探测Router 白名单 manifest 拒绝未知 hash 任意查询ADR-043
Router 深度/成本/批量限制v1.1 防止深度嵌套 / 高成本 / 批量查询 DoS配置 max_depth=10 / max_cost=1000 / max_batch_size=5
Resolver 字段级 @RequirePermissionv1.1 防止越权访问字段,每个 Resolver 必须声明权限点50 resolver 审计后补齐 19 个 TS 守卫

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 提示用户刷新生效
  • 插件加载失败显示错误兜底不影响其他插件PluginErrorBoundary
  • 跨插件状态共享正常class-selector 切换班级 → grades-widget 自动响应)
  • 插件 props 三层合并正确PropsMerger
  • RSC 服务端预取正常:首屏 HTML 直出 Config + initialData
  • useWidgetQuery 自动经 Apollo Client 路由到 apollo-router
  • URL Search Params 可分享、可前进后退
  • config-fetcher 三级降级apollo-router → config-service 直连 → 空默认

11.2 非功能验收

  • pnpm run lint + pnpm run typecheck 零错误
  • pnpm run test 95/95 通过v1.1lib/api 7 domain 55 用例 + 安全栈 10 用例 + Shell/Lifecycle/Context 30 用例)
  • 0 处 widget 内联 gql 字面量v1.1 强制arch:scan 违规检测)
  • 所有插件遵守设计令牌ESLint 强制,无硬编码颜色/字体)
  • apollo-router 启用 APQ + PQ Manifest + 深度/成本/批量限制v1.1
  • 50 个 GraphQL resolver @auth 审计完成35 TS + 15 Python19 个 TS resolver 补齐 @RequirePermissionv1.1
  • arch.db 更新004 文档同步
  • Shell 首屏 LCP < 2s需真实环境压测验证
  • 插件加载耗时 < 500msdynamic import 缓存命中后,需真实环境验证)
  • 单元测试覆盖率 ≥ 80%(当前覆盖核心纯函数 + lib/api 全量admin domain 仅 4 用例待补,插件组件测试待补)
  • E2E 测试tests/e2e/portal-shell.spec.ts待补
  • 视觉回归测试5 种 layout 截图,待补)
  • 生产部署前 APOLLO_REQUIRE_PQ_MANIFEST=true + APOLLO_ROUTER_INTROSPECTION=false 写入部署 envv1.1 follow-up

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 用例v1.1
单元测试 lib/api sidebar domain src/lib/api/__tests__/sidebar.test.tsx 9 用例v1.1
单元测试 lib/api topbar domain src/lib/api/__tests__/topbar.test.tsx 9 用例v1.1
单元测试 lib/api teacher domain src/lib/api/__tests__/teacher.test.tsx 7 用例v1.1
单元测试 lib/api student domain src/lib/api/__tests__/student.test.tsx 9 用例v1.1
单元测试 lib/api parent domain src/lib/api/__tests__/parent.test.tsx 11 用例v1.1
单元测试 lib/api admin domain src/lib/api/__tests__/admin.test.tsx 4 用例v1.1,待补)
单元测试 PQ Manifest + APQ + 深度限制 src/lib/api/__tests__/security.test.ts 10 用例v1.1
单元测试 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 v1.1
E2E apollo-router 拒绝未知 PQ hash tests/e2e/graphql-pq-manifest.spec.ts v1.1
视觉回归 5 种 layout 截图对比 tests/visual/portal-shell.spec.ts

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 代理

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

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
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
P1二期 第三方插件上传 + iframe 沙箱 规划
P2二期 插件市场在线商店 规划
P3二期 canvas 拖拽编辑器 规划
P4二期 next-intl 完整 i18n 规划
P5二期 视觉回归测试自动化 规划

13. 数据访问层与 GraphQL 安全栈v1.1 新增)

来源: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. 术语表

术语 定义
Shell 微内核宿主,渲染 Layout 框架 + Slots + PluginLoader不含业务逻辑
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 微内核架构,核心仅含基础框架,功能以插件形式扩展
APQv1.1 Automatic Persisted Queries前端只发 query hashsha256不发明文 query
PQ Manifestv1.1 sha256(query) → query 文本的白名单 JSONapollo-router 据此解析未知 hash
4 层数据访问分层v1.1 Widget → API → Operations → Hook废弃 widget 内联 gql 字面量ADR-042
@RequirePermissionv1.1 NestJS GraphQL Resolver 字段级权限装饰器,每个 Resolver 必须声明权限点
RouterAuthGuardv1.1 子图 Guard校验 Router-Authorization header拒绝非 Router 的直接 GraphQL 请求

附录 A文件清单

apps/portal-shell/
├─ src/
│  ├─ app/
│  │  ├─ api/
│  │  │  ├─ health/route.ts          # liveness
│  │  │  └─ ready/route.ts           # readiness
│  │  ├─ shell/[[...route]]/page.tsx # RSC 入口
│  │  ├─ globals.css                 # 全局样式 + Tailwind
│  │  ├─ layout.tsx                  # RootLayout + 字体
│  │  └─ page.tsx                    # 重定向 /shell
│  ├─ lib/
│  │  ├─ __tests__/plugin-context.test.ts
│  │  ├─ api/                       # v1.1 数据访问层
│  │  │  ├─ __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                   # 共享类型契约
│  │  ├─ usePluginConfig.ts         # SWR 静默刷新
│  │  ├─ useWidgetQuery.ts          # 统一查询 Hook
│  │  └─ useWidgetMutation.ts       # 统一变更 Hook
│  ├─ providers/
│  │  ├─ ApolloProvider.tsx
│  │  ├─ AuthProvider.tsx
│  │  └─ ThemeI18nProvider.tsx
│  ├─ shell/
│  │  ├─ __tests__/
│  │  │  ├─ PluginLifecycle.test.ts  # 12 用例
│  │  │  └─ Registry.test.ts         # 6 用例
│  │  ├─ ClientShell.tsx             # 客户端入口
│  │  ├─ LayoutManager.tsx           # 5 Layout 模板
│  │  ├─ PluginLifecycle.ts          # 生命周期管理
│  │  ├─ PluginLoader.tsx            # 骨架 + ErrorBoundary
│  │  ├─ PluginStore.ts              # Zustand 全局状态
│  │  ├─ PropsMerger.ts              # 三层合并
│  │  ├─ Registry.tsx                # 31 插件注册表
│  │  ├─ Shell.tsx                   # 微内核入口
│  │  └─ SlotRenderer.tsx            # slot 渲染器
│  ├─ styles/
│  │  └─ tokens.css                  # 设计令牌映射
│  └─ 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
├─ .eslintrc.tokens.js               # 设计令牌 ESLint 规则
├─ codegen.yml                       # v1.1 graphql-codegen 配置
├─ Dockerfile                        # standalone 构建
├─ eslint.config.js
├─ next.config.js                    # transpilePackages + 反向代理
├─ package.json
├─ postcss.config.js
├─ public/
│  └─ pq-manifest.json               # v1.1 PQ Manifest51 query hash → query 白名单)
├─ scripts/
│  ├─ generate-pq-manifest.ts        # v1.1 PQ Manifest 生成脚本
│  └─ normalize-schema.ts            # v1.1 schema 归一化(移除 federation 指令)
├─ tailwind.config.js
├─ tsconfig.json                     # paths 别名
├─ vitest.config.ts                  # jsdom + 别名
└─ README.md                         # 本文件

附录 B常用命令

# 开发
pnpm --filter @edu/portal-shell run dev          # 启动 dev server :4010
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
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 用例)

# v1.1 数据层与安全栈
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 用例)

# 架构扫描
pnpm run arch:scan                                # 更新 arch.db
pnpm run arch:query -- module-deps                # 查模块依赖

附录 C关联文档索引

文档 用途
004 架构影响地图 架构设计意图唯一源§11.7 数据访问层 + 安全栈、§16.5 子阶段)
portal-shell 仪表盘 spec 模块设计源v1.0
portal-shell 数据抽象与 GraphQL 加固 spec v1.1 数据访问层 + 安全栈设计源
portal-shell 数据抽象与 GraphQL 加固 plan v1.1 20-task 实施 plan
GraphQL @auth 审计报告 50 resolver @auth 审计M4
项目规则 强制约束
UI 设计系统 纸感编辑器设计风格
known-issues §1.11/§2.17 Apollo Router + portal-shell 已知问题速查
local-stack runbook 本地运维手册
端口分配 端口唯一源

本文件是 portal-shell 模块的架构文档v1.12026-07-17遵循 arc42 模板结构 + C4 模型可视化。后续代码变更须按 项目规则 §1 同步更新本文件 + 运行 pnpm run arch:scan 更新 arch.db。