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