1076 lines
55 KiB
Markdown
1076 lines
55 KiB
Markdown
# Portal Shell 架构文档(Modular Monolith + Micro-kernel)
|
||
|
||
> 文档编号:0020
|
||
> 版本:v1.0
|
||
> 日期:2026-07-14
|
||
> 状态:架构评审通过,待实施
|
||
> 架构范式:Modular Monolith + Micro-kernel Architecture
|
||
> 文档规范:C4 模型 + 4+1 视图 + ADR(Architecture Decision Records)+ ISO/IEC 25010 质量属性
|
||
> 关联文档:
|
||
>
|
||
> - [0010 架构蓝图](./0010_architecture.md) §4 微前端(本架构取代该章节)
|
||
> - [004 架构影响地图](./004_architecture_impact_map.md) §1.1a 技术分层视角
|
||
> - [设计 spec v2.1](../superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md)
|
||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||
> - [UI 设计系统](../standards/ui-design-system.md)
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [执行摘要](#1-执行摘要)
|
||
2. [C4 Level 1:系统上下文(System Context)](#2-c4-level-1系统上下文system-context)
|
||
3. [C4 Level 2:容器(Container)](#3-c4-level-2容器container)
|
||
4. [C4 Level 3:组件(Component)](#4-c4-level-3组件component)
|
||
5. [4+1 视图](#5-41-视图)
|
||
6. [核心场景(Scenarios)](#6-核心场景scenarios)
|
||
7. [架构决策记录(ADR)](#7-架构决策记录adr)
|
||
8. [质量属性(ISO/IEC 25010)](#8-质量属性isoiec-25010)
|
||
9. [安全架构](#9-安全架构)
|
||
10. [部署架构](#10-部署架构)
|
||
11. [演进路径](#11-演进路径)
|
||
12. [风险登记册](#12-风险登记册)
|
||
13. [附录](#13-附录)
|
||
|
||
---
|
||
|
||
## 1. 执行摘要
|
||
|
||
### 1.1 架构定位
|
||
|
||
Portal Shell 是 Edu 平台的**统一前端门户**,采用 **Modular Monolith + Micro-kernel** 架构范式,取代原 4 个独立 portal(teacher / student / parent / admin)的 Module Federation 微前端方案。
|
||
|
||
| 维度 | 决策 |
|
||
| -------- | --------------------------------------------------- |
|
||
| 架构范式 | Modular Monolith + Micro-kernel |
|
||
| 部署模型 | 单 Next.js 应用,单 Docker 容器 |
|
||
| 插件模型 | 内置插件(编译时)+ 第三方插件(二期,iframe 沙箱) |
|
||
| 状态管理 | URL Search Params + Zustand(替代 EventBus) |
|
||
| 数据获取 | RSC 服务端预取 + SWR 客户端刷新 |
|
||
| 配置驱动 | 后端 IAM DB 存储 JSON,SWR 静默刷新 |
|
||
|
||
### 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-tokens,ESLint 强制约束 |
|
||
| P10 | **单服务部署** | 一个 Dockerfile,一个容器,无 MF 远程加载 |
|
||
|
||
### 1.4 适用范围
|
||
|
||
- 本架构适用于 `apps/portal-shell/` 应用
|
||
- 现有 4 个旧 portal 保留并行运行,逐步迁移至 Portal Shell
|
||
- 后端微服务、BFF、网关层**无需改动**(仅 IAM 扩展配置存储)
|
||
|
||
---
|
||
|
||
## 2. C4 Level 1:系统上下文(System Context)
|
||
|
||
### 2.1 系统上下文图
|
||
|
||
```mermaid
|
||
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 容器图
|
||
|
||
```mermaid
|
||
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 组件图
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph RSC["Server Components(RSC 层)"]
|
||
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)
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
**关注点**:功能分解与领域边界
|
||
|
||
```mermaid
|
||
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 预取时序
|
||
|
||
```mermaid
|
||
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 # RootLayout(Providers 挂载)
|
||
│ │ └── 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)
|
||
|
||
**关注点**:部署拓扑、网络、容器
|
||
|
||
```mermaid
|
||
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. 核心场景(Scenarios)
|
||
|
||
### 6.1 场景一:用户登录并加载仪表盘
|
||
|
||
**参与者**:教师用户
|
||
**前置条件**:用户已在 IAM 注册,admin 已配置教师角色插件集合
|
||
|
||
```mermaid
|
||
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 已登录,教师用户在线
|
||
|
||
```mermaid
|
||
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 同时可见
|
||
|
||
```mermaid
|
||
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 加载失败
|
||
|
||
```mermaid
|
||
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-003:RSC 服务端预取替代 CSR 瀑布流
|
||
|
||
| 字段 | 内容 |
|
||
| -------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||
| 状态 | 已接受 |
|
||
| 日期 | 2026-07-14 |
|
||
| 背景 | v2.0 方案 Shell SSR 渲染骨架 → 客户端水合 → 客户端拉 Config → dynamic import 插件 → 插件拉 BFF 数据,4 层串行瀑布流,LCP 差 |
|
||
| 决策 | 在 RSC(Server Component)中服务端调 IAM gRPC 获取 Config,并发调 BFF 预取各插件 initialData,Config + initialData 随 HTML 直出 |
|
||
| 理由 | ① 消除 CSR 瀑布流,仪表盘秒开;② 服务端并发预取比客户端串行快;③ initialData 通过 SWR fallbackData 无缝衔接客户端刷新 |
|
||
| 后果 | ① RSC 代码需处理 gRPC 客户端(服务端运行);② 预取逻辑需维护(新增插件需加 prefetch case) |
|
||
| 替代方案 | A) 全 CSR + 骨架屏(瀑布流,弃用);B) 全 SSR(插件 SSR 复杂,弃用);C) 混合 SSR + CSR(策略分裂,弃用) |
|
||
|
||
### ADR-004:SWR 静默刷新替代 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 方案插件按当前角色自行调用对应 BFF,universal 插件需判断 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-origin,API 通过 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 + Zustand(DevTools 可检视) |
|
||
| 可修改性 | 新增功能不影响现有 | 插件化 + 配置驱动 |
|
||
|
||
### 8.7 可移植性(Portability)
|
||
|
||
| 特性 | 目标 | 实现措施 |
|
||
| -------- | ------------------ | ------------------------------------------ |
|
||
| 适应性 | 单 Docker 容器部署 | standalone 模式 + 官方 node:22-alpine 镜像 |
|
||
| 可安装性 | CI/CD 一键部署 | docker compose up --build |
|
||
|
||
---
|
||
|
||
## 9. 安全架构
|
||
|
||
### 9.1 认证与授权
|
||
|
||
```mermaid
|
||
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 第三方插件沙箱(二期)
|
||
|
||
```mermaid
|
||
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 容器化
|
||
|
||
```dockerfile
|
||
# 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 部署
|
||
|
||
```yaml
|
||
# 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 迁移策略
|
||
|
||
```mermaid
|
||
graph LR
|
||
A["现状:4 个旧 portal<br/>并行运行"] --> B["P1-P8:Portal 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 参考资料
|
||
|
||
- [设计 spec v2.1](../superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md)
|
||
- [0010 架构蓝图](./0010_architecture.md)
|
||
- [004 架构影响地图](./004_architecture_impact_map.md)
|
||
- [C4 模型](https://c4model.com/)
|
||
- [4+1 视图模型](https://en.wikipedia.org/wiki/4%2B1_archural_view_model)
|
||
- [ISO/IEC 25010](https://iso25000.com/index.php/en/iso-25000-standards/iso-25010)
|
||
- [Next.js App Router](https://nextjs.org/docs/app)
|
||
- [Zustand](https://github.com/pmndrs/zustand)
|
||
- [SWR](https://swr.vercel.app/)
|
||
- [Halo 插件机制](https://docs.halo.run/)
|
||
- [Figma QuickJS 沙箱](https://www.figma.com/blog/an-update-on-plugin-security/)
|
||
|
||
### 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 的架构唯一源。任何架构变更需更新本文档并提交架构评审。
|