Files
Edu/docs/architecture/0020_portal_shell_architecture.md

55 KiB
Raw Permalink Blame History

Portal Shell 架构文档Modular Monolith + Micro-kernel

文档编号0020 版本v1.0 日期2026-07-14 状态:架构评审通过,待实施 架构范式Modular Monolith + Micro-kernel Architecture 文档规范C4 模型 + 4+1 视图 + ADRArchitecture Decision Records+ ISO/IEC 25010 质量属性 关联文档:


目录

  1. 执行摘要
  2. C4 Level 1系统上下文System Context
  3. C4 Level 2容器Container
  4. C4 Level 3组件Component
  5. 4+1 视图
  6. 核心场景Scenarios
  7. 架构决策记录ADR
  8. 质量属性ISO/IEC 25010
  9. 安全架构
  10. 部署架构
  11. 演进路径
  12. 风险登记册
  13. 附录

1. 执行摘要

1.1 架构定位

Portal Shell 是 Edu 平台的统一前端门户,采用 Modular Monolith + Micro-kernel 架构范式,取代原 4 个独立 portalteacher / student / parent / admin的 Module Federation 微前端方案。

维度 决策
架构范式 Modular Monolith + Micro-kernel
部署模型 单 Next.js 应用,单 Docker 容器
插件模型 内置插件(编译时)+ 第三方插件二期iframe 沙箱)
状态管理 URL Search Params + Zustand替代 EventBus
数据获取 RSC 服务端预取 + SWR 客户端刷新
配置驱动 后端 IAM DB 存储 JSONSWR 静默刷新

1.2 架构目标

  1. 可配置性页面框架Layout+ 插件集合 + 插件 props 全部由后台配置驱动admin 改配置 → 用户刷新生效,无需重新部署
  2. 可扩展性:插件即卡片,新增功能只需开发插件 + admin 配置启用,无需修改 Shell
  3. 可维护性:单代码库、单 Dockerfile、单服务部署告别 4 套独立 portal 的重复维护
  4. 性能RSC 服务端预取 Config + initialData 直出仪表盘秒开LCP < 2s
  5. 安全性:内置插件信任加载,第三方插件 iframe 沙箱隔离BFF 请求统一代理

1.3 架构原则

编号 原则 说明
P1 Shell 是纯宿主 Shell 只负责 Layout / Config / Registry / Loader不含业务逻辑
P2 插件即卡片 所有功能单元统一为"插件",无"卡片"与"插件"之分
P3 RSC 服务端预取 Config + initialData 在服务端预取,随 HTML 直出,消除 CSR 瀑布流
P4 配置驱动 可见性、布局、props 全部由 DB 配置驱动,三层合并
P5 强隔离 插件间禁止直接 import通过 URL/Zustand 共享状态
P6 URL 即状态 高频/可分享上下文进 URL符合 React 单向数据流
P7 静默刷新 配置刷新用 SWR revalidateOnFocus不依赖 WebSocket 推送
P8 统一数据访问 useWidgetQuery 统一 Hook自动按 role 路由 BFF
P9 设计系统强制 所有插件必须使用 @edu/ui-tokensESLint 强制约束
P10 单服务部署 一个 Dockerfile一个容器无 MF 远程加载

1.4 适用范围

  • 本架构适用于 apps/portal-shell/ 应用
  • 现有 4 个旧 portal 保留并行运行,逐步迁移至 Portal Shell
  • 后端微服务、BFF、网关层无需改动(仅 IAM 扩展配置存储)

2. C4 Level 1系统上下文System Context

2.1 系统上下文图

graph TB
    subgraph External["外部参与者"]
        Teacher["教师用户"]
        Student["学生用户"]
        Parent["家长用户"]
        Admin["管理员"]
    end

    subgraph PortalShell["Portal Shell 系统"]
        Shell["Portal Shell<br/>统一前端门户<br/>Modular Monolith + Micro-kernel"]
    end

    subgraph Internal["内部依赖系统"]
        IAM["IAM 服务<br/>认证授权 + 插件配置存储"]
        BFF["BFF 层<br/>teacher-bff / student-bff / parent-bff"]
        Gateway["API Gateway<br/>JWT 校验 + 限流"]
    end

    Teacher --> Shell
    Student --> Shell
    Parent --> Shell
    Admin --> Shell

    Shell -->|"RSC gRPC 拉取配置"| IAM
    Shell -->|"RSC 预取 + 客户端查询<br/>useWidgetQuery 自动路由"| BFF
    Shell -->|"HTTP/HTTPS 经网关"| Gateway
    Gateway --> BFF
    BFF -->|"gRPC"| IAM

    style Shell fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style IAM fill:#fff3e0,stroke:#e65100
    style BFF fill:#fff3e0,stroke:#e65100
    style Gateway fill:#fff3e0,stroke:#e65100

2.2 系统边界

边界 协议 方向 说明
用户 ↔ Portal Shell HTTPS 双向 浏览器渲染 HTML + 交互
Portal Shell ↔ IAM gRPC 出站 RSC 服务端拉取 PluginConfigResponse
Portal Shell ↔ BFF GraphQL/HTTPS 出站 RSC 预取 + 客户端 useWidgetQuery
Portal Shell ↔ API Gateway HTTPS 出站 静态资源 + 路由代理
Admin ↔ IAM REST/HTTPS 双向 Admin CRUD 插件配置

2.3 外部系统约束

系统 约束
IAM 必须扩展 6 张配置表plugin_registry 等),提供 GetPluginConfig gRPC RPC
BFF 已就绪,无需改动;插件按 role 自动路由到对应 BFF
API Gateway 新增 /portal-shell 路由,代理到 portal-shell 容器
push-gateway 无需改动(配置刷新改用 SWR 客户端轮询)

