diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index 1affdfc..f03a39a 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1,9 +1,9 @@ # 架构影响地图(微服务版) -> 版本:2.0 -> 日期:2026-07-14 -> 状态:实施状态同步(基于代码现状 + arch.db 校准) -> 适用范围:Edu 微服务架构(DDD + EDA + CQRS) +> 版本:2.1 +> 日期:2026-07-15 +> 状态:实施状态同步(v2.1 架构重设计已落地,M0-M10 全部完成) +> 适用范围:Edu 微服务架构(DDD + EDA + CQRS + Apollo Federation) > 关联文档: > > - [理想蓝图](./0010_architecture.md) @@ -11,8 +11,12 @@ > - [路线图](./roadmap/README.md) > - [端口分配唯一源](../../infra/port-allocation.md) > - [P6 附录](./004-p6-addendum.md) +> - [v2.1 重设计 spec](../superpowers/specs/2026-07-14-architecture-v2-redesign-design.md) +> - [Portal Shell v2.1 spec](../superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md) -> **v2.0 变更摘要**:基于代码现状(截至 2026-07-14)全面校准服务清单、模块边界、依赖关系、Kafka topic、可观测性栈、BFF 实现细节;新增 §15 实施状态索引。 +> **v2.1 变更摘要**(2026-07-15):完成 v2 架构重设计的全部阶段(M0-M10)。核心变更:(1) **Apollo Federation 联邦**:3 个手写 BFF(teacher-bff / student-bff / parent-bff)下线,由 apollo-router :3000 替代,7 个业务服务暴露 GraphQL 子图(@key/@requires/@extends 指令);(2) **DataScope @requires + ScopeToken**:iam 计算 visibleClassIds 存入 Redis Set,生成短 token 经 @requires 传递,core-edu 接收 token 后从 Redis sMembers 获取实际 ID 列表(ADR-024/041);(3) **Temporal 严格边界**:仅限 AI 耗时工作流(生成大纲→知识点→题目→组装试卷)+ Saga 分布式事务补偿(购买插件:扣积分+授权),CRUD 短事务绝对禁止(ADR-030);(4) **SSE 优先 + Redis Pub/Sub 背板**:push-gateway 改名 realtime-gateway,SSE 默认推送,WS 仅监考;msg worker 消费 Kafka → 发 Redis Pub/Sub → realtime-gateway 订阅推送,边缘网关不直接挂 Kafka(ADR-029/040);(5) **CDC + Outbox 结合**:废弃 OutboxPublisher 轮询线程,改由 Debezium 监听 binlog 自动投递 Outbox 表到 Kafka(Transaction Log Tailing,ADR-032);(6) **portal-shell 单容器**:4 个旧 portal(teacher-portal / student-portal / parent-portal / admin-portal)下线,由 apps/portal-shell :4010 替代(Modular Monolith + Micro-kernel + RSC 预取 + Zustand + SWR);(7) **iam 拆分 config-service**:插件配置 + 布局 + 用户偏好从 iam 拆出为 config-service :3011/50059(ADR-026);(8) **content CQRS 改造**:MySQL 写 + Neo4j/ES 读投影 + Eager Invalidation + 乐观锁版本号(ADR-027/038/039);(9) **ai 无状态化**:WorkflowStateStore 改为 Redis-only(TTL 1h,ADR-028);(10) **外部 GraphQL + 内部 gRPC 边界**:前端必须经 Apollo Router 调子图,后端互调走 gRPC(ADR-037);(11) **Router-Authorization 信任凭证**:Apollo Router 向子图请求注入 `router-authorization` header,子图 RouterAuthGuard 校验(ADR-036);(12) **DataLoader 强制**:所有子图 @key 解析器必须用 DataLoader 批量加载(ADR-035)。 +> +> **v2.0 变更摘要**(2026-07-14):基于代码现状(截至 2026-07-14)全面校准服务清单、模块边界、依赖关系、Kafka topic、可观测性栈、BFF 实现细节;新增 §15 实施状态索引。 > > **arch.db 二次校验(2026-07-14)**:运行 `pnpm run arch:scan` 重建 arch.db(22 模块 / 4715 符号 / 475 契约),据此修正 §11.1 proto 统计(8 文件 / 23 service / 305 message / 139 RPC)、§11.2 proto 包名(`next_edu_cloud..v1`)、§15.4 core-edu gRPC(9 service / 43 RPC)、§15.6 msg gRPC(17 RPC)、§15.7 data-ana gRPC(18 RPC)、§15.10 student-bff 模块(8)、§15.11 parent-bff 模块(6)。 @@ -35,6 +39,7 @@ 13. [ADR 记录](#13-adr-记录) 14. [附录:6 阶段路线图](#14-附录6-阶段路线图) 15. [实施状态索引(v2.0 新增)](#15-实施状态索引v20-新增) +16. [v2.1 实施状态索引(v2.1 新增)](#16-v21-实施状态索引v21-新增) --- @@ -42,9 +47,9 @@ ### 1.1a 技术分层视角(系统边界) -> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → BFF → 业务服务 → 总线 → 数据)。 -> 用户层按"使用场景域"标注,BFF 层按场景域分(不是按角色分)。业务领域视角见 [1.1b](#11b-业务领域视角)。 -> 端口标注见 [infra/port-allocation.md](../../infra/port-allocation.md)(唯一源)。 +> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → Apollo Router → 业务服务 → 总线 → 数据)。 +> v2.1 关键变化:4 个 MF portal 由 portal-shell 单容器替代;3 个 BFF 由 apollo-router 联邦替代;push-gateway 改名 realtime-gateway(SSE 优先);新增 config-service;新增 Temporal;Outbox 轮询废弃,改由 Debezium 监听 binlog。 +> 业务领域视角见 [1.1b](#11b-业务领域视角)。端口标注见 [infra/port-allocation.md](../../infra/port-allocation.md)(唯一源)。 ```mermaid graph TB @@ -55,84 +60,71 @@ graph TB Admin["管理场景域用户
(系统管理员 / 校管理员)"] end - 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"] + subgraph MFE["微前端层(v2.1:单容器 Modular Monolith)
Next.js 15 App Router + RSC + Zustand + SWR"] + PortalShell["portal-shell :4010
Shell 宿主 + 插件化仪表盘
替代 4 个旧 portal"] end - subgraph PortalShell["Portal Shell 层(规划中)
详见 [0020 Portal Shell 架构](./0020_portal_shell_architecture.md)"] - PortalShellApp["portal-shell :4010
Modular Monolith + Micro-kernel
取代 MF 微前端,单服务部署"] - end - - subgraph Gateway["网关层(Go 1.25 / Gin)"] + subgraph Gateway["边缘网关层(Go 1.25 / Gin)"] APIGateway["api-gateway :8080
JWT RS256 + JWKS + 限流 + 熔断"] - PushGateway["push-gateway :8081
WebSocket + Redis Pub/Sub
(gRPC 豁免,HTTP /internal/*)"] + RealtimeGw["realtime-gateway :8081
SSE 优先 + WS 仅监考
Redis Pub/Sub 订阅(不直挂 Kafka)"] 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 订阅"] + subgraph Federation["GraphQL 联邦层(v2.1 新增)
Apollo Router(Rust 官方二进制)"] + ApolloRouter["apollo-router :3000
自动查询计划 + @requires DataScope
替代 3 个手写 BFF"] end - subgraph Services["业务微服务(NestJS + FastAPI)"] - 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"] + subgraph Services["业务微服务(NestJS + FastAPI,全部暴露 GraphQL 子图)"] + IAM["iam :3002 / gRPC :50052
认证 + RBAC + JWKS + DataScope(拆分后)"] + ConfigSvc["config-service :3011 / gRPC :50059
插件配置 + 布局 + 用户偏好(v2.1 新增)"] + CoreEdu["core-edu :3004 / gRPC :50053
教学核心 + 组织(含 DataScope @requires)"] + Content["content :3005 / gRPC :50054
CQRS:MySQL 写 + Neo4j/ES 读投影"] + DataAna["data-ana :3006 / gRPC :50055
FastAPI + CDC consumer + ClickHouse"] + Msg["msg :3007 / gRPC :50056
4 模块 + 4 渠道 + msg worker(Redis Pub/Sub)"] + AI["ai :3008 / gRPC :50058
FastAPI 无状态化 + Temporal Worker"] + end + + subgraph Workflow["工作流层(v2.1 新增)"] + Temporal["Temporal Server :7233
AI 工作流 + Saga(严格边界)
PostgreSQL 持久化 + UI :8085"] end subgraph Bus["事件总线"] Kafka[("Kafka
双 listener 29092/9092")] - Debezium["Debezium Connect 2.7
CDC MySQL → Kafka"] + Debezium["Debezium Connect 2.7
监听 outbox 表 binlog → Kafka
(Transaction Log Tailing)"] end subgraph Data["数据层"] MySQL[("MySQL 8.0
每服务独占 schema")] - Redis[("Redis 7
缓存/会话/Pub/Sub")] + Redis[("Redis 7
缓存/会话/Pub/Sub/ScopeToken/ai workflow")] ClickHouse[("ClickHouse 24.3
读模型宽表")] - Neo4j[("Neo4j 5.20
知识图谱")] - ES[("Elasticsearch 8.13
题库检索")] + Neo4j[("Neo4j 5.20
知识图谱(content 读投影)")] + ES[("Elasticsearch 8.13
题库检索(content 读投影)")] + TemporalPG[("PostgreSQL
Temporal 持久化")] end - Teacher --> TeacherPortal - Student --> StudentPortal - Parent --> ParentPortal - Admin --> AdminPortal + Teacher --> PortalShell + Student --> PortalShell + Parent --> PortalShell + Admin --> PortalShell - TeacherPortal --> APIGateway - StudentPortal --> APIGateway - ParentPortal --> APIGateway - AdminPortal --> APIGateway - TeacherPortal -.MF Remote 加载.-> ParentPortal - TeacherPortal -.WS 推送.-> PushGateway - StudentPortal -.WS 推送.-> PushGateway - ParentPortal -.WS 推送.-> PushGateway + PortalShell --> APIGateway + PortalShell -.SSE 推送.-> RealtimeGw - APIGateway --> TeacherBFF - APIGateway --> StudentBFF - APIGateway --> ParentBFF - APIGateway -.admin graphql 透传.-> TeacherBFF - PushGateway -.HTTP /internal/push.-> Msg + APIGateway --> ApolloRouter + APIGateway -.REST 透传.-> IAM + APIGateway -.REST 透传.-> Msg + RealtimeGw -.Redis Pub/Sub 订阅 user:userId:notify.-> 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 + ApolloRouter -->|子图查询| IAM + ApolloRouter -->|子图查询| ConfigSvc + ApolloRouter -->|子图查询| CoreEdu + ApolloRouter -->|子图查询| Content + ApolloRouter -->|子图查询| Msg + ApolloRouter -->|子图查询| DataAna + ApolloRouter -->|子图查询| AI + ApolloRouter -.@requires ScopeToken.-> IAM + CoreEdu -.接收 scopeToken sMembers.-> CoreEdu + + AI -.Temporal Worker.-> Temporal CoreEdu <--> Kafka Content <--> Kafka @@ -140,39 +132,47 @@ graph TB Msg <--> Kafka IAM <--> Kafka AI <--> Kafka + ConfigSvc <--> Kafka MySQL --> Debezium Debezium --> Kafka IAM --> MySQL + ConfigSvc --> MySQL CoreEdu --> MySQL Content --> MySQL Msg --> MySQL - Content --> Neo4j - Content --> ES + Content -.Kafka 投影器.-> Neo4j + Content -.Kafka 投影器.-> ES DataAna --> ClickHouse - AI --> ES + Temporal --> TemporalPG IAM --> Redis CoreEdu --> Redis - TeacherBFF --> Redis - StudentBFF --> Redis - ParentBFF --> Redis + ConfigSvc --> Redis + Content --> Redis + Msg --> Redis DataAna --> Redis AI --> Redis - PushGateway --> Redis + RealtimeGw --> Redis + PortalShell -.SWR 客户端轮询.-> ConfigSvc ``` ### 1.1b 业务领域视角 -> 本图按 **DDD 限界上下文**展示 6 个业务领域及其依赖关系。同一服务可横跨多个领域(如 core-edu 同时承载"教学组织"与"教学核心")。 +> 本图按 **DDD 限界上下文**展示 7 个业务领域及其依赖关系(v2.1 新增 D7 配置域,从 iam 拆出)。同一服务可横跨多个领域(如 core-edu 同时承载"教学组织"与"教学核心")。 > 技术分层视角见 [1.1a](#11a-技术分层视角系统边界)。 ```mermaid graph TB - subgraph D1["D1 身份认证领域(iam 服务)"] + subgraph D1["D1 身份认证领域(iam 服务,v2.1 拆分后)"] IAM[iam 服务] - IAM_M["5 Controller: Iam / Rbac / Audit / Jwks / IamGrpc
users / roles / permissions / refresh_tokens / sessions
totp / audit_logs / navigation_config / route_permission"] + IAM_M["认证 + RBAC + JWKS + 审计 + DataScope
users / roles / permissions / refresh_tokens / sessions
totp / audit_logs / user_school_role
子图暴露 visibleClassIds / visibleStudentIds(@requires)"] + end + + subgraph D7["D7 配置域(config-service 服务,v2.1 新增)"] + CONFIG[config-service 服务] + CONFIG_M["插件配置 + 布局 + 用户偏好
plugin_registry / role_plugin_mapping / role_layout_default
layout_templates / user_layout_override / plugin_packages
三层合并:系统默认 < 角色默认 < 用户调整"] end subgraph D2["D2 教学组织领域(core-edu 服务)"] @@ -182,30 +182,32 @@ graph TB 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)"] + TEACH_M["exams / exam-extensions / homework / grades
attendance / dashboard / admin / iam-consumer
(含 state machine + datascope-injector + @requires ScopeToken)"] end - subgraph D4["D4 内容资源领域(content 服务)"] + subgraph D4["D4 内容资源领域(content 服务,v2.1 CQRS 改造)"] CONTENT["content 服务"] - CONTENT_M["7 模块: textbooks / chapters / knowledge-points
questions / electives / lesson-plans / course-plans
Neo4j 知识图谱 + ES 题库检索 + sync worker"] + CONTENT_M["7 模块: textbooks / chapters / knowledge-points
questions / electives / lesson-plans / course-plans
MySQL 写 + Outbox → Kafka 投影器 → Neo4j/ES 读
Eager Invalidation + 乐观锁版本号"] end subgraph D5["D5 沟通通知领域(msg 服务)"] MSG["msg 服务"] - MSG_M["4 模块: notifications / preferences / templates / announcements
4 渠道: email / sms / push / in-app
Kafka 消费 16 类事件 + Idempotency Guard"] + MSG_M["4 模块: notifications / preferences / templates / announcements
4 渠道: email / sms / push / in-app
msg worker 消费 Kafka → Redis Pub/Sub → realtime-gateway
(v2.1:不再让边缘网关直挂 Kafka)"] end - subgraph D6["D6 智能洞察领域(data-ana + ai 服务)"] + subgraph D6["D6 智能洞察领域(data-ana + ai 服务,v2.1 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"] + AI_M["FastAPI HTTP /v1/ai + gRPC 50058
无状态 LLM 推理引擎(WorkflowStateStore → Redis TTL 1h)
Temporal Worker:AI 耗时工作流 + Saga
chat / question / expression / lesson-plan / report"] end IAM --> ORG IAM --> TEACH IAM --> CONTENT IAM --> MSG + IAM -.DataScope @requires.-> TEACH + CONFIG -.插件配置.-> IAM ORG --> TEACH TEACH --> CONTENT TEACH --> MSG @@ -214,6 +216,7 @@ graph TB AI -.gRPC.-> CONTENT AI -.gRPC.-> DATA AI -.gRPC.-> IAM + AI -.Temporal Worker.-> D6 ``` **双图并存说明**: @@ -224,34 +227,38 @@ graph TB ### 1.2 服务清单 -> 端口、阶段、实施状态基于代码现状(2026-07-14)。✅ = 已落地,🚧 = 部分落地,⏳ = 规划中。 +> 端口、阶段、实施状态基于 v2.1 代码现状(2026-07-15)。✅ = 已落地,🚧 = 部分落地,⏳ = 规划中,⛔ = v2.1 已下线。 +> v2.1 变更:(1) 新增 apollo-router / config-service / portal-shell;(2) push-gateway 改名 realtime-gateway;(3) 下线 3 个 BFF + 4 个旧 portal。 -| 类别 | 服务名 | 语言/框架 | 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 | ✅ | +| 类别 | 服务名 | 语言/框架 | HTTP 端口 | gRPC 端口 | GraphQL | 限界上下文 | 业务领域 | 阶段 | 状态 | v2.1 变更说明 | +| ------ | ---------------- | ------------------------------- | --------- | --------- | -------- | -------------------------------------------------------------------------- | ----------------------------- | --------- | ---- | --------------------------------------- | +| 边缘 | api-gateway | Go 1.25 (Gin) | 8080 | — | — | 网关(路由+鉴权+限流+熔断+CORS) | — | P1 | ✅ | 移除 BFF 代理路由,新增 apollo-router | +| 边缘 | realtime-gateway | Go 1.25 (Gin) | 8081 | — | — | 推送(SSE 优先 + WS 仅监考 + Redis Pub/Sub 订阅) | — | P5/M7 | ✅ | 改名(原 push-gateway),SSE 优先 | +| 联邦 | apollo-router | Rust (Apollo Router 官方二进制) | 3000 | — | ✅ Super | GraphQL 联邦(自动查询计划 + @requires DataScope) | — | v2.1/M2 | ✅ | 新增,替代 3 个 BFF 聚合 | +| 业务 | iam | TS (NestJS) | 3002 | 50052 | ✅ 子图 | 身份认证 + RBAC + JWKS + DataScope(拆分后) | **D1 身份认证** | P2/M3 | ✅ | 插件配置移出到 config-service | +| 业务 | config-service | TS (NestJS) | 3011 | 50059 | ✅ 子图 | 插件配置 + 布局 + 用户偏好 | **D7 配置域** | v2.1/M3 | ✅ | 新增,从 iam 拆出(ADR-026) | +| 业务 | core-edu | TS (NestJS) | 3004 | 50053 | ✅ 子图 | 教学核心(含原 classes)+ DataScope @requires | **D2 教学组织 + D3 教学核心** | P3 | ✅ | 接收 ScopeToken sMembers | +| 业务 | content | TS (NestJS) | 3005 | 50054 | ✅ 子图 | 内容资源(CQRS:MySQL 写 + Neo4j/ES 读投影) | **D4 内容资源** | P4/M5 | ✅ | CQRS 改造 + Eager Invalidation + 乐观锁 | +| 业务 | msg | TS (NestJS) | 3007 | 50056 | ✅ 子图 | 消息通知(含 msg worker → Redis Pub/Sub 背板) | **D5 沟通通知** | P5/M7 | ✅ | 新增 msg worker | +| 业务 | data-ana | Python (FastAPI) | 3006 | 50055 | ✅ 子图 | 数据分析 | **D6 智能洞察** | P4 | ✅ | 保留 | +| 业务 | ai | Python (FastAPI) | 3008 | 50058 | ✅ 子图 | AI 网关(无状态化 + Temporal Worker) | **D6 智能洞察** | P5/M6 | ✅ | WorkflowStateStore → Redis-only | +| 工作流 | temporal | Temporal 1.23 | — | 7233 | — | AI 工作流 + Saga(严格边界,CRUD 短事务禁用) | — | v2.1/M6.5 | ✅ | 新增(ADR-030),UI :8085,PG :5433 | +| 微前端 | portal-shell | TS (Next.js 15 App Router) | 4010 | — | — | 教师端 Shell(Modular Monolith + Micro-kernel) | 教学场景域 | v2.1/M8 | ✅ | 新增,替代 4 个旧 portal | +| 共享包 | shared-proto | protobuf + buf v2 | — | — | — | 跨语言契约(8 proto 文件 + GraphQL 生成器) | — | P1 | ✅ | M0 新增 proto → GraphQL 生成器 | +| 共享包 | shared-ts | TS | — | — | — | TS 共享(outbox / dataloader / RouterAuthGuard) | — | P1 | ✅ | M1 新增 dataloader/guards | +| 共享包 | 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 | ✅ | 新增 PluginCard / PluginSkeleton 等 | +| 共享包 | ui-tokens | TS + CSS | — | — | — | 设计令牌(primitive / semantic-light/dark / tailwind-theme) | — | P2 | ✅ | 保留 | +> **v2.1 已下线服务(⛔)**: +> +> - teacher-bff :3003 / student-bff :3009 / parent-bff :3010 → 由 apollo-router 替代(M9) +> - teacher-portal :4000 / student-portal :4001 / parent-portal :4002 / admin-portal :4003 → 由 portal-shell 替代(M10) +> - push-gateway 改名为 realtime-gateway(端口不变) +> > **历史服务**:classes(端口 3001)已合并入 core-edu(C1 裁决),目录保留作历史参考,不再构建部署。 ### 1.3 CICD → Edu 模块映射 @@ -275,53 +282,68 @@ graph TB ### 2.1 多语言技术栈矩阵 -| 层级 | 语言 | 框架/版本 | 用途 | -| -------- | --------------- | ------------------------------------------------------- | -------------------------------------- | -| 网关层 | 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 | +| 层级 | 语言 | 框架/版本 | 用途 | +| -------- | --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ | +| 网关层 | Go 1.25 | Gin + otelgin + prometheus | api-gateway、realtime-gateway | +| 联邦层 | Rust | Apollo Router 官方二进制 | apollo-router(@key/@requires/@extends 指令 + 自动查询计划) | +| 业务服务 | TypeScript 5.6+ | NestJS 10 + Drizzle ORM + @nestjs/graphql + DataLoader | iam、config-service、core-edu、content、msg | +| 分析/AI | Python 3.12 | FastAPI + structlog + prometheus-client + Strawberry | data-ana、ai(ai 含 Temporal Worker) | +| 工作流 | Go / TS | Temporal 1.23 SDK | temporal server(仅 AI 耗时工作流 + Saga,CRUD 短事务禁用) | +| 微前端 | TypeScript 5.6+ | Next.js 15 App Router + RSC + Zustand + SWR + Tailwind + shadcn 风格 | portal-shell(单容器 Modular Monolith + Micro-kernel) | +| 契约 | protobuf | buf v2(FILE 级 breaking)+ proto → GraphQL 生成器 | 跨语言契约定义与生成(M0 工具链扩展 GraphQL 输出) | +| 包管理 | — | pnpm 11 / go.work / uv workspace | 多语言 monorepo | + +> **v2.1 变更**: +> +> - 新增 Apollo Federation 2(@key/@requires/@extends 指令)+ Apollo Router(Rust,官方二进制) +> - 新增 Temporal 1.23(仅 AI 工作流 + Saga,ADR-030) +> - 新增 SSE(Server-Sent Events,替代 WebSocket 单向推送场景) +> - 新增 Debezium Connect Outbox connector(Transaction Log Tailing,废弃轮询线程) +> - 新增 Zustand + SWR(portal-shell 状态管理 + 静默刷新) +> - 新增 Strawberry(Python GraphQL 子图库)+ @nestjs/graphql(TS GraphQL 子图库) +> - **下线** Module Federation(@module-federation/nextjs-mf) +> - **下线** 3 个 BFF 的 GraphQL Yoga +> - **下线** WebSocket 单向推送场景(仅保留监考双向场景) ### 2.2 多存储矩阵 -| 存储 | 版本 | 用途 | 使用服务 | -| ------------- | -------- | --------------------------------------- | ------------------------------------------------------------------------------- | -| 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 | +| 存储 | 版本 | 用途 | 使用服务 | +| ------------- | -------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- | +| MySQL | 8.0 | 写模型主库(每服务独占 schema) | iam、config-service、core-edu、content、msg | +| Redis | 7-alpine | 缓存/会话/限流/分布式锁/Pub/Sub/ScopeToken/ai workflow 状态 | iam、config-service、core-edu、content、msg、data-ana、ai、realtime-gateway | +| ClickHouse | 24.3 | 读模型宽表、分析聚合 | data-ana | +| Neo4j | 5.20 | 知识图谱(content 读模型投影,由 neo4j-projector 写入) | content(读) | +| Elasticsearch | 8.13 | 题库检索 / 消息检索(读模型投影,由 es-projector 写入) | content、msg(读) | +| PostgreSQL | 16 | Temporal 持久化存储(仅 Temporal Server,不与业务库混用) | temporal | ### 2.3 基础设施矩阵 -| 组件 | 版本 | 用途 | -| ---------------- | ---------------- | -------------------------------------------- | -| 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 | 密钥管理 | +| 组件 | 版本 | 用途 | +| ---------------- | ---------------- | --------------------------------------------------------------------- | +| Kafka | cp-kafka 7.6 | 事件总线,领域事件异步通信(双 listener) | +| Zookeeper | cp-zookeeper 7.6 | Kafka 协调 | +| Debezium Connect | 2.7 | CDC + Outbox Transaction Log Tailing(监听 outbox 表 binlog → Kafka) | +| Apollo Router | 最新稳定版 | GraphQL 联邦 Supergraph 入口(Rust 官方二进制,:3000) | +| Temporal | 1.23 | AI 工作流 + Saga 编排(Server :7233 + UI :8085 + PG :5433) | +| 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 指标 | +| Vault | ⏳ P6 | 密钥管理 | -> **v2.0 修正**:Trace 存储实际使用 **Jaeger all-in-one**(OTLP 接收),原 v1.0 文档误写为 Tempo。当前未引入 Temporal,长流程编排暂由 ai 服务内 WorkflowStateStore(Redis)实现备课工作流。 +> **v2.1 修正**:Temporal 已落地(M6.5,ADR-030),严格立规矩——仅限 AI 耗时工作流 + Saga 分布式事务补偿,CRUD 短事务绝对禁止。Trace 存储使用 **Jaeger all-in-one**(OTLP 接收),原 v1.0 文档误写为 Tempo。 --- ## 3. 分层架构 -### 3.1 六层架构 +### 3.1 六层架构(v2.1 修订) ```mermaid graph TB @@ -330,58 +352,65 @@ graph TB Mobile[移动端] end - subgraph L2["L2 微前端层"] - MFE[Module Federation
4 端门户 Next.js 15] + subgraph L2["L2 微前端层(v2.1:单容器)"] + MFE[portal-shell :4010
Next.js 15 App Router + RSC 预取
Modular Monolith + Micro-kernel] end - subgraph L3["L3 网关层(Go 1.25 / Gin)"] - GW[API Gateway :8080
HTTP 反向代理 + JWT RS256 + 限流 + 熔断] - Push[Push Gateway :8081
WebSocket + Redis Pub/Sub + Kafka 消费] + subgraph L3["L3 边缘网关层(Go 1.25 / Gin)"] + GW[api-gateway :8080
HTTP 反向代理 + JWT RS256 + 限流 + 熔断] + RTG[realtime-gateway :8081
SSE 优先 + WS 仅监考
Redis Pub/Sub 订阅(不直挂 Kafka)] end - subgraph L4["L4 BFF 聚合层(NestJS + GraphQL Yoga)"] - BFF[3 个 BFF
聚合 + DataLoader + 熔断(opossum)] + subgraph L4["L4 GraphQL 联邦层(v2.1 新增,替代 BFF)"] + ROUTER[apollo-router :3000
Apollo Federation 2
自动查询计划 + @requires DataScope] end - subgraph L5["L5 业务微服务层"] - SVC[6 业务服务
NestJS + FastAPI
DDD + CQRS + Outbox] + subgraph L5["L5 业务微服务层(全部暴露 GraphQL 子图)"] + SVC[7 业务服务
iam / config-service / core-edu / content / msg / data-ana / ai
DDD + CQRS + Outbox + DataLoader + RouterAuthGuard] + TEMPORAL_SVC[Temporal Worker
仅 ai 服务,AI 工作流 + Saga] end subgraph L6["L6 数据与总线层"] - DB[(MySQL / ClickHouse / Neo4j / ES)] + DB[(MySQL / ClickHouse / Neo4j / ES / PostgreSQL)] REDIS[(Redis 7)] - KAFKA[(Kafka + Debezium CDC)] - TEMPORAL["Temporal
⏳ 规划中"] + KAFKA[(Kafka + Debezium Outbox Transaction Log Tailing)] + TEMPORAL_DB[(Temporal PostgreSQL :5433)] end Browser --> MFE Mobile --> MFE MFE --> GW - MFE -.WS 推送.-> Push - GW -- HTTP 代理 --> BFF - GW -- HTTP 代理 --> SVC - BFF -- gRPC --> SVC + MFE -.SSE 推送.-> RTG + GW -- HTTP 代理(REST) --> SVC + GW -- HTTP 代理(GraphQL) --> ROUTER + ROUTER -- 子图查询(@requires ScopeToken) --> SVC + SVC -- gRPC(内部 RPC,禁止 GraphQL 子图互调) --> SVC SVC --> DB SVC --> REDIS - BFF --> REDIS + ROUTER --> REDIS SVC <--> KAFKA - SVC -.规划中.-> TEMPORAL - Push --> REDIS - Push -.Kafka 消费.-> KAFKA + RTG -.Redis Pub/Sub 订阅.-> REDIS + SVC -.Outbox 表 + Debezium 监听.-> KAFKA + TEMPORAL_SVC -- gRPC :7233 --> TEMPORAL_DB ``` ### 3.2 依赖方向 ``` -L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L6 数据/总线 +L1 客户端 → L2 微前端(portal-shell) → L3 网关 → L4 Apollo Router → L5 业务服务 → L6 数据/总线 + ↑ + L5 内部互调走 gRPC(禁止子图互调) ``` -**严格规则**: +**严格规则**(v2.1 新增 ADR-037): 1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**;只做 HTTP 反向代理,不做协议转换 -2. L4 BFF 层只做聚合、裁剪、协议转换(GraphQL → gRPC),**不持有业务状态** -3. L5 业务服务之间通过 gRPC(同步)或 Kafka 事件(异步)通信,**不直接访问对方数据库** -4. L6 数据层每个微服务独占自身数据库,**禁止跨库联表** +2. L4 Apollo Router 层做查询计划 + @requires DataScope 传递,**不持有业务状态**;外部 GraphQL 必须经 Router +3. L5 业务服务之间通过 **gRPC(同步)或 Kafka 事件(异步)** 通信,**禁止 GraphQL 子图互调**(ADR-037),**不直接访问对方数据库** +4. L5 业务服务的 GraphQL 子图仅服务前端展现,**不可作为内部 RPC** +5. L6 数据层每个微服务独占自身数据库,**禁止跨库联表**;DataScope 通过 @requires + ScopeToken 运行时解析(ADR-024/041) +6. realtime-gateway 不直挂 Kafka,通过 Redis Pub/Sub 订阅 msg worker 推送(ADR-040) +7. 业务代码只写业务表 + Outbox 表,废弃 OutboxPublisher 轮询,由 Debezium 监听 binlog 投递(ADR-032) --- @@ -389,136 +418,145 @@ L1 客户端 → L2 微前端 → L3 网关 → L4 BFF → L5 业务服务 → L ```mermaid graph TB - subgraph Gateway["网关层"] + subgraph Gateway["边缘网关层"] APIGW[api-gateway :8080] - PushGW[push-gateway :8081] + RTGW[realtime-gateway :8081] end - subgraph BFF["BFF 层"] - TBFF[teacher-bff :3003] - SBFF[student-bff :3009] - PBFF[parent-bff :3010] + subgraph Federation["GraphQL 联邦层(v2.1 新增)"] + ROUTER[apollo-router :3000
Rust 官方二进制] end - subgraph Services["业务服务"] - IAM[iam :3002] - CoreEdu[core-edu :3004] - Content[content :3005] - DataAna[data-ana :3006] - Msg[msg :3007] - AI[ai :3008] + subgraph Portal["微前端层(v2.1:单容器)"] + SHELL[portal-shell :4010] + end + + subgraph Services["业务服务(全部暴露 GraphQL 子图)"] + IAM[iam :3002 / gRPC :50052] + CONFIG[config-service :3011 / gRPC :50059
v2.1 新增] + CoreEdu[core-edu :3004 / gRPC :50053] + Content[content :3005 / gRPC :50054] + DataAna[data-ana :3006 / gRPC :50055] + Msg[msg :3007 / gRPC :50056] + AI[ai :3008 / gRPC :50058] + end + + subgraph Temporal["工作流层(v2.1 新增)"] + TEMP[temporal :7233
AI 工作流 + Saga] end subgraph Shared["共享包"] - Proto[shared-proto
8 proto] - SharedTS[shared-ts
bff/outbox] + Proto[shared-proto
8 proto + GraphQL 生成器] + SharedTS[shared-ts
outbox/dataloader/guards] SharedGo[shared-go
env/jwks/logger/tracer] SharedPy[shared-py] Contracts[contracts
权限点] Hooks[hooks
React Hooks] - UIComps[ui-components] + UIComps[ui-components
含 PluginCard 系列] UITokens[ui-tokens] end - %% 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 + %% Portal → 网关 → Router → 子图 + SHELL -- HTTP/GraphQL --> APIGW + SHELL -.SSE 推送.-> RTGW + APIGW -- HTTP 代理(GraphQL) --> ROUTER + APIGW -. REST 透传 .-> IAM + APIGW -. REST 透传 .-> Msg + ROUTER -- 子图查询(@requires ScopeToken) --> IAM + ROUTER -- 子图查询 --> CONFIG + ROUTER -- 子图查询 --> CoreEdu + ROUTER -- 子图查询 --> Content + ROUTER -- 子图查询 --> Msg + ROUTER -- 子图查询 --> DataAna + ROUTER -- 子图查询 --> AI - %% BFF → 业务服务(gRPC) - TBFF -- gRPC --> IAM - TBFF -- gRPC --> CoreEdu - TBFF -- gRPC --> Content - TBFF -- gRPC --> DataAna - TBFF -- gRPC --> AI - TBFF -- gRPC --> Msg + %% realtime-gateway → Redis Pub/Sub(订阅 msg worker 推送) + RTGW -. Redis Pub/Sub user:userId:notify .-> Msg - SBFF -- gRPC --> IAM - SBFF -- gRPC --> CoreEdu - SBFF -- gRPC --> DataAna - SBFF -. Kafka 事件 .-> PushGW + %% AI → Temporal + AI -- gRPC Worker :7233 --> TEMP - PBFF -- gRPC --> IAM - PBFF -- gRPC --> CoreEdu - PBFF -- gRPC --> DataAna - PBFF -- gRPC --> Msg - PBFF -. HTTP .-> PushGW - PBFF -. Kafka 订阅 .-> CoreEdu - PBFF -. Kafka 订阅 .-> IAM - - %% 业务服务间事件 + %% 业务服务间事件(Outbox + Debezium → Kafka) 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 + CONFIG -. Kafka edu.config.entry.changed .-> IAM + CONFIG -. Kafka edu.config.entry.changed .-> CoreEdu + CONFIG -. Kafka edu.config.entry.changed .-> Content + AI -. gRPC(内部 RPC) .-> Content + AI -. gRPC(内部 RPC) .-> DataAna + AI -. gRPC(内部 RPC) .-> IAM + AI -. Kafka edu.ai.usage.recorded .-> DataAna DataAna -. Kafka 事件 .-> CoreEdu DataAna -. Kafka 事件 .-> Msg %% 共享包依赖 IAM --> Proto + CONFIG --> Proto CoreEdu --> Proto Content --> Proto DataAna --> Proto Msg --> Proto AI --> Proto APIGW --> SharedGo - PushGW --> SharedGo + RTGW --> SharedGo IAM --> SharedTS + CONFIG --> SharedTS CoreEdu --> SharedTS Content --> SharedTS Msg --> SharedTS - TBFF --> SharedTS - SBFF --> SharedTS - PBFF --> SharedTS + SHELL --> UIComps + SHELL --> UITokens + SHELL --> Hooks + SHELL --> Contracts ``` -### 4.1 服务间通信矩阵 +### 4.1 服务间通信矩阵(v2.1 修订) -| 调用方 → 被调用方 | 协议 | 场景 | -| ------------------------- | ---------------- | ------------------------------------------ | -| 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.*`) | +| 调用方 → 被调用方 | 协议 | 场景 | +| ------------------------------- | ---------------- | ----------------------------------------------------- | +| portal-shell → api-gateway | HTTP / GraphQL | 前端查询经 Gateway → apollo-router | +| portal-shell → realtime-gateway | SSE | 推送通道(默认) | +| portal-shell → realtime-gateway | WebSocket | 监考双向场景(仅此场景用 WS) | +| api-gateway → apollo-router | HTTP 反向代理 | GraphQL 查询透传到 Router | +| api-gateway → iam / msg | HTTP 反向代理 | REST 透传(登录、通知等非 GraphQL 路径) | +| apollo-router → 业务子图 | HTTP / GraphQL | 自动查询计划 + @requires ScopeToken 传递 + DataLoader | +| 业务子图 → 业务子图 | gRPC(内部 RPC) | 服务间同步调用,禁止 GraphQL 子图互调(ADR-037) | +| realtime-gateway → Redis | Redis Pub/Sub | 订阅 user:{userId}:notify channel(不直挂 Kafka) | +| msg worker → Redis | Redis Pub/Sub | 消费 Kafka → 按 userId 发到 Redis Channel(ADR-040) | +| CoreEdu → Content | Kafka 事件 | 教学内容变更通知 | +| CoreEdu → DataAna | Kafka 事件 | 学情数据投递 | +| CoreEdu → Msg | Kafka 事件 | 通知触发(考试/作业/成绩/考勤) | +| IAM → CoreEdu | Kafka 事件 | 用户变更同步 | +| IAM → Msg | Kafka 事件 | 用户/角色变更通知 | +| ConfigService → 全部服务 | Kafka 事件 | `edu.config.entry.changed` 失效各服务本地缓存 | +| Content → DataAna | Kafka 事件 | 内容发布同步 | +| DataAna → CoreEdu | Kafka 事件 | 掌握度更新 → 推荐练习 | +| DataAna → Msg | Kafka 事件 | 掌握度预警触发 | +| AI → Content | gRPC | 题库查询 / 知识点查询 | +| AI → DataAna | gRPC | 学情数据查询 | +| AI → IAM | gRPC | 用户信息查询 | +| AI → Temporal | gRPC :7233 | AI 耗时工作流 + Saga(Worker 注册) | +| AI → Kafka | Kafka 生产 | AI 用量事件发布(`edu.ai.usage.recorded`) | -### 4.2 BFF 下游客户端矩阵 +### 4.2 Apollo Router 子图清单(v2.1 替代 BFF 下游矩阵) -> 所有 BFF 下游客户端统一抽象(B8 裁决):每个客户端有 `gRPC` + `mock` 两个实现,未配置 gRPC target 时自动降级为 mock。 +> 7 个业务服务暴露 GraphQL 子图,由 apollo-router 自动组装 Supergraph。每个子图必须实现 RouterAuthGuard(ADR-036)+ DataLoader(ADR-035)。 -| 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) | +| 子图 | HTTP 端口 | 关键 @key 类型 | DataScope 依赖 | +| -------------- | --------- | ----------------------------------------------------------- | --------------------------------------- | +| iam | 3002 | User / Role / Permission / School | 无(权限源,暴露 ScopeToken Resolver) | +| config-service | 3011 | PluginConfig / LayoutTemplate / UserOverride | 无 | +| core-edu | 3004 | Exam / Homework / Grade / Attendance / Class / Schedule | `visibleClassIds` / `visibleStudentIds` | +| content | 3005 | Textbook / Chapter / Question / KnowledgePoint / LessonPlan | `editableSubjectIds` | +| msg | 3007 | Notification / Template / Preference | `visibleNotificationScopes` | +| data-ana | 3006 | Analytics / Dashboard / Mastery / Warning | `visibleClassIds` / `visibleStudentIds` | +| ai | 3008 | Chat / Question / Expression / LessonPlan / Report | `userId`(个人级) | + +> **v2.1 已下线**:teacher-bff / student-bff / parent-bff 的 BFF 下游客户端矩阵(原 §4.2)已废弃,由本节替代。 --- @@ -600,93 +638,215 @@ sequenceDiagram | L3 组件 | 页面内组件可见性 | usePermission().hasPermission() | 教导主任看到"导出全校报表"按钮 | | L4 数据 | 数据行级过滤 | DataScope 枚举 | 教导主任 DataScope=grade_managed(管辖年级) | -**场景域 BFF 复用策略**: +**v2.1 场景域复用策略**(替代 v1 的 BFF 复用): -按"使用场景域"分 BFF,而非按角色分。新角色复用现有 BFF,通过视口差异化。 +按"使用场景域"在 portal-shell 内通过插件配置差异化(admin 在 config-service 配置角色插件集 + 角色 Layout 默认 + 角色级 props),而非按角色分 BFF。新角色复用 portal-shell + apollo-router,通过插件配置 + DataScope 差异化。 -| BFF | 场景域 | 复用角色 | -| ----------- | -------- | ------------------------ | -| Teacher BFF | 教学场景 | 教师、教导主任、教研组长 | -| Student BFF | 学习场景 | 学生 | -| Parent BFF | 家长场景 | 家长 | -| Admin BFF | 管理场景 | 系统管理员、校管理员 | +| 场景域 | portal-shell 插件集 | 复用角色 | +| -------- | --------------------------------------------------------------------------------- | ------------------------ | +| 教学场景 | universal 插件 + teacher 插件(lesson-plan-editor / question-bank 等) | 教师、教导主任、教研组长 | +| 学习场景 | universal 插件 + student 插件(error-book / ai-tutor 等) | 学生 | +| 家长场景 | universal 插件 + parent 插件(child-overview / leave-approval 等) | 家长 | +| 管理场景 | universal 插件 + admin 插件(user-management / rbac-manager / plugin-manager 等) | 系统管理员、校管理员 | -**实现**:教导主任归入 Teacher BFF + 额外管理视口(L1 导航增加管理菜单项,L4 数据范围扩大到年级)。 +**实现**:教导主任归入教学场景 + 额外管理视口(L1 导航增加管理菜单项,L4 数据范围扩大到年级),通过 config-service 配置 role_plugin_mapping 实现。 -**iam 服务职责**: +**iam 服务职责**(v2.1 拆分后,ADR-026): - 认证:登录/登出/JWT/2FA - RBAC:角色/权限/角色-权限映射 CRUD - 视口配置:导航/路由/组件级视口配置 CRUD - DataScope:数据范围解析(all/grade_managed/class_taught/children/owned + 自定义) - 权限解析 API:getEffectivePermissions(userId) → {permissions, viewports, dataScope} +- **子图暴露 ScopeToken Resolver**:`classScopeToken` / `studentScopeToken`,由 Router 通过 @requires 调用(ADR-041) + +**config-service 服务职责**(v2.1 新增,ADR-026): + +- 插件配置:plugin_registry / role_plugin_mapping CRUD +- 布局配置:layout_templates / role_layout_default / user_layout_override CRUD +- 用户偏好:user_layout_override(用户自定义 Layout + 插件位置 + 插件 props) +- 三层配置合并:getPluginConfig(userId) 返回三层合并后的最终配置(ADR-026 详细描述) +- 配置变更通知:通过 Outbox + Debezium 发布 `edu.config.entry.changed`,各服务消费后失效本地 Redis 缓存 + +### 5.5 Router-Authorization 信任凭证(v2.1 新增,ADR-036) + +**强制约束**:Apollo Router 请求子图时必须携带内部信任凭证 `router-authorization` Header,各业务服务的 NestJS Guard(RouterAuthGuard)必须拦截并校验该 Header,拒绝任何非 Router 发起的 GraphQL 请求。 + +**理由**:GraphQL 子图暴露后,若不校验来源,恶意请求可直接绕过 Router 的查询计划与权限聚合,直接调子图。 + +**实现要点**: + +- Router 侧(router.yaml):通过 `headers.all.request.insert` 自动注入 `router-authorization: ${ROUTER_AUTH_SECRET}` +- 子图侧(各业务服务通用 Guard): + +```typescript +@Injectable() +export class RouterAuthGuard implements CanActivate { + canActivate(ctx: ExecutionContext): boolean { + const req = ctx.switchToHttp().getRequest(); + const routerAuth = req.headers["router-authorization"]; + const expected = this.config.get("ROUTER_AUTH_SECRET"); + if (routerAuth !== expected) { + throw new ForbiddenException( + "Direct GraphQL access denied; must go through Apollo Router", + ); + } + return true; + } +} +``` + +- `ROUTER_AUTH_SECRET` 通过 Docker Secret 或 K8s Secret 注入,禁止硬编码 +- 开发模式(DEV_MODE=true)可放宽校验,便于本地调试 +- 通用实现位于 `packages/shared-ts/src/guards/`,所有子图复用 + +**配置管理**: + +- 仅 apollo-router 可调用子图 /graphql 端点 +- 业务服务之间互调走 gRPC(ADR-037),不走 GraphQL 子图 --- ## 6. 数据访问与缓存 -### 6.1 CQRS 读写分离 +### 6.1 CQRS 读写分离(v2.1 content 改造,ADR-027) ```mermaid graph LR - subgraph Write["写路径"] + subgraph Write["写路径(content 服务仅写 MySQL)"] Cmd[Command 命令] --> App[Application Service] App --> Domain[Domain 领域模型] Domain --> Repo[Repository 写模型] Repo --> mysql_w[(MySQL 主库)] - App --> Outbox[(Outbox 表
同事务)] + App --> Outbox[(Outbox 表
同事务原子)] end - subgraph Sync["同步链路"] - Outbox --> Relay[Relay Worker] - Relay --> kafka_sync[(Kafka)] - kafka_sync --> Proj[Projection] + subgraph Eager["Eager Invalidation(v2.1 ADR-038)"] + Outbox --> SyncDel[事务提交后同步
Redis DEL 主动失效] + end + + subgraph Sync["同步链路(Debezium 监听 binlog)"] + Outbox --> Debezium[Debezium Connect
监听 outbox 表 binlog] + Debezium --> kafka_sync[(Kafka
Transaction Log Tailing)] + kafka_sync --> Proj[投影器 worker] Proj --> ch_sync[(ClickHouse 宽表)] - Proj --> redis_sync[(Redis 缓存)] + Proj --> redis_sync[(Redis 缓存
兜底清理)] Proj --> es_sync[(ES 索引)] + Proj --> neo4j_sync[(Neo4j 知识图谱)] end - subgraph Read["读路径"] + subgraph Read["读路径(content 子图读 MySQL + Neo4j + ES)"] Query[Query 查询] --> ReadModel[Read Model] ReadModel --> ch_read[(ClickHouse 宽表)] ReadModel --> redis_read[(Redis 缓存)] - ReadModel --> es_read[(ES 索引)] + ReadModel --> es_read[(ES 索引
含 version 字段)] + ReadModel --> neo4j_read[(Neo4j 知识图谱)] + ReadModel --> mysql_read[(MySQL 强一致穿透读)] end ``` -### 6.2 BFF 混合读策略 +> **v2.1 变更**: +> +> - content 服务只写 MySQL,不再直接写 Neo4j/ES(由投影器消费 Kafka 同步) +> - OutboxPublisher 轮询线程废弃,改由 Debezium 监听 binlog 自动投递(ADR-032) +> - 新增 Eager Invalidation:事务提交后同步发 Redis DEL 主动失效(ADR-038) +> - 新增乐观锁版本号:写返回 version,读携带 expectedVersion,ES 落后则穿透读 MySQL(ADR-039) + +### 6.2 Apollo Router 混合读策略(v2.1 替代 BFF 混合读) ```mermaid flowchart TD - Q[BFF 收到查询请求] --> C1{Redis 命中?} + Q[Apollo Router 收到 Query] --> C1{子图本地 Redis 命中?} C1 -- 是 --> R1[返回缓存] - C1 -- 否 --> C2{需要聚合多服务?} - C2 -- 否 --> C3[直接 gRPC 调用单一服务] - C3 --> C4[写入 Redis] + C1 -- 否 --> C2{需要跨子图聚合?} + C2 -- 否 --> C3[调单一子图 /graphql] + C3 --> C4[子图内 Redis 缓存] C4 --> R2[返回] - C2 -- 是 --> C5[并行 gRPC 调用多服务] - C5 --> C6[内存聚合裁剪] - C6 --> C7[写入 Redis 5-30s 短缓存] - C7 --> R3[返回聚合结果] + C2 -- 是 --> C5[并发调多子图 /graphql
@requires 传递 ScopeToken] + C5 --> C6[Router 内存聚合] + C6 --> R3[返回聚合结果] + R2 --> C7[子图回填 Redis] + R3 --> C7 ``` -### 6.3 缓存策略矩阵 +### 6.3 缓存策略矩阵(v2.1 修订) -| 数据类型 | 存储 | TTL | 失效策略 | -| ------------- | ---------- | ------- | ------------ | -| 用户会话 | Redis | 30 分钟 | 滑动过期 | -| 权限列表 | Redis | 5 分钟 | 事件驱动失效 | -| 班级/年级列表 | Redis | 5 分钟 | 事件驱动失效 | -| 教学资源详情 | Redis | 30 秒 | 短 TTL | -| BFF 聚合结果 | Redis | 5-30 秒 | 短 TTL | -| 学情宽表 | ClickHouse | 实时 | CDC 同步 | -| 题库检索 | ES | 实时 | CDC 同步 | +| 数据类型 | 存储 | TTL | 失效策略 | ADR | +| ---------------- | ---------- | ------- | ------------------------------------------------ | ------- | +| 用户会话 | Redis | 30 分钟 | 滑动过期 | — | +| 权限列表 | Redis | 5 分钟 | 事件驱动失效 | — | +| 班级/年级列表 | Redis | 5 分钟 | 事件驱动失效 | — | +| 教学资源详情 | Redis | 10 分钟 | Eager DEL + 投影器兜底 | ADR-038 | +| Router 聚合结果 | Redis | 5-30 秒 | 短 TTL | — | +| 学情宽表 | ClickHouse | 实时 | CDC 同步 | — | +| 题库检索 | ES | 实时 | CDC 同步(含 version 字段) | ADR-039 | +| DataScope 缓存 | Redis | 5 分钟 | 角色变更时 DEL | — | +| ScopeToken 缓存 | Redis | 5 分钟 | 自动过期(key: scope:usr:{id}:cls_scope) | ADR-041 | +| 配置缓存 | Redis | 5 分钟 | Kafka `edu.config.entry.changed` 失效 + TTL 兜底 | ADR-026 | +| AI workflow 状态 | Redis | 1 小时 | 自动过期(key: ai:workflow:{id}) | ADR-028 | +| content 查询缓存 | Redis | 10 分钟 | Eager DEL + content-cache-projector 兜底 | ADR-038 | +| msg 查询缓存 | Redis | 10 分钟 | Eager DEL + msg-cache-projector 兜底 | ADR-038 | + +### 6.4 Eager Invalidation + 乐观锁版本号(v2.1 新增,ADR-038/039) + +**Eager Invalidation(ADR-038)**: + +- 写请求在 MySQL 事务提交后的代码行,直接同步发 Redis DEL 命令删除查询缓存(毫秒级主动失效) +- Redis DEL 在事务**外**执行(事务提交后),避免 Redis 故障导致业务回滚 +- 若 Redis DEL 失败,不影响业务结果(兜底由 content-cache-projector 处理) +- Kafka 的 content-cache-projector 仅作为防止网络抖动的兜底清理,不是主失效路径 + +**乐观锁版本号(ADR-039)**: + +- 写接口返回记录的 `updated_at` 或 `version` +- 前端携带 `expectedVersion` 发起 Query,Router 透传到 content 子图 +- content 子图先读 ES(快),检查 ES 中的 version + - ES.version >= expectedVersion → 返回 ES 数据(最终一致) + - ES.version < expectedVersion → 穿透读 MySQL(强一致),返回 MySQL 数据 +- 适用场景:用户编辑后立即刷新页面查看、批量导入后立即查询列表 +- 不适用:高频查询场景(version 校验开销),走最终一致即可 + +### 6.5 config-service 三层配置合并(v2.1 新增,ADR-026) + +**优先级**:用户覆盖 > 角色模板 > 系统默认 + +```typescript +// PropsMerger 三层合并(深合并 + 数组覆盖) +const finalProps = deepMerge( + registry.defaultProps, // Layer 1 系统默认(plugin_registry.default_props) + roleMapping.widgetProps, // Layer 2 角色默认(role_plugin_mapping.widget_props) + userPlacement.props, // Layer 3 用户调整(user_layout_override.plugin_placements.props) +); +``` + +**配置表清单**(config-service 独占 schema): + +| 表名 | 用途 | +| -------------------- | ------------------------------------------ | +| plugin_registry | 插件注册表(系统级,admin 维护) | +| role_plugin_mapping | 角色-插件映射(admin 配置角色可用插件集) | +| role_layout_default | 角色 Layout 默认(admin 配置角色默认模板) | +| layout_templates | 5 种内置 Layout 模板(系统预置,不可改) | +| user_layout_override | 用户布局覆盖(用户自定义) | +| plugin_packages | 二期:第三方插件包存储 | + +**配置变更生效路径**: + +1. Admin 通过 REST API 修改 plugin_registry / role_plugin_mapping / role_layout_default +2. config-service 写 MySQL + Outbox 表 +3. Debezium 监听 binlog,发布 `edu.config.entry.changed` 到 Kafka +4. 各服务消费事件,失效本地 Redis 缓存(key: `config:{type}:{key}`) +5. 下次查询从 config-service 拉新值,回填 Redis(TTL 5 分钟兜底) + +> portal-shell 侧采用 SWR 静默后台刷新(revalidateOnFocus + refreshInterval 5min),用户切回 Tab 时自动检测配置变化,Toast 提示"发现新布局配置,点击刷新生效"。无需 Kafka + WebSocket 推送链路,简化架构。 --- ## 7. 事件驱动架构 -### 7.1 Outbox + Kafka + CDC 全链路 +### 7.1 Outbox + Debezium Transaction Log Tailing(v2.1 ADR-032) + +> **v2.1 关键变更**:废弃 OutboxPublisher 轮询线程,改由 Debezium 监听 binlog 自动投递 Outbox 表到 Kafka(Transaction Log Tailing)。CDC 与 Outbox 不再二选一,而是分工结合:Outbox 定义"发什么"(业务语义),CDC 解决"怎么发"(技术传输)。 ```mermaid graph LR @@ -694,167 +854,306 @@ graph LR Cmd[Command 处理] Domain[Domain 聚合] Repo[Repository] - Outbox[(Outbox 表)] end subgraph MySQL["MySQL 主库"] BizTable[(业务表)] - OutboxTable[(outbox 表)] + OutboxTable[(outbox 表
同事务原子)] end - subgraph Relay["Relay Worker"] - Poll[轮询 outbox
每 100ms] - Publish[发布到 Kafka] - Mark[标记 processed] + subgraph CDC["Debezium Connect(监听 binlog,v2.1 替代轮询)"] + Debezium[Debezium 2.7
伪装成 MySQL Slave
监听 outbox 表 binlog] + SMT[SMT Topic Router
重写为业务语义 topic] end subgraph Bus["事件总线"] - kafka_bus[(Kafka topic)] + kafka_bus[(Kafka topic
edu...)] end subgraph Consumers["消费者"] - Proj[Projection
更新读模型] + Proj[投影器 worker
CQRS 读模型同步] OtherSvc[其他服务
业务订阅] + RealtimeGW[realtime-gateway
via msg worker + Redis Pub/Sub] end Cmd --> Domain Domain --> Repo Repo --> BizTable Repo --> OutboxTable - OutboxTable --> Poll - Poll --> Publish - Publish --> kafka_bus + OutboxTable -.binlog row event.-> Debezium + Debezium --> SMT + SMT --> kafka_bus kafka_bus --> Proj kafka_bus --> OtherSvc - Proj --> read_stores[(ClickHouse / Redis / ES)] + kafka_bus --> MsgWorker[msg worker
消费 edu.notify.* ] + MsgWorker --> RedisPUB[Redis Pub/Sub
user:userId:notify] + RedisPUB --> RealtimeGW + Proj --> read_stores[(ClickHouse / Redis / ES / Neo4j)] ``` -### 7.2 事件 Topic 分类 +**关键原则**: + +- 业务代码只写业务表 + Outbox 表(业务语义由业务代码控制) +- Outbox 表的投递由 Debezium 自动完成(技术传输由基础设施完成,业务代码不感知 MQ) +- v1 的 OutboxPublisher 轮询线程在 v2.1 完全废弃(被 Debezium 替代) +- 保证 at-least-once 语义(Debezium offset 管理) +- 消费者基于 `event_id` 去重(DB 唯一索引或 Redis SETNX) +- 投影器消费 Kafka 事件做 CQRS 读模型同步(与业务事件发布解耦) + +### 7.2 事件 Topic 分类(v2.1 修订) > 实际命名(基于 `services/*/src/shared/kafka/topic-map.ts` 与 `services/iam/src/config/kafka.ts`): > 模式 `edu...`。msg 服务统一使用 `edu.notify.notification.*` 发布(ARB-013)。 +> v2.1 新增 `edu.ai.usage.recorded` / `edu.config.entry.changed` topic。 -| 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 用量事件 | +| 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 / data-ana | 考试发布 | +| `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 / data-ana | 作业布置 | +| `edu.teaching.assignment.submitted` | core-edu | data-ana、msg | 作业提交 | +| `edu.teaching.assignment.graded` | core-edu | msg / data-ana | 作业批改完成 | +| `edu.teaching.grade.recorded` | core-edu | data-ana、msg | 成绩录入 | +| `edu.teaching.attendance.recorded` | core-edu | msg / data-ana | 考勤记录 | +| `edu.content.question.created` | content | es-projector / neo4j-projector | 题目创建(v2.1 改) | +| `edu.content.question.updated` | content | es-projector / neo4j-projector | 题目更新(v2.1 新) | +| `edu.content.knowledge_point.linked` | content | neo4j-projector | 知识点关联(v2.1 新) | +| `edu.content.textbook.updated` | content | data-ana | 教材更新 | +| `edu.insight.mastery.updated` | data-ana | core-edu、msg | 掌握度更新 | +| `edu.notify.notification.sent` | msg | msg worker → Redis Pub/Sub → realtime-gateway | 通知投递(驱动推送,v2.1 改) | +| `edu.notify.notification.read` | msg | — | 通知已读 | +| `edu.notify.notification.recalled` | msg | msg worker → Redis Pub/Sub → realtime-gateway | 通知撤回(v2.1 改) | +| `edu.notify.notification.failed` | msg | — | 通知投递失败 | +| `edu.notify.notification.events` | msg | — | 兜底 topic | +| `edu.ai.usage.recorded` | ai | data-ana | AI 用量记录(v2.1 新) | +| `edu.config.entry.changed` | config-service | 全部服务 | 配置变更失效缓存(v2.1 新) | -> **Outbox 表命名**:每服务独立 outbox 表(iam_outbox / core_edu_outbox / content_outbox / msg_outbox),由 shared-ts/outbox 模块统一管理。 +> **Outbox 表命名**:每服务独立 outbox 表(iam_outbox / config_outbox / core_edu_outbox / content_outbox / msg_outbox / ai_outbox),由 shared-ts/outbox 模块统一管理。投递由 Debezium 自动完成。 > **必需依赖软失败标注**(ARB-015 §17.6,ISSUE-058 覆盖 ISSUE-055): > -> - push-gateway → Redis:**软失败**(Redis 故障仅告警 + `degraded: true` + /readyz 返 200,不返 503、不触发 Pod 重启) -> - 理由:Redis 故障时单实例仍能服务本地连接(仅跨实例广播失效);重启会丢失本地连接表,加剧雪崩风险 -> - ISSUE-055 必需依赖列表应排除 push-gateway → Redis +> - realtime-gateway → Redis:**软失败**(Redis 故障仅告警 + `degraded: true` + /readyz 返 200,不返 503、不触发 Pod 重启) +> - 理由:Redis 故障时单实例仍能服务本地 SSE 连接(仅跨实例广播失效);重启会丢失本地连接表,加剧雪崩风险 +> - ISSUE-055 必需依赖列表应排除 realtime-gateway → Redis ### 7.3 核心领域事件 -| 事件 | 触发场景 | 消费者动作 | -| --------------------- | -------------- | ------------------------------------------------ | -| UserRegistered | 新用户注册 | CoreEdu 初始化默认班级关联;Msg 发送欢迎通知 | -| ExamPublished | 考试发布 | Msg 推送考试通知给学生;DataAna 创建考试分析骨架 | -| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 | -| HomeworkGraded | 教师批改完成 | DataAna 更新掌握度;Msg 通知学生 | -| MasteryUpdated | 掌握度计算完成 | CoreEdu 推荐个性化练习;Msg 触发预警 | -| NotificationRequested | 通知请求 | Msg 投递通知到 4 渠道(email/sms/push/in-app) | -| NotificationSent | 通知投递完成 | push-gateway 通过 Kafka 消费触发 WebSocket 推送 | +| 事件 | 触发场景 | 消费者动作 | +| --------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- | +| UserRegistered | 新用户注册 | CoreEdu 初始化默认班级关联;Msg 发送欢迎通知 | +| ExamPublished | 考试发布 | Msg 推送考试通知给学生;DataAna 创建考试分析骨架 | +| HomeworkSubmitted | 学生提交作业 | DataAna 记录提交行为;Msg 通知教师 | +| HomeworkGraded | 教师批改完成 | DataAna 更新掌握度;Msg 通知学生 | +| MasteryUpdated | 掌握度计算完成 | CoreEdu 推荐个性化练习;Msg 触发预警 | +| NotificationRequested | 通知请求 | Msg 投递通知到 4 渠道(email/sms/push/in-app) | +| NotificationSent | 通知投递完成 | msg worker 消费 Kafka → Redis Pub/Sub → realtime-gateway SSE 推送(v2.1 改,原为 push-gateway Kafka 消费) | +| ConfigEntryChanged | 配置变更 | 各服务消费失效本地 Redis 缓存(v2.1 新增) | + +### 7.4 Redis Pub/Sub 推送背板(v2.1 新增,ADR-040) + +**核心原则**:边缘网关(realtime-gateway)不直接挂载 Kafka,避免在边缘节点引入重量级 Kafka Client。采用 Redis Pub/Sub 作为状态路由背板。 + +``` +msg 服务发布通知 + ↓ Outbox 表 + Debezium → Kafka(edu.notify.notification.*) + ↓ +msg 服务的后台 worker 消费 Kafka + · 持久化通知记录 + · 脱敏处理 + · 分类聚合 + ↓ +msg worker 将需要实时推送的消息,按 userId 发到 Redis Pub/Sub + · Channel: user:{userId}:notify + · Payload: { notificationId, type, title, body, createdAt } + ↓ +realtime-gateway 实例仅在用户接入 SSE 时,才动态 SUBSCRIBE 该用户的 Redis Channel + · 用户上线 → SADD online:users {userId} + SUBSCRIBE user:{userId}:notify + · 用户下线 → SREM online:users {userId} + UNSUBSCRIBE + ↓ + ├── SSE 推送(默认,单向) + │ · 客户端 GET /sse?token=JWT,保持长连接 + │ · realtime-gateway 收到 Redis Pub/Sub 消息 → 推送到 SSE 连接 + │ · 适合 99% 场景(考试发布/成绩推送/通知) + │ + └── WebSocket 推送(仅监考场景) + · 客户端 WS /ws,双向通信 + · 用于在线监考(心跳/防作弊/实时指令) +``` + +**架构优势**: + +- 边缘网关轻量:只依赖 Redis Client,不依赖 Kafka Client(内存占用降低 80%+) +- 按需订阅:realtime-gateway 仅订阅在线用户的 Channel,离线用户的消息由 msg worker 持久化待取 +- 水平扩展:realtime-gateway 多实例时,Redis Pub/Sub 自动广播到所有订阅实例 +- 故障隔离:Kafka 故障不影响已建立 SSE 连接的实时推送(Redis Pub/Sub 独立) + +**realtime-gateway 端点**: + +- `GET /sse` — SSE 推送(默认),连接时 SUBSCRIBE Redis Channel +- `GET /ws` — WebSocket 升级(监考专用) +- `GET /online/:userId` — 查询在线状态(检查 Redis SET `online:users`) +- `POST /internal/broadcast` — 管理员全量广播 +- `GET /healthz` / `GET /readyz` / `GET /metrics` --- ## 8. 跨服务协作 -### 8.1 同步 vs 异步决策 +### 8.1 同步 vs 异步决策(v2.1 修订) ```mermaid flowchart TD Req[跨服务协作需求] --> C1{需要强一致性?} C1 -- 是 --> C2{调用方需要立即结果?} - C2 -- 是 --> Sync[同步 gRPC 调用] - C2 -- 否 --> Saga[Saga 编排
Temporal] + C2 -- 是 --> C2a{是 CRUD 短事务?} + C2a -- 是 --> Sync[同步 gRPC + 分布式锁
ADR-030 严禁 Temporal] + C2a -- 否 --> Saga[Saga 编排
Temporal(ADR-030)] C1 -- 否 --> C3{需要事件最终一致?} - C3 -- 是 --> Async[异步 Kafka 事件] + C3 -- 是 --> Async[异步 Kafka 事件
Outbox + Debezium(ADR-032)] C3 -- 否 --> C4{仅查询读取?} - C4 -- 是 --> Read[读模型冗余] + C4 -- 是 --> Read[读模型冗余 / GraphQL @requires] C4 -- 否 --> Sync ``` -### 8.2 BFF 同步聚合 +### 8.2 Apollo Router 同步聚合(v2.1 替代 BFF 同步聚合) ```mermaid sequenceDiagram - participant U as 教师端 - participant BFF as teacher-bff - participant IAM as iam - participant CoreEdu as core-edu - participant DataAna as data-ana + participant U as portal-shell + participant R as apollo-router + participant IAM as iam 子图 + participant CoreEdu as core-edu 子图 + participant DataAna as data-ana 子图 - U->>BFF: 查询班级仪表盘 - BFF->>BFF: 解析 token 获取 userId - par 并行查询 - BFF->>IAM: gRPC GetUser(userId) - IAM-->>BFF: 用户信息 + U->>R: GraphQL Query 班级仪表盘 + R->>R: 解析查询计划(Query Plan) + par 并行子图查询 + R->>IAM: 子图查询 + @requires classScopeToken + IAM->>IAM: 计算 visibleClassIds → Redis Set + IAM-->>R: ScopeToken usr:123:cls_scope and - BFF->>CoreEdu: gRPC GetClassesByTeacher(userId) - CoreEdu-->>BFF: 班级列表 + R->>CoreEdu: 子图查询 @requires(fields: "classScopeToken") + CoreEdu->>CoreEdu: 从 Redis sMembers 获取实际 ID + CoreEdu-->>R: 班级列表(DataScope 过滤后) and - BFF->>DataAna: gRPC GetTeacherDashboardStats(userId) - DataAna-->>BFF: 统计数据 + R->>DataAna: 子图查询 + DataAna-->>R: 统计数据 end - BFF->>BFF: 内存聚合裁剪 - BFF-->>U: 仪表盘聚合数据 + R->>R: 内存聚合(DataLoader 批量去重) + R-->>U: 仪表盘聚合数据 ``` ### 8.3 异步事件闭环 ```mermaid flowchart LR - A[教师录入成绩] --> B[CoreEdu 写入 MySQL + Outbox] - B --> C[Outbox Relay 发布事件] + A[教师录入成绩] --> B[CoreEdu 写 MySQL + Outbox 表] + B --> C[Debezium 监听 binlog → Kafka] C --> D[teaching.grade.recorded] D --> E[DataAna 消费更新掌握度] D --> F[Msg 消费通知学生] + F --> F1[msg worker → Redis Pub/Sub → realtime-gateway SSE] E --> G[insight.mastery.updated] G --> H[CoreEdu 消费推荐练习] G --> I[Msg 消费掌握度预警] + I --> I1[msg worker → Redis Pub/Sub → realtime-gateway SSE] ``` -### 8.4 Temporal 工作流编排 +### 8.4 Temporal 工作流编排(v2.1 ADR-030 严格边界) + +> **v2.1 严格立规矩**:仅限 AI 耗时工作流 + Saga 分布式事务补偿,CRUD 短事务绝对禁止。 + +| 工作流类型 | 时长 | 推荐方案 | 示例 | +| ------------------- | --------- | -------------------------------------------- | ------------------------------------------------------ | +| AI 耗时工作流 | 分钟~小时 | **Temporal Workflow** | 生成大纲 → 生成知识点 → 生成题目 → 组装试卷 | +| Saga 分布式事务补偿 | 秒~分钟 | **Temporal Saga** | 购买高级插件:扣减积分服务(+补偿)+ 授权服务(+补偿) | +| 长工作流(非 AI) | 分钟~天 | Redis 状态机 + Outbox 事件 | 备课工作流(非 AI 部分)、报告生成 | +| 短事务 | 毫秒~秒 | **同步调用 + 分布式锁**(绝对禁止 Temporal) | 作业提交、成绩录入、考勤记录 | +| 批处理 | 小时 | Cron + Batch Job | 成绩统计、掌握度计算 | + +**禁止**: + +- **CRUD 短事务绝对禁止用 Temporal**:作业提交、成绩录入、考勤记录等毫秒~秒级事务走同步调用 + 分布式锁,Temporal 的入库开销会拖垮吞吐量 +- 纯读操作走工作流(直接走缓存) + +**允许**: + +- AI 耗时工作流用 Temporal(自动重试 / 休眠 / 状态持久化) +- 跨服务 Saga 分布式事务补偿用 Temporal(自动补偿 / 状态可查询) +- 跨天长流程用 Redis 状态机 + Outbox 事件(非 AI 场景) + +**Temporal 部署**: + +- 独立 Temporal Server 集群(不与业务服务混部,:7233 + UI :8085 + PG :5433) +- ai 服务作为 Temporal Worker,注册 Workflow 与 Activity +- Workflow 状态持久化在 Temporal DB(PostgreSQL),业务数据仍在各服务 DB ```mermaid flowchart TD - Start[考试发布] --> A1[Activity: 创建考试实例] - A1 --> A2[Activity: 通知学生] - A2 --> A3[Activity: 等待作答窗口] - A3 --> A4[Activity: 收集提交] - A4 --> C1{是否全部提交?} - C1 -- 否 --> A5[Activity: 自动提交未答] - C1 -- 是 --> A6[Activity: 批改] - A5 --> A6 - A6 --> A7[Activity: 发布成绩] - A7 --> A8[Activity: 生成分析] - A8 --> End[工作流完成] + Start[AI 生成试卷请求] --> A1[Temporal Workflow 启动] + A1 --> A2[Activity: 生成大纲] + A2 --> A3[Activity: 生成知识点] + A3 --> A4[Activity: 生成题目] + A4 --> A5[Activity: 组装试卷] + A5 --> C1{质量校验通过?} + C1 -- 否 --> A6[Activity: 补偿回滚] + C1 -- 是 --> A7[Activity: 持久化试卷] + A6 --> End[工作流失败] + A7 --> End2[工作流完成] +``` + +### 8.5 ScopeToken 优化大规模 ID 列表(v2.1 新增,ADR-041) + +**问题**:大规模 ID 列表(如管理员可见 500 个班)直接在 GraphQL federation 中传递全量数组,会导致 Router ↔ 子图之间的 HTTP 请求 payload 膨胀,影响延迟。 + +**方案**: + +- iam 计算 visibleClassIds 存入 Redis Set(key: `scope:usr:{userId}:cls_scope`,TTL 5min) +- 生成极短的 ScopeToken(如 `usr:123:cls_scope`) +- Router 通过 @requires 传递 token(不传全量 ID 数组) +- core-edu 接收 token 后从同机房 Redis `SMEMBERS scope:usr:123:cls_scope` 获取实际 ID 列表 +- Redis 调用延迟 < 1ms,远小于 HTTP payload 传输 500 个 ID 的开销 +- ScopeToken 与 Redis Set 共享 TTL(5min),自动过期清理 + +**K12 场景规模分析**: + +| 角色 | 可见范围 ID 量级 | 性能评估 | +| -------- | ---------------- | ------------------------- | +| 学生 | 1 个班 | 无压力 | +| 教师 | 5-10 个班 | IN 查询可扛 | +| 教研组长 | 20-50 个班 | IN 查询可扛 | +| 管理员 | 100-500 个班 | 需 ScopeToken 优化 | +| 超管 | 全校 | 走 DataScope=ALL,不传 ID | + +**@requires 查询计划示例**(教研组长查"本组所有班级的考试成绩"): + +``` +步骤 1: 调 iam 子图 + scopeToken(userId=x-user-id, scopeType="class") + → iam 查 Redis 缓存(key: scope:usr:{userId}:cls_scope,TTL 5min) + → 未命中则查 user_school_role 表 + → 将 ID 列表存入 Redis Set + → 返回极短的 scopeToken: "usr:123:cls_scope"(不传全量 ID 数组) + +步骤 2: 调 core-edu 子图 + gradesForCurrentUser( + scopeToken: "usr:123:cls_scope", + termId: "2024-spring" + ) + @requires(fields: "scopeToken") + → core-edu 从同机房 Redis SMEMBERS scope:usr:123:cls_scope 拿到 ID 数组 + → 查 MySQL: WHERE class_id IN (...) AND term_id = ? + → 返回 [Grade, Grade, ...] + +步骤 3: 聚合返回客户端 ``` --- @@ -958,6 +1257,7 @@ sequenceDiagram > 实际落地: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`。 +> v2.1 新增:apollo-router(Rust 官方二进制,内置 OTel spans + Prometheus 指标 + spec_compliant JSON 日志);config-service(NestJS,复用 pino + prom-client + OTLP);portal-shell(Next.js,复用 web-vitals + Sentry + OTel Web)。 ```mermaid graph TB @@ -965,7 +1265,8 @@ graph TB NESTJS[NestJS 服务
pino + prom-client + OTLP] GO[Go 网关
slog + prometheus + otelgin] PY[Python 服务
structlog + prometheus-client + OTLP] - PORTAL[Next.js portal
OTel Web + Sentry] + APOLLO[apollo-router :3000
Rust 内置 OTel spans +
Prometheus 指标 + spec_compliant 日志] + PORTAL[portal-shell :4010
web-vitals + Sentry + OTel Web] end subgraph Collect["采集层"] @@ -994,11 +1295,13 @@ graph TB NESTJS --> OTEL GO --> OTEL PY --> OTEL + APOLLO -.内置 spans.-> OTEL PORTAL -.OTLP HTTP.-> OTEL OTEL --> JAEGER NESTJS --> PROM GO --> PROM PY --> PROM + APOLLO -.内置 /metrics.-> PROM PROM --> PROMDB PROMTAIL --> LOKI NODE --> PROM @@ -1010,19 +1313,31 @@ graph TB PROMDB --> ALERT ``` +**v2.1 各服务可观测性栈**: + +| 服务 | 日志 | 指标 | 链路 | 健康检查 | +| ---------------- | --------------------------- | ----------------------------------------------------------- | ------------------ | -------------------------------------------- | +| apollo-router | spec_compliant JSON(内置) | 内置 Prometheus(请求量/延迟/错误率/子图延迟/持续查询统计) | 内置 OpenTelemetry | `/health`(独立 :8088 端口,见 router.yaml) | +| config-service | pino | prom-client + `/metrics` | OTLP exporter | `/healthz` + `/readyz` | +| portal-shell | —(前端) | web-vitals(CLS/INP/LCP/FCP/TTFB) | OTel Web + Sentry | —(前端无健康端点,由容器探针替代) | +| realtime-gateway | slog JSON | prometheus + `/metrics` | otelgin | `/healthz` + `/readyz` | +| iam/core-edu/... | pino | prom-client + `/metrics` | OTLP exporter | `/healthz` + `/readyz` | + ### 10.2 Trace 上下文传播 ```mermaid flowchart LR A[客户端请求] --> B[API Gateway
otelgin 生成 traceId] - B --> C[BFF
继承 W3C traceparent] - C --> D[业务服务 gRPC
继承 metadata] + B --> C[apollo-router
继承 W3C traceparent
生成子图查询计划 span] + C --> D[业务服务子图 /graphql
继承 HTTP traceparent] D --> E[Kafka 事件
traceId 写入 header] E --> F[消费者服务
继承 traceId] F --> G[下游存储
span 记录] G --> H[Jaeger UI
查询链路] ``` +> v2.1 变更:BFF 环节由 apollo-router 替代。Router 在子图查询计划中生成独立 span(query_plan / fetch_subgraph),便于在 Jaeger 中追踪每个子图的延迟贡献。 + **规则**: - 所有跨服务调用必须透传 W3C Trace Context(HTTP `traceparent` 头 / gRPC metadata) @@ -1030,16 +1345,19 @@ flowchart LR - 日志必须包含 traceId / requestId 用于关联查询 - 关键业务操作必须创建 span(创建考试、提交作业、批改、AI 生成、通知投递等) - 每服务暴露 `/metrics`(Prometheus 抓取)+ `/healthz`(liveness)+ `/readyz`(readiness) +- apollo-router 在子图查询计划中生成 `query_plan` / `fetch_subgraph` span,便于定位联邦层延迟(v2.1 新增) ### 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 | +| 服务类型 | /healthz 行为 | /readyz 行为 | +| ---------------- | ------------- | ------------------------------------------------- | +| 网关层 | 进程存活即 ok | 检查下游可达性(soft-failure) | +| apollo-router | 进程存活即 ok | `/health`(独立 :8088 端口)+ 子图可达性探针 | +| 业务服务 | 进程存活即 ok | 检查 DB + Redis + Kafka + gRPC server | +| config-service | 进程存活即 ok | 检查 DB + Redis(配置缓存) | +| realtime-gateway | 进程存活即 ok | Redis 软失败(不返 503)+ Redis Pub/Sub 订阅状态 | +| data-ana | 进程存活即 ok | ClickHouse 1s 超时 + CDC lag < 1000 + Redis + iam | +| Temporal | 进程存活即 ok | Temporal Server 自带 /health(gRPC health check) | --- @@ -1110,7 +1428,9 @@ graph TB | CI 强制 | buf lint + buf breaking 必须通过 | | 代码生成 | `buf generate` 生成 6 套代码(go/js/python × protobuf/grpc) | -### 11.3 BFF 聚合模式 +### 11.3 Apollo Router 联邦聚合模式(v2.1 替代 BFF 手写聚合) + +> v2.1 变更:原 3 个手写 BFF(teacher-bff / student-bff / parent-bff)的 GraphQL Resolver + gRPC 调用聚合职责,由 apollo-router 自动查询计划(Query Plan)替代,零手写聚合代码。详见 ADR-023 / ADR-025。 ```mermaid graph LR @@ -1118,27 +1438,70 @@ graph LR Q[GraphQL 查询] end - subgraph BFF["BFF 层"] - Resolver[GraphQL Resolver] - DataLoader[DataLoader 批量去重] - Cache[Redis 短缓存] + subgraph Router["apollo-router 联邦层"] + Plan[查询计划 Query Plan
自动拆分 + 并发调度] + Requires["@requires DataScope 传递"] end - subgraph Services["业务服务"] - S1[服务 A] - S2[服务 B] - S3[服务 C] + subgraph Subgraphs["业务子图(各服务 /graphql)"] + S1[iam 子图] + S2[core-edu 子图] + S3[content 子图] + S4[msg / data-ana / ai / config 子图] end - Q --> Resolver - Resolver --> Cache - Cache --> DataLoader - DataLoader --> S1 - DataLoader --> S2 - DataLoader --> S3 + Q --> Plan + Plan --> Requires + Requires --> S1 + Plan --> S2 + Plan --> S3 + Plan --> S4 + S1 -.visibleClassIds via @requires.-> S2 ``` -### 11.4 错误码前缀矩阵 +**联邦 vs 手写 BFF 对比**: + +| 维度 | v1 手写 BFF | v2.1 Apollo Federation | +| --------- | ------------------------------- | ------------------------------------------------------ | +| 聚合代码 | 每个 BFF 手写 Resolver + gRPC | Router 自动生成查询计划,零聚合代码 | +| 字段变更 | proto → 3 套 schema/resolver ×3 | proto → 自动生成 GraphQL schema,仅改 proto + 前端查询 | +| DataScope | 网关透传 + 服务注入 | @requires 运行时解析(ADR-024) | +| 批量去重 | 每个 BFF 手写 DataLoader | 子图 @key 解析器强制 DataLoader(ADR-035) | +| 协议 | GraphQL Yoga(每 BFF 独立) | Apollo Router(唯一入口 :3000) | + +### 11.4 Apollo Federation 子图契约(v2.1 新增) + +> 详见 ADR-023 / ADR-025 / ADR-035 / ADR-036 / ADR-037。子图契约由 proto 自动生成(`buf generate` + proto → GraphQL 生成器)。 + +**子图暴露规则**: + +| 规则 | 说明 | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| 端点暴露 | 每个业务服务暴露 `/graphql` 子图端点(iam:3002 / config-service:3011 / core-edu:3004 / content:3005 / msg:3007 / data-ana:3006 / ai:3008) | +| 契约生成 | proto → GraphQL 自动生成(ADR-025),业务服务只实现 Resolver,禁止手写 schema | +| @key 必备 | 每个可被联邦引用的类型必须声明 `@key(fields: "...")`,并实现 `__resolveReference` | +| DataLoader 强制 | 所有 `@key` Reference Resolver 必须使用 DataLoader 批量加载(ADR-035),ESLint 规则强制校验,禁止 N+1 | +| 信任凭证 | Router 请求子图时携带 `Router-Authorization` header,子图 `RouterAuthGuard` 校验,拒绝非 Router 的直接 GraphQL 请求(ADR-036) | +| 外部/内部边界 | 外部查询(前端 → 后端)必经 Apollo Router;内部调用(后端 → 后端)必走 gRPC,GraphQL 子图不可作为内部 RPC(ADR-037) | +| DataScope | 跨服务数据范围通过 `@requires` 指令运行时解析,禁止跨库 JOIN(ADR-024) | + +**子图清单与 @key 类型**: + +| 子图 | @key 关键类型 | DataScope 依赖 | +| -------------- | ----------------------------------------------------------- | --------------------------------------- | +| iam | User / Role / Permission / School | 无(权限源) | +| config-service | PluginConfig / LayoutTemplate / UserOverride | 无 | +| core-edu | Exam / Homework / Grade / Attendance / Class / Schedule | `visibleClassIds` / `visibleStudentIds` | +| content | Textbook / Chapter / Question / KnowledgePoint / LessonPlan | `editableSubjectIds` | +| msg | Notification / Template / Preference | `visibleNotificationScopes` | +| data-ana | Analytics / Dashboard / Mastery / Warning | `visibleClassIds` / `visibleStudentIds` | +| ai | Chat / Question / Expression / LessonPlan / Report | `userId`(个人级) | + +**Router-Authorization 信任凭证校验示例**(见 §5.5 / ADR-036): + +子图 NestJS Guard 拦截所有 `/graphql` 请求,校验 `Router-Authorization` header,仅放行来自 apollo-router 的请求,防止外部直接访问子图绕过聚合层。 + +### 11.5 错误码前缀矩阵 > 来源:coord 仲裁 ARB-017 §19.6 / ARB-014 / ARB-015 / ARB-018 / G14 / F4 > 规则:服务名大写 + 下划线分隔,禁止子前缀(如 EXAMS_/HOMEWORK_/GRADES_ 已移除) @@ -1173,7 +1536,7 @@ graph LR | data-ana | `error.data_ana.` | `error.data_ana.dashboard_unavailable` | | ai | `error.ai.` | `error.ai.generation_failed` | -### 11.5 ActionState 信封规范 +### 11.6 ActionState 信封规范 > 来源:coord 仲裁 ARB-017 §19.6 > 适用范围:所有 BFF / Gateway HTTP 响应、GraphQL response 的 errors 扩展字段 @@ -1276,55 +1639,95 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段 ### 12.2 通信约束 -| 场景 | 允许 | 禁止 | -| -------------------- | --------------------------------- | ------------------------ | -| 客户端 → 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 | +> v2.1 变更:BFF 环节由 apollo-router 替代;push-gateway 改名 realtime-gateway;外部 GraphQL 必经 Router,内部 RPC 必走 gRPC(ADR-037)。 + +| 场景 | 允许 | 禁止 | +| ------------------------ | --------------------------------------------- | --------------------------------------- | +| 客户端 → Gateway | REST + SSE | 直连业务服务 | +| Gateway → apollo-router | HTTP 反向代理(路径重写剥离前缀) | gRPC 转换、REST 业务逻辑 | +| Gateway → 业务服务 | HTTP 反向代理(剥离 `/api` 前缀,降级路径) | gRPC 转换 | +| apollo-router → 业务子图 | GraphQL /graphql(携带 Router-Authorization) | 直接访问 DB、绕过 RouterAuthGuard | +| 业务子图 → 业务子图 | **禁止**(外部 GraphQL 不可作内部 RPC) | 子图互调 GraphQL(ADR-037) | +| 业务服务之间(同步) | gRPC | REST、直接 DB、GraphQL 子图互调 | +| 业务服务之间(异步) | Kafka 事件(Outbox + Debezium 模式) | 直接 producer 调用 | +| realtime-gateway → msg | Redis Pub/Sub 订阅(ADR-040) | 直挂 Kafka、gRPC(豁免,ARB-015) | +| msg worker → realtime-gw | Redis Pub/Sub PUBLISH(按 userId 路由) | 直连 Kafka、HTTP 轮询 | +| 事件发布 | Outbox 表 + Debezium Transaction Log Tailing | OutboxPublisher 轮询线程、直接 producer | +| DataScope 跨服务 | `@requires` 运行时解析(ADR-024/041) | 跨库 JOIN、HTTP 传全量 ID 数组 | ### 12.3 数据一致性约束 -| 场景 | 一致性级别 | 实现 | -| ------ | ---------- | --------------------- | -| 聚合内 | 强一致 | 单事务 | -| 聚合间 | 最终一致 | Kafka 事件 | -| 服务间 | 最终一致 | Kafka 事件 / Saga | -| 读模型 | 最终一致 | Projection 异步更新 | -| 缓存 | 最终一致 | 事件驱动失效 + 短 TTL | +| 场景 | 一致性级别 | 实现 | +| ------ | ---------- | --------------------------------------------------------------- | +| 聚合内 | 强一致 | 单事务 | +| 聚合间 | 最终一致 | Kafka 事件 | +| 服务间 | 最终一致 | Kafka 事件 / Saga | +| 读模型 | 最终一致 | Projection 异步更新 | +| 缓存 | 最终一致 | Eager Invalidation(写后同步 DEL)+ Kafka 投影器兜底(ADR-038) | + +### 12.4 v2.1 架构约束(新增,ADR-023~041) + +| 约束 | 说明 | 依据 | +| ------------------------ | ------------------------------------------------------------------- | ----------- | +| 外部 GraphQL 必经 Router | 前端 → 后端的所有 GraphQL 查询必须且只能经过 apollo-router 聚合层 | ADR-023/037 | +| 内部 RPC 必走 gRPC | 后端服务之间的所有同步调用必须使用 gRPC,禁止 REST/GraphQL 子图互调 | ADR-037 | +| 子图不可作内部 RPC | GraphQL 子图纯粹服务前端展现,禁止作为微服务间内部 RPC 协议 | ADR-037 | +| DataScope 必用 @requires | 跨服务数据范围必须通过 `@requires` 指令运行时解析,禁止跨库 JOIN | ADR-024/041 | +| @key 必用 DataLoader | 所有子图 `@key` Reference Resolver 必须使用 DataLoader 批量加载 | ADR-035 | +| Router-Authorization | Router 请求子图携带信任凭证 header,子图 Guard 校验,拒绝非 Router | ADR-036 | +| Outbox + CDC 结合 | 业务代码只写 Outbox 表,Debezium 监听 binlog 投递,禁止轮询线程 | ADR-032 | +| Temporal 严格边界 | 仅限 AI 耗时工作流 + Saga,CRUD 短事务绝对禁止用 Temporal | ADR-030 | +| SSE 优先 | 推送默认走 SSE,WS 仅限监考等强双向场景 | ADR-029 | +| Eager Invalidation | 写后同步 Redis DEL,Kafka 投影器仅作兜底 | ADR-038 | +| 乐观锁版本号回传 | 写接口返回 version,读携带 expectedVersion,ES 落后则穿透读 MySQL | ADR-039 | --- ## 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 工作流编排 | 长流程编排、可观测、可回滚 | ⏳ 规划中(当前由 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)实际落地 | 已采纳 | +| 编号 | 决策 | 原因 | 状态 | +| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 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 微前端 | 独立部署、技术栈无关、渐进迁移 | ⛔ 废弃(v2.1 由 ADR-033 portal-shell 替代) | +| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | ✅ 已采纳(v2.1 ADR-030 严格边界:仅限 AI 工作流 + Saga,CRUD 短事务禁用) | +| 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/* | 已采纳(v2.1 push-gateway 改名 realtime-gateway,ADR-034) | +| ADR-017 | classes 服务合并入 core-edu | 教学组织与教学核心共享聚合根边界 | 已采纳 | +| ADR-018 | BFF 统一 GraphQL Yoga + DataLoader + opossum 熔断 | 协议统一、批量去重、降级模式 B(degraded 子字段) | ⛔ 废弃(v2.1 由 ADR-023 Apollo Federation 替代) | +| ADR-019 | admin-portal GraphQL 经 teacher-bff 命名空间 | 复用 teacher-bff resolver,admin role 中间件强制隔离 | ⛔ 废弃(v2.1 由 ADR-033 portal-shell 替代) | +| ADR-020 | parent-portal 作为 MF Remote 被 teacher-portal 加载 | 家长入口可由教师端嵌入,统一 Shell 宿主 | ⛔ 废弃(v2.1 由 ADR-033 portal-shell 替代) | +| ADR-021 | Trace 存储 Jaeger 替代 Tempo | all-in-one 镜像部署简单,OTLP 原生支持 | 已采纳 | +| ADR-022 | 共享包 ui-tokens 替代原 shared-tokens 命名 | 三层令牌(primitive/semantic/tailwind-theme)实际落地 | 已采纳 | +| ADR-023 | 采用 Apollo Federation 替代 3 个手写 BFF | 工业标准,消灭样板代码(每字段改动从 6 处 → 2 处) | ✅ 已采纳(v2.1) | +| ADR-024 | DataScope 通过 @requires 运行时解析 | 解决跨库 JOIN 悖论,子图间传递可见范围 | ✅ 已采纳(v2.1) | +| ADR-025 | proto → GraphQL 自动生成 | 单一数据源,字段变更只改 proto,禁止手写 schema | ✅ 已采纳(v2.1) | +| ADR-026 | iam 拆分 config-service | 避免上帝服务(iam 承载认证+RBAC+审计+JWKS+插件配置,职责过载) | ✅ 已采纳(v2.1,config-service :3011/:50059) | +| ADR-027 | content CQRS 改造 | 写读分离,故障隔离(MySQL 写 + Neo4j/ES 读投影) | ✅ 已采纳(v2.1) | +| ADR-028 | ai 无状态化 | 明确边界,可扩展(WorkflowStateStore → Redis-only,TTL 1h) | ✅ 已采纳(v2.1) | +| ADR-029 | SSE 优先 + WS 仅监考 | K12 场景单向为主,WS 心跳/握手开销过大 | ✅ 已采纳(v2.1) | +| ADR-030 | Temporal 严格边界引入(v2.1 修订) | AI 工作流 + Saga 用 Temporal;CRUD 短事务绝对禁止,走同步调用 + 分布式锁 | ✅ 已采纳(v2.1,Temporal :7233/UI :8085/PG :5433) | +| ADR-031 | config-service + Redis 轻量配置中心 | K12 场景,不引入 etcd/Consul(过重) | ✅ 已采纳(v2.1) | +| ADR-032 | Outbox + CDC 结合(v2.1 修订) | 业务写 Outbox 表,Debezium 监听 binlog 自动投递(Transaction Log Tailing),废弃轮询线程 | ✅ 已采纳(v2.1) | +| ADR-033 | portal-shell 单容器替代 4 portal | Modular Monolith + Micro-kernel,减少重复(单 Dockerfile) | ✅ 已采纳(v2.1,portal-shell :4010) | +| ADR-034 | realtime-gateway 改名 | SSE 优先定位(原 push-gateway) | ✅ 已采纳(v2.1) | +| ADR-035 | DataLoader 强制(@key 解析器) | 消除 N+1 查询,ESLint 规则强制校验 | ✅ 已采纳(v2.1) | +| ADR-036 | Router-Authorization 信任凭证 | 拒绝非 Router 的直接 GraphQL 请求,防止绕过聚合层 | ✅ 已采纳(v2.1) | +| ADR-037 | 外部 GraphQL + 内部 gRPC 边界 | GraphQL 仅服务前端展现,后端互调走 gRPC(性能 + 强类型 + 流式) | ✅ 已采纳(v2.1) | +| ADR-038 | Eager Invalidation(主动失效) | 写后同步 Redis DEL,Kafka 投影器仅作兜底,降低读延迟 | ✅ 已采纳(v2.1) | +| ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version,读携带 expectedVersion,ES 落后则穿透读 MySQL | ✅ 已采纳(v2.1) | +| ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafka,msg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳(v2.1) | +| ADR-041 | ScopeToken 优化大规模 ID 列表 | 不传全量数组,传极短 token,子图从 Redis SMEMBERS 获取,降低 HTTP payload 开销 | ✅ 已采纳(v2.1) | --- @@ -1537,4 +1940,66 @@ graph LR --- -> **本文件 v2.0 已基于代码现状(2026-07-14)全面校准。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。** +## 16. v2.1 实施状态索引(v2.1 新增) + +> 本节记录 v2.1 架构重设计(Apollo Federation + portal-shell)的实施进度。 +> 设计源:[v2 重设计 spec](../superpowers/specs/2026-07-14-architecture-v2-redesign-design.md) v2.1 + [portal-shell 仪表盘 spec](../superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md) v2.1。 +> 实施遵循 11 阶段迁移计划(M0-M11),跨模块变更顺序:shared-proto → 业务服务 → apollo-router → portal-shell(见 [项目规则 §14.4](../../.trae/rules/project_rules.md))。 + +### 16.1 迁移阶段状态 + +| 阶段 | 内容 | 退出标准 | 依赖 | 状态 | +| ---- | --------------------------------------------------------- | ---------------------------------------------- | ------- | ------- | +| M0 | proto → GraphQL 代码生成工具链 | `buf generate` 输出 GraphQL schema | 无 | ✅ 完成 | +| M0.5 | Debezium Connect 部署 + Outbox connector | Debezium 监听 outbox 表推送到 Kafka | 无 | ✅ 完成 | +| M1 | 各服务暴露 GraphQL 子图 + DataLoader + RouterAuthGuard | `/graphql` 端点可查询,非 Router 请求被拒 | M0 | ✅ 完成 | +| M2 | apollo-router 部署 + Supergraph 组装 | Router 可聚合查询 | M1 | ✅ 完成 | +| M3 | iam 拆分 config-service | config-service 独立运行(:3011/:50059) | M1 | ✅ 完成 | +| M4 | DataScope @requires + ScopeToken 实现 | 教师查询可见班级成绩正确 | M2 | ✅ 完成 | +| M5 | content CQRS 改造(投影器 + Eager Invalidation + 乐观锁) | Neo4j/ES 通过投影器同步,写后读一致 | M0.5/M1 | ✅ 完成 | +| M6 | ai 无状态化 | ai 不存业务数据,状态在 Redis(TTL 1h) | M1 | ✅ 完成 | +| M6.5 | Temporal Server 部署 + ai 接入 Worker | AI 耗时工作流可运行(:7233/UI :8085/PG :5433) | M6 | ✅ 完成 | +| M7 | realtime-gateway SSE 优先 + Redis Pub/Sub | SSE 推送可用,边缘不挂 Kafka | 无 | ✅ 完成 | +| M8 | portal-shell 接入 apollo-router | portal-shell 查询走 Router | M2/M4 | ✅ 完成 | +| M9 | 旧 BFF 下线(teacher/student/parent-bff) | 流量为 0 | M8 | ✅ 完成 | +| M10 | 旧 portal 下线 | 流量为 0 | M8 | ✅ 完成 | +| M11 | 废弃各服务 OutboxPublisher 轮询线程 | 无服务使用轮询投递 | M0.5 | ✅ 完成 | +| M12 | arch.db + 004 文档同步 | arch:scan 通过 | 全部 | ✅ 完成 | + +### 16.2 v2.1 新增/变更服务实施状态 + +| 服务 | 类型 | 端口 | 关键变更 | 状态 | +| ---------------- | ---- | ----------------- | ------------------------------------------------------------ | ------- | +| apollo-router | 新增 | :3000 | Apollo Federation 聚合层,替代 3 BFF(ADR-023) | ✅ 完成 | +| config-service | 新增 | :3011/:50059 | 从 iam 拆分,插件配置 + 布局 + 用户偏好(ADR-026) | ✅ 完成 | +| portal-shell | 新增 | :4010 | Modular Monolith 单容器,替代 4 portal(ADR-033) | ✅ 完成 | +| Temporal | 新增 | :7233/:8085/:5433 | AI 工作流 + Saga 引擎,严格边界(ADR-030) | ✅ 完成 | +| realtime-gateway | 改名 | :8081 | 原 push-gateway,SSE 优先 + Redis Pub/Sub(ADR-029/034/040) | ✅ 完成 | +| iam | 拆分 | :3002/:50052 | 移除插件配置职责,专注认证+RBAC+JWKS+DataScope | ✅ 完成 | +| content | 改造 | :3005/:50054 | CQRS:MySQL 写 + Neo4j/ES 读投影(ADR-027) | ✅ 完成 | +| ai | 改造 | :3008/:50058 | 无状态化,WorkflowStateStore → Redis(ADR-028) | ✅ 完成 | + +### 16.3 v2.1 下线服务 + +| 服务 / 应用 | 下线原因 | 对应阶段 | +| ------------------------ | --------------------------------------------------- | -------- | +| teacher-bff | 流量切到 apollo-router 后下线 | M9 | +| student-bff | 流量切到 apollo-router 后下线 | M9 | +| parent-bff | 流量切到 apollo-router 后下线 | M9 | +| teacher-portal | 流量切到 portal-shell 后下线 | M10 | +| student-portal | 流量切到 portal-shell 后下线 | M10 | +| parent-portal | 流量切到 portal-shell 后下线 | M10 | +| admin-portal | 流量切到 portal-shell 后下线 | M10 | +| OutboxPublisher 轮询线程 | 由 Debezium Transaction Log Tailing 替代(ADR-032) | M11 | + +### 16.4 v2.1 关联 spec 文档 + +| 文档 | 用途 | +| --------------------------------------------------------------------------- | ---------------------------------------------------------- | +| `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 spec(ADR-023~041 决策源) | +| `docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md` | portal-shell 插件化仪表盘设计 spec | +| `infra/port-allocation.md` | 端口分配唯一源(含 apollo-router/config-service/Temporal) | + +--- + +> **本文件 v2.1 已基于代码现状(2026-07-15)全面校准,反映 Apollo Federation + portal-shell 架构重设计(M0-M11 全部完成)。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。**