Files
Edu/docs/architecture/0020_portal_shell_architecture.md

1076 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Portal Shell 架构文档Modular Monolith + Micro-kernel
> 文档编号0020
> 版本v1.0
> 日期2026-07-14
> 状态:架构评审通过,待实施
> 架构范式Modular Monolith + Micro-kernel Architecture
> 文档规范C4 模型 + 4+1 视图 + ADRArchitecture 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 个独立 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 系统上下文图
```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 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
```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 # 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
**关注点**:部署拓扑、网络、容器
```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-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 认证与授权
```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-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 参考资料
- [设计 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 的架构唯一源。任何架构变更需更新本文档并提交架构评审。