3. C4 Level 2容器Container

3.1 容器图

graph TB
    subgraph Browser["用户浏览器"]
        Client["Portal Shell Client<br/>Next.js App Router<br/>RSC + Client Components"]
    end

    subgraph PortalShellContainer["Portal Shell 容器(单 Docker"]
        NextServer["Next.js Server<br/>Node.js 22<br/>RSC 渲染 + gRPC 客户端"]
        StaticAssets["静态资源<br/>JS/CSS/图片<br/>CDN 分发"]
    end

    subgraph IAMContainer["IAM 容器"]
        IAMService["IAM Service<br/>NestJS<br/>gRPC :50052"]
        IAMDB[("IAM MySQL<br/>6 张配置表")]
    end

    subgraph BFFContainer["BFF 容器群"]
        TeacherBFF["teacher-bff :3003"]
        StudentBFF["student-bff :3009"]
        ParentBFF["parent-bff :3010"]
    end

    subgraph GatewayContainer["API Gateway 容器"]
        APIGateway["api-gateway :8080<br/>Go/Gin"]
    end

    Client -->|"HTTPS"| NextServer
    NextServer -->|"gRPC"| IAMService
    NextServer -->|"RSC 预取"| TeacherBFF
    NextServer -->|"RSC 预取"| StudentBFF
    NextServer -->|"RSC 预取"| ParentBFF
    Client -->|"useWidgetQuery<br/>客户端查询"| APIGateway
    APIGateway --> TeacherBFF
    APIGateway --> StudentBFF
    APIGateway --> ParentBFF
    IAMService --> IAMDB

    style NextServer fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style Client fill:#f3e5f5,stroke:#4a148c
    style IAMService fill:#fff3e0,stroke:#e65100
    style APIGateway fill:#e8f5e9,stroke:#1b5e20

3.2 容器清单

容器 技术 端口 职责
Portal Shell Server Next.js 15 + Node.js 22 4010 RSC 渲染 + gRPC 客户端 + 静态资源
IAM Service NestJS 3002 / gRPC 50052 插件配置存储 + GetPluginConfig RPC
BFF 层 NestJS + GraphQL Yoga 3003/3009/3010 业务数据聚合(已就绪)
API Gateway Go 1.25 + Gin 8080 JWT 校验 + 限流 + 路由(已就绪)

3.3 数据流

目的 协议 触发
F1 配置拉取 Next.js Server IAM gRPC 用户访问页面RSC
F2 业务数据预取 Next.js Server BFF GraphQL RSC 渲染时并发预取
F3 客户端查询 Browser API Gateway HTTPS useWidgetQuery 客户端刷新
F4 配置刷新 Browser IAM经 Gateway HTTPS SWR revalidateOnFocus
F5 Admin 配置 Browser IAM经 Gateway HTTPS Admin REST CRUD

4. C4 Level 3组件Component

4.1 Portal Shell 组件图

graph TB
    subgraph RSC["Server ComponentsRSC 层)"]
        ShellPage["ShellPage<br/>app/shell/[[...route]]/page.tsx"]
        IamClient["iam-client.ts<br/>gRPC 客户端"]
        BffClients["bff-clients.ts<br/>GraphQL 客户端(按 role"]
        PropsMerger["PropsMerger<br/>三层 props 合并"]
    end

    subgraph Shell["Shell 微内核Client Components"]
        ClientShell["ClientShell<br/>接收 RSC props"]
        LayoutManager["LayoutManager<br/>5 种 Layout 模板"]
        SlotRenderer["SlotRenderer<br/>按 Config 渲染插件"]
        PluginLoader["PluginLoader<br/>dynamic import + 骨架屏"]
        Registry["Registry<br/>plugin_id → lazy 组件"]
        PluginStore["PluginStore<br/>Zustand 全局状态"]
    end

    subgraph Widgets["内置插件源码"]
        Universal["universal/<br/>grades/homework/schedule/..."]
        Sidebar["sidebar/<br/>class-selector/term-switcher"]
        Topbar["topbar/<br/>search/notification/user-menu"]
        RoleSpecific["teacher/student/parent/admin/<br/>单角色专属插件"]
    end

    subgraph Lib["数据请求抽象"]
        UseWidgetQuery["useWidgetQuery<br/>自动按 role 路由 BFF"]
        UsePluginConfig["usePluginConfig<br/>SWR 静默刷新配置"]
    end

    subgraph Providers["Providers"]
        AuthProvider["AuthProvider<br/>JWT + RBAC"]
        ThemeI18n["ThemeI18nProvider<br/>主题 + i18n"]
    end

    ShellPage --> IamClient
    ShellPage --> BffClients
    ShellPage --> PropsMerger
    ShellPage --> ClientShell

    ClientShell --> LayoutManager
    ClientShell --> SlotRenderer
    SlotRenderer --> PluginLoader
    PluginLoader --> Registry
    ClientShell --> PluginStore

    Registry --> Universal
    Registry --> Sidebar
    Registry --> Topbar
    Registry --> RoleSpecific

    Universal --> UseWidgetQuery
    Universal --> UsePluginConfig
    RoleSpecific --> UseWidgetQuery

    ClientShell --> AuthProvider
    ClientShell --> ThemeI18n

    style ShellPage fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style ClientShell fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    style Registry fill:#e8f5e9,stroke:#1b5e20
    style UseWidgetQuery fill:#fff3e0,stroke:#e65100

4.2 组件职责

