From 0b858d9069c3a492398a4fae34b675882f797a45 Mon Sep 17 00:00:00 2001
From: SpecialX <47072643+wangxiner55@users.noreply.github.com>
Date: Tue, 14 Jul 2026 18:34:40 +0800
Subject: [PATCH] =?UTF-8?q?docs(docs):=20=E6=96=B0=E5=A2=9E=200020=20Porta?=
=?UTF-8?q?l=20Shell=20=E6=9E=B6=E6=9E=84=E6=96=87=E6=A1=A3(C4+4+1+ADR)=20?=
=?UTF-8?q?+=20=E8=AE=BE=E8=AE=A1=20spec=20v2.1=20+=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=200010/004=20=E6=8C=87=E5=90=91=E6=96=B0=E6=9E=B6=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/architecture/0010_architecture.md | 69 +-
.../0020_portal_shell_architecture.md | 1075 +++++++++++++++
.../004_architecture_impact_map.md | 938 +++++++++----
...14-portal-shell-widget-dashboard-design.md | 1191 +++++++++++++++++
4 files changed, 2964 insertions(+), 309 deletions(-)
create mode 100644 docs/architecture/0020_portal_shell_architecture.md
create mode 100644 docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md
diff --git a/docs/architecture/0010_architecture.md b/docs/architecture/0010_architecture.md
index a5bec92..fb44407 100644
--- a/docs/architecture/0010_architecture.md
+++ b/docs/architecture/0010_architecture.md
@@ -8,13 +8,13 @@
code
Mermaid
graph TB
- subgraph ClientLayer["1. 多端访问层 (Micro-Frontends)"]
- direction LR
- MFE1[Teacher Portal]
- MFE2[Student Portal]
- MFE3[Parent App]
- MFE4[Admin Console]
- end
+subgraph ClientLayer["1. 多端访问层 (Micro-Frontends)"]
+direction LR
+MFE1[Teacher Portal]
+MFE2[Student Portal]
+MFE3[Parent App]
+MFE4[Admin Console]
+end
subgraph EdgeLayer["2. 边缘与网关层 (API Gateway & BFF)"]
WAF[WAF / CDN / 防火墙]
@@ -49,25 +49,26 @@ graph TB
WAF --> Gateway
Gateway --> BFF1 & BFF2 & BFF3
BFF1 & BFF2 & BFF3 --> ServiceLayer
-
+
%% 服务与存储的交互
CoreEdu --> MySQL
IAM --> MySQL
Content --> Neo4j
DataAna --> ClickHouse
ServiceLayer --> Redis
-
+
%% CDC 与总线
MySQL -- "CDC (Debezium)
捕获变更" --> Kafka
ServiceLayer -- "发布领域事件
Outbox Pattern" --> Kafka
Kafka -- "消费事件" --> ServiceLayer
Kafka -- "同步读视图" --> ClickHouse
Kafka -- "索引同步" --> ES
+
二、 彻底解耦的四大核心设计模式
在最优架构中,系统通过以下企业级模式解决耦合问题:
1. 读写分离 (CQRS):解决“上帝模块”与复杂聚合查询
-在当前的 Next_Edu 中,Dashboard(仪表盘)需要查询所有模块。在企业级架构中,写入逻辑和查询逻辑是完全物理隔离的。
+ 在当前的 Next_Edu 中,Dashboard(仪表盘)需要查询所有模块。在企业级架构中,写入逻辑和查询逻辑是完全物理隔离的。
Command (写) 链路:教师批改作业,请求只打到 CoreEdu 微服务,该服务只更新 MySQL 中的作业表,并迅速返回。
@@ -78,7 +79,7 @@ Query (读) 链路:系统背后通过 CDC(如 Debezium)监听 MySQL 的 bi
收益:前端 Dashboard 请求 BFF 时,BFF 只查 ClickHouse 中的那一张宽表,毫秒级返回。没有 Join,没有跨服务调用,系统彻底解耦。
2. 发件箱模式 (Transactional Outbox):解决跨模块数据一致性
-如果模块 A 成功了,模块 B 失败了怎么办?企业级系统绝对不允许跨服务的数据库事务 (如 2PC),因为会拖垮性能。
+ 如果模块 A 成功了,模块 B 失败了怎么办?企业级系统绝对不允许跨服务的数据库事务 (如 2PC),因为会拖垮性能。
设计:教师发布考试时,CoreEdu 服务在同一个本地事务中做两件事:
@@ -91,19 +92,37 @@ Query (读) 链路:系统背后通过 CDC(如 Debezium)监听 MySQL 的 bi
收益:实现了 100% 保证的不丢消息的最终一致性。业务逻辑无需关心外部模块是否存活。
3. 编排与协同 (Orchestration vs. Choreography)
-企业级架构处理复杂业务流(例如:考试创建 -> 智能组卷 -> 题目查重 -> 通知分发 -> 家长推送)必须区分两种模式:
+ 企业级架构处理复杂业务流(例如:考试创建 -> 智能组卷 -> 题目查重 -> 通知分发 -> 家长推送)必须区分两种模式:
协同 (Choreography - 基于事件):适用于低耦合业务。发完作业后,发出 HomeworkCreated 事件,通知服务、积分服务各自监听,互相不知道对方存在。
编排 (Orchestration - 基于工作流):适用于强状态依赖的业务。引入工作流引擎(如 Temporal 或 Camunda)。由一个中央 Coordinator 负责指挥:“先调 AI 生成题目,成功后再调试卷服务,如果失败就执行补偿逻辑回滚”。
4. 前端微前端化 (Micro-Frontends)
-后端的解耦如果不配合前端的解耦,依然是一场灾难。
+ 后端的解耦如果不配合前端的解耦,依然是一场灾难。
设计:通过 Module Federation(模块联邦)或 qiankun,将巨大的前端应用拆解。
收益:“排课组”的前端和后端可以独立发版,“题库组”的前端和后端可以独立发版。页面的组装在运行时由宿主框架完成。
+> **⚠️ 架构演进说明(2026-07-14)**
+>
+> 本节描述的 MF 微前端方案已在实施评估中被**取代**。经过对 Module Federation + 独立 widget 容器方案的深入分析,发现存在部署运维复杂(8+ 容器)、与 Next.js SSR 兼容性差、首屏瀑布流等问题。
+>
+> **新方案**:采用 **Modular Monolith + Micro-kernel** 架构(单 Next.js + dynamic import + 配置驱动),在保留"前端解耦 + 独立发版"核心目标的同时,大幅降低部署复杂度(1 容器)并提升首屏性能(RSC 服务端预取)。
+>
+> 详见:[0020 Portal Shell 架构文档](./0020_portal_shell_architecture.md)
+>
+> **演进对比**:
+>
+> | 维度 | 本节(MF 微前端) | 新方案(Modular Monolith) |
+> | ---------- | -------------------- | -------------------------- |
+> | 部署 | 8+ 独立容器 | 1 容器 |
+> | 首屏 | MF 远程加载慢 | RSC 服务端预取秒开 |
+> | 跨前端通信 | EventBus(状态黑盒) | URL + Zustand(可追踪) |
+> | 独立发版 | widget 可独立发版 | 插件随 Shell 发版 |
+> | 第三方扩展 | 天然支持 | 二期 iframe 沙箱 |
+
三、 基础设施与扩展性设计
统一 API 网关 (API Gateway)
@@ -122,23 +141,23 @@ SSE 或 WebSocket 不再由业务容器承载。设立专门的 Push Gateway 服
题库检索:题目内容实时同步到 Elasticsearch,支持分词、拼音、公式模糊全文搜索,而不是 MySQL 的 FULLTEXT。
四、 对比:为什么这是“最优”?
-维度 当前的单体/模块化 (Next_Edu V3) 现代企业级架构 结果差异
-模块依赖 物理隔离,但代码级强引用 (import) 纯事件通信与 API 契约 任意微服务宕机/重构,完全不影响其他服务
-聚合查询 并行查 5 个模块的 DB 再在内存拼装 后台预计算,直接查 ClickHouse 宽表 Dashboard 响应从 1.5s 降至 50ms
-技术栈绑定 全部被绑死在 TypeScript + Next.js 异构。AI网关用Python,高并发网关用Go 能够根据业务特性选择最优技术
-实时推送 Next.js SSE,连接数多了容易爆内存 独立的 Push Gateway + Redis PubSub 支持全校十万人同时在线答题的广播推送
-容灾与扩容 只能整个应用一起扩容 核心教学扩 50 个 Pod,后台审计缩至 1 个 Pod 资源利用率极高,抗高并发能力呈指数级提升
+维度 当前的单体/模块化 (Next_Edu V3) 现代企业级架构 结果差异
+模块依赖 物理隔离,但代码级强引用 (import) 纯事件通信与 API 契约 任意微服务宕机/重构,完全不影响其他服务
+聚合查询 并行查 5 个模块的 DB 再在内存拼装 后台预计算,直接查 ClickHouse 宽表 Dashboard 响应从 1.5s 降至 50ms
+技术栈绑定 全部被绑死在 TypeScript + Next.js 异构。AI网关用Python,高并发网关用Go 能够根据业务特性选择最优技术
+实时推送 Next.js SSE,连接数多了容易爆内存 独立的 Push Gateway + Redis PubSub 支持全校十万人同时在线答题的广播推送
+容灾与扩容 只能整个应用一起扩容 核心教学扩 50 个 Pod,后台审计缩至 1 个 Pod 资源利用率极高,抗高并发能力呈指数级提升
总结
企业级架构的本质是承认分布式环境下的不完美,用基础设施的复杂性来换取业务代码的简单性。
业务模块不再需要操心“谁要我的数据”、“如何拼装别人的数据”、“事务失败怎么回滚”。它们只做一件事:接收指令 -> 改变自身领域模型 -> 将改变广播给全宇宙(Event Bus).
-
蓝图设计的合理性(几乎全对,但有几个地方需要微调)
+
1. 各层选型精准,但有一处“过度理想”
-API Gateway + BFF + GraphQL:
-Gateway 做统一鉴权限流,BFF 做面向前端的聚合,GraphQL 允许不同端按需取数——这在多端下非常正确。
-✅ 唯一需要警惕的是 GraphQL 的 N+1 和深度嵌套,必须在 BFF 层用 DataLoader 批量化解,否则可能引入严重的下游放大调用。
+ API Gateway + BFF + GraphQL:
+ Gateway 做统一鉴权限流,BFF 做面向前端的聚合,GraphQL 允许不同端按需取数——这在多端下非常正确。
+ ✅ 唯一需要警惕的是 GraphQL 的 N+1 和深度嵌套,必须在 BFF 层用 DataLoader 批量化解,否则可能引入严重的下游放大调用。
事件总线选 Kafka/Pulsar:
对业务事件和 CDC 均用同一总线,完全可行。
@@ -157,7 +176,7 @@ Push Gateway 独立 + Redis Pub/Sub:
✅ 注意:Redis Pub/Sub 不持久化,如果有消息可靠性要求(离线再上线收不到历史推送),需要引入一个持久化队列或消息主题,由 Push Gateway 消费后推送给在线用户,同时标记已读。
2. 四大核心模式评价
-CQRS:正确,这是消除“上帝模块”的根本解。
+ CQRS:正确,这是消除“上帝模块”的根本解。
Transactional Outbox + CDC:
这是 唯一能保证最终一致性的分布式事务方案,比 2PC 和 Saga 都更适合高并发教育场景。
@@ -171,4 +190,4 @@ Choreography vs Orchestration:
在多团队并行开发下必要,但教育产品交互一致性要求高,需要强约束的设计系统和跨应用状态共享机制(如通过 BFF 或客户端共享 token + 用户上下文)。模块联邦 + 统一 UI 库是标配。
3. 基础设施层的一个潜在疏漏
-你提到了 WAF、Gateway、Kafka、异构存储,但没有提到统一的配置中心与服务发现(如 Consul/Nacos/K8s Service)。在微服务体系下,服务动态扩缩容、配置热更新(如限流阈值、功能开关)必须同步规划,否则运维会变得困难。这一层通常与 Service Mesh(Istio)配合,可以在架构图中补上。
+ 你提到了 WAF、Gateway、Kafka、异构存储,但没有提到统一的配置中心与服务发现(如 Consul/Nacos/K8s Service)。在微服务体系下,服务动态扩缩容、配置热更新(如限流阈值、功能开关)必须同步规划,否则运维会变得困难。这一层通常与 Service Mesh(Istio)配合,可以在架构图中补上。
diff --git a/docs/architecture/0020_portal_shell_architecture.md b/docs/architecture/0020_portal_shell_architecture.md
new file mode 100644
index 0000000..d10fc67
--- /dev/null
+++ b/docs/architecture/0020_portal_shell_architecture.md
@@ -0,0 +1,1075 @@
+# 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
统一前端门户
Modular Monolith + Micro-kernel"]
+ end
+
+ subgraph Internal["内部依赖系统"]
+ IAM["IAM 服务
认证授权 + 插件配置存储"]
+ BFF["BFF 层
teacher-bff / student-bff / parent-bff"]
+ Gateway["API Gateway
JWT 校验 + 限流"]
+ end
+
+ Teacher --> Shell
+ Student --> Shell
+ Parent --> Shell
+ Admin --> Shell
+
+ Shell -->|"RSC gRPC 拉取配置"| IAM
+ Shell -->|"RSC 预取 + 客户端查询
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
Next.js App Router
RSC + Client Components"]
+ end
+
+ subgraph PortalShellContainer["Portal Shell 容器(单 Docker)"]
+ NextServer["Next.js Server
Node.js 22
RSC 渲染 + gRPC 客户端"]
+ StaticAssets["静态资源
JS/CSS/图片
CDN 分发"]
+ end
+
+ subgraph IAMContainer["IAM 容器"]
+ IAMService["IAM Service
NestJS
gRPC :50052"]
+ IAMDB[("IAM MySQL
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
Go/Gin"]
+ end
+
+ Client -->|"HTTPS"| NextServer
+ NextServer -->|"gRPC"| IAMService
+ NextServer -->|"RSC 预取"| TeacherBFF
+ NextServer -->|"RSC 预取"| StudentBFF
+ NextServer -->|"RSC 预取"| ParentBFF
+ Client -->|"useWidgetQuery
客户端查询"| 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
app/shell/[[...route]]/page.tsx"]
+ IamClient["iam-client.ts
gRPC 客户端"]
+ BffClients["bff-clients.ts
GraphQL 客户端(按 role)"]
+ PropsMerger["PropsMerger
三层 props 合并"]
+ end
+
+ subgraph Shell["Shell 微内核(Client Components)"]
+ ClientShell["ClientShell
接收 RSC props"]
+ LayoutManager["LayoutManager
5 种 Layout 模板"]
+ SlotRenderer["SlotRenderer
按 Config 渲染插件"]
+ PluginLoader["PluginLoader
dynamic import + 骨架屏"]
+ Registry["Registry
plugin_id → lazy 组件"]
+ PluginStore["PluginStore
Zustand 全局状态"]
+ end
+
+ subgraph Widgets["内置插件源码"]
+ Universal["universal/
grades/homework/schedule/..."]
+ Sidebar["sidebar/
class-selector/term-switcher"]
+ Topbar["topbar/
search/notification/user-menu"]
+ RoleSpecific["teacher/student/parent/admin/
单角色专属插件"]
+ end
+
+ subgraph Lib["数据请求抽象"]
+ UseWidgetQuery["useWidgetQuery
自动按 role 路由 BFF"]
+ UsePluginConfig["usePluginConfig
SWR 静默刷新配置"]
+ end
+
+ subgraph Providers["Providers"]
+ AuthProvider["AuthProvider
JWT + RBAC"]
+ ThemeI18n["ThemeI18nProvider
主题 + 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> {
+ 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;
+ 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;
+ };
+}
+```
+
+---
+
+## 5. 4+1 视图
+
+### 5.1 逻辑视图(Logical View)
+
+**关注点**:功能分解与领域边界
+
+```mermaid
+graph LR
+ subgraph ShellKernel["Shell 微内核"]
+ Layout["Layout 子系统
5 种模板"]
+ Config["Config 子系统
三层合并"]
+ Registry["Registry 子系统
插件清单"]
+ Loader["Loader 子系统
懒加载"]
+ State["State 子系统
URL + Zustand"]
+ end
+
+ subgraph PluginDomain["插件域(按用途分类)"]
+ Universal["Universal 插件域
跨角色复用"]
+ Topbar["Topbar 插件域
顶栏功能"]
+ Sidebar["Sidebar 插件域
侧栏功能"]
+ Teacher["Teacher 插件域
教师专属"]
+ Student["Student 插件域
学生专属"]
+ Parent["Parent 插件域
家长专属"]
+ Admin["Admin 插件域
管理员专属"]
+ end
+
+ subgraph InfraDomain["基础设施域"]
+ Auth["认证授权"]
+ DataFetch["数据获取
useWidgetQuery"]
+ DesignSystem["设计系统
@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
(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 直出
(Layout 骨架 + Config + initialData)
+ Browser->>Browser: 水合 + dynamic import 插件
+ Browser->>Browser: 插件用 initialData 渲染(秒开)
+
+ Note over Browser: 后台静默刷新(SWR)
+ Browser->>NextServer: GET /api/plugin-config
(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["浏览器
教师/学生/家长/管理员"]
+ end
+
+ subgraph DockerHost["Docker Compose 主机(/opt/edu/)"]
+ subgraph GatewayTier["网关层"]
+ Gateway["api-gateway :8080
JWT + 限流 + 路由"]
+ end
+
+ subgraph ShellTier["Portal Shell 层"]
+ PortalShell["portal-shell :4010
单 Next.js 容器
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
+ 6 张配置表"]
+ OtherServices["core-edu / content / msg / data-ana / ai"]
+ end
+
+ subgraph DataTier["数据层"]
+ MySQL[("MySQL 8.0
IAM DB")]
+ Redis[("Redis 7
缓存")]
+ 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: 三层合并
(系统默认 ← 角色 ← 用户)
+ IAM-->>Shell: PluginConfigResponse
(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 直出
(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 触发
(用户切回 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 变更触发所有
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
httpOnly + Secure + SameSite=Strict"| Gateway["API Gateway"]
+ Gateway -->|"JWT 校验
RS256 + JWKS"| Shell["Portal Shell"]
+ Shell -->|"x-user-id/role
注入请求头"| RSC["RSC Server Component"]
+ RSC -->|"gRPC
带 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 进程
持有 JWT/cookie"]
+ ShellAPI["Shell API 表面
白名单代理"]
+ end
+
+ subgraph Sandbox["iframe 沙箱(第三方插件)"]
+ Plugin["第三方插件 JS
sandbox=allow-scripts
禁止 same-origin"]
+ end
+
+ Shell -->|"postMessage
白名单 API 调用"| Plugin
+ Plugin -->|"postMessage
请求 BFF/导航"| ShellAPI
+ ShellAPI -->|"代理请求
权限校验"| BFF["BFF"]
+
+ Note over Plugin: 无法访问主站
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
并行运行"] --> B["P1-P8:Portal Shell
新路由 /shell/*"]
+ B --> C["P9:用户逐步迁移
旧 portal 流量下降"]
+ C --> D["P9 完成:旧 portal 下线
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 的架构唯一源。任何架构变更需更新本文档并提交架构评审。
diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md
index f498148..28f0eeb 100644
--- a/docs/architecture/004_architecture_impact_map.md
+++ b/docs/architecture/004_architecture_impact_map.md
@@ -1,14 +1,18 @@
# 架构影响地图(微服务版)
-> 版本:1.0
-> 日期:2026-07-07
-> 状态:基线发布
+> 版本:2.0
+> 日期:2026-07-14
+> 状态:实施状态同步(基于代码现状校准)
> 适用范围:Edu 微服务架构(DDD + EDA + CQRS)
> 关联文档:
>
> - [理想蓝图](./0010_architecture.md)
> - [项目规则](../../.trae/rules/project_rules.md)
> - [路线图](./roadmap/README.md)
+> - [端口分配唯一源](../../infra/port-allocation.md)
+> - [P6 附录](./004-p6-addendum.md)
+
+> **v2.0 变更摘要**:基于代码现状(截至 2026-07-14)全面校准服务清单、模块边界、依赖关系、Kafka topic、可观测性栈、BFF 实现细节;新增 §15 实施状态索引。
---
@@ -28,6 +32,7 @@
12. [架构约束](#12-架构约束)
13. [ADR 记录](#13-adr-记录)
14. [附录:6 阶段路线图](#14-附录6-阶段路线图)
+15. [实施状态索引(v2.0 新增)](#15-实施状态索引v20-新增)
---
@@ -37,6 +42,7 @@
> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → BFF → 业务服务 → 总线 → 数据)。
> 用户层按"使用场景域"标注,BFF 层按场景域分(不是按角色分)。业务领域视角见 [1.1b](#11b-业务领域视角)。
+> 端口标注见 [infra/port-allocation.md](../../infra/port-allocation.md)(唯一源)。
```mermaid
graph TB
@@ -47,44 +53,48 @@ graph TB
Admin["管理场景域用户
(系统管理员 / 校管理员)"]
end
- subgraph MFE["微前端层(Module Federation)"]
- TeacherPortal["teacher-portal
教学场景域前端"]
- StudentPortal["student-portal
学习场景域前端"]
- ParentPortal["parent-portal
家长场景域前端"]
- AdminPortal["admin-portal
管理场景域前端"]
+ subgraph MFE["微前端层(Module Federation)
Next.js 15 + standalone"]
+ TeacherPortal["teacher-portal :4000
Shell 宿主 + 教学场景域"]
+ StudentPortal["student-portal :4001
Remote"]
+ ParentPortal["parent-portal :4002
Remote(被 teacher-portal 加载)"]
+ AdminPortal["admin-portal :4003
Remote"]
end
- subgraph Gateway["网关层(Go)"]
- APIGateway[api-gateway
Gin + JWT + 限流]
- PushGateway[push-gateway
WebSocket/SSE]
+ subgraph PortalShell["Portal Shell 层(规划中)
详见 [0020 Portal Shell 架构](./0020_portal_shell_architecture.md)"]
+ PortalShellApp["portal-shell :4010
Modular Monolith + Micro-kernel
取代 MF 微前端,单服务部署"]
end
- subgraph BFF["BFF 聚合层(NestJS)
按使用场景域分 BFF(不是按角色分)"]
- TeacherBFF["teacher-bff
教学场景域聚合"]
- StudentBFF["student-bff
学习场景域聚合"]
- ParentBFF["parent-bff
家长场景域聚合"]
+ subgraph Gateway["网关层(Go 1.25 / Gin)"]
+ APIGateway["api-gateway :8080
JWT RS256 + JWKS + 限流 + 熔断"]
+ PushGateway["push-gateway :8081
WebSocket + Redis Pub/Sub
(gRPC 豁免,HTTP /internal/*)"]
+ end
+
+ subgraph BFF["BFF 聚合层(NestJS / GraphQL Yoga)
按使用场景域分 BFF"]
+ TeacherBFF["teacher-bff :3003
Teacher + Admin Resolvers
6 下游 gRPC 客户端"]
+ StudentBFF["student-bff :3009
Cache + CircuitBreaker + Event"]
+ ParentBFF["parent-bff :3010
Aggregation + Kafka 订阅"]
end
subgraph Services["业务微服务(NestJS + FastAPI)"]
- IAM[iam
身份认证]
- CoreEdu[core-edu
教学核心]
- Content[content
内容资源]
- DataAna[data-ana
数据分析 Python]
- Msg[msg
消息通知]
- AI[ai
AI 网关 Python]
+ IAM["iam :3002 / gRPC :50052
5 Controller + Outbox"]
+ CoreEdu["core-edu :3004 / gRPC :50053
10 模块(含合并的 classes)"]
+ Content["content :3005 / gRPC :50054
7 模块 + Neo4j + ES sync"]
+ DataAna["data-ana :3006 / gRPC :50055
FastAPI + CDC consumer"]
+ Msg["msg :3007 / gRPC :50056
4 模块 + 4 渠道 + Kafka 16 事件"]
+ AI["ai :3008 / gRPC :50058
FastAPI + LLM Failover + Workflow"]
end
subgraph Bus["事件总线"]
- Kafka[(Kafka)]
- Debezium[Debezium CDC]
+ Kafka[("Kafka
双 listener 29092/9092")]
+ Debezium["Debezium Connect 2.7
CDC MySQL → Kafka"]
end
subgraph Data["数据层"]
- MySQL[(MySQL
每服务独占)]
- Redis[(Redis
缓存/会话)]
- ClickHouse[(ClickHouse
读模型宽表)]
- Neo4j[(Neo4j
知识图谱)]
- ES[(Elasticsearch
题库检索)]
+ MySQL[("MySQL 8.0
每服务独占 schema")]
+ Redis[("Redis 7
缓存/会话/Pub/Sub")]
+ ClickHouse[("ClickHouse 24.3
读模型宽表")]
+ Neo4j[("Neo4j 5.20
知识图谱")]
+ ES[("Elasticsearch 8.13
题库检索")]
end
Teacher --> TeacherPortal
@@ -96,40 +106,59 @@ graph TB
StudentPortal --> APIGateway
ParentPortal --> APIGateway
AdminPortal --> APIGateway
- TeacherPortal -.推送.-> PushGateway
- StudentPortal -.推送.-> PushGateway
+ TeacherPortal -.MF Remote 加载.-> ParentPortal
+ TeacherPortal -.WS 推送.-> PushGateway
+ StudentPortal -.WS 推送.-> PushGateway
+ ParentPortal -.WS 推送.-> PushGateway
APIGateway --> TeacherBFF
APIGateway --> StudentBFF
APIGateway --> ParentBFF
+ APIGateway -.admin graphql 透传.-> TeacherBFF
+ PushGateway -.HTTP /internal/push.-> Msg
TeacherBFF --> IAM
TeacherBFF --> CoreEdu
TeacherBFF --> Content
TeacherBFF --> DataAna
+ TeacherBFF --> AI
+ TeacherBFF --> Msg
StudentBFF --> IAM
StudentBFF --> CoreEdu
+ StudentBFF --> DataAna
ParentBFF --> IAM
ParentBFF --> CoreEdu
+ ParentBFF --> DataAna
+ ParentBFF --> Msg
+ ParentBFF -.HTTP.-> PushGateway
CoreEdu <--> Kafka
Content <--> Kafka
DataAna <--> Kafka
Msg <--> Kafka
IAM <--> Kafka
+ AI <--> Kafka
MySQL --> Debezium
Debezium --> Kafka
IAM --> MySQL
CoreEdu --> MySQL
- Content --> Neo4j
- DataAna --> ClickHouse
+ Content --> MySQL
Msg --> MySQL
+ Content --> Neo4j
+ Content --> ES
+ DataAna --> ClickHouse
AI --> ES
IAM --> Redis
CoreEdu --> Redis
+ TeacherBFF --> Redis
+ StudentBFF --> Redis
+ ParentBFF --> Redis
+ DataAna --> Redis
+ AI --> Redis
+ PushGateway --> Redis
```
### 1.1b 业务领域视角
@@ -139,36 +168,36 @@ graph TB
```mermaid
graph TB
- subgraph D1["D1 身份认证领域"]
+ subgraph D1["D1 身份认证领域(iam 服务)"]
IAM[iam 服务]
- IAM_M[users / roles / permissions
refresh_tokens / sessions]
+ IAM_M["5 Controller: Iam / Rbac / Audit / Jwks / IamGrpc
users / roles / permissions / refresh_tokens / sessions
totp / audit_logs / navigation_config / route_permission"]
end
- subgraph D2["D2 教学组织领域"]
- ORG[core-edu 服务
classes 模块]
- ORG_M[classes / subjects / enrollment]
+ subgraph D2["D2 教学组织领域(core-edu 服务)"]
+ ORG["core-edu 服务
classes + scheduling + leave-requests"]
+ ORG_M["classes / teacher-associations / subjects
schedule / leave-requests"]
end
- subgraph D3["D3 教学核心领域"]
- TEACH[core-edu 服务
exams/homework/grades]
- TEACH_M[exams / homework / grades
courses / lessons / schedule / attendance]
+ subgraph D3["D3 教学核心领域(core-edu 服务)"]
+ TEACH["core-edu 服务
exams + homework + grades + attendance"]
+ TEACH_M["exams / exam-extensions / homework / grades
attendance / dashboard / admin / iam-consumer
(含 state machine + datascope-injector)"]
end
- subgraph D4["D4 内容资源领域"]
- CONTENT[content 服务]
- CONTENT_M[textbooks / knowledge-points
questions / grading / search]
+ subgraph D4["D4 内容资源领域(content 服务)"]
+ CONTENT["content 服务"]
+ CONTENT_M["7 模块: textbooks / chapters / knowledge-points
questions / electives / lesson-plans / course-plans
Neo4j 知识图谱 + ES 题库检索 + sync worker"]
end
- subgraph D5["D5 沟通通知领域"]
- MSG[msg 服务]
- MSG_M[messaging / notifications / announcements]
+ subgraph D5["D5 沟通通知领域(msg 服务)"]
+ MSG["msg 服务"]
+ MSG_M["4 模块: notifications / preferences / templates / announcements
4 渠道: email / sms / push / in-app
Kafka 消费 16 类事件 + Idempotency Guard"]
end
- subgraph D6["D6 智能洞察领域"]
- DATA[data-ana 服务]
- AI[ai 服务]
- DATA_M[analytics / dashboard / diagnostic]
- AI_M[AI 备课 / 出题 / 分析 / 搜索]
+ subgraph D6["D6 智能洞察领域(data-ana + ai 服务)"]
+ DATA["data-ana 服务"]
+ AI["ai 服务"]
+ DATA_M["FastAPI HTTP /analytics + gRPC 50055
CDC consumer + ClickHouse 宽表
analytics / dashboard / diagnostic / warnings / mastery"]
+ AI_M["FastAPI HTTP /v1/ai + gRPC 50058
LLM FailoverChain + Prompt Service + Quality Gate
chat / question / expression / lesson-plan / report"]
end
IAM --> ORG
@@ -180,6 +209,9 @@ graph TB
TEACH --> MSG
CONTENT --> DATA
TEACH --> DATA
+ AI -.gRPC.-> CONTENT
+ AI -.gRPC.-> DATA
+ AI -.gRPC.-> IAM
```
**双图并存说明**:
@@ -190,27 +222,35 @@ graph TB
### 1.2 服务清单
-| 类别 | 服务名 | 语言/框架 | 限界上下文 | 业务领域 | 阶段 |
-| -------- | -------------- | ---------------- | --------------- | ----------------------------- | ---- |
-| 基础设施 | api-gateway | Go (Gin) | 网关 | — | P1 |
-| 基础设施 | push-gateway | Go (Gin) | 推送 | — | P5 |
-| BFF | teacher-bff | TS (NestJS) | 教师聚合 | 教学场景域 | P2 |
-| BFF | student-bff | TS (NestJS) | 学生聚合 | 学习场景域 | P3 |
-| BFF | parent-bff | TS (NestJS) | 家长聚合 | 家长场景域 | P4 |
-| 业务 | iam | TS (NestJS) | 身份认证 | **D1 身份认证** | P2 |
-| 业务 | core-edu | TS (NestJS) | 教学核心 | **D2 教学组织 + D3 教学核心** | P3 |
-| 业务 | content | TS (NestJS) | 内容资源 | **D4 内容资源** | P4 |
-| 业务 | data-ana | Python (FastAPI) | 数据分析 | **D6 智能洞察** | P4 |
-| 业务 | msg | TS (NestJS) | 消息通知 | **D5 沟通通知** | P5 |
-| 业务 | ai | Python (FastAPI) | AI 网关 | **D6 智能洞察** | P5 |
-| 微前端 | teacher-portal | TS (Next.js) | 教师端 | 教学场景域 | P2 |
-| 微前端 | student-portal | TS (Next.js) | 学生端 | 学习场景域 | P3 |
-| 微前端 | parent-portal | TS (Next.js) | 家长端 | 家长场景域 | P4 |
-| 微前端 | admin-portal | TS (Next.js) | 管理端 | 管理场景域 | P6 |
-| 共享包 | shared-proto | TS | protobuf 契约 | — | P1 |
-| 共享包 | shared-ts | TS | TS 共享工具 | — | P1 |
-| 共享包 | shared-go | Go | Go 共享工具 | — | P1 |
-| 共享包 | shared-py | Python | Python 共享工具 | — | P4 |
+> 端口、阶段、实施状态基于代码现状(2026-07-14)。✅ = 已落地,🚧 = 部分落地,⏳ = 规划中。
+
+| 类别 | 服务名 | 语言/框架 | HTTP 端口 | gRPC 端口 | 限界上下文 | 业务领域 | 阶段 | 状态 |
+| -------- | -------------- | -------------------------- | --------- | --------- | -------------------------------------------------------------------------- | ----------------------------- | ---- | ---- |
+| 基础设施 | api-gateway | Go 1.25 (Gin) | 8080 | — | 网关(路由+鉴权+限流+熔断+CORS) | — | P1 | ✅ |
+| 基础设施 | push-gateway | Go 1.25 (Gin) | 8081 | — | 推送(WS + Redis Pub/Sub + Kafka 消费) | — | P5 | ✅ |
+| BFF | teacher-bff | TS (NestJS + GraphQL Yoga) | 3003 | — | 教师聚合 + Admin 命名空间聚合 | 教学场景域 | P2 | ✅ |
+| BFF | student-bff | TS (NestJS + GraphQL Yoga) | 3009 | — | 学生聚合 | 学习场景域 | P3 | ✅ |
+| BFF | parent-bff | TS (NestJS + GraphQL Yoga) | 3010 | — | 家长聚合 | 家长场景域 | P4 | ✅ |
+| 业务 | iam | TS (NestJS) | 3002 | 50052 | 身份认证 | **D1 身份认证** | P2 | ✅ |
+| 业务 | core-edu | TS (NestJS) | 3004 | 50053 | 教学核心(含原 classes) | **D2 教学组织 + D3 教学核心** | P3 | ✅ |
+| 业务 | content | TS (NestJS) | 3005 | 50054 | 内容资源 | **D4 内容资源** | P4 | ✅ |
+| 业务 | data-ana | Python (FastAPI) | 3006 | 50055 | 数据分析 | **D6 智能洞察** | P4 | ✅ |
+| 业务 | msg | TS (NestJS) | 3007 | 50056 | 消息通知 | **D5 沟通通知** | P5 | ✅ |
+| 业务 | ai | Python (FastAPI) | 3008 | 50058 | AI 网关 | **D6 智能洞察** | P5 | ✅ |
+| 微前端 | teacher-portal | TS (Next.js 15) | 4000 | — | 教师端(MF Shell) | 教学场景域 | P2 | ✅ |
+| 微前端 | student-portal | TS (Next.js 15) | 4001 | — | 学生端(MF Remote) | 学习场景域 | P3 | ✅ |
+| 微前端 | parent-portal | TS (Next.js 15) | 4002 | — | 家长端(MF Remote,被 teacher-portal 加载) | 家长场景域 | P4 | ✅ |
+| 微前端 | admin-portal | TS (Next.js 15) | 4003 | — | 管理端(MF Remote) | 管理场景域 | P6 | ✅ |
+| 共享包 | shared-proto | protobuf + buf v2 | — | — | 跨语言契约(8 proto 文件) | — | P1 | ✅ |
+| 共享包 | shared-ts | TS | — | — | TS 共享(bff / outbox) | — | P1 | ✅ |
+| 共享包 | shared-go | Go | — | — | Go 共享(env / jwks / logger / tracer) | — | P1 | ✅ |
+| 共享包 | shared-py | Python | — | — | Python 共享 | — | P4 | 🚧 |
+| 共享包 | contracts | TS | — | — | 跨端权限点常量 | — | P3 | 🚧 |
+| 共享包 | hooks | TS (React) | — | — | React Hooks(auth / permission / viewports / graphql / trace / a11y) | — | P2 | ✅ |
+| 共享包 | ui-components | TS (shadcn 风格) | — | — | UI 组件库(data-table / form / modal / chart / filter-bar / status-badge) | — | P2 | ✅ |
+| 共享包 | ui-tokens | TS + CSS | — | — | 设计令牌(primitive / semantic-light/dark / tailwind-theme) | — | P2 | ✅ |
+
+> **历史服务**:classes(端口 3001)已合并入 core-edu(C1 裁决),目录保留作历史参考,不再构建部署。
### 1.3 CICD → Edu 模块映射
@@ -233,38 +273,47 @@ graph TB
### 2.1 多语言技术栈矩阵
-| 层级 | 语言 | 框架 | 用途 |
-| -------- | --------------- | ------------------------------ | -------------------------- |
-| 网关层 | Go 1.22+ | Gin | API Gateway、Push Gateway |
-| 业务服务 | TypeScript 5.5+ | NestJS 10 | IAM、CoreEdu、Content、Msg |
-| 分析/AI | Python 3.12+ | FastAPI | DataAna、AI 网关 |
-| BFF | TypeScript 5.5+ | NestJS 10 + GraphQL | 教师/学生/家长聚合 |
-| 微前端 | TypeScript 5.5+ | Next.js 15 + Module Federation | 4 端门户 |
-| 契约 | protobuf | buf | 跨语言契约定义 |
+| 层级 | 语言 | 框架/版本 | 用途 |
+| -------- | --------------- | ------------------------------------------------------- | -------------------------------------- |
+| 网关层 | Go 1.25 | Gin + otelgin + prometheus | api-gateway、push-gateway |
+| 业务服务 | TypeScript 5.6+ | NestJS 10 + Drizzle ORM | iam、core-edu、content、msg |
+| 分析/AI | Python 3.12 | FastAPI + structlog + prometheus-client | data-ana、ai |
+| BFF | TypeScript 5.6+ | NestJS 10 + GraphQL Yoga + DataLoader + opossum(熔断) | teacher-bff / student-bff / parent-bff |
+| 微前端 | TypeScript 5.6+ | Next.js 15 + Module Federation + Tailwind + shadcn 风格 | 4 端门户 |
+| 契约 | protobuf | buf v2(FILE 级 breaking) | 跨语言契约定义与生成 |
+| 包管理 | — | pnpm 11 / go.work / uv workspace | 多语言 monorepo |
### 2.2 多存储矩阵
-| 存储 | 用途 | 使用服务 |
-| ------------- | ------------------------ | -------------------------- |
-| MySQL 8 | 写模型主库(每服务独占) | IAM、CoreEdu、Content、Msg |
-| Redis 7 | 缓存、会话、限流计数 | 全部服务 |
-| ClickHouse | 读模型宽表、分析聚合 | DataAna、CoreEdu(读模型) |
-| Neo4j | 知识图谱、前置依赖 | Content |
-| Elasticsearch | 题库全文检索 | Content、AI |
+| 存储 | 版本 | 用途 | 使用服务 |
+| ------------- | -------- | --------------------------------------- | ------------------------------------------------------------------------------- |
+| MySQL | 8.0 | 写模型主库(每服务独占 schema) | iam、core-edu、content、msg |
+| Redis | 7-alpine | 缓存、会话、限流计数、分布式锁、Pub/Sub | iam、core-edu、teacher-bff、student-bff、parent-bff、data-ana、ai、push-gateway |
+| ClickHouse | 24.3 | 读模型宽表、分析聚合 | data-ana |
+| Neo4j | 5.20 | 知识图谱、前置依赖 | content |
+| Elasticsearch | 8.13 | 题库全文检索、消息全文检索 | content、msg |
### 2.3 基础设施矩阵
-| 组件 | 用途 |
-| ------------- | ----------------------------------- |
-| Kafka | 事件总线,领域事件异步通信 |
-| Debezium | CDC,MySQL Binlog → Kafka 实时同步 |
-| Temporal | 工作流编排(考试生命周期、AI 编排) |
-| OpenTelemetry | 分布式追踪 |
-| Loki | 日志聚合 |
-| Tempo | 分布式 Trace 存储 |
-| Prometheus | 指标采集 |
-| Grafana | 可观测性可视化 |
-| Vault | 密钥管理(P6) |
+| 组件 | 版本 | 用途 |
+| ---------------- | ---------------- | -------------------------------------------- |
+| Kafka | cp-kafka 7.6 | 事件总线,领域事件异步通信(双 listener) |
+| Zookeeper | cp-zookeeper 7.6 | Kafka 协调 |
+| Debezium Connect | 2.7 | CDC,MySQL Binlog → Kafka 实时同步 |
+| OpenTelemetry | SDK + OTLP | 分布式追踪(HTTP / gRPC 自动埋点) |
+| Jaeger | all-in-one 1.57 | 分布式 Trace 存储 + UI(OTLP 4317/4318) |
+| Loki | 3.2.1 | 日志聚合 |
+| Promtail | 3.2.1 | 日志采集 |
+| Prometheus | v2.51.0 | 指标采集(--web.enable-lifecycle,15d 保留) |
+| Alertmanager | v0.27.0 | 告警路由与抑制 |
+| Grafana | 10.4.0 | 可观测性可视化 |
+| node-exporter | v1.8.2 | 主机指标 |
+| mysqld-exporter | v0.15.1 | MySQL 指标(命令行参数模式) |
+| redis-exporter | v1.67.0 | Redis 指标 |
+| Temporal | ⏳ 规划中 | 工作流编排(考试生命周期、AI 编排) |
+| Vault | ⏳ P6 | 密钥管理 |
+
+> **v2.0 修正**:Trace 存储实际使用 **Jaeger all-in-one**(OTLP 接收),原 v1.0 文档误写为 Tempo。当前未引入 Temporal,长流程编排暂由 ai 服务内 WorkflowStateStore(Redis)实现备课工作流。
---
@@ -280,37 +329,43 @@ graph TB
end
subgraph L2["L2 微前端层"]
- MFE[Module Federation
4 端门户]
+ MFE[Module Federation
4 端门户 Next.js 15]
end
- subgraph L3["L3 网关层"]
- GW[API Gateway Go
路由+鉴权+限流+熔断]
- Push[Push Gateway Go
WebSocket/SSE]
+ subgraph L3["L3 网关层(Go 1.25 / Gin)"]
+ GW[API Gateway :8080
HTTP 反向代理 + JWT RS256 + 限流 + 熔断]
+ Push[Push Gateway :8081
WebSocket + Redis Pub/Sub + Kafka 消费]
end
- subgraph L4["L4 BFF 聚合层"]
- BFF[BFF NestJS
聚合+裁剪+协议转换]
+ subgraph L4["L4 BFF 聚合层(NestJS + GraphQL Yoga)"]
+ BFF[3 个 BFF
聚合 + DataLoader + 熔断(opossum)]
end
subgraph L5["L5 业务微服务层"]
- SVC[6 业务服务
DDD+CQRS]
+ SVC[6 业务服务
NestJS + FastAPI
DDD + CQRS + Outbox]
end
subgraph L6["L6 数据与总线层"]
- DB[(MySQL/CH/Neo4j/ES)]
- KAFKA[(Kafka)]
- TEMPORAL[Temporal]
+ DB[(MySQL / ClickHouse / Neo4j / ES)]
+ REDIS[(Redis 7)]
+ KAFKA[(Kafka + Debezium CDC)]
+ TEMPORAL["Temporal
⏳ 规划中"]
end
Browser --> MFE
Mobile --> MFE
MFE --> GW
- MFE -.推送.-> Push
- GW --> BFF
- BFF --> SVC
+ MFE -.WS 推送.-> Push
+ GW -- HTTP 代理 --> BFF
+ GW -- HTTP 代理 --> SVC
+ BFF -- gRPC --> SVC
SVC --> DB
+ SVC --> REDIS
+ BFF --> REDIS
SVC <--> KAFKA
- SVC --> TEMPORAL
+ SVC -.规划中.-> TEMPORAL
+ Push --> REDIS
+ Push -.Kafka 消费.-> KAFKA
```
### 3.2 依赖方向
@@ -321,8 +376,8 @@ L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L
**严格规则**:
-1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**
-2. L4 BFF 层只做聚合、裁剪、协议转换,**不持有业务状态**
+1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**;只做 HTTP 反向代理,不做协议转换
+2. L4 BFF 层只做聚合、裁剪、协议转换(GraphQL → gRPC),**不持有业务状态**
3. L5 业务服务之间通过 gRPC(同步)或 Kafka 事件(异步)通信,**不直接访问对方数据库**
4. L6 数据层每个微服务独占自身数据库,**禁止跨库联表**
@@ -333,86 +388,135 @@ L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L
```mermaid
graph TB
subgraph Gateway["网关层"]
- APIGW[api-gateway]
- PushGW[push-gateway]
+ APIGW[api-gateway :8080]
+ PushGW[push-gateway :8081]
end
subgraph BFF["BFF 层"]
- TBFF[teacher-bff]
- SBFF[student-bff]
- PBFF[parent-bff]
+ TBFF[teacher-bff :3003]
+ SBFF[student-bff :3009]
+ PBFF[parent-bff :3010]
end
subgraph Services["业务服务"]
- IAM[iam]
- CoreEdu[core-edu]
- Content[content]
- DataAna[data-ana]
- Msg[msg]
- AI[ai]
+ IAM[iam :3002]
+ CoreEdu[core-edu :3004]
+ Content[content :3005]
+ DataAna[data-ana :3006]
+ Msg[msg :3007]
+ AI[ai :3008]
end
subgraph Shared["共享包"]
- Proto[shared-proto]
- SharedTS[shared-ts]
+ Proto[shared-proto
8 proto]
+ SharedTS[shared-ts
bff/outbox]
+ SharedGo[shared-go
env/jwks/logger/tracer]
+ SharedPy[shared-py]
+ Contracts[contracts
权限点]
+ Hooks[hooks
React Hooks]
+ UIComps[ui-components]
+ UITokens[ui-tokens]
end
- APIGW --> TBFF
- APIGW --> SBFF
- APIGW --> PBFF
- PushGW --> Msg
+ %% Gateway → BFF(HTTP 反向代理 + 路径重写)
+ APIGW -- HTTP 代理 --> TBFF
+ APIGW -- HTTP 代理 --> SBFF
+ APIGW -- HTTP 代理 --> PBFF
+ APIGW -. /api/admin/graphql 透传 .-> TBFF
+ APIGW -- HTTP 代理 --> IAM
+ APIGW -- HTTP 代理 --> CoreEdu
+ APIGW -- HTTP 代理 --> Content
+ APIGW -- HTTP 代理 --> Msg
+ APIGW -- HTTP 代理 --> AI
+ APIGW -- HTTP 代理 --> DataAna
+ PushGW -. HTTP /internal/push .-> Msg
+ PushGW -. Kafka edu.notify.notification.sent .-> Msg
- TBFF --> IAM
- TBFF --> CoreEdu
- TBFF --> Content
- TBFF --> DataAna
- TBFF --> AI
+ %% BFF → 业务服务(gRPC)
+ TBFF -- gRPC --> IAM
+ TBFF -- gRPC --> CoreEdu
+ TBFF -- gRPC --> Content
+ TBFF -- gRPC --> DataAna
+ TBFF -- gRPC --> AI
+ TBFF -- gRPC --> Msg
- SBFF --> IAM
- SBFF --> CoreEdu
- SBFF --> Content
- SBFF --> DataAna
+ SBFF -- gRPC --> IAM
+ SBFF -- gRPC --> CoreEdu
+ SBFF -- gRPC --> DataAna
+ SBFF -. Kafka 事件 .-> PushGW
- PBFF --> IAM
- PBFF --> CoreEdu
- PBFF --> DataAna
- PBFF --> Msg
+ PBFF -- gRPC --> IAM
+ PBFF -- gRPC --> CoreEdu
+ PBFF -- gRPC --> DataAna
+ PBFF -- gRPC --> Msg
+ PBFF -. HTTP .-> PushGW
+ PBFF -. Kafka 订阅 .-> CoreEdu
+ PBFF -. Kafka 订阅 .-> IAM
- CoreEdu -.事件.-> Content
- CoreEdu -.事件.-> DataAna
- CoreEdu -.事件.-> Msg
- Content -.事件.-> DataAna
- IAM -.事件.-> CoreEdu
- IAM -.事件.-> Msg
- AI -.gRPC.-> Content
- AI -.gRPC.-> DataAna
+ %% 业务服务间事件
+ CoreEdu -. Kafka 事件 .-> Content
+ CoreEdu -. Kafka 事件 .-> DataAna
+ CoreEdu -. Kafka 事件 .-> Msg
+ Content -. Kafka 事件 .-> DataAna
+ IAM -. Kafka 事件 .-> CoreEdu
+ IAM -. Kafka 事件 .-> Msg
+ AI -. gRPC .-> Content
+ AI -. gRPC .-> DataAna
+ AI -. gRPC .-> IAM
+ DataAna -. Kafka 事件 .-> CoreEdu
+ DataAna -. Kafka 事件 .-> Msg
+ %% 共享包依赖
IAM --> Proto
CoreEdu --> Proto
Content --> Proto
DataAna --> Proto
Msg --> Proto
AI --> Proto
-
+ APIGW --> SharedGo
+ PushGW --> SharedGo
IAM --> SharedTS
CoreEdu --> SharedTS
Content --> SharedTS
Msg --> SharedTS
+ TBFF --> SharedTS
+ SBFF --> SharedTS
+ PBFF --> SharedTS
```
### 4.1 服务间通信矩阵
-| 调用方 → 被调用方 | 协议 | 场景 |
-| ------------------ | ---------- | ---------------- |
-| api-gateway → BFF | gRPC | 请求路由 |
-| BFF → 业务服务 | gRPC | 同步查询聚合 |
-| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 |
-| CoreEdu → DataAna | Kafka 事件 | 学情数据投递 |
-| CoreEdu → Msg | Kafka 事件 | 通知触发 |
-| IAM → CoreEdu | Kafka 事件 | 用户变更同步 |
-| AI → Content | gRPC | 题库查询 |
-| AI → DataAna | gRPC | 学情数据查询 |
-| push-gateway → Msg | gRPC | 推送通道建立 |
+| 调用方 → 被调用方 | 协议 | 场景 |
+| ------------------------- | ---------------- | ------------------------------------------ |
+| api-gateway → BFF | HTTP 反向代理 | 路由分发 + 剥离 `/api/v1/{bff}` 前缀 |
+| api-gateway → 业务服务 | HTTP 反向代理 | 剥离 `/api` 前缀,保留 `/v1/{domain}/*` |
+| api-gateway → teacher-bff | HTTP 透传 | `/api/admin/graphql` → `/graphql`(admin) |
+| BFF → 业务服务 | gRPC | 同步查询聚合 + DataLoader 批量去重 |
+| push-gateway → msg | HTTP /internal/* | msg 主动推送(X-Internal-Key 鉴权) |
+| push-gateway → msg | Kafka 消费 | 消费 `edu.notify.notification.sent` |
+| parent-bff → push-gateway | HTTP | 触发家长端推送 |
+| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 |
+| CoreEdu → DataAna | Kafka 事件 | 学情数据投递(9 类事件) |
+| CoreEdu → Msg | Kafka 事件 | 通知触发(考试/作业/成绩/考勤) |
+| IAM → CoreEdu | Kafka 事件 | 用户变更同步(6 类事件) |
+| IAM → Msg | Kafka 事件 | 用户/角色变更通知(6 类事件) |
+| Content → DataAna | Kafka 事件 | 内容发布同步 |
+| DataAna → CoreEdu | Kafka 事件 | 掌握度更新 → 推荐练习 |
+| DataAna → Msg | Kafka 事件 | 掌握度预警触发 |
+| AI → Content | gRPC | 题库查询 / 知识点查询 |
+| AI → DataAna | gRPC | 学情数据查询 |
+| AI → IAM | gRPC | 用户信息查询 |
+| AI → Kafka | Kafka 生产 | AI 用量事件发布(`edu.ai.usage.*`) |
+
+### 4.2 BFF 下游客户端矩阵
+
+> 所有 BFF 下游客户端统一抽象(B8 裁决):每个客户端有 `gRPC` + `mock` 两个实现,未配置 gRPC target 时自动降级为 mock。
+
+| BFF | 下游客户端(gRPC :port) |
+| ----------- | ---------------------------------------------------------------------------------------------------- |
+| teacher-bff | iam (:50052) / core-edu (:50053) / content (:50054) / data-ana (:50055) / msg (:50056) / ai (:50058) |
+| student-bff | iam / core-edu / data-ana |
+| parent-bff | iam / core-edu / data-ana / msg / push-gateway (HTTP) |
---
@@ -420,32 +524,40 @@ graph TB
### 5.1 JWT RS256 认证流程
+> 实施细节:api-gateway 通过 JWKS Fetcher(shared-go/jwks)缓存 RS256 公钥校验 JWT;
+> 校验通过后注入 `x-user-id` / `x-user-roles` / `x-user-data-scope` / `x-request-id` 头部,HTTP 反向代理到下游服务;
+> 下游服务(NestJS)通过 `AuthMiddleware` 解析头部、`PermissionGuard`(APP_GUARD)做权限校验。
+> DEV_MODE=true 时跳过 JWT 校验,接受 `dev-token`。
+
```mermaid
sequenceDiagram
participant U as 用户
- participant GW as API Gateway
- participant IAM as IAM 服务
- participant SVC as 业务服务
+ participant GW as API Gateway (Go)
+ participant IAM as iam 服务
+ participant SVC as 下游服务 (NestJS / FastAPI)
rect rgb(240, 248, 255)
Note over U,IAM: 阶段 1: 登录签发
- U->>GW: POST /auth/login {username, password}
- GW->>IAM: gRPC Login(username, password)
- IAM->>IAM: 校验密码 + 查询权限
- IAM->>IAM: 生成 JWT(RS256 私钥签发)
+ U->>GW: POST /api/v1/iam/login {username, password}
+ GW->>IAM: HTTP 反向代理(无鉴权路由)
+ IAM->>IAM: bcrypt 校验 + 查询权限 (PermissionCacheService)
+ IAM->>IAM: 生成 JWT(RS256 私钥签发)+ refresh_token
+ IAM->>IAM: 写 iam_outbox(USER_EVENTS)
IAM-->>GW: {accessToken, refreshToken, userInfo}
- GW-->>U: 200 {accessToken, refreshToken}
+ GW-->>U: 200 Set-Cookie httpOnly + ActionState 信封
end
rect rgb(240, 255, 240)
Note over U,SVC: 阶段 2: 请求鉴权
- U->>GW: GET /api/classes + Authorization: Bearer
- GW->>GW: RS256 公钥校验签名
- GW->>GW: 校验 exp/iss/aud
- GW->>GW: 提取 userId/role/dataScope
- GW->>SVC: gRPC 请求 + 透传 userId/role/dataScope metadata
- SVC->>SVC: 权限校验(requirePermission)
- SVC-->>GW: 响应数据
+ U->>GW: GET /api/v1/classes + Authorization: Bearer
+ GW->>GW: JWKS Fetcher 取 RS256 公钥
+ GW->>GW: 校验签名 + exp/iss/aud
+ GW->>GW: 提取 userId/roles/dataScope
+ GW->>SVC: HTTP 代理 + 注入 x-user-* 头部
+ SVC->>SVC: AuthMiddleware 解析头部
+ SVC->>SVC: PermissionGuard (APP_GUARD) 校验权限
+ SVC->>SVC: DataScopeInjector 注入 WHERE 条件
+ SVC-->>GW: ActionState 响应
GW-->>U: 200 响应
end
```
@@ -617,16 +729,37 @@ graph LR
### 7.2 事件 Topic 分类
-| Topic 模式 | 示例 | 生产者 | 消费者 |
-| ----------------------------------- | ---------- | ------- | ------------ |
-| `edu.identity.user.created` | 用户创建 | IAM | CoreEdu、Msg |
-| `edu.identity.user.updated` | 用户更新 | IAM | CoreEdu、Msg |
-| `edu.org.class.created` | 班级创建 | CoreEdu | DataAna |
-| `edu.teaching.assignment.submitted` | 作业提交 | CoreEdu | DataAna、Msg |
-| `edu.teaching.exam.published` | 考试发布 | CoreEdu | Msg |
-| `edu.teaching.grade.recorded` | 成绩录入 | CoreEdu | DataAna、Msg |
-| `edu.content.question.published` | 题目发布 | Content | AI、ES |
-| `edu.insight.mastery.updated` | 掌握度更新 | DataAna | CoreEdu、Msg |
+> 实际命名(基于 `services/*/src/shared/kafka/topic-map.ts` 与 `services/iam/src/config/kafka.ts`):
+> 模式 `edu...`。msg 服务统一使用 `edu.notify.notification.*` 发布(ARB-013)。
+
+| Topic | 生产者 | 消费者 | 说明 |
+| -------------------------------------- | -------- | ---------------------------- | -------------------- |
+| `edu.identity.user.created` | iam | core-edu (iam-consumer)、msg | 用户创建 |
+| `edu.identity.user.updated` | iam | core-edu、msg | 用户更新 |
+| `edu.identity.user.deleted` | iam | msg | 用户删除 |
+| `edu.identity.user.role_changed` | iam | core-edu、msg | 用户角色变更 |
+| `edu.identity.role.created` | iam | msg | 角色创建 |
+| `edu.identity.role.updated` | iam | msg | 角色更新 |
+| `edu.teaching.exam.published` | core-edu | msg | 考试发布 |
+| `edu.teaching.exam.extended` | core-edu | msg | 考试延时(实时) |
+| `edu.teaching.exam.force_submitted` | core-edu | msg | 考试强制交卷(实时) |
+| `edu.teaching.exam.question_reordered` | core-edu | msg | 题序打乱(实时) |
+| `edu.teaching.homework.assigned` | core-edu | msg | 作业布置 |
+| `edu.teaching.assignment.submitted` | core-edu | data-ana、msg | 作业提交 |
+| `edu.teaching.assignment.graded` | core-edu | msg | 作业批改完成 |
+| `edu.teaching.grade.recorded` | core-edu | data-ana、msg | 成绩录入 |
+| `edu.teaching.attendance.recorded` | core-edu | msg | 考勤记录 |
+| `edu.content.question.published` | content | ai、ES sync worker | 题目发布 |
+| `edu.content.textbook.updated` | content | data-ana | 教材更新 |
+| `edu.insight.mastery.updated` | data-ana | core-edu、msg | 掌握度更新 |
+| `edu.notify.notification.sent` | msg | push-gateway | 通知投递(驱动推送) |
+| `edu.notify.notification.read` | msg | — | 通知已读 |
+| `edu.notify.notification.recalled` | msg | — | 通知撤回 |
+| `edu.notify.notification.failed` | msg | — | 通知投递失败 |
+| `edu.notify.notification.events` | msg | — | 兜底 topic |
+| `edu.ai.usage.*` | ai | (data-ana 规划中) | AI 用量事件 |
+
+> **Outbox 表命名**:每服务独立 outbox 表(iam_outbox / core_edu_outbox / content_outbox / msg_outbox),由 shared-ts/outbox 模块统一管理。
> **必需依赖软失败标注**(ARB-015 §17.6,ISSUE-058 覆盖 ISSUE-055):
>
@@ -643,7 +776,8 @@ graph LR
| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 |
| HomeworkGraded | 教师批改完成 | DataAna 更新掌握度;Msg 通知学生 |
| MasteryUpdated | 掌握度计算完成 | CoreEdu 推荐个性化练习;Msg 触发预警 |
-| NotificationRequested | 通知请求 | Msg 投递通知到多渠道 |
+| NotificationRequested | 通知请求 | Msg 投递通知到 4 渠道(email/sms/push/in-app) |
+| NotificationSent | 通知投递完成 | push-gateway 通过 Kafka 消费触发 WebSocket 推送 |
---
@@ -820,40 +954,56 @@ sequenceDiagram
### 10.1 三支柱可观测性
+> 实际落地:Jaeger all-in-one 接收 OTLP(HTTP 4318 / gRPC 4317),Loki + Promtail 采集容器日志,Prometheus 抓取 `/metrics` 端点。
+> 所有 NestJS 服务通过 `shared/observability/{logger,metrics,tracer}.ts` 注册;Go 网关通过 `shared-go/{logger,tracer}` + `otelgin`;Python 服务通过 `structlog` + `opentelemetry-instrumentation-fastapi` + `prometheus-client`。
+
```mermaid
graph TB
subgraph App["应用层"]
- SVC[业务服务]
- GW[网关]
- BFF[BFF]
+ NESTJS[NestJS 服务
pino + prom-client + OTLP]
+ GO[Go 网关
slog + prometheus + otelgin]
+ PY[Python 服务
structlog + prometheus-client + OTLP]
+ PORTAL[Next.js portal
OTel Web + Sentry]
end
subgraph Collect["采集层"]
- OTEL[OpenTelemetry SDK]
- PROM[Prometheus Exporter]
+ OTEL[OpenTelemetry SDK
OTLP exporter]
+ PROM[Prometheus 抓取
/metrics]
+ PROMTAIL[Promtail
容器日志]
end
subgraph Storage["存储层"]
- LOKI[(Loki
日志)]
- TEMPO[(Tempo
Trace)]
- PROMDB[(Prometheus
指标)]
+ LOKI[(Loki 3.2.1
日志)]
+ JAEGER[(Jaeger 1.57
Trace,OTLP 4317/4318)]
+ PROMDB[(Prometheus v2.51
指标 15d 保留)]
+ end
+
+ subgraph Exporters["Exporters"]
+ NODE[node-exporter :9100
host.docker.internal]
+ MYSQL[mysqld-exporter :9104]
+ REDIS[redis-exporter :9121]
end
subgraph Vis["可视化层"]
- GRAFANA[Grafana
统一面板]
- ALERT[AlertManager
告警]
+ GRAFANA[Grafana 10.4
统一面板 :3030]
+ ALERT[Alertmanager v0.27
告警路由]
end
- SVC --> OTEL
- GW --> OTEL
- BFF --> OTEL
- OTEL --> LOKI
- OTEL --> TEMPO
- SVC --> PROM
- GW --> PROM
+ NESTJS --> OTEL
+ GO --> OTEL
+ PY --> OTEL
+ PORTAL -.OTLP HTTP.-> OTEL
+ OTEL --> JAEGER
+ NESTJS --> PROM
+ GO --> PROM
+ PY --> PROM
PROM --> PROMDB
+ PROMTAIL --> LOKI
+ NODE --> PROM
+ MYSQL --> PROM
+ REDIS --> PROM
LOKI --> GRAFANA
- TEMPO --> GRAFANA
+ JAEGER --> GRAFANA
PROMDB --> GRAFANA
PROMDB --> ALERT
```
@@ -862,20 +1012,32 @@ graph TB
```mermaid
flowchart LR
- A[客户端请求] --> B[API Gateway
生成 traceId]
- B --> C[BFF
继承 traceId]
- C --> D[业务服务
继承 traceId]
+ A[客户端请求] --> B[API Gateway
otelgin 生成 traceId]
+ B --> C[BFF
继承 W3C traceparent]
+ C --> D[业务服务 gRPC
继承 metadata]
D --> E[Kafka 事件
traceId 写入 header]
E --> F[消费者服务
继承 traceId]
- F --> G[数据存储
span 记录]
+ F --> G[下游存储
span 记录]
+ G --> H[Jaeger UI
查询链路]
```
**规则**:
-- 所有跨服务调用必须透传 W3C Trace Context
+- 所有跨服务调用必须透传 W3C Trace Context(HTTP `traceparent` 头 / gRPC metadata)
- Kafka 事件必须将 traceId 写入消息 header
-- 日志必须包含 traceId 用于关联查询
-- 关键业务操作必须创建 span(创建考试、提交作业、批改等)
+- 日志必须包含 traceId / requestId 用于关联查询
+- 关键业务操作必须创建 span(创建考试、提交作业、批改、AI 生成、通知投递等)
+- 每服务暴露 `/metrics`(Prometheus 抓取)+ `/healthz`(liveness)+ `/readyz`(readiness)
+
+### 10.3 健康检查与降级
+
+| 服务类型 | /healthz 行为 | /readyz 行为 |
+| ------------ | ------------- | ------------------------------------------------- |
+| 网关层 | 进程存活即 ok | 检查下游可达性(soft-failure) |
+| BFF | 进程存活即 ok | 检查 Redis + 下游 gRPC probe(6 个 probe) |
+| 业务服务 | 进程存活即 ok | 检查 DB + Redis + Kafka + gRPC server |
+| push-gateway | 进程存活即 ok | Redis 软失败(不返 503)+ Kafka consumer 状态 |
+| data-ana | 进程存活即 ok | ClickHouse 1s 超时 + CDC lag < 1000 + Redis + iam |
---
@@ -883,57 +1045,68 @@ flowchart LR
### 11.1 Protobuf 契约体系
+> 实际 proto 文件位于 `packages/shared-proto/proto/`,共 8 个:`ai.proto` / `analytics.proto` / `classes.proto` / `content.proto` / `core_edu.proto` / `events.proto` / `iam.proto` / `msg.proto`。
+> arch.db 统计(2026-07-14):8 proto 文件 / 23 service / 305 message / 139 RPC。
+> buf v2 配置:`lint STANDARD`(含 5 项 except 豁免)+ `breaking FILE` 级别。
+> `buf.gen.yaml` 生成 6 套代码:protocolbuffers {go/js/python} + grpc {go/node/python},输出到 `shared-{go,ts,py}/gen/proto/`。
+
```mermaid
graph TB
- subgraph Proto["protobuf 契约仓库"]
- Identity[identity/v1
user/role/permission]
- Org[org/v1
class/subject/enrollment]
- Teaching[teaching/v1
course/assignment/exam]
- Content[content/v1
textbook/question]
- Comm[comm/v1
message/notification]
- Insight[insight/v1
report/mastery]
+ subgraph Proto["shared-proto/proto/ (8 文件)"]
+ IamProto[iam.proto
IamService]
+ CoreEduProto[core_edu.proto
9 Service: Exam/Homework/Grade/
Attendance/Class/Schedule/
LeaveRequest/Dashboard/Admin]
+ ClassesProto[classes.proto
ClassService(历史保留)]
+ ContentProto[content.proto
Textbook/Chapter/
KnowledgeGraph/Question/
Elective/LessonPlan/CoursePlan]
+ MsgProto[msg.proto
Notification/Preference/
Template Service]
+ DataAnaProto[analytics.proto
AnalyticsService]
+ AiProto[ai.proto
AiService]
+ EventsProto[events.proto
领域事件 schema]
end
- subgraph Gen["代码生成"]
- Buf[buf generate]
- TS[TS 生成代码
@bufbuild/protobuf]
- Go[Go 生成代码
protobuf-go]
- Py[Python 生成代码
betterproto]
+ subgraph Gen["buf generate (buf v2)"]
+ BufGen[buf generate]
+ TSGen[shared-ts/gen/proto
@bufbuild/protobuf + grpc-node]
+ GoGen[shared-go/gen/proto
protobuf-go + grpc-go]
+ PyGen[shared-py/gen/proto
protobuf + grpc-python]
end
subgraph Services["消费服务"]
- SvcTS[NestJS 服务]
- SvcGo[Go 网关]
- SvcPy[Python 服务]
+ SvcTS[NestJS 服务
iam/core-edu/content/msg
teacher-bff/student-bff/parent-bff]
+ SvcGo[Go 网关
api-gateway/push-gateway]
+ SvcPy[Python 服务
data-ana/ai]
end
- Identity --> Buf
- Org --> Buf
- Teaching --> Buf
- Content --> Buf
- Comm --> Buf
- Insight --> Buf
+ IamProto --> BufGen
+ CoreEduProto --> BufGen
+ ClassesProto --> BufGen
+ ContentProto --> BufGen
+ MsgProto --> BufGen
+ DataAnaProto --> BufGen
+ AiProto --> BufGen
+ EventsProto --> BufGen
- Buf --> TS
- Buf --> Go
- Buf --> Py
+ BufGen --> TSGen
+ BufGen --> GoGen
+ BufGen --> PyGen
- TS --> SvcTS
- Go --> SvcGo
- Py --> SvcPy
+ TSGen --> SvcTS
+ GoGen --> SvcGo
+ PyGen --> SvcPy
```
### 11.2 契约规则
-| 规则 | 说明 |
-| -------- | -------------------------------------- |
-| 包命名 | `edu.[context].[aggregate].v[version]` |
-| 版本化 | 破坏性变更必须升版本(v1 → v2) |
-| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
-| 消息命名 | PascalCase |
-| 字段命名 | snake_case |
-| 注释 | 每个 message 和字段必须注释 |
-| CI 强制 | buf lint + buf breaking 必须通过 |
+| 规则 | 说明 |
+| ---------- | ------------------------------------------------------------------------------------------------------------ |
+| 包命名 | proto 包名采用 `next_edu_cloud..v1` 格式(如 `next_edu_cloud.iam.v1`、`next_edu_cloud.core_edu.v1`) |
+| Topic 命名 | Kafka topic 采用 `edu...` 格式(如 `edu.identity.user.created`) |
+| 版本化 | 破坏性变更必须升版本(v1 → v2),buf breaking FILE 级别 CI 强制 |
+| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
+| 消息命名 | PascalCase |
+| 字段命名 | snake_case |
+| 注释 | 每个 message 和字段必须注释 |
+| CI 强制 | buf lint + buf breaking 必须通过 |
+| 代码生成 | `buf generate` 生成 6 套代码(go/js/python × protobuf/grpc) |
### 11.3 BFF 聚合模式
@@ -1101,14 +1274,16 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段
### 12.2 通信约束
-| 场景 | 允许 | 禁止 |
-| -------------------- | ---------------- | ------------------- |
-| 客户端 → Gateway | REST + WebSocket | 直连业务服务 |
-| Gateway → BFF | gRPC | REST |
-| BFF → 业务服务 | gRPC | 直接访问 DB |
-| 业务服务之间(同步) | gRPC + 必要时 | REST、直接 DB |
-| 业务服务之间(异步) | Kafka 事件 | 直接 producer 调用 |
-| 事件发布 | Outbox 模式 | 直接 Kafka producer |
+| 场景 | 允许 | 禁止 |
+| -------------------- | --------------------------------- | ------------------------ |
+| 客户端 → Gateway | REST + WebSocket | 直连业务服务 |
+| Gateway → BFF | HTTP 反向代理(路径重写剥离前缀) | gRPC 转换、REST 业务逻辑 |
+| Gateway → 业务服务 | HTTP 反向代理(剥离 `/api` 前缀) | gRPC 转换 |
+| BFF → 业务服务 | gRPC + DataLoader 批量去重 | 直接访问 DB |
+| push-gateway → msg | HTTP /internal/* + Kafka 消费 | gRPC(豁免,ARB-015) |
+| 业务服务之间(同步) | gRPC | REST、直接 DB |
+| 业务服务之间(异步) | Kafka 事件(Outbox 模式) | 直接 producer 调用 |
+| 事件发布 | Outbox 模式 | 直接 Kafka producer |
### 12.3 数据一致性约束
@@ -1124,22 +1299,30 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段
## 13. ADR 记录
-| 编号 | 决策 | 原因 | 状态 |
-| ------- | ----------------------------- | --------------------------------- | ------ |
-| ADR-001 | 采用 DDD 限界上下文划分服务 | 业务边界清晰,独立演进 | 已采纳 |
-| ADR-002 | 采用 NestJS 作为业务服务框架 | TS 生态成熟,装饰器 + DI 适合 DDD | 已采纳 |
-| ADR-003 | 采用 Go 作为网关语言 | 高并发、低内存、适合网关场景 | 已采纳 |
-| ADR-004 | 采用 Python 作为分析/AI 语言 | 数据科学/AI 生态丰富 | 已采纳 |
-| ADR-005 | 采用 CQRS 读写分离 | 读多写少,读模型可独立优化 | 已采纳 |
-| ADR-006 | 采用 Outbox 模式发布事件 | 保证事务与事件最终一致 | 已采纳 |
-| ADR-007 | 采用 Kafka 作为事件总线 | 高吞吐、持久化、成熟生态 | 已采纳 |
-| ADR-008 | 采用 Debezium CDC | 解耦 Outbox Relay,减少业务侵入 | 已采纳 |
-| ADR-009 | 采用 protobuf + buf 契约先行 | 多语言契约统一、版本化、CI 强制 | 已采纳 |
-| ADR-010 | 采用 JWT RS256 非对称签名 | 网关公钥校验无需共享私钥 | 已采纳 |
-| ADR-011 | 采用 DataScope 6 级数据范围 | 满足 K12 多层级数据隔离 | 已采纳 |
-| ADR-012 | 采用 Module Federation 微前端 | 独立部署、技术栈无关、渐进迁移 | 已采纳 |
-| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | 已采纳 |
-| ADR-014 | 采用 ClickHouse 读模型宽表 | 分析查询亚秒级响应 | 已采纳 |
+| 编号 | 决策 | 原因 | 状态 |
+| ------- | --------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |
+| ADR-001 | 采用 DDD 限界上下文划分服务 | 业务边界清晰,独立演进 | 已采纳 |
+| ADR-002 | 采用 NestJS 作为业务服务框架 | TS 生态成熟,装饰器 + DI 适合 DDD | 已采纳 |
+| ADR-003 | 采用 Go 作为网关语言 | 高并发、低内存、适合网关场景 | 已采纳 |
+| ADR-004 | 采用 Python 作为分析/AI 语言 | 数据科学/AI 生态丰富 | 已采纳 |
+| ADR-005 | 采用 CQRS 读写分离 | 读多写少,读模型可独立优化 | 已采纳 |
+| ADR-006 | 采用 Outbox 模式发布事件 | 保证事务与事件最终一致 | 已采纳 |
+| ADR-007 | 采用 Kafka 作为事件总线 | 高吞吐、持久化、成熟生态 | 已采纳 |
+| ADR-008 | 采用 Debezium CDC | 解耦 Outbox Relay,减少业务侵入 | 已采纳 |
+| ADR-009 | 采用 protobuf + buf 契约先行 | 多语言契约统一、版本化、CI 强制 | 已采纳 |
+| ADR-010 | 采用 JWT RS256 非对称签名 | 网关公钥校验无需共享私钥 | 已采纳 |
+| ADR-011 | 采用 DataScope 6 级数据范围 | 满足 K12 多层级数据隔离 | 已采纳 |
+| ADR-012 | 采用 Module Federation 微前端 | 独立部署、技术栈无关、渐进迁移 | 已采纳 |
+| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | ⏳ 规划中(当前由 ai 内 WorkflowStateStore 临时实现) |
+| ADR-014 | 采用 ClickHouse 读模型宽表 | 分析查询亚秒级响应 | 已采纳 |
+| ADR-015 | api-gateway 采用 HTTP 反向代理(非 gRPC 转换) | Go 网关只做 HTTP,简化部署;下游 HTTP/gRPC 双协议 | 已采纳 |
+| ADR-016 | push-gateway 豁免 gRPC | 基础设施层无流式推送场景;msg/student-bff/parent-bff 改用 HTTP /internal/* | 已采纳 |
+| ADR-017 | classes 服务合并入 core-edu | 教学组织与教学核心共享聚合根边界 | 已采纳 |
+| ADR-018 | BFF 统一 GraphQL Yoga + DataLoader + opossum 熔断 | 协议统一、批量去重、降级模式 B(degraded 子字段) | 已采纳 |
+| ADR-019 | admin-portal GraphQL 经 teacher-bff 命名空间 | 复用 teacher-bff resolver,admin role 中间件强制隔离 | 已采纳 |
+| ADR-020 | parent-portal 作为 MF Remote 被 teacher-portal 加载 | 家长入口可由教师端嵌入,统一 Shell 宿主 | 已采纳 |
+| ADR-021 | Trace 存储 Jaeger 替代 Tempo | all-in-one 镜像部署简单,OTLP 原生支持 | 已采纳 |
+| ADR-022 | 共享包 ui-tokens 替代原 shared-tokens 命名 | 三层令牌(primitive/semantic/tailwind-theme)实际落地 | 已采纳 |
---
@@ -1158,13 +1341,200 @@ graph LR
### 14.2 服务与阶段映射
-| 阶段 | 周期 | 交付服务 | 退出标准 |
-| ----------- | ------- | ----------------------------------------------------- | ------------------------------ |
-| P1 地基 | M1-M3 | api-gateway、classes(黄金模板)、shared-proto | classes 域 CRUD 端到端跑通 |
-| P2 身份 | M4-M6 | iam、teacher-bff、teacher-portal 骨架 | 教师可登录并看到空白 Dashboard |
-| P3 核心教学 | M7-M10 | core-edu(合并 classes)、student-bff、student-portal | 考试→作答→批改→成绩全链路 |
-| P4 内容分析 | M11-M13 | content、data-ana、parent-bff、parent-portal | 知识图谱查询 + 学情宽表 5s |
-| P5 沟通AI | M14-M16 | msg、push-gateway、ai | 全校广播 + AI 辅助出题 |
-| P6 硬化 | M17-M18 | admin-portal、Service Mesh | 99.9% 可用性 + 独立扩缩容 |
+> 状态基于代码现状(2026-07-14)。✅ 已落地 / 🚧 部分落地 / ⏳ 规划中。
-> 详细规划见 [路线图目录](./roadmap/README.md)
+| 阶段 | 周期 | 交付服务 | 退出标准 | 实际状态 |
+| ----------- | ------- | ----------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
+| P1 地基 | M1-M3 | api-gateway、classes(黄金模板)、shared-proto | classes 域 CRUD 端到端跑通 | ✅ 完成(classes 已合并入 core-edu) |
+| P2 身份 | M4-M6 | iam、teacher-bff、teacher-portal 骨架 | 教师可登录并看到空白 Dashboard | ✅ 完成 |
+| P3 核心教学 | M7-M10 | core-edu(合并 classes)、student-bff、student-portal | 考试→作答→批改→成绩全链路 | ✅ 完成(含 LeaveRequests/Dashboard/Admin/IamConsumer 扩展) |
+| P4 内容分析 | M11-M13 | content、data-ana、parent-bff、parent-portal | 知识图谱查询 + 学情宽表 5s | ✅ 完成(含 7 模块 + Neo4j + ES sync + CDC consumer + 11 analytics 端点) |
+| P5 沟通AI | M14-M16 | msg、push-gateway、ai | 全校广播 + AI 辅助出题 | ✅ 完成(msg 4 模块 4 渠道 + push-gateway WS + Kafka + ai 11 端点 + 备课工作流) |
+| P6 硬化 | M17-M18 | admin-portal、Service Mesh | 99.9% 可用性 + 独立扩缩容 | 🚧 admin-portal 已落地;Service Mesh / Vault / 99.9% SLO 待硬化 |
+
+> 详细规划见 [路线图目录](./roadmap/README.md)、[tech-debt](./roadmap/tech-debt.md)、[pending-features](./roadmap/pending-features.md)
+
+---
+
+## 15. 实施状态索引(v2.0 新增)
+
+> 本节按服务/包罗列实际落地的模块、Controller、关键组件,作为代码现状的快速索引。
+> 详细模块设计见各服务 `README.md` 与 `docs/01-understanding.md`。
+
+### 15.1 api-gateway(Go :8080)
+
+- **中间件链**:Recovery → otelgin → RequestID → CORS → SecurityHeaders → RequestBodyLimit(10MB) → RateLimit(100rps+20burst) → CircuitBreaker → AuthMiddleware(JWKS RS256) → Metrics
+- **路由组**:
+ - `/healthz`、`/readyz`、`/metrics`(公开)
+ - `/api/v1/{classes,iam,exams,homework,grades,textbooks,chapters,knowledge-points,questions,notifications,messages,announcements,ai,analytics,dashboard}/*` → HTTP 反向代理
+ - `/api/v1/{teacher,student,parent}/*` → BFF 反向代理(剥离 `/api/v1/{bff}` 前缀)
+ - `/api/admin/graphql` → teacher-bff `/graphql`(AdminRoleMiddleware 强制)
+- **关键依赖**:`shared-go/jwks`(公钥缓存)、`shared-go/env`、`shared-go/logger`、`shared-go/tracer`
+
+### 15.2 push-gateway(Go :8081)
+
+- **WebSocket 端点**:`/ws`(JWT RS256 via query `?token=` 或 Authorization 头)
+- **内部 HTTP API**:`/internal/push`、`/internal/broadcast`、`/internal/online/:userID`(X-Internal-Key 鉴权)
+- **跨实例广播**:Redis Pub/Sub(SubscribeAll + RebuildPresenceOnStartup)
+- **Kafka 消费**:`edu.notify.notification.sent`(ARB-013)
+- **健康检查**:`/healthz`(liveness)、`/readyz`(Redis 软失败 + Kafka consumer 状态)
+- **依赖**:`hub`(in-memory connection registry)、`redisclient`、`kafkaconsumer`、`ws`、`observability`
+
+### 15.3 iam(NestJS :3002 / gRPC :50052)
+
+- **Controllers**:IamController、RbacController、AuditController、JwksController、IamGrpcController
+- **Services**:IamService、JwksService、PermissionCacheService(Redis)、TokenBlacklistService
+- **Outbox**:`iam_outbox` 表,发布到 `edu.identity.user.*` topic
+- **APP_GUARD**:PermissionGuard(DB 驱动 + Redis 缓存,I3 裁决)
+- **AuthMiddleware** 应用范围:`v1/iam/me`、`logout`、`change-password`、`viewports`、`permissions/effective`、`children`、`roles`、`permissions`、`users`、`audit`、`totp`
+
+### 15.4 core-edu(NestJS :3004 / gRPC :50053)
+
+- **业务模块(10)**:
+ - exams(含 exam-extensions、exam-state-machine)
+ - homework(含 homework-state-machine)
+ - grades(含 grade-calculator)
+ - attendance
+ - classes(含 teacher-associations,C1 合并自原 classes 服务)
+ - scheduling(含 schedule-conflict 检测)
+ - leave-requests(P3.13)
+ - dashboard
+ - admin
+ - iam-consumer(消费 IAM 6 类事件)
+- **共享组件**:datascope-injector(Repository 层 WHERE 注入)、outbox(core_edu_outbox)、global-error.filter
+- **gRPC 服务(9,43 RPC)**:ExamService / HomeworkService / GradeService / AttendanceService / ClassService / ScheduleService / LeaveRequestService / DashboardService / AdminService
+
+### 15.5 content(NestJS :3005 / gRPC :50054)
+
+- **业务模块(7)**:textbooks、chapters、knowledge-points、questions、electives、lesson-plans、course-plans
+- **gRPC controllers(7)**:Textbook / Chapter / KnowledgeGraph / Question / Elective / LessonPlan / CoursePlan
+- **同步 worker**:es-sync.worker(题库 ES 索引)、neo4j-sync.worker(知识图谱节点)
+- **Outbox**:content_outbox,发布 `edu.content.*` 事件
+- **存储**:MySQL(业务表)+ Neo4j(知识图谱)+ Elasticsearch(题库检索)
+
+### 15.6 msg(NestJS :3007 / gRPC :50056)
+
+- **业务模块(4)**:notifications、preferences、templates、announcements
+- **gRPC controllers(3,17 RPC)**:NotificationService(9 RPC)/ NotificationPreferenceService(2 RPC)/ NotificationTemplateService(6 RPC,ARB-008 裁剪)
+- **渠道(4)**:email、sms、push、in-app(channel-dispatcher 路由)
+- **Kafka 消费**:16 类事件(iam 6 + core-edu 9 + data-ana 1),见 `shared/kafka/topic-map.ts`
+- **Kafka 生产**:4 类 `edu.notify.notification.*` 事件 + 1 兜底 topic
+- **辅助组件**:push-gateway.client(HTTP 调用 /internal/push)、idempotency.guard(Redis SETNX 去重)、Outbox(msg_outbox)
+- **存储**:MySQL + Elasticsearch(消息全文检索)+ Redis(幂等性)
+
+### 15.7 data-ana(FastAPI :3006 / gRPC :50055)
+
+- **HTTP 端点(3 基础 + 11 业务 = 14)**:
+ - 基础:`/`、`/healthz`、`/readyz`
+ - 业务(全部 ActionState 信封):
+ - `/analytics/class/{class_id}/performance`
+ - `/analytics/student/{student_id}/{weakness,trend,errorbook,mastery}`
+ - `/analytics/{teacher,student,parent,admin}/dashboard`
+ - `/analytics/warnings` + `/analytics/warnings/trigger`
+ - `/analytics/class/{class_id}/mastery-distribution`
+- **gRPC 服务**:AnalyticsService(18 RPC)
+- **CDC 消费者**:手动 commit,lag 阈值 1000(超过 readyz 返 503)
+- **/readyz 4 依赖**:ClickHouse(1s)+ CDC consumer(lag<1000)+ Redis(200ms)+ iam gRPC(2s)
+- **存储**:ClickHouse(宽表)+ Redis(缓存)+ iam gRPC(用户信息)
+
+### 15.8 ai(FastAPI :3008 / gRPC :50058)
+
+- **HTTP 端点(11)**:
+ - `/healthz`、`/readyz`
+ - `/v1/ai/chat`(非流式)+ `/v1/ai/chat/stream`(SSE)
+ - `/v1/ai/generate/question` + `/v1/ai/generate/question/stream`(SSE)
+ - `/v1/ai/optimize/expression`
+ - `/v1/ai/lesson-plan/generate` + `/v1/ai/lesson-plan/status/{workflow_id}` + `/v1/ai/lesson-plan/confirm/{workflow_id}`
+ - `/v1/ai/generate/report`(class_summary / student_detail / exam_analysis)
+- **gRPC 服务**:AiService(9 RPC)
+- **核心组件**:LLM FailoverChain(4 适配器 + 熔断 + 故障切换)、PromptTemplateService(Jinja2 + YAML)、QualityGate(RuleValidator + LLMJudge)
+- **用量与配额**:UsageRecorder(Redis)+ QuotaEnforcer + KafkaProducer(`edu.ai.usage.*`)
+- **限流**:RateLimiter(Redis 三维度令牌桶:user/ip/school)
+- **安全**:PII redactor + 输入清洗 + 输出审核
+- **备课工作流**:4 步编排 + WorkflowStateStore(Redis TTL)
+- **下游 gRPC 客户端**:ContentClientGrpc / DataAnaClientGrpc / IamClientGrpc(连接失败降级,不阻断启动)
+
+### 15.9 teacher-bff(NestJS :3003 / GraphQL Yoga)
+
+- **模块**:GraphQLModule(Teacher + Admin Resolvers,ARB-001 扁平合并)+ HealthModule + MiddlewareModule
+- **下游客户端(6)**:iam / core-edu / content / data-ana / msg / ai(B8 裁决统一抽象,gRPC + mock 双实现)
+- **GraphQL 端点**:`/graphql`(同时承载 teacher 与 admin 命名空间)
+- **SSE 控制器**:`ai-chat-sse.controller.ts`(透传 ai 服务流式响应)
+- **Health probes(6)**:iam-grpc / core-edu-grpc / content-grpc / data-ana-grpc / ai-grpc / msg-grpc + redis probe
+
+### 15.10 student-bff(NestJS :3009 / GraphQL Yoga)
+
+- **模块(8)**:CacheModule(Global Redis)+ DownstreamModule(Global gRPC)+ CircuitBreakerModule(Global opossum)+ HealthModule + DataLoaderModule + StudentModule(GraphQL Yoga + Resolver)+ PushGatewayModule(HTTP /internal/push)+ EventModule(Kafka 事件订阅 + push-gateway 推送)
+- **下游客户端**:iam / core-edu / data-ana
+
+### 15.11 parent-bff(NestJS :3010 / GraphQL Yoga)
+
+- **模块**:HealthModule + GraphqlModule(Yoga /v1/graphql)+ ClientsModule(iam + core-edu + data-ana + msg + push-http)+ KafkaModule(cache-invalidation + notification-push handler)
+- **聚合组件**:AggregationModule(orchestrator + fallback-strategy + child-guard + response-mapper)
+- **DataLoader**:dataloader.factory + loaders(批量去重)
+- **缓存**:Redis + LRU cache + cache-key.builder
+
+### 15.12 teacher-portal(Next.js 15 :4000,MF Shell)
+
+- **路由组**(25+ 业务页面):dashboard、classes、schedule、exams、homework、grades、attendance、analytics、knowledge-graph、lesson-plans、course-plans、textbooks、questions、notifications、students、ai-assist、ai-lesson-plan、ai-report、diagnostic、error-book、practice、leave、elective、schedule-changes、settings
+- **MF Remote 加载**:ParentPortalRemote(在 teacher-portal 内嵌入家长端视图)
+- **GraphQL 客户端文件**:graphql.ts(base)+ graphql-p4.ts / p5.ts / p7-admin.ts / p7-advanced.ts / p7-exams.ts / p7-grades.ts / p7-insights.ts(按阶段渐进扩展)
+- **可观测性**:observability-provider(OTel Web + Sentry + web-vitals + performance-dashboard)
+- **国际化**:messages/{en,zh-CN}.json
+- **认证**:lib/auth.ts(cookie 迁移 + token 刷新 + cross-tab sync)
+
+### 15.13 student-portal / parent-portal / admin-portal(Next.js 15 :4001/:4002/:4003,MF Remote)
+
+- **student-portal**:路由组 leave + 主页;exam-types 类型;mocks/handlers + server
+- **parent-portal**:login + 主页 + providers;child-store(Zustand);middleware.ts;PWA(manifest + sw.js)
+- **admin-portal**:login + admin layout;hooks(use-classes/files/roles/school/students/teachers/users/graphql);lib/{auth,i18n,permissions,web-vitals}
+
+### 15.14 共享包实施状态
+
+| 包 | 关键导出 | 状态 |
+| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---- |
+| shared-proto | 8 proto 文件 + buf.yaml + buf.gen.yaml(生成 6 套代码) | ✅ |
+| shared-ts | `bff/`(logger)、`outbox/`(OutboxModule + publisher + schema + types,被 iam/core-edu/content/msg/BFF 共享) | ✅ |
+| shared-go | `env/`(config.Load)、`jwks/`(Fetcher,RS256 公钥缓存)、`logger/`(slog JSON)、`tracer/`(OTLP init) | ✅ |
+| shared-py | pyproject.toml(uv workspace member) | 🚧 |
+| contracts | `STUDENT_PERMISSIONS` 常量(12 权限点) | 🚧 |
+| hooks | use-auth / use-permission / use-viewports / use-graphql-client / use-trace-id / use-a11y-id / use-aria-live / use-api / use-toast | ✅ |
+| ui-components | data-table / filter-bar / form / modal / chart / calendar / status-badge / empty / loading + utils/cn | ✅ |
+| ui-tokens | primitive.css / semantic-light.css / semantic-dark.css / tailwind-theme.css / all.css + colors/shadows/spacing/typography (.ts) | ✅ |
+
+### 15.15 部署与运维实施状态
+
+- **本地开发**:`infra/docker-compose.yml`(基础设施)+ pnpm dev / go run / uv run
+- **最小化部署**:`infra/docker-compose.minimal.yml`
+- **生产部署**:`infra/docker-compose.deploy.yml`(build: 替代 image:)
+- **测试部署**:`infra/docker-compose.test.yml`
+- **监控栈**:`infra/docker-compose.monitoring.yml`(observability profile)
+- **K8s Helm chart**:`infra/k8s/helm/`(edu-platform umbrella + 各子 chart)
+- **CI/CD**:`.github/workflows/ci.yml`(quality-ts / quality-go / quality-proto / deploy,no-push 本地构建模式)
+- **备份**:`infra/backup/backup-mysql.sh`(每服务独立,保留 7 天)
+- **混沌工程**:`infra/chaos/experiments.yaml`
+- **WAF**:`infra/security/waf-rules.conf`
+- **端口分配**:`infra/port-allocation.md`(唯一源)
+
+### 15.16 文档体系实施状态
+
+| 路径 | 用途 |
+| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| `docs/architecture/004_architecture_impact_map.md` | 本文件(架构影响地图,设计意图唯一源) |
+| `docs/architecture/0010_architecture.md` | 理想蓝图(目标态) |
+| `docs/architecture/004-p6-addendum.md` | P6 硬化附录 |
+| `docs/architecture/roadmap/` | tech-debt / pending-features / integration-test-phase |
+| `docs/architecture/runbooks/` | p6-hardening / post-p6-followup / incident-response |
+| `docs/architecture/issues/` | 协调记录(coord / matrix / workline)+ contracts/ + objections/ + worklines/ |
+| `docs/architecture/ai-allocation.md` | AI 模块分配 |
+| `docs/architecture/ai-work-orchestration.md` | AI 工作编排 |
+| `docs/architecture/coord-cross-review.md` | coord 交叉审查 |
+| `docs/architecture/coord-final-decisions.md` | coord 最终裁决(ARB-001~022+) |
+| `docs/architecture/president-final-rulings.md` | president 最终裁决(batch 0.9+) |
+| `docs/modules//README.md` | 各服务模块文档(iam/core-edu/content/msg/data-ana/ai/api-gateway/push-gateway/classes) |
+| `docs/standards/` | coding-standards / cicd-runbook / full-stack-runbook / git-workflow / local-dev-runbook / multi-ai-collaboration / ui-design-system |
+| `docs/troubleshooting/known-issues.md` | 已知问题速查(索引式场景→技术映射) |
+
+---
+
+> **本文件 v2.0 已基于代码现状(2026-07-14)全面校准。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。**
diff --git a/docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md b/docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md
new file mode 100644
index 0000000..d4461c4
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md
@@ -0,0 +1,1191 @@
+# Portal Shell 插件化仪表盘设计(Modular Monolith + Micro-kernel)
+
+> 版本:v2.1(应用 React 单体哲学优化:URL 状态 + RSC 预取 + SWR 刷新 + 统一 Hook + 沙箱防御)
+> 日期:2026-07-14
+> 状态:待评审
+> 关联:
+>
+> - [0010 架构蓝图](../../architecture/0010_architecture.md) §4 微前端
+> - [004 架构影响地图](../../architecture/004_architecture_impact_map.md)
+> - [UI 设计系统](../../standards/ui-design-system.md)
+> - CICD 参考:`e:\Desktop\CICD\src\modules\dashboard\config\widget-configs.ts`
+> - Halo 插件机制参考:JAR 分发 · 扩展点注入 · 完整生命周期
+
+---
+
+## 1. 背景与目标
+
+### 1.1 问题诊断
+
+当前 Edu 前端由 4 个独立 Next.js portal 组成(teacher / student / parent / admin),存在以下问题:
+
+1. **拆分粒度错误**:后端按"领域"拆微服务,前端按"角色"拆 4 个粗 portal,边界不对齐
+2. **重复度高**:4 套独立 Dockerfile、i18n、auth-provider、layout、design tokens
+3. **复用性差**:跨角色复用功能(grades / homework / schedule)在 4 个 portal 各实现一遍
+4. **耦合度高**:每个 portal 既当 Shell 又内置业务,职责混淆
+5. **配置驱动缺失**:角色变更需改 4 个 portal 代码,无法后台动态配置
+6. **扩展性差**:新增功能需修改多个 portal,无法像 Halo 那样插件化扩展
+
+### 1.2 方向选择
+
+抛弃 v1.0 的 Module Federation 微前端方案(7 个独立 widget 容器、运行时远程加载、部署运维复杂),采用 **Modular Monolith + Micro-kernel** 架构:
+
+- **单 Next.js 项目**,单服务部署,单 Dockerfile
+- **dynamic import 懒加载**,插件按需加载
+- **前端 Registry + 后端 JSON Config**,配置驱动渲染
+- **插件即卡片**,统一术语,MVP 内置插件,二期支持第三方上传(参考 Halo)
+
+### 1.3 设计目标
+
+1. **页面框架可配置**:TopBar + SideNav + ContentArea 三 region,预置 5 种内置 Layout 模板,admin 配置角色默认,用户可切换
+2. **插件即卡片**:所有功能单元统一为"插件"(含单角色专属功能如备课画布、错题本),分类管理,admin 可启用/禁用/配置
+3. **配置驱动可见性**:admin 改配置 → 用户刷新生效,无需重新部署
+4. **声明式 props**:插件通过 manifest 声明 propsSchema,admin 配置默认 props,用户可调整,三层合并
+5. **强隔离**:插件间禁止直接 import,仅通过 EventBus 通信;插件与 Shell 仅通过 PluginProps 契约交互
+6. **设计系统一致**:所有插件强制遵守纸感编辑器设计风格,使用 @edu/ui-tokens
+7. **单服务部署**:一个 Dockerfile,一个容器,无 MF 远程加载
+
+### 1.4 非目标
+
+- 不重写后端微服务、BFF、网关
+- 不替换现有 4 个 portal(并行运行,逐步迁移)
+- 不实现插件拖拽编辑器(canvas 模板预留接口,但不做拖拽实现)
+- MVP 不实现第三方插件上传(二期,预留接口与 UI 入口)
+- 不实现插件市场在线商店(二期仅支持本地 ZIP 上传)
+
+---
+
+## 2. 整体架构
+
+### 2.1 Modular Monolith + Micro-kernel
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ apps/portal-shell/(单 Next.js App Router · 单 Docker) │
+│ │
+│ src/app/shell/[[...route]]/page.tsx ← RSC(Server Component)│
+│ ① 服务端调 IAM gRPC 获取 PluginConfigResponse │
+│ ② 服务端并发调 BFF 预取各插件 initialData(Promise.all) │
+│ ③ 三层 props 合并(PropsMerger) │
+│ ④ 将 Config + initialData 作为 props 传给 Client Shell │
+│ │
+│ src/shell/(Client Component) ← 微内核 │
+│ Shell.tsx Layout 框架 + Slots │
+│ Registry.ts 插件清单(plugin_id → lazy) │
+│ PluginLoader.tsx dynamic import + ssr:false + 骨架屏 │
+│ SlotRenderer.tsx 按 Config 渲染插件列表(含 initialData)│
+│ PropsMerger.ts 三层 props 合并 │
+│ PluginStore.ts Zustand 全局状态(theme/locale/sidebar)│
+│ │
+│ src/widgets/ ← 内置插件源码 │
+│ universal/ sidebar/ topbar/ │
+│ teacher/ student/ parent/ admin/ │
+│ │
+│ src/lib/ ← 数据请求抽象 │
+│ useWidgetQuery.ts 统一 BFF GraphQL 查询(自动按 role 路由)│
+│ useWidgetMutation.ts 统一 BFF GraphQL 变更 │
+│ usePluginConfig.ts SWR 静默刷新配置(revalidateOnFocus) │
+│ │
+└──────────────────────────────────────────────────────────────┘
+ ↕ RSC 服务端预取(消除 CSR 瀑布流)↕
+┌──────────────────────────────────────────────────────────────┐
+│ IAM DB(配置存储) │
+│ plugin_registry / role_plugin_mapping / user_layout_override│
+│ layout_templates / plugin_packages(二期) │
+└──────────────────────────────────────────────────────────────┘
+ ↕ RSC 服务端并发预取 initialData ↕
+┌──────────────────────────────────────────────────────────────┐
+│ BFF 层(已就绪,无需改动) │
+│ teacher-bff / student-bff / parent-bff │
+│ useWidgetQuery 自动按 role 路由到对应 BFF │
+└──────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 核心三元组:Shell + Registry + Config
+
+| 组件 | 职责 | 位置 | 时机 |
+| ------------ | -------------------------------------------------------------------------- | ----------------------- | ---------- |
+| **Shell** | 微内核,渲染 Layout 框架 + Slots + ConfigProvider + PluginLoader,不含业务 | `src/shell/` | 运行时 |
+| **Registry** | 插件清单,`plugin_id → dynamic import 组件`映射。内置插件编译时登记 | `src/shell/Registry.ts` | 编译时 |
+| **Config** | 渲染配置 JSON,决定当前用户在哪些 Slots 渲染哪些插件。三层合并 | IAM DB | 运行时拉取 |
+
+### 2.3 关键设计原则
+
+1. **Shell 是纯宿主**:只负责 Layout / Config / Registry / Loader,不含任何业务逻辑
+2. **插件即卡片**:所有功能单元统一为"插件",无"卡片"与"插件"之分。含单角色专属功能(备课画布、错题本等)
+3. **RSC 服务端预取**:Shell 在 RSC(Server Component)中直接调 IAM gRPC 获取 Config,按需并发调 BFF 预取插件初始数据,Config + initialData 随 HTML 直出,客户端水合后 dynamic import 加载插件组件(initialData 已在 HTML 中),消除 CSR 瀑布流,实现仪表盘"秒开"
+4. **dynamic import 懒加载**:插件按需加载,首屏只加载可见 slot 的插件,非可见插件滚动到视口再加载
+5. **强隔离**:插件间禁止直接 import,通过 URL Search Params + Zustand Store 共享状态;插件与 Shell 仅通过 PluginProps 契约交互
+6. **配置驱动**:admin 改配置 → SWR 静默后台刷新(revalidateOnFocus + refreshInterval)→ Toast 提示用户刷新,无需重新部署(内置插件增删仍需重新构建)
+7. **单服务部署**:一个 Dockerfile,一个容器,无 MF 远程加载,无独立 widget 容器
+
+---
+
+## 3. 插件目录分类
+
+内置插件源码按**用途分类**组织在 `src/widgets//` 下:
+
+```
+src/widgets/
+├─ universal/ # 通用插件(跨角色复用,按角色渲染不同视图)
+│ ├─ grades-widget/
+│ │ ├─ index.tsx # 插件入口(接收 PluginProps)
+│ │ ├─ plugin.manifest.ts # 插件元数据(id/分类/版本/slot/size/propsSchema)
+│ │ ├─ TeacherView.tsx # 教师视图
+│ │ ├─ StudentView.tsx # 学生视图
+│ │ ├─ ParentView.tsx # 家长视图
+│ │ └─ __tests__/
+│ ├─ homework-widget/
+│ ├─ schedule-widget/
+│ ├─ attendance-widget/
+│ ├─ exams-widget/
+│ ├─ notifications-widget/
+│ └─ announcements-widget/
+│
+├─ sidebar/ # 侧栏插件(插入 SideNav slot)
+│ ├─ class-selector/ # 班级选择器(教师)
+│ ├─ child-selector/ # 孩子选择器(家长)
+│ ├─ term-switcher/ # 学期切换
+│ └─ quick-actions/ # 快捷操作
+│
+├─ topbar/ # 顶栏插件(插入 TopBar slot)
+│ ├─ global-search/ # 全局搜索
+│ ├─ notification-bell/ # 通知铃铛
+│ ├─ user-menu/ # 用户菜单
+│ └─ locale-switcher/ # 语言切换
+│
+├─ teacher/ # 教师专属插件(仅教师可见)
+│ ├─ lesson-plan-editor/ # 备课画布(V4 锚点/inline-node)
+│ ├─ question-bank/ # 题库管理
+│ ├─ textbook-manager/ # 教材管理
+│ └─ scheduling-rules/ # 排课规则
+│
+├─ student/ # 学生专属插件
+│ ├─ error-book/ # 错题本
+│ ├─ learning-path/ # 学习路径
+│ ├─ elective-selector/ # 选课
+│ └─ ai-tutor/ # AI 辅导
+│
+├─ parent/ # 家长专属插件
+│ ├─ child-overview/ # 多孩子总览
+│ └─ leave-approval/ # 请假审批
+│
+└─ admin/ # 管理员专属插件
+ ├─ user-management/ # 用户管理
+ ├─ rbac-manager/ # 角色权限
+ ├─ school-settings/ # 学校设置
+ ├─ audit-logs/ # 审计日志
+ ├─ invitation-codes/ # 邀请码
+ └─ plugin-manager/ # 插件管理(admin 配置面板本身也是插件)
+```
+
+| 分类 | 用途 | 插入 slot | 跨角色 | 示例 |
+| ----------- | ---------------------------- | --------- | ------ | ---------------------------------- |
+| `universal` | 通用功能,按角色渲染不同视图 | main-* | 是 | grades / homework / schedule |
+| `sidebar` | 侧栏功能(选择器/切换器) | side | 部分 | class-selector / term-switcher |
+| `topbar` | 顶栏功能(搜索/通知/菜单) | top | 是 | global-search / notification-bell |
+| `teacher` | 教师专属,不跨角色复用 | main-* | 否 | lesson-plan-editor / question-bank |
+| `student` | 学生专属 | main-* | 否 | error-book / ai-tutor |
+| `parent` | 家长专属 | main-* | 否 | child-overview / leave-approval |
+| `admin` | 管理员专属 | main-* | 否 | user-management / plugin-manager |
+
+---
+
+## 4. Layout + Slot 模型
+
+### 4.1 5 种内置 Layout 模板
+
+| 模板 ID | 显示名 | 布局描述 | 可用 slots | 适用场景 |
+| --------- | -------- | ----------------------------------- | ---------------------------- | -------------- |
+| `classic` | 经典三栏 | TopBar + SideNav + Main | top / side / main | 默认(仪表盘) |
+| `focus` | 聚焦 | TopBar + 全宽 Main | top / main | 备课/编辑器 |
+| `split` | 双栏 | TopBar + 左右等分 Main | top / main-left / main-right | 对比/批改 |
+| `triple` | 三栏内容 | TopBar + SideNav + Main + RightRail | top / side / main / right | 数据分析 |
+| `canvas` | 自由画布 | TopBar + 自由摆放(拖拽预留) | top / canvas-grid | 个性化 |
+
+### 4.2 Slot 系统
+
+每个 Layout 模板预定义一组 slot,插件配置到 slot 中渲染:
+
+- **top slot**:TopBar 区域,可插入多个 topbar 类插件(logo / search / notification-bell / user-menu / locale-switcher)
+- **side slot**:SideNav 区域,可插入 sidebar 类插件(class-selector / term-switcher / 导航项)
+- **main slot**:主内容区,可插入 universal / teacher / student / parent / admin 类插件
+- **main-left / main-right slot**:split 模板的左右等分
+- **right slot**:triple 模板的右侧栏
+- **canvas-grid slot**:canvas 模板的自由摆放区(MVP 不实现拖拽,仅按 grid 排列)
+
+### 4.3 三层配置模型
+
+**优先级**:用户覆盖 > 角色模板 > 系统默认
+
+#### Layer 1: 系统默认(plugin_registry 表)
+
+- 插件元数据:id / 分类 / 版本 / 默认 slot / 默认 size / 默认 props
+- 由开发者在 `plugin.manifest.ts` 声明,构建时同步到 DB
+
+#### Layer 2: 角色模板(role_plugin_mapping 表)
+
+- admin 配置:角色可用哪些插件、角色默认 Layout、角色级 props 默认值
+- 如:教师角色启用 grades-widget + homework-widget,默认 classic 模板,grades-widget 默认显示本学期
+
+#### Layer 3: 用户覆盖(user_layout_override 表)
+
+- 用户自定义:切换 Layout 模板、调整插件位置、隐藏插件、调整插件 props
+- 仅限角色可用集内操作
+
+### 4.4 插件 props 三层合并
+
+插件通过 `plugin.manifest.ts` 声明 `propsSchema`(JSON Schema),admin 配置面板自动渲染表单。
+
+**props 合并优先级**:用户调整 > 角色默认 > 系统默认
+
+- **系统默认**:开发者在 `plugin.manifest.ts` 的 `defaultProps` 声明,构建时由 `sync-builtin-plugins.ts` 脚本同步到 `plugin_registry.default_props` 字段。运行时 Shell 从 DB 读取(DB 是唯一源,避免 manifest 与 DB 不一致)。
+- **角色默认**:admin 通过配置面板写入 `role_plugin_mapping.widget_props`。
+- **用户调整**:用户在插件内调整,写入 `user_layout_override.plugin_placements[].props`。
+
+```typescript
+// PropsMerger 三层合并(深合并 + 数组覆盖)
+const finalProps = deepMerge(
+ registry.defaultProps, // 系统默认(plugin_registry 表)
+ roleMapping.widgetProps, // 角色默认(role_plugin_mapping 表)
+ userPlacement.props, // 用户调整(user_layout_override 表)
+);
+```
+
+---
+
+## 5. 插件契约
+
+### 5.1 PluginProps 契约
+
+```typescript
+// packages/shared-ts/src/contracts/plugin.ts
+
+export interface PluginProps> {
+ /** 插件实例 ID(同一插件多实例时区分) */
+ instanceId: string;
+ /** 当前用户角色 */
+ role: "admin" | "teacher" | "student" | "parent";
+ /** 当前用户信息(来自 IAM) */
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ dataScope: string;
+ };
+ /** 当前 slot 信息 */
+ slot: {
+ name: string; // 'main-top' | 'side' | 'top' | ...
+ layoutId: string; // 'classic' | 'focus' | ...
+ size?: { colSpan: number; rowSpan: number };
+ };
+ /** 插件自定义 props(三层合并后的最终值) */
+ props: TProps;
+ /** 服务端预取的初始数据(RSC 直出,避免客户端瀑布流) */
+ initialData?: unknown;
+}
+
+// 注:跨插件状态不通过 props 传递,而是插件自行调用:
+// - useSearchParams() 读取 URL 上下文(classId/childId/termId/view)
+// - usePluginStore() 读取 Zustand 全局状态(theme/locale/sidebarCollapsed)
+// - useWidgetQuery() 读取 BFF 业务数据(自动按 role 路由)
+// 这样插件无需接收 eventBus/ctx 等"注入"依赖,符合 React 函数式哲学
+
+export interface PluginManifest {
+ pluginId: string;
+ version: string;
+ /** 兼容的 Shell 版本范围(semver range,如 "^1.0.0") */
+ requiredShellVersion: string;
+ /** React 组件(默认导出) */
+ Component: React.ComponentType;
+ /** 插件元数据 */
+ metadata: {
+ displayName: string;
+ description: string;
+ category:
+ | "universal"
+ | "sidebar"
+ | "topbar"
+ | "teacher"
+ | "student"
+ | "parent"
+ | "admin";
+ requiredRoles: string[];
+ defaultSlot: string;
+ defaultSize: { colSpan: number; rowSpan: number };
+ /** 插件可配置的 props schema(JSON Schema,用于 admin 配置面板自动渲染表单) */
+ propsSchema?: JSONSchema;
+ /** 系统默认 props(与 propsSchema 配合) */
+ defaultProps?: Record;
+ };
+}
+```
+
+### 5.2 跨插件状态管理(URL Params + Zustand)
+
+抛弃 EventBus 发布订阅模式(微前端跨框架通信的无奈之举,在 React 单体中会导致"状态黑盒"、极难 Debug、不支持 React DevTools 追踪)。采用 React 单向数据流哲学:**URL 驱动 + Zustand 全局状态**。
+
+#### 5.2.1 URL Search Params(高频/可分享上下文)
+
+适合**需要 URL 分享、浏览器前进后退**的全局上下文:
+
+```typescript
+// packages/shared-ts/src/contracts/plugin-context.ts
+
+/** URL 驱动的全局上下文(可分享、可前进后退) */
+export interface UrlPluginContext {
+ /** 当前选中的班级 ID(教师视角,class-selector 切换时更新 URL) */
+ classId?: string;
+ /** 当前选中的孩子 ID(家长视角) */
+ childId?: string;
+ /** 当前选中的学期 */
+ termId?: string;
+ /** 当前视图模式(如 grades-widget 的 'list' | 'chart') */
+ view?: string;
+}
+
+// 读取:useSearchParams()
+// 写入:router.push('?classId=xxx')
+// 响应:其他插件通过 useSearchParams() 自动响应,触发重新渲染
+```
+
+```typescript
+// 示例:class-selector 插件切换班级
+import { useRouter, useSearchParams } from "next/navigation";
+
+export function ClassSelector() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const currentClassId = searchParams.get("classId");
+
+ const handleSelect = (classId: string) => {
+ const params = new URLSearchParams(searchParams);
+ params.set("classId", classId);
+ router.push(`?${params.toString()}`);
+ // grades-widget 等插件自动响应,无需 EventBus 广播
+ };
+
+ return ;
+}
+
+// 示例:grades-widget 插件响应班级切换
+import { useSearchParams } from "next/navigation";
+
+export function GradesWidget() {
+ const classId = useSearchParams().get("classId");
+ // classId 变化自动触发重新渲染和重新查询
+ const { data } = useWidgetQuery(GET_GRADES, { classId });
+ return ;
+}
+```
+
+#### 5.2.2 Zustand Store(低频/不可分享状态)
+
+适合**不需要 URL 分享、纯 UI 状态**的全局上下文:
+
+```typescript
+// packages/shared-ts/src/contracts/plugin-store.ts
+
+import { create } from "zustand";
+
+interface PluginStore {
+ /** 主题模式(light/dark) */
+ theme: "light" | "dark";
+ setTheme: (theme: "light" | "dark") => void;
+
+ /** i18n locale */
+ locale: "zh-CN" | "en";
+ setLocale: (locale: "zh-CN" | "en") => void;
+
+ /** Sidebar 折叠状态 */
+ sidebarCollapsed: boolean;
+ toggleSidebar: () => void;
+
+ /** 通知已读标记(纯 UI 状态,不进 URL) */
+ unreadNotificationIds: string[];
+ markNotificationsRead: (ids: string[]) => void;
+}
+
+export const usePluginStore = create((set) => ({
+ theme: "light",
+ setTheme: (theme) => set({ theme }),
+ locale: "zh-CN",
+ setLocale: (locale) => set({ locale }),
+ sidebarCollapsed: false,
+ toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
+ unreadNotificationIds: [],
+ markNotificationsRead: (ids) =>
+ set((s) => ({
+ unreadNotificationIds: s.unreadNotificationIds.filter(
+ (id) => !ids.includes(id),
+ ),
+ })),
+}));
+```
+
+#### 5.2.3 URL vs Zustand 选型标准
+
+| 状态类型 | 存储 | 示例 | 理由 |
+| ------------------ | ------------------------------ | --------------------------------- | -------------------------------------- |
+| 可分享、可前进后退 | URL Search Params | classId / termId / view | 用户可复制链接分享,浏览器前进后退生效 |
+| 纯 UI、不可分享 | Zustand Store | theme / locale / sidebarCollapsed | 不需要 URL 体现,避免 URL 污染 |
+| 插件内部私有 | 插件内部 useState / useReducer | 表单临时值、弹窗开关 | 最小作用域原则 |
+
+#### 5.2.4 弃用 EventBus 的收益
+
+- ✅ 数据流向清晰:URL 是单一数据源,DevTools Network/URL 栏可见
+- ✅ 支持 React DevTools 追踪:Zustand 状态可被 DevTools 检视
+- ✅ 符合 React 单向数据流:状态变更 → 自动重渲染,无发布订阅黑盒
+- ✅ URL 可分享:用户可分享带 classId 的链接,直接定位到特定班级视图
+- ✅ 浏览器前进后退:URL 变更天然支持历史导航
+- ✅ 减少 Debug 成本:无需维护事件名常量、订阅/取消订阅生命周期
+
+### 5.3 插件加载与错误处理
+
+- **加载态**:PluginLoader 显示 ``,骨架样式由 `@edu/ui-components` 提供
+- **错误态**:插件加载失败时显示 ``,ErrorBoundary 隔离,单个插件失败不影响其他
+- **超时**:dynamic import 超时 10s 后降级显示错误态
+- **版本兼容**:插件 manifest 携带 `requiredShellVersion`,Shell 启动时校验,不兼容则拒绝加载并提示 admin 升级
+- **懒加载**:首屏只加载可见 slot 的插件,非可见插件使用 IntersectionObserver 滚动到视口再加载
+
+### 5.4 插件生命周期
+
+| 阶段 | 内置插件 | 第三方插件(二期) |
+| ----------- | --------------------- | ------------------ |
+| registered | 编译时登记到 Registry | 安装时登记到 DB |
+| enabled | admin 启用 | admin 启用 |
+| loaded | dynamic import 加载 | script 注入加载 |
+| active | 渲染并挂载 | 渲染并挂载 |
+| disabled | admin 禁用,不渲染 | admin 禁用,不渲染 |
+| uninstalled | 不可卸载(内置) | admin 卸载,删除包 |
+
+### 5.5 RSC 服务端预取流程(消除 CSR 瀑布流)
+
+**v2.0 瀑布流问题**:Shell SSR 骨架 → 客户端水合 → 客户端拉 Config → 客户端 dynamic import 插件 → 插件拉 BFF 数据。4 层串行,LCP 差。
+
+**v2.1 优化**:利用 Next.js App Router 的 Server Components,在服务端完成 Config 拉取 + initialData 预取,随 HTML 直出。
+
+```typescript
+// src/app/shell/[[...route]]/page.tsx(Server Component)
+import { headers } from "next/headers";
+import { iamClient } from "@/lib/iam-client";
+import { bffClients } from "@/lib/bff-clients";
+import { PropsMerger } from "@/shell/PropsMerger";
+import ClientShell from "@/shell/ClientShell";
+
+export default async function ShellPage() {
+ // ① 从请求头获取 JWT,解析出 userId 和 role
+ const headerList = headers();
+ const userId = headerList.get("x-user-id")!;
+ const role = headerList.get("x-user-role") as "admin" | "teacher" | "student" | "parent";
+
+ // ② 服务端调 IAM gRPC 获取三层合并后的 PluginConfigResponse
+ const config = await iamClient.getPluginConfig({ userId });
+
+ // ③ 服务端并发预取各插件的 initialData
+ const pluginsWithData = await Promise.all(
+ config.plugins.map(async (placement) => {
+ const initialData = await prefetchPluginData(placement, role, bffClients);
+ return { ...placement, initialData };
+ })
+ );
+
+ // ④ Config + initialData 作为 props 传给 Client Shell
+ return ;
+}
+
+/** 按插件类型预取初始数据(仅首屏可见插件) */
+async function prefetchPluginData(placement, role, bffClients): Promise {
+ const bffClient = bffClients[role]; // 自动按 role 路由
+ switch (placement.pluginId) {
+ case "grades-widget":
+ return bffClient.query(GET_GRADES, { classId: placement.props.classId });
+ case "notifications-widget":
+ return bffClient.query(GET_NOTIFICATIONS, { limit: 10 });
+ // ... 其他插件
+ default:
+ return null;
+ }
+}
+```
+
+**数据流对比:**
+
+```
+v2.0 瀑布流(4 层串行,LCP 差):
+ HTML 骨架 → 水合 → 拉 Config → import 插件 → 插件拉数据 → 渲染
+ [SSR] [client] [client] [client] [client] [client]
+ ──────────┴────────┴───────────┴────────────┴───────────┴────────
+ 总耗时 = SSR + 水合 + Config RTT + import RTT + BFF RTT
+
+v2.1 RSC 预取(2 层并行,LCP 秒开):
+ 服务端:RSC 拉 Config + 并发预取 initialData → HTML 直出(含数据)
+ 客户端:水合 → dynamic import 插件组件 → 用 initialData 渲染
+ [RSC 并行] [client]
+ ───────────────────────────────────────────────┴────────────────
+ 总耗时 = max(Config RTT, BFF RTT) + 水合 + import RTT
+ (Config 和 BFF 在服务端并行,不阻塞客户端)
+```
+
+**约束:**
+
+- `prefetchPluginData` 仅预取**首屏可见 slot** 的插件数据(非可见插件滚动到视口再加载)
+- 插件组件仍用 `dynamic(() => import(...), { ssr: false })` 懒加载(避免插件 JS 阻塞首屏)
+- 插件接收 `initialData` 作为 SWR/useWidgetQuery 的 `fallbackData`,首次渲染无需客户端请求
+- 后续数据刷新(如手动刷新、轮询)仍走客户端 useWidgetQuery
+
+### 5.6 统一 BFF 请求 Hook(Data Fetching Abstraction)
+
+**痛点**:universal 分类插件(grades/homework/schedule 等)跨角色复用,如果每个插件自行判断角色并拼接 BFF URL,会导致大量重复样板代码。
+
+**方案**:在 `src/lib/` 中封装统一的 `useWidgetQuery` 和 `useWidgetMutation`,内部自动读取 `AuthProvider` 中的 role,自动路由到对应 BFF。插件开发者只关心写 GraphQL 查询语句。
+
+```typescript
+// src/lib/useWidgetQuery.ts
+import useSWR from "swr";
+import { useAuth } from "@/providers/AuthProvider";
+
+const BFF_ROUTE_MAP = {
+ admin: "/api/admin-bff/graphql",
+ teacher: "/api/teacher-bff/graphql",
+ student: "/api/student-bff/graphql",
+ parent: "/api/parent-bff/graphql",
+} as const;
+
+/**
+ * 统一 BFF GraphQL 查询 Hook
+ * 自动按当前用户 role 路由到对应 BFF
+ * 插件开发者只需提供 GraphQL 查询语句和变量
+ */
+export function useWidgetQuery<
+ TData = unknown,
+ TVars = Record,
+>(
+ query: string,
+ variables?: TVars,
+ options?: {
+ fallbackData?: TData; // RSC 预取的 initialData
+ refreshInterval?: number;
+ enabled?: boolean;
+ },
+) {
+ const { role } = useAuth();
+ const bffUrl = BFF_ROUTE_MAP[role];
+
+ return useSWR(
+ options?.enabled === false ? null : [bffUrl, query, variables],
+ async ([url, q, vars]) => {
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: q, variables: vars }),
+ credentials: "include",
+ });
+ const json = await res.json();
+ if (json.errors) throw new Error(json.errors[0].message);
+ return json.data;
+ },
+ {
+ fallbackData: options?.fallbackData, // RSC 预取数据直出
+ revalidateOnFocus: false, // 业务数据不自动刷新(配置才自动刷新)
+ refreshInterval: options?.refreshInterval,
+ },
+ );
+}
+
+/**
+ * 统一 BFF GraphQL 变更 Hook
+ */
+export function useWidgetMutation<
+ TData = unknown,
+ TVars = Record,
+>(mutation: string) {
+ const { role } = useAuth();
+ const bffUrl = BFF_ROUTE_MAP[role];
+
+ return async (variables: TVars): Promise => {
+ const res = await fetch(bffUrl, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: mutation, variables }),
+ credentials: "include",
+ });
+ const json = await res.json();
+ if (json.errors) throw new Error(json.errors[0].message);
+ return json.data;
+ };
+}
+```
+
+**插件使用示例:**
+
+```typescript
+// grades-widget 插件(无需关心路由到哪个 BFF)
+import { useWidgetQuery, useWidgetMutation } from "@/lib/useWidgetQuery";
+
+const GET_GRADES = `
+ query GetGrades($classId: ID!, $termId: ID!) {
+ grades(classId: $classId, termId: $termId) { studentId score }
+ }
+`;
+
+export function GradesWidget({ initialData }: PluginProps) {
+ const classId = useSearchParams().get("classId");
+ const termId = useSearchParams().get("termId");
+
+ const { data, isLoading } = useWidgetQuery(GET_GRADES, { classId, termId }, {
+ fallbackData: initialData, // RSC 预取的数据
+ });
+
+ if (isLoading) return ;
+ return ;
+}
+```
+
+**收益:**
+
+- ✅ 插件开发者无需关心 BFF 路由策略
+- ✅ role 切换时自动路由到对应 BFF(universal 插件天然支持多角色)
+- ✅ RSC 预取数据通过 `fallbackData` 无缝衔接,首次渲染无瀑布流
+- ✅ 统一错误处理、统一 credentials、统一 TypeScript 类型推导
+
+---
+
+## 6. 数据模型(IAM 扩展)
+
+### 6.1 表结构
+
+```sql
+-- 插件注册表(系统级,admin 维护;内置插件由构建脚本同步)
+plugin_registry (
+ plugin_id TEXT PRIMARY KEY, -- 'grades-widget'
+ category TEXT, -- 'universal'|'sidebar'|'topbar'|'teacher'|'student'|'parent'|'admin'
+ version TEXT, -- semver
+ display_name TEXT,
+ description TEXT,
+ required_roles TEXT[], -- ['teacher','student','parent']
+ default_slot TEXT, -- 'main-top'
+ default_size JSONB, -- {colSpan:2,rowSpan:1}
+ default_props JSONB, -- 系统默认 props
+ props_schema JSONB, -- JSON Schema(admin 配置面板用)
+ is_builtin BOOLEAN, -- true=内置,false=第三方
+ is_active BOOLEAN, -- 全局启用/禁用
+ package_url TEXT, -- 二期:第三方插件包 URL
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP
+);
+
+-- 角色-插件映射(admin 配置角色可用插件集 + 角色 Layout 默认)
+role_plugin_mapping (
+ role TEXT, -- 'teacher'
+ plugin_id TEXT, -- 'grades-widget'
+ slot TEXT, -- 'main-top'(角色默认 slot)
+ sort_order INT, -- 显示顺序
+ is_enabled BOOLEAN, -- 角色是否启用
+ widget_props JSONB, -- 角色默认 props(覆盖系统默认)
+ PRIMARY KEY (role, plugin_id)
+);
+
+-- 角色 Layout 默认(admin 配置角色默认模板)
+role_layout_default (
+ role TEXT PRIMARY KEY,
+ layout_id TEXT, -- 'classic'|'focus'|'split'|'triple'|'canvas'
+ slot_overrides JSONB -- 角色级 slot 默认
+);
+
+-- Layout 模板(系统预置,不可改)
+layout_templates (
+ layout_id TEXT PRIMARY KEY, -- 'classic'|'focus'|'split'|'triple'|'canvas'
+ display_name TEXT,
+ description TEXT,
+ available_slots JSONB, -- ["top","side","main","right"]
+ layout_schema JSONB, -- {grid:{rows,cols,areas},resizable,draggable}
+ is_active BOOLEAN
+);
+
+-- 用户布局覆盖(用户自定义)
+user_layout_override (
+ user_id TEXT PRIMARY KEY,
+ active_layout TEXT, -- 当前选用模板
+ slot_overrides JSONB, -- 用户自定义 slot 项
+ plugin_placements JSONB, -- [{plugin_id,slot,sort_order,size,props}]
+ hidden_plugins TEXT[] -- 用户隐藏的插件
+);
+
+-- 二期:第三方插件包存储
+plugin_packages (
+ package_id TEXT PRIMARY KEY,
+ plugin_id TEXT, -- 关联 plugin_registry
+ version TEXT,
+ manifest_json JSONB, -- 解析后的 manifest
+ storage_path TEXT, -- ZIP 包存储路径
+ status TEXT, -- 'uploaded'|'installed'|'error'
+ uploaded_by TEXT,
+ uploaded_at TIMESTAMP
+);
+```
+
+### 6.2 IAM gRPC 接口扩展
+
+```protobuf
+// packages/shared-proto/proto/iam.proto 新增
+
+message GetPluginConfigRequest {
+ string user_id = 1;
+}
+
+message PluginConfigResponse {
+ LayoutTemplate active_layout = 1;
+ repeated SlotConfig slots = 2;
+ repeated PluginPlacement plugins = 3;
+ repeated PluginRegistryItem registry = 4;
+}
+
+message LayoutTemplate {
+ string layout_id = 1;
+ string display_name = 2;
+ string description = 3;
+ repeated string available_slots = 4;
+ string layout_schema_json = 5;
+}
+
+message SlotConfig {
+ string slot_name = 1;
+ repeated string nav_items = 2;
+}
+
+message PluginPlacement {
+ string plugin_id = 1;
+ string slot = 2;
+ int32 sort_order = 3;
+ string size_json = 4;
+ string props_json = 5; // 三层合并后的最终 props
+ bool is_visible = 6;
+}
+
+message PluginRegistryItem {
+ string plugin_id = 1;
+ string category = 2;
+ string version = 3;
+ string display_name = 4;
+ string description = 5;
+ repeated string required_roles = 6;
+ bool is_builtin = 7;
+ bool is_active = 8;
+}
+```
+
+### 6.3 Admin CRUD API
+
+| 方法 | 路径 | 用途 | MVP |
+| ------ | ------------------------------------- | ------------------------------------ | ------- |
+| GET | `/admin/plugins` | 列出所有 plugin_registry 项 | ✅ |
+| PUT | `/admin/plugins/:id` | 更新插件配置(启用/禁用/默认 props) | ✅ |
+| GET | `/admin/role-plugin-mapping` | 列出角色-插件映射 | ✅ |
+| PUT | `/admin/role-plugin-mapping/:role` | 批量更新角色插件配置 | ✅ |
+| GET | `/admin/layout-templates` | 列出 5 种内置模板 | ✅ |
+| PUT | `/admin/role-layout-default/:role` | 配置角色默认 Layout | ✅ |
+| GET | `/admin/user-layout-override/:userId` | 查看用户自定义 | ✅ |
+| DELETE | `/admin/user-layout-override/:userId` | 重置用户自定义 | ✅ |
+| POST | `/admin/plugins/upload` | 上传第三方插件包(ZIP) | ⏸️ 二期 |
+| DELETE | `/admin/plugins/:id/uninstall` | 卸载第三方插件 | ⏸️ 二期 |
+
+### 6.4 配置变更生效路径(SWR 静默刷新)
+
+**抛弃 Kafka + WebSocket 推送链路**(对 Dashboard 布局这种低频变更,引入 Kafka 和 WebSocket 的研发和运维成本过高,属于过度设计)。
+
+改用 **SWR 静默后台刷新**机制:
+
+```typescript
+// src/lib/usePluginConfig.ts
+import useSWR from "swr";
+
+const fetcher = (url: string) => fetch(url).then((r) => r.json());
+
+export function usePluginConfig(initialConfig: PluginConfigResponse) {
+ const { data, mutate } = useSWR(
+ "/api/plugin-config",
+ fetcher,
+ {
+ fallbackData: initialConfig, // RSC 直出的初始配置作为 fallback
+ revalidateOnFocus: true, // 用户切回 Tab 时静默刷新
+ revalidateOnReconnect: true, // 网络恢复时静默刷新
+ refreshInterval: 300_000, // 每 5 分钟静默刷新一次
+ dedupingInterval: 60_000, // 1 分钟内去重
+ onSuccess: (newData, key, config) => {
+ // 检测到配置变化时,Toast 提示用户刷新
+ if (hasConfigChanged(initialConfig, newData)) {
+ showToast("发现新布局配置,点击刷新生效", {
+ action: { label: "刷新", onClick: () => window.location.reload() },
+ duration: 0, // 常驻直到用户操作
+ });
+ }
+ },
+ },
+ );
+
+ return { config: data, refresh: mutate };
+}
+```
+
+**生效路径:**
+
+1. Admin 通过 REST API 修改 `plugin_registry` / `role_plugin_mapping` / `role_layout_default`
+2. 用户侧 SWR 静默刷新触发(切回 Tab / 5 分钟轮询 / 网络恢复)
+3. SWR 检测到配置变化,Toast 提示"发现新布局配置,点击刷新生效"
+4. 用户点击刷新或下次刷新页面时,RSC 重新预取配置生效
+5. **无需 Kafka、无需 WebSocket、无需 push-gateway 改动**
+
+**收益:**
+
+- ✅ 砍掉 Kafka Outbox 事件链路(IAM 侧)
+- ✅ 砍掉 msg 服务消费 + push-gateway WebSocket 推送链路
+- ✅ 砍掉前端 WebSocket 客户端订阅逻辑
+- ✅ 利用 SWR 成熟机制,代码量减少 80%+
+- ✅ 配置变更感知延迟 ≤ 5 分钟(refreshInterval)或即时(revalidateOnFocus),对低频变更完全够用
+
+---
+
+## 7. 设计系统一致性
+
+### 7.1 强制约束
+
+- 所有插件必须使用 `@edu/ui-tokens` 的设计令牌(`hsl(var(--*))` / `var(--font-*)` / `var(--space-*)`)
+- 所有插件必须使用 `@edu/ui-components` 的基础组件
+- 禁止硬编码颜色(`#hex`)、字体(`'Inter'`)、字号(`font-size: Npx`)、Tailwind 任意值(`w-[Npx]`)
+- ESLint 规则 `no-restricted-syntax` + `design-tokens/no-hardcoded-fonts` 强制约束
+- 插件必须使用 `PluginCard` 包装组件(纸感风格容器)
+
+### 7.2 新增"插件视觉规范"章节
+
+补充到 `docs/standards/ui-design-system.md`:
+
+- **插件容器**:使用 `PluginCard`,纸感背景 `hsl(var(--card))`,圆角 `var(--radius-md)`,内边距 `var(--space-5)`
+- **插件标题**:`font-family: var(--font-family-sans)`,`font-size: var(--font-size-4)`,`font-weight: var(--weight-semibold)`
+- **插件加载态**:``
+- **插件错误态**:``,居中显示错误图标 + 重试按钮
+- **插件间距**:slot 内插件间距 `var(--space-4)`,slot 间距 `var(--space-6)`
+- **响应式**:mobile 单列,tablet 2 列,desktop 按 layout 配置
+
+### 7.3 新增 UI 组件
+
+- `PluginCard`:纸感卡片容器,强制设计令牌
+- `PluginSkeleton`:5 种 skeleton 变体(card/list/chart/stats/table)
+- `PluginErrorFallback`:错误兜底组件
+- `SlotPlaceholder`:空 slot 占位(admin 模式下显示"添加插件"按钮)
+- `PropsConfigForm`:根据 propsSchema 自动渲染的配置表单(admin 配置面板用)
+
+---
+
+## 8. 迁移路径
+
+### 8.1 阶段划分
+
+| 阶段 | 内容 | 验收标准 |
+| ----------- | -------------------------------------------------------------------------------------------- | --------------------------------------------- |
+| P1 | IAM 扩展(5 表 + gRPC + admin API)+ proto 契约 + 共享包契约 | gRPC 调用返回正确配置,admin CRUD 可用 |
+| P2 | Shell 宿主(apps/portal-shell)+ 5 种 Layout 模板 + ConfigProvider + Registry + PluginLoader | Shell 启动、登录、渲染空 Layout、切换模板 |
+| P3 | 首张插件(grades-widget)+ dynamic import 跑通 + 三层 props 合并 | grades-widget 在 Shell 中渲染,props 合并正确 |
+| 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(二期) | 第三方插件上传 + 运行时加载 + 沙箱隔离 | 第三方插件可上传安装运行 |
+
+### 8.2 旧 Portal 处理
+
+- 现有 4 个 portal(teacher / student / parent / admin)**保留并行运行**
+- 新 Shell 使用路由前缀 `/shell/*` 避免冲突
+- 用户通过入口链接逐步引导到新 Shell
+- 迁移完成后,4 个旧 portal 下线,portal-shell 路由改为 `/`
+
+### 8.3 回滚策略
+
+- 任意阶段失败,回滚到上一阶段
+- 旧 portal 始终可用,新 Shell 失败不影响现有用户
+- IAM 新增表与现有表无外键依赖,可独立回滚
+
+---
+
+## 9. 完整对接清单
+
+### 9.1 前端新建
+
+| 路径 | 职责 | 动作 |
+| ------------------------------------------------------- | ------------------------------------------------------------------------ | ---- |
+| `apps/portal-shell/` | Shell 宿主应用(单 Next.js App Router) | 新建 |
+| `apps/portal-shell/src/app/layout.tsx` | RootLayout,挂载 Providers | 新建 |
+| `apps/portal-shell/src/app/shell/[[...route]]/page.tsx` | Shell 入口(RSC Server Component),服务端拉取 Config + 预取 initialData | 新建 |
+| `apps/portal-shell/src/shell/ClientShell.tsx` | Client Component,接收 RSC props,渲染 Layout + Plugins | 新建 |
+| `apps/portal-shell/src/shell/Shell.tsx` | Layout 框架 + Slots | 新建 |
+| `apps/portal-shell/src/shell/Registry.ts` | 插件清单(plugin_id → dynamic import) | 新建 |
+| `apps/portal-shell/src/shell/PluginLoader.tsx` | dynamic import + 骨架屏 + ErrorBoundary | 新建 |
+| `apps/portal-shell/src/shell/PluginLifecycle.ts` | install/enable/disable 管理 | 新建 |
+| `apps/portal-shell/src/shell/SlotRenderer.tsx` | 按 Config 渲染插件列表 | 新建 |
+| `apps/portal-shell/src/shell/PropsMerger.ts` | 三层 props 合并 | 新建 |
+| `apps/portal-shell/src/shell/LayoutManager.tsx` | 5 种 Layout 模板渲染器 | 新建 |
+| `apps/portal-shell/src/shell/PluginStore.ts` | Zustand 全局状态(theme/locale/sidebar) | 新建 |
+| `apps/portal-shell/src/lib/iam-client.ts` | IAM gRPC 客户端(服务端调用) | 新建 |
+| `apps/portal-shell/src/lib/bff-clients.ts` | BFF GraphQL 客户端(按 role 路由) | 新建 |
+| `apps/portal-shell/src/lib/useWidgetQuery.ts` | 统一 BFF 查询 Hook(自动按 role 路由 + fallbackData) | 新建 |
+| `apps/portal-shell/src/lib/useWidgetMutation.ts` | 统一 BFF 变更 Hook | 新建 |
+| `apps/portal-shell/src/lib/usePluginConfig.ts` | SWR 静默刷新配置(revalidateOnFocus + refreshInterval) | 新建 |
+| `apps/portal-shell/src/providers/AuthProvider.tsx` | JWT + RBAC,提供 useAuth() | 新建 |
+| `apps/portal-shell/src/providers/ThemeI18nProvider.tsx` | 主题 + i18n | 新建 |
+| `apps/portal-shell/next.config.js` | Next.js 配置(standalone 模式) | 新建 |
+| `apps/portal-shell/Dockerfile` | 独立容器构建(单服务) | 新建 |
+| `apps/portal-shell/vitest.config.ts` | Shell 单元测试 | 新建 |
+
+### 9.2 内置插件源码
+
+| 路径 | 分类 | 动作 |
+| --------------------------------------------------------------- | ---------------------------- | ---- |
+| `apps/portal-shell/src/widgets/universal/grades-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/homework-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/schedule-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/attendance-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/exams-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/notifications-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/universal/announcements-widget/` | universal | 新建 |
+| `apps/portal-shell/src/widgets/sidebar/class-selector/` | sidebar | 新建 |
+| `apps/portal-shell/src/widgets/sidebar/child-selector/` | sidebar | 新建 |
+| `apps/portal-shell/src/widgets/sidebar/term-switcher/` | sidebar | 新建 |
+| `apps/portal-shell/src/widgets/topbar/global-search/` | topbar | 新建 |
+| `apps/portal-shell/src/widgets/topbar/notification-bell/` | topbar | 新建 |
+| `apps/portal-shell/src/widgets/topbar/user-menu/` | topbar | 新建 |
+| `apps/portal-shell/src/widgets/topbar/locale-switcher/` | topbar | 新建 |
+| `apps/portal-shell/src/widgets/teacher/lesson-plan-editor/` | teacher | 新建 |
+| `apps/portal-shell/src/widgets/teacher/question-bank/` | teacher | 新建 |
+| `apps/portal-shell/src/widgets/student/error-book/` | student | 新建 |
+| `apps/portal-shell/src/widgets/student/ai-tutor/` | student | 新建 |
+| `apps/portal-shell/src/widgets/parent/child-overview/` | parent | 新建 |
+| `apps/portal-shell/src/widgets/admin/user-management/` | admin | 新建 |
+| `apps/portal-shell/src/widgets/admin/rbac-manager/` | admin | 新建 |
+| `apps/portal-shell/src/widgets/admin/plugin-manager/` | admin(配置面板本身) | 新建 |
+| 每个 `*/plugin.manifest.ts` | 插件元数据 + propsSchema | 新建 |
+| 每个 `*/index.tsx` | 插件入口(接收 PluginProps) | 新建 |
+
+### 9.3 共享包扩展
+
+| 路径 | 变更 | 动作 |
+| ---------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------- |
+| `packages/shared-ts/src/contracts/plugin.ts` | 新增 PluginProps / PluginManifest 类型契约 | 新增导出 |
+| `packages/shared-ts/src/contracts/layout.ts` | 新增 LayoutTemplate / SlotConfig / PluginPlacement 类型 | 新增导出 |
+| `packages/shared-ts/src/contracts/plugin-store.ts` | Zustand 全局状态 schema(theme/locale/sidebar) | 新增导出 |
+| `packages/shared-ts/src/contracts/plugin-context.ts` | URL Search Params 上下文 schema(classId/childId/termId/view) | 新增导出 |
+| `packages/ui-components/src/` | 新增 PluginCard / PluginSkeleton / PluginErrorFallback / SlotPlaceholder / PropsConfigForm | 新增组件 |
+| `packages/ui-tokens/src/` | 无需变更(现有令牌足够) | 复用 |
+| `packages/hooks/src/` | 新增 usePluginConfig(SWR)/ usePluginStore(Zustand) | 新增 hooks |
+| `pnpm-workspace.yaml` | 新增 `apps/portal-shell` | 修改 |
+
+### 9.4 后端 IAM 扩展
+
+| 路径 | 变更 | 动作 |
+| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------- |
+| `services/iam/src/iam/iam.schema.ts` | 新增 plugin_registry / role_plugin_mapping / role_layout_default / layout_templates / user_layout_override / plugin_packages 表 | 新增 schema |
+| `services/iam/src/iam/iam.service.ts` | 新增 getPluginRegistry / getRolePluginMapping / getUserLayoutOverride / mergePluginConfig / mergeProps 方法 | 新增方法 |
+| `services/iam/src/iam/iam.grpc.controller.ts` | 新增 GetPluginConfig RPC(合并三层配置返回) | 新增 RPC |
+| `packages/shared-proto/proto/iam.proto` | 新增 PluginConfigRequest/Response、LayoutTemplate、PluginRegistryItem 等 message | 新增 proto |
+| `services/iam/scripts/seed-plugins.ts` | 初始化 5 个 Layout 模板 + 内置插件注册项 | 新建 seed |
+| `services/iam/src/iam/admin.controller.ts` | 新增 admin CRUD plugin_registry / role_plugin_mapping REST API | 新增 controller |
+| `services/iam/scripts/sync-builtin-plugins.ts` | 构建时从 portal-shell 的 manifest 同步到 plugin_registry 表 | 新建脚本 |
+
+### 9.5 BFF 层
+
+| 路径 | 变更 | 动作 |
+| ----------------------- | ---------------------------------------------------------------- | -------- |
+| `services/teacher-bff/` | 无需变更,插件直接调用现有 GraphQL | 复用 |
+| `services/student-bff/` | 无需变更 | 复用 |
+| `services/parent-bff/` | 无需变更 | 复用 |
+| 插件配置查询 | 由 Shell 直接调 IAM gRPC(不经 BFF),保持配置查询与业务查询分离 | 路由策略 |
+
+### 9.6 网关层
+
+| 路径 | 变更 | 动作 |
+| ------------------------------------------------ | -------------------------------------------------------------- | -------- |
+| `services/api-gateway/internal/proxy/proxy.go` | 新增 `/portal-shell` 路由(单服务) | 新增路由 |
+| `services/api-gateway/internal/config/config.go` | 新增 portal-shell 服务地址配置 | 新增配置 |
+| `services/push-gateway/` | 无需变更(配置刷新改用 SWR 客户端轮询,不依赖 WebSocket 推送) | 复用 |
+
+### 9.7 部署与基础设施
+
+| 路径 | 变更 | 动作 |
+| --------------------------------- | -------------------------------------------------------- | -------- |
+| `infra/docker-compose.deploy.yml` | 新增 portal-shell 服务(单服务,无独立 widget 容器) | 新增服务 |
+| `infra/port-allocation.md` | 新增 portal-shell 端口(4010) | 新增 |
+| `infra/init-sql/01-init.sql` | 新增 IAM 6 张表的 DDL | 新增 |
+| `apps/portal-shell/Dockerfile` | standalone 模式构建(单容器) | 新建 |
+| `.github/workflows/ci.yml` | 新增 portal-shell 构建作业(单作业,无 widget 独立构建) | 修改 |
+
+### 9.8 设计系统对接
+
+| 路径 | 变更 | 动作 |
+| ------------------------------------ | --------------------------------- | ----------- |
+| `docs/standards/ui-design-system.md` | 新增"插件视觉规范"章节 | 补充章节 |
+| `packages/ui-tokens/` | 无需变更 | 复用 |
+| `packages/ui-components/` | 新增 PluginCard 等组件 | 新增组件 |
+| `apps/portal-shell/src/styles/` | 复用现有 globals.css + tokens | 复用 |
+| 插件内部样式 | 必须使用 @edu/ui-tokens,禁硬编码 | ESLint 强制 |
+
+### 9.9 测试
+
+| 路径 | 内容 | 动作 |
+| -------------------------------------------- | --------------------------------------------------------------------------------- | ---- |
+| `apps/portal-shell/vitest.config.ts` | Shell 单元测试(Shell/ConfigProvider/Registry/PluginLoader/EventBus/PropsMerger) | 新建 |
+| `apps/portal-shell/src/widgets/*/__tests__/` | 每个插件单元测试 | 新建 |
+| `tests/e2e/portal-shell.spec.ts` | E2E:登录 → 加载 layout → 渲染插件 → 切换 layout | 新建 |
+| `tests/e2e/plugin-config.spec.ts` | E2E:admin 改配置 → 用户刷新生效 | 新建 |
+| `tests/visual/portal-shell.spec.ts` | 视觉回归:5 种 layout 截图对比 | 新建 |
+
+### 9.10 架构元数据
+
+| 路径 | 变更 | 动作 |
+| -------------------------------------------------- | -------------------------------------- | -------- |
+| `scripts/arch-scan/` | 扫描 `apps/portal-shell`,更新 arch.db | 扫描更新 |
+| `docs/architecture/004_architecture_impact_map.md` | 新增 portal-shell 模块章节 | 补充章节 |
+| `docs/troubleshooting/known-issues.md` | 新增 portal-shell 分区 | 补充分区 |
+| `apps/portal-shell/README.md` | Shell 架构文档 | 新建 |
+
+### 9.11 Proto 契约
+
+见 §6.2 `iam.proto` 新增 message 定义。
+
+### 9.12 现有 4 个 Portal 处理
+
+| 路径 | 处理 | 动作 |
+| ---------------------- | --------------------------------------------- | -------- |
+| `apps/teacher-portal/` | 保留并行运行,逐步迁移 | 保留 |
+| `apps/student-portal/` | 保留并行运行 | 保留 |
+| `apps/parent-portal/` | 保留并行运行 | 保留 |
+| `apps/admin-portal/` | 保留并行运行 | 保留 |
+| `apps/portal-shell/` | 新 Shell,路由前缀 `/shell/*` | 新建 |
+| 迁移完成后 | 4 个旧 portal 下线,portal-shell 路由改为 `/` | 最终切换 |
+
+---
+
+## 10. 风险与缓解
+
+| 风险 | 影响 | 缓解 |
+| ----------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 插件数量增长导致首屏 bundle 过大 | 首屏加载慢 | dynamic import 按需加载 + IntersectionObserver 滚动加载 + 首屏只加载可见 slot |
+| 单体架构插件间隐式耦合 | 维护困难 | ESLint 禁止跨 widgets 目录 import + 架构扫描检测违规 + 强制通过 URL/Zustand 共享状态(禁止直接 import 其他插件) |
+| 单角色专属功能(如备课画布)作为插件受 props 契约约束 | 复杂功能实现受限 | PluginProps 设计足够灵活(initialData + props 任意 JSON);复杂功能可在插件内部自行组织,仅对外暴露必要契约;跨插件状态通过 URL/Zustand 共享,不通过 props 注入 |
+| IAM 配置查询压力 | RSC 每次请求查询 6 张表 | 复用 IAM 现有 cacheFn(ttl=60s)+ Redis 缓存;RSC 侧使用 React cache() 对同一请求内的重复查询去重;配置变更后 SWR 客户端轮询自然刷新 |
+| 插件 props 三层合并逻辑复杂 | props 不一致 | PropsMerger 集中实现 + 单元测试覆盖;合并算法:深合并 + 数组覆盖 |
+| admin 配置面板表单自动渲染 | propsSchema 复杂时表单体验差 | 基于 JSON Schema 的表单库(react-jsonschema-form 或自研)+ 支持 uiSchema 自定义 |
+| 二期第三方插件沙箱隔离 | 安全风险 | 在单体应用中直接 script 注入第三方不信任代码等于交出主站最高权限(XSS、Token 窃取)。**MVP 不实现第三方插件**;二期必须采用以下方案之一:① **iframe + postMessage**(最安全,完全隔离 DOM/JS/CSS,通过 postMessage 通信,但样式/高度自适应复杂);② **QuickJS 隔离沙箱**(参考 Figma,在 Web Worker 中运行第三方 JS,通过 RPC 通信,性能好但实现复杂);③ **Shadow DOM + Proxy 限定 API 表面**(折中,样式隔离 + JS API 白名单代理,但安全性弱于 iframe)。**推荐 iframe + postMessage**,详见 §10.1 |
+
+### 10.1 第三方插件沙箱方案(二期规划)
+
+针对 v2.1 §5.4 中"二期第三方插件 script 注入加载"的风险,详细规划沙箱方案:
+
+#### 方案对比
+
+| 方案 | 隔离强度 | 性能 | 实现复杂度 | 通信方式 | 推荐度 |
+| -------------------- | -------------- | ---------------------- | ---------- | ------------------ | ---------------------- |
+| iframe + postMessage | 强(完全隔离) | 中(每个插件独立进程) | 中 | postMessage 序列化 | ✅ 推荐 |
+| QuickJS Web Worker | 强(JS 隔离) | 好(Worker 并行) | 高 | RPC 代理 | △ 备选 |
+| Shadow DOM + Proxy | 中(样式隔离) | 好(同进程) | 低 | 直接调用 + Proxy | ✗ 不推荐(安全性不足) |
+| 动态 script 注入 | 弱(无隔离) | 好(同进程) | 低 | window 挂载 | ✗ 弃用 |
+
+#### 推荐方案:iframe + postMessage
+
+```typescript
+// 二期:第三方插件通过 iframe 加载
+// 1. 第三方插件 ZIP 包含 manifest.json + index.html + JS bundle
+// 2. 后端解压到 /plugins/{pluginId}/ 静态服务
+// 3. Shell 通过 iframe 加载 /plugins/{pluginId}/index.html
+// 4. 通过 postMessage 通信,限定 API 表面
+
+
+
+// Shell 侧 API 表面(通过 postMessage 暴露给第三方插件)
+const SANDBOXED_API = {
+ 'getPluginContext': () => ({ role, userId, classId, termId }),
+ 'queryBFF': (query, vars) => bffClient.query(query, vars), // 代理 BFF 请求
+ 'mutateBFF': (mutation, vars) => bffClient.mutate(mutation, vars),
+ 'navigate': (url) => router.push(url), // 代理路由
+ 'setHeight': (height) => resizeIframe(height), // 高度自适应
+ // 注意:禁止暴露 localStorage、cookies、window 对象
+};
+```
+
+**安全约束:**
+
+- iframe `sandbox="allow-scripts"` 禁止 same-origin,第三方无法访问主站 cookie/localStorage
+- 所有 API 通过 postMessage 代理,白名单机制限定可调用 API
+- BFF 请求由 Shell 代理(第三方不直接持有 JWT),Shell 可做权限校验
+- CSP 头限制第三方插件只能加载自身域名的资源
+
+---
+
+## 11. 验收标准
+
+### 11.1 功能验收
+
+- [ ] 5 种 Layout 模板可切换,slot 正确渲染
+- [ ] 7 张 universal 插件 + sidebar/topbar 插件 + 单角色专属插件加载正常
+- [ ] admin 可通过 REST API 配置角色插件集合
+- [ ] admin 可配置角色默认 Layout 模板
+- [ ] admin 可配置插件默认 props(基于 propsSchema 自动渲染表单)
+- [ ] admin 可查看/重置用户自定义布局
+- [ ] 用户可切换 Layout 模板(在可用集内)
+- [ ] 用户可隐藏/显示插件
+- [ ] 用户可调整插件 props(在 propsSchema 允许范围内)
+- [ ] admin 改配置 → SWR 静默刷新检测到变化 → Toast 提示用户刷新生效
+- [ ] 插件加载失败显示错误兜底,不影响其他插件
+- [ ] 跨插件状态共享正常(class-selector 切换班级 → grades-widget 自动响应)
+- [ ] 插件 props 三层合并正确
+- [ ] RSC 服务端预取正常:首屏 HTML 直出 Config + initialData
+- [ ] useWidgetQuery 自动按 role 路由到对应 BFF
+- [ ] URL Search Params 可分享、可前进后退
+
+### 11.2 非功能验收
+
+- [ ] `pnpm run lint` + `pnpm run typecheck` 零错误
+- [ ] Shell 首屏 LCP < 2s(SSR 骨架 + client 水合 + 按需加载)
+- [ ] 插件加载耗时 < 500ms(dynamic import 缓存命中后)
+- [ ] 单元测试覆盖率 ≥ 80%
+- [ ] E2E 测试通过
+- [ ] 视觉回归测试通过(5 种 layout 截图)
+- [ ] 所有插件遵守设计令牌(ESLint 强制)
+- [ ] arch.db 更新,004 文档同步
+
+---
+
+## 12. 参考资料
+
+- [CICD widget-configs.ts](file:///e:/Desktop/CICD/src/modules/dashboard/config/widget-configs.ts) - 单应用 widget 配置驱动参考
+- [0010 架构蓝图](../../architecture/0010_architecture.md) §4 微前端 - 统一 UI 库
+- [UI 设计系统](../../standards/ui-design-system.md) - 纸感编辑器设计风格
+- [项目规则](../../.trae/rules/project_rules.md) - DDD 分层、设计令牌、多 AI 协作规范
+- Halo 插件机制 - JAR 分发 · 扩展点注入 · 完整生命周期 · 插件市场参考
+- [Next.js Dynamic Import](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading) - dynamic import + ssr:false
+- [Next.js Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) - RSC 服务端预取参考
+- [Zustand](https://github.com/pmndrs/zustand) - React 全局状态管理(替代 EventBus)
+- [SWR](https://swr.vercel.app/) - 数据请求 + 静默后台刷新(替代 WebSocket 推送)
+- [Figma QuickJS 沙箱](https://www.figma.com/blog/an-update-on-plugin-security/) - 第三方插件隔离参考
+
+---
+
+## 13. 版本演进对比
+
+| 维度 | v1.0(MF 微前端) | v2.0(Modular Monolith) | v2.1(React 单体哲学优化) |
+| -------------- | --------------------------- | --------------------------- | ------------------------------------- |
+| 架构 | MF 2.0 + 7 独立 widget 容器 | 单 Next.js + dynamic import | 单 Next.js App Router + RSC |
+| 部署 | 8+ 个容器 | 1 个容器 | 1 个容器 |
+| 加载 | 运行时 MF 远程加载 | 编译时打包 + dynamic import | RSC 服务端预取 + dynamic import |
+| 首屏 | MF 远程加载慢 | CSR 瀑布流(4 层串行) | RSC 直出 Config + initialData(秒开) |
+| 跨插件通信 | EventBus 发布订阅 | EventBus 发布订阅 | URL Search Params + Zustand |
+| 配置刷新 | Kafka + WebSocket 推送 | Kafka + WebSocket 推送 | SWR 静默刷新(revalidateOnFocus) |
+| BFF 请求 | 插件自行判断 role 路由 | 插件自行判断 role 路由 | useWidgetQuery 统一 Hook 自动路由 |
+| 第三方插件沙箱 | script 注入(不安全) | script 注入(不安全) | iframe + postMessage(二期) |
+| 复杂度 | 高 | 低 | 最低 |
+| 旧 portal 迁移 | 并行运行 | 并行运行 | 并行运行 |