组件 类型 职责 依赖
ShellPage RSC 服务端拉取 Config + 预取 initialData传给 ClientShell iam-client, bff-clients, PropsMerger
ClientShell Client 接收 RSC props渲染 Layout + Plugins LayoutManager, SlotRenderer, AuthProvider
LayoutManager Client 5 种 Layout 模板渲染classic/focus/split/triple/canvas
SlotRenderer Client 按 Config 渲染 slot 内插件列表 PluginLoader
PluginLoader Client dynamic import 懒加载 + 骨架屏 + ErrorBoundary Registry
Registry Const plugin_id → lazy 组件映射(编译时清单) widgets/*
PluginStore Client Zustand 全局状态theme/locale/sidebar zustand
PropsMerger Shared 三层 props 合并(系统默认 ← 角色 ← 用户)
useWidgetQuery Hook 统一 BFF GraphQL 查询,自动按 role 路由 AuthProvider, SWR
usePluginConfig Hook SWR 静默刷新配置revalidateOnFocus SWR
AuthProvider Provider JWT + RBAC提供 useAuth()
ThemeI18nProvider Provider 主题 + i18n

4.3 插件契约Component Contract

// packages/shared-ts/src/contracts/plugin.ts

export interface PluginProps<TProps = Record<string, unknown>> {
  instanceId: string;
  role: "admin" | "teacher" | "student" | "parent";
  user: { id: string; name: string; email: string; dataScope: string };
  slot: {
    name: string;
    layoutId: string;
    size?: { colSpan: number; rowSpan: number };
  };
  props: TProps; // 三层合并后的最终 props
  initialData?: unknown; // RSC 预取的初始数据
}

export interface PluginManifest {
  pluginId: string;
  version: string;
  requiredShellVersion: string; // semver range
  Component: React.ComponentType<PluginProps>;
  metadata: {
    displayName: string;
    description: string;
    category:
      | "universal"
      | "sidebar"
      | "topbar"
      | "teacher"
      | "student"
      | "parent"
      | "admin";
    requiredRoles: string[];
    defaultSlot: string;
    defaultSize: { colSpan: number; rowSpan: number };
    propsSchema?: JSONSchema; // admin 配置面板自动渲染表单
    defaultProps?: Record<string, unknown>;
  };
}

5. 4+1 视图

5.1 逻辑视图Logical View

关注点:功能分解与领域边界

graph LR
    subgraph ShellKernel["Shell 微内核"]
        Layout["Layout 子系统<br/>5 种模板"]
        Config["Config 子系统<br/>三层合并"]
        Registry["Registry 子系统<br/>插件清单"]
        Loader["Loader 子系统<br/>懒加载"]
        State["State 子系统<br/>URL + Zustand"]
    end

    subgraph PluginDomain["插件域(按用途分类)"]
        Universal["Universal 插件域<br/>跨角色复用"]
        Topbar["Topbar 插件域<br/>顶栏功能"]
        Sidebar["Sidebar 插件域<br/>侧栏功能"]
        Teacher["Teacher 插件域<br/>教师专属"]
        Student["Student 插件域<br/>学生专属"]
        Parent["Parent 插件域<br/>家长专属"]
        Admin["Admin 插件域<br/>管理员专属"]
    end

    subgraph InfraDomain["基础设施域"]
        Auth["认证授权"]
        DataFetch["数据获取<br/>useWidgetQuery"]
        DesignSystem["设计系统<br/>@edu/ui-tokens"]
        I18n["国际化"]
    end

    ShellKernel --> PluginDomain
    PluginDomain --> InfraDomain
    ShellKernel --> InfraDomain

5.2 进程视图Process View

关注点并发、性能、RSC 预取时序

sequenceDiagram
    participant Browser as 浏览器
    participant NextServer as Next.js Server (RSC)
    participant IAM as IAM Service
    participant BFF as BFF 层

    Browser->>NextServer: HTTPS 请求 /shell/*
    NextServer->>IAM: gRPC GetPluginConfig(userId)
    IAM-->>NextServer: PluginConfigResponse<br/>(layout + plugins + props)

    par 并发预取各插件 initialData
        NextServer->>BFF: GET_GRADES (teacher-bff)
        BFF-->>NextServer: grades data
    and
        NextServer->>BFF: GET_NOTIFICATIONS
        BFF-->>NextServer: notifications data
    and
        NextServer->>BFF: GET_SCHEDULE
        BFF-->>NextServer: schedule data
    end

    NextServer-->>Browser: HTML 直出<br/>(Layout 骨架 + Config + initialData)
    Browser->>Browser: 水合 + dynamic import 插件
    Browser->>Browser: 插件用 initialData 渲染(秒开)

    Note over Browser: 后台静默刷新SWR
    Browser->>NextServer: GET /api/plugin-config<br/>(revalidateOnFocus)
    NextServer->>IAM: gRPC GetPluginConfig
    IAM-->>NextServer: 最新配置
    NextServer-->>Browser: 200 OK配置变化时 Toast 提示刷新)

5.3 开发视图Development View

关注点:代码组织、模块划分、分层

apps/portal-shell/                          # 单 Next.js 应用
├── src/
│   ├── app/                                # App Router
│   │   ├── layout.tsx                      # RootLayoutProviders 挂载)
│   │   └── shell/[[...route]]/
│   │       └── page.tsx                    # RSC 入口(服务端预取)
│   │
│   ├── shell/                              # 微内核Client Components
│   │   ├── ClientShell.tsx                 # 接收 RSC props
│   │   ├── LayoutManager.tsx               # 5 种 Layout 模板
│   │   ├── SlotRenderer.tsx                # 按 Config 渲染插件
│   │   ├── PluginLoader.tsx                # dynamic import + 骨架屏
│   │   ├── Registry.ts                     # 插件清单(编译时)
│   │   ├── PluginStore.ts                  # Zustand 全局状态
│   │   └── PropsMerger.ts                  # 三层 props 合并
│   │
│   ├── widgets/                            # 内置插件源码(按用途分类)
│   │   ├── universal/                      # 通用插件7 张)
│   │   ├── sidebar/                        # 侧栏插件
│   │   ├── topbar/                         # 顶栏插件
│   │   ├── teacher/                        # 教师专属
│   │   ├── student/                        # 学生专属
│   │   ├── parent/                         # 家长专属
│   │   └── admin/                          # 管理员专属
│   │
│   ├── lib/                                # 数据请求抽象
│   │   ├── iam-client.ts                   # IAM gRPC 客户端
│   │   ├── bff-clients.ts                  # BFF GraphQL 客户端
│   │   ├── useWidgetQuery.ts               # 统一 BFF 查询 Hook
│   │   ├── useWidgetMutation.ts            # 统一 BFF 变更 Hook
│   │   └── usePluginConfig.ts              # SWR 配置刷新
│   │
│   ├── providers/                          # Providers
│   │   ├── AuthProvider.tsx                # JWT + RBAC
│   │   └── ThemeI18nProvider.tsx           # 主题 + i18n
│   │
│   └── styles/                             # 全局样式(复用 tokens
│
├── next.config.js                          # standalone 模式
├── Dockerfile                              # 单容器构建
└── vitest.config.ts                        # 单元测试

packages/shared-ts/src/contracts/           # 跨应用契约
├── plugin.ts                               # PluginProps / PluginManifest
├── layout.ts                               # LayoutTemplate / SlotConfig
├── plugin-store.ts                         # Zustand schema
└── plugin-context.ts                       # URL 上下文 schema

packages/ui-components/src/                 # 新增 UI 组件
├── PluginCard.tsx                          # 纸感卡片容器
├── PluginSkeleton.tsx                      # 5 种 skeleton 变体
├── PluginErrorFallback.tsx                 # 错误兜底
├── SlotPlaceholder.tsx                     # 空 slot 占位
└── PropsConfigForm.tsx                     # JSON Schema 表单

services/iam/src/iam/                       # IAM 扩展
├── iam.schema.ts                           # 新增 6 张配置表
├── iam.service.ts                          # 新增配置查询/合并方法
├── iam.grpc.controller.ts                  # 新增 GetPluginConfig RPC
└── admin.controller.ts                     # Admin CRUD REST API

5.4 物理视图Physical View

关注点:部署拓扑、网络、容器

graph TB
    subgraph Client["客户端"]
        Browser["浏览器<br/>教师/学生/家长/管理员"]
    end

    subgraph DockerHost["Docker Compose 主机(/opt/edu/"]
        subgraph GatewayTier["网关层"]
            Gateway["api-gateway :8080<br/>JWT + 限流 + 路由"]
        end

        subgraph ShellTier["Portal Shell 层"]
            PortalShell["portal-shell :4010<br/>单 Next.js 容器<br/>RSC + 静态资源"]
        end

        subgraph BFFTier["BFF 层"]
            TeacherBFF["teacher-bff :3003"]
            StudentBFF["student-bff :3009"]
            ParentBFF["parent-bff :3010"]
        end

        subgraph ServiceTier["业务服务层"]
            IAM["iam :3002 / gRPC :50052<br/>+ 6 张配置表"]
            OtherServices["core-edu / content / msg / data-ana / ai"]
        end

        subgraph DataTier["数据层"]
            MySQL[("MySQL 8.0<br/>IAM DB")]
            Redis[("Redis 7<br/>缓存")]
        end
    end

    Browser -->|"HTTPS"| Gateway
    Gateway --> PortalShell
    Gateway --> BFFTier
    PortalShell -->|"RSC gRPC 直连"| IAM
    PortalShell -->|"RSC 预取直连"| BFFTier
    BFFTier --> IAM
    BFFTier --> OtherServices
    IAM --> MySQL
    IAM --> Redis

    style PortalShell fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style Gateway fill:#e8f5e9,stroke:#1b5e20
    style IAM fill:#fff3e0,stroke:#e65100

5.5 场景视图Scenarios

§6 核心场景


6. 核心场景Scenarios

6.1 场景一:用户登录并加载仪表盘

参与者:教师用户 前置条件:用户已在 IAM 注册admin 已配置教师角色插件集合

sequenceDiagram
    participant User as 教师用户
    participant Browser as 浏览器
    participant Gateway as API Gateway
    participant Shell as Portal Shell (RSC)
    participant IAM as IAM Service
    participant BFF as teacher-bff

    User->>Browser: 访问 /shell/
    Browser->>Gateway: GET /shell/ (带 JWT Cookie)
    Gateway->>Gateway: JWT 校验 + 限流
    Gateway->>Shell: 转发请求 + 注入 x-user-id/role

    Shell->>IAM: gRPC GetPluginConfig(userId)
    IAM->>IAM: 三层合并<br/>(系统默认 ← 角色 ← 用户)
    IAM-->>Shell: PluginConfigResponse<br/>(classic layout + 5 plugins)

    par RSC 并发预取 initialData
        Shell->>BFF: GET_GRADES
        BFF-->>Shell: grades data
    and
        Shell->>BFF: GET_HOMEWORK
        BFF-->>Shell: homework data
    and
        Shell->>BFF: GET_SCHEDULE
        BFF-->>Shell: schedule data
    end

    Shell-->>Browser: HTML 直出<br/>(Layout 骨架 + Config + initialData)
    Browser->>Browser: 水合 + dynamic import 插件
    Browser->>Browser: 插件用 initialData 渲染(秒开)
    Browser-->>User: 仪表盘完整展示

关键性能指标

  • RSC 服务端预取总耗时 = max(IAM gRPC RTT, BFF GraphQL RTT) ≈ 50-100ms
  • HTML 直出后客户端水合 + dynamic import ≈ 200-500ms
  • 首屏 LCP < 2s

6.2 场景二Admin 配置插件并生效

参与者:管理员、教师用户 前置条件Admin 已登录,教师用户在线

sequenceDiagram
    participant Admin as 管理员
    participant Browser1 as Admin 浏览器
    participant Gateway as API Gateway
    participant IAM as IAM Service
    participant Browser2 as 教师浏览器
    participant Shell as Portal Shell (RSC)

    Admin->>Browser1: 禁用 grades-widget 插件
    Browser1->>Gateway: PUT /admin/plugins/grades-widget {is_active: false}
    Gateway->>IAM: 转发JWT 校验)
    IAM->>IAM: 更新 plugin_registry.is_active = false
    IAM-->>Browser1: 200 OK

    Note over Browser2: 教师用户在线SWR 静默刷新
    Browser2->>Browser2: revalidateOnFocus 触发<br/>(用户切回 Tab)
    Browser2->>Gateway: GET /api/plugin-config
    Gateway->>Shell: 转发
    Shell->>IAM: gRPC GetPluginConfig
    IAM-->>Shell: 最新配置grades-widget 已禁用)
    Shell-->>Browser2: 200 OK配置变化

    Browser2->>Browser2: 检测到配置变化
    Browser2->>Browser2: Toast 提示"发现新布局配置,点击刷新"
    Note over Browser2: 用户点击刷新
    Browser2->>Shell: 重新请求 /shell/
    Shell->>IAM: gRPC GetPluginConfig
    IAM-->>Shell: 最新配置
    Shell-->>Browser2: HTML 直出grades-widget 不渲染)
    Browser2-->>Browser2: 仪表盘更新(无 grades-widget

关键设计

  • 无需 Kafka、无需 WebSocket、无需 push-gateway 改动
  • 配置变更感知延迟 ≤ 5 分钟refreshInterval或即时revalidateOnFocus
  • 用户主动刷新后生效(避免热更新导致的状态丢失)

6.3 场景三:跨插件状态联动(班级切换)

参与者:教师用户 前置条件教师已加载仪表盘class-selector 和 grades-widget 同时可见

sequenceDiagram
    participant User as 教师用户
    participant ClassSelector as class-selector 插件
    participant URL as URL Search Params
    participant GradesWidget as grades-widget 插件
    participant BFF as teacher-bff

    User->>ClassSelector: 选择"三年二班"
    ClassSelector->>URL: router.push('?classId=class-3-2')
    Note over URL: URL 变更触发所有<br/>useSearchParams 订阅者重渲染

    GradesWidget->>GradesWidget: useSearchParams() 检测到 classId 变化
    GradesWidget->>BFF: useWidgetQuery(GET_GRADES, {classId: 'class-3-2'})
    BFF-->>GradesWidget: 新班级成绩数据
    GradesWidget-->>User: 成绩表格更新

关键设计

  • 无 EventBus 发布订阅黑盒
  • URL 是单一数据源DevTools 可见、可分享、可前进后退
  • 符合 React 单向数据流URL 变更 → 自动重渲染 → 自动重新查询

6.4 场景四:插件加载失败容错

参与者:教师用户 前置条件:某插件 JS bundle 加载失败

flowchart TB
    A[PluginLoader 加载插件] --> B{dynamic import 成功?}
    B -->|是| C[渲染插件组件]
    B -->|否| D[ErrorBoundary 捕获]
    D --> E[显示 PluginErrorFallback]
    E --> F[提供"重试"按钮]
    F --> G{用户点击重试?}
    G -->|是| A
    G -->|否| H[保持错误态,不影响其他插件]

    style D fill:#ffebee,stroke:#c62828
    style E fill:#fff3e0,stroke:#e65100

关键设计

  • 单个插件失败不影响其他插件ErrorBoundary 隔离)
  • 提供重试机制
  • 超时 10s 后降级显示错误态

7. 架构决策记录ADR

ADR-001采用 Modular Monolith 替代 Module Federation

字段 内容
状态 已接受
日期 2026-07-14
背景 原 v1.0 方案采用 MF 2.0 + 7 个独立 widget 容器,部署运维复杂,与 Next.js SSR 兼容性差
决策 采用 Modular Monolith + Micro-kernel单 Next.js 应用 + dynamic import
理由 ① 部署复杂度从 8+ 容器降至 1 容器;② RSC 完全可用MF 与 SSR 兼容差);③ 插件源码在同一 app 内,开发体验好;④ 配置驱动仍可实现可见性动态控制
后果 ① 插件无法独立部署发版(需 Shell 重新构建);② 第三方插件需二期 iframe 沙箱方案
替代方案 A) MF 2.0 微前端v1.0弃用B) 基于 teacher-portal 改造业务耦合弃用C) 从零重写(工作量过大,弃用)

ADR-002抛弃 EventBus采用 URL Search Params + Zustand

字段 内容
状态 已接受
日期 2026-07-14
背景 v2.0 方案保留 EventBus 用于跨插件通信(班级切换、学期切换)
决策 采用 URL Search Params高频/可分享上下文)+ Zustand Store低频/纯 UI 状态)
理由 ① EventBus 是微前端跨框架通信的无奈之举,在 React 单体中导致"状态黑盒";② URL 是单一数据源DevTools 可追踪、可分享、可前进后退;③ 符合 React 单向数据流哲学;④ Zustand 状态可被 React DevTools 检视
后果 ① URL 变长(可通过 query string 优化);② 纯 UI 状态需 Zustand增加一个依赖
替代方案 A) EventBus状态黑盒弃用B) React Context性能问题跨插件共享时重渲染范围大C) Redux样板代码过多

ADR-003RSC 服务端预取替代 CSR 瀑布流

字段 内容
状态 已接受
日期 2026-07-14
背景 v2.0 方案 Shell SSR 渲染骨架 → 客户端水合 → 客户端拉 Config → dynamic import 插件 → 插件拉 BFF 数据4 层串行瀑布流LCP 差
决策 在 RSCServer Component中服务端调 IAM gRPC 获取 Config并发调 BFF 预取各插件 initialDataConfig + initialData 随 HTML 直出
理由 ① 消除 CSR 瀑布流,仪表盘秒开;② 服务端并发预取比客户端串行快;③ initialData 通过 SWR fallbackData 无缝衔接客户端刷新
后果 ① RSC 代码需处理 gRPC 客户端(服务端运行);② 预取逻辑需维护(新增插件需加 prefetch case
替代方案 A) 全 CSR + 骨架屏瀑布流弃用B) 全 SSR插件 SSR 复杂弃用C) 混合 SSR + CSR策略分裂弃用

ADR-004SWR 静默刷新替代 Kafka + WebSocket 推送

字段 内容
状态 已接受
日期 2026-07-14
背景 v2.0 方案 Admin 改配置 → Kafka Outbox → msg 服务消费 → push-gateway WebSocket 推送 → 前端提示
决策 采用 SWR revalidateOnFocus + refreshInterval(5min),检测到配置变化时 Toast 提示用户刷新
理由 ① Dashboard 布局是低频变更,引入 Kafka + WebSocket 链路属过度设计;② SWR 成熟稳定,代码量减少 80%+;③ 配置感知延迟 ≤ 5min 或即时focus对低频变更完全够用
后果 ① 配置生效需用户手动刷新(非实时热更新);② 用户长时间不切 Tab 可能延迟感知
替代方案 A) Kafka + WebSocket过度设计弃用B) Server-Sent Events单向推送仍需后端改动C) 轮询(无 focus 触发,体验差)

ADR-005统一 useWidgetQuery Hook 自动路由 BFF

字段 内容
状态 已接受
日期 2026-07-14
背景 v2.0 方案插件按当前角色自行调用对应 BFFuniversal 插件需判断 role 并拼接 BFF URL重复样板代码
决策 封装 useWidgetQuery Hook内部读取 AuthProvider 的 role自动路由到对应 BFF
理由 ① 插件开发者只关心 GraphQL 语句;② role 切换自动路由;③ RSC 预取数据通过 fallbackData 无缝衔接
后果 ① useWidgetQuery 需维护 BFF 路由映射;② 新增 BFF 需更新映射表
替代方案 A) 插件自行判断 role重复代码弃用B) BFF 层统一入口(违反 BFF 按场景域拆分原则)

ADR-006第三方插件采用 iframe + postMessage 沙箱

字段 内容
状态 已接受(二期实施)
日期 2026-07-14
背景 v2.0 方案第三方插件用动态 script 注入加载在单体应用中等于交出主站最高权限XSS、Token 窃取)
决策 二期第三方插件采用 iframe + postMessage 沙箱sandbox="allow-scripts" 禁止 same-originAPI 通过 postMessage 代理
理由 ① 完全隔离 DOM/JS/CSS② 第三方无法访问主站 cookie/localStorage③ BFF 请求由 Shell 代理,可做权限校验
后果 ① 样式/高度自适应复杂;② 通信需序列化postMessage③ 性能略差(每个插件独立进程)
替代方案 A) 动态 script 注入XSS 风险弃用B) QuickJS Web Worker实现复杂备选C) Shadow DOM + Proxy安全性不足弃用

ADR-007插件按用途分类组织app 内目录)

字段 内容
状态 已接受
日期 2026-07-14
背景 Widget 源码物理组织方式有多种选择app 内目录 / monorepo 子包 / 领域分组目录)
决策 采用 app 内目录按用途分类universal/sidebar/topbar/teacher/student/parent/admin
理由 ① 最简单,无需 workspace 配置;② import 路径短;③ 与 Halo 插件分类理念一致;④ 支持插件市场按分类检索
后果 ① widget 与 Shell 在同一 package.json依赖耦合② widget 无法独立版本(可接受,单体架构下无需)
替代方案 A) monorepo 子包配置繁琐弃用B) 领域分组目录(与用途分类重叠,弃用)

8. 质量属性ISO/IEC 25010

8.1 功能适合性Functional Suitability

特性 目标 实现措施
完备性 覆盖 4 个旧 portal 的核心功能 7 张 universal 插件 + sidebar/topbar 插件 + 单角色专属插件
正确性 配置驱动渲染正确 三层 props 合并 + 单元测试覆盖
适当性 插件化架构匹配需求 插件即卡片admin 可配置

8.2 性能效率Performance Efficiency

特性 目标 实现措施
时间行为 LCP < 2s RSC 服务端预取 + initialData 直出
资源利用 首屏 JS < 300KB dynamic import 按需加载 + IntersectionObserver 滚动加载
容量 支持 1000 并发用户 单容器 + Next.js standalone + Redis 缓存配置

8.3 兼容性Compatibility

特性 目标 实现措施
共存性 与旧 portal 并行运行 路由前缀 /shell/* 避免冲突
互操作性 与现有 BFF/IAM 兼容 复用现有 gRPC/GraphQL 契约,仅 IAM 扩展配置表

8.4 可靠性Reliability

特性 目标 实现措施
成熟性 99.9% 可用性 单容器部署 + Docker restart: always + 健康检查
容错性 单插件失败不影响其他 ErrorBoundary 隔离 + PluginErrorFallback
可恢复性 配置错误可回滚 admin 重置用户布局 + DB 备份恢复

8.5 安全性Security

特性 目标 实现措施
保密性 JWT 不泄露 httpOnly Cookie + 第三方插件 iframe 沙箱
完整性 配置不被篡改 Admin RBAC + 审计日志
不可抵赖性 配置变更可追溯 admin 操作审计日志

8.6 可维护性Maintainability

特性 目标 实现措施
模块性 插件可独立开发测试 强隔离 + ESLint 禁止跨 widgets import
可重用性 universal 插件跨角色复用 按角色渲染不同视图 + useWidgetQuery 自动路由
可分析性 状态可追踪 URL + ZustandDevTools 可检视)
可修改性 新增功能不影响现有 插件化 + 配置驱动

8.7 可移植性Portability

特性 目标 实现措施
适应性 单 Docker 容器部署 standalone 模式 + 官方 node:22-alpine 镜像
可安装性 CI/CD 一键部署 docker compose up --build

9. 安全架构

9.1 认证与授权

graph LR
    Browser["浏览器"] -->|"JWT Cookie<br/>httpOnly + Secure + SameSite=Strict"| Gateway["API Gateway"]
    Gateway -->|"JWT 校验<br/>RS256 + JWKS"| Shell["Portal Shell"]
    Shell -->|"x-user-id/role<br/>注入请求头"| RSC["RSC Server Component"]
    RSC -->|"gRPC<br/>带 JWT"| IAM["IAM Service"]
    IAM -->|"RBAC 校验"| DB["IAM DB"]

9.2 插件信任模型

插件类型 信任级别 加载方式 隔离
内置插件 完全信任 dynamic import编译时打包 同进程ErrorBoundary 兜底
第三方插件(二期) 零信任 iframe + postMessage sandbox="allow-scripts",跨域隔离

9.3 第三方插件沙箱(二期)

graph TB
    subgraph MainSite["主站Portal Shell"]
        Shell["Shell 进程<br/>持有 JWT/cookie"]
        ShellAPI["Shell API 表面<br/>白名单代理"]
    end

    subgraph Sandbox["iframe 沙箱(第三方插件)"]
        Plugin["第三方插件 JS<br/>sandbox=allow-scripts<br/>禁止 same-origin"]
    end

    Shell -->|"postMessage<br/>白名单 API 调用"| Plugin
    Plugin -->|"postMessage<br/>请求 BFF/导航"| ShellAPI
    ShellAPI -->|"代理请求<br/>权限校验"| BFF["BFF"]

    Note over Plugin: 无法访问主站<br/>cookie/localStorage/DOM

    style Plugin fill:#ffebee,stroke:#c62828
    style ShellAPI fill:#e8f5e9,stroke:#1b5e20

安全约束:

  • iframe sandbox="allow-scripts" 禁止 same-origin
  • 第三方无法访问主站 cookie/localStorage/DOM
  • 所有 API 通过 postMessage 代理,白名单机制
  • BFF 请求由 Shell 代理(第三方不直接持有 JWT
  • CSP 头限制第三方插件资源加载域

9.4 OWASP Top 10 防护

风险 防护措施
A01 失效的访问控制 JWT + RBAC + Admin 操作审计
A02 加密失败 HTTPS + JWT RS256 + httpOnly Cookie
A03 注入 GraphQL 参数化查询 + Zod 验证
A04 不安全设计 插件契约 + propsSchema + 输入验证
A05 安全配置错误 CORS 白名单 + CSP + 环境变量校验
A07 身份认证失败 JWT 过期 + 刷新机制 + 限流
A08 数据完整性失败 第三方插件 iframe 沙箱 + Subresource Integrity
A09 日志监控失败 结构化日志 + 审计日志 + Prometheus 指标

10. 部署架构

10.1 容器化

# apps/portal-shell/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/ ./packages/
COPY apps/portal-shell/ ./apps/portal-shell/
RUN corepack enable && pnpm install --frozen-lockfile
RUN pnpm --filter @edu/portal-shell build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/portal-shell/.next/standalone ./
COPY --from=builder /app/apps/portal-shell/.next/static ./apps/portal-shell/.next/static
COPY --from=builder /app/apps/portal-shell/public ./apps/portal-shell/public
EXPOSE 4010
CMD ["node", "apps/portal-shell/server.js"]

10.2 Docker Compose 部署

# infra/docker-compose.deploy.yml新增 portal-shell 服务)
portal-shell:
  build:
    context: ./repo
    dockerfile: apps/portal-shell/Dockerfile
  container_name: edu-portal-shell
  ports:
    - "4010:4010"
  environment:
    - NODE_ENV=production
    - IAM_GRPC_URL=iam:50052
    - TEACHER_BFF_URL=teacher-bff:3003
    - STUDENT_BFF_URL=student-bff:3009
    - PARENT_BFF_URL=parent-bff:3010
  depends_on:
    - iam
    - teacher-bff
    - student-bff
    - parent-bff
  restart: always
  healthcheck:
    test: ["CMD", "wget", "--spider", "-q", "http://localhost:4010/healthz"]
    interval: 30s
    timeout: 10s
    retries: 3

10.3 CI/CD 流水线

阶段 内容 触发
quality lint + typecheck + test + build 分支 push
deploy docker compose up --build + 健康检查 合并到 main

10.4 端口分配

服务 端口 用途
portal-shell 4010 统一前端门户
api-gateway 8080 API 网关
teacher-bff 3003 教师 BFF
student-bff 3009 学生 BFF
parent-bff 3010 家长 BFF
iam 3002 / gRPC 50052 认证授权 + 配置存储

11. 演进路径

11.1 阶段划分

阶段 内容 验收标准 状态
P1 IAM 扩展6 表 + gRPC + admin API+ proto 契约 + 共享包契约 gRPC 返回正确配置admin CRUD 可用 待启动
P2 Shell 宿主 + 5 种 Layout 模板 + RSC ConfigProvider + Registry + PluginLoader Shell 启动、登录、渲染空 Layout、切换模板 待启动
P3 首张插件grades-widget+ dynamic import 跑通 + 三层 props 合并 + RSC 预取 grades-widget 渲染props 合并正确initialData 直出 待启动
P4 剩余 6 张 universal 插件 + sidebar/topbar 插件 所有 universal/sidebar/topbar 插件可用 待启动
P5 单角色专属插件lesson-plan-editor / error-book / ai-tutor / user-management 单角色专属插件可用 待启动
P6 Admin 配置面板plugin-manager 插件)+ 用户自定义 UI admin 可配置角色插件,用户可切换 Layout 待启动
P7 api-gateway 路由 + docker-compose 部署 + CI/CD 容器化部署CI 流水线通过 待启动
P8 E2E + 视觉回归测试 测试全通过 待启动
P9 用户逐步迁移,旧 portal 下线 旧 portal 流量为 0 待启动
P10二期 第三方插件上传 + iframe 沙箱 + 插件市场 第三方插件可上传安装运行 未来

11.2 旧 Portal 迁移策略

graph LR
    A["现状4 个旧 portal<br/>并行运行"] --> B["P1-P8Portal Shell<br/>新路由 /shell/*"]
    B --> C["P9用户逐步迁移<br/>旧 portal 流量下降"]
    C --> D["P9 完成:旧 portal 下线<br/>portal-shell 路由改为 /"]
    D --> E["P10第三方插件市场"]

    style A fill:#ffebee,stroke:#c62828
    style B fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    style D fill:#e8f5e9,stroke:#1b5e20
    style E fill:#fff3e0,stroke:#e65100

11.3 回滚策略

  • 任意阶段失败,回滚到上一阶段
  • 旧 portal 始终可用,新 Shell 失败不影响现有用户
  • IAM 新增表与现有表无外键依赖,可独立回滚
  • 代码回滚git revert + CI 重新部署

12. 风险登记册

ID 风险 影响 概率 缓解措施 负责人
R01 插件数量增长导致首屏 bundle 过大 首屏加载慢 dynamic import 按需加载 + IntersectionObserver 滚动加载 + 首屏只加载可见 slot 前端
R02 单体架构插件间隐式耦合 维护困难 ESLint 禁止跨 widgets 目录 import + 架构扫描检测违规 + 强制 URL/Zustand 共享状态 前端
R03 单角色专属功能(如备课画布)作为插件受 props 契约约束 复杂功能实现受限 PluginProps 设计灵活initialData + props 任意 JSON复杂功能在插件内部自行组织 前端
R04 IAM 配置查询压力 RSC 每次请求查询 6 张表 IAM cacheFn(ttl=60s) + Redis 缓存 + RSC 侧 React cache() 去重 后端
R05 插件 props 三层合并逻辑复杂 props 不一致 PropsMerger 集中实现 + 单元测试覆盖 + 深合并 + 数组覆盖 前端
R06 admin 配置面板表单自动渲染 propsSchema 复杂时表单体验差 基于 JSON Schema 表单库 + uiSchema 自定义 前端
R07 二期第三方插件沙箱隔离 安全风险 MVP 不实现;二期 iframe + postMessage 完全隔离 前端+安全
R08 RSC 预取逻辑维护成本 新增插件需加 prefetch case 预取逻辑集中管理 + 插件 manifest 声明 prefetch 方法 前端
R09 SWR 配置刷新延迟 用户感知配置变更延迟 ≤ 5min 对低频变更可接受;关键变更可通知用户手动刷新 前端
R10 旧 portal 迁移周期长 双套代码维护 逐步迁移 + 明确迁移优先级 + 旧 portal 冻结新功能 全员

13. 附录

13.1 术语表

术语 定义
Shell 微内核,渲染 Layout + Slots + Loader不含业务
Plugin/Widget 插件/卡片,功能单元,统一术语
Registry 插件注册表plugin_id → dynamic import 组件映射
Config 渲染配置 JSON决定用户看到哪些插件
Slot Layout 中的占位区域,可插入插件
Layout 页面框架模板classic/focus/split/triple/canvas
RSC React Server Component服务端组件
SWR Stale-While-Revalidate数据请求库
MVP Minimum Viable Product最小可行产品

13.2 参考资料

13.3 变更历史

版本 日期 变更
v1.0 2026-07-14 初始版本,基于设计 spec v2.1 编写完整架构文档C4 + 4+1 + ADR + ISO 25010

文档结束

本架构文档遵循 C4 模型 + 4+1 视图 + ADR + ISO/IEC 25010 国际通用规范,作为 Portal Shell 的架构唯一源。任何架构变更需更新本文档并提交架构评审。