Files
Edu/docs/architecture/004_architecture_impact_map.md

2006 lines
130 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构影响地图(微服务版)
> 版本2.1
> 日期2026-07-15
> 状态实施状态同步v2.1 架构重设计已落地M0-M10 全部完成)
> 适用范围Edu 微服务架构DDD + EDA + CQRS + Apollo Federation
> 关联文档:
>
> - [理想蓝图](./0010_architecture.md)
> - [项目规则](../../.trae/rules/project_rules.md)
> - [路线图](./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.1 变更摘要**2026-07-15完成 v2 架构重设计的全部阶段M0-M10。核心变更(1) **Apollo Federation 联邦**3 个手写 BFFteacher-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-gatewaySSE 默认推送WS 仅监考msg worker 消费 Kafka → 发 Redis Pub/Sub → realtime-gateway 订阅推送,边缘网关不直接挂 KafkaADR-029/040(5) **CDC + Outbox 结合**:废弃 OutboxPublisher 轮询线程,改由 Debezium 监听 binlog 自动投递 Outbox 表到 KafkaTransaction Log TailingADR-032(6) **portal-shell 单容器**4 个旧 portalteacher-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/50059ADR-026(8) **content CQRS 改造**MySQL 写 + Neo4j/ES 读投影 + Eager Invalidation + 乐观锁版本号ADR-027/038/039(9) **ai 无状态化**WorkflowStateStore 改为 Redis-onlyTTL 1hADR-028(10) **外部 GraphQL + 内部 gRPC 边界**:前端必须经 Apollo Router 调子图,后端互调走 gRPCADR-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.db22 模块 / 4715 符号 / 475 契约),据此修正 §11.1 proto 统计8 文件 / 23 service / 305 message / 139 RPC、§11.2 proto 包名(`next_edu_cloud.<domain>.v1`、§15.4 core-edu gRPC9 service / 43 RPC、§15.6 msg gRPC17 RPC、§15.7 data-ana gRPC18 RPC、§15.10 student-bff 模块8、§15.11 parent-bff 模块6
---
## 目录
1. [项目概述](#1-项目概述)
2. [技术栈](#2-技术栈)
3. [分层架构](#3-分层架构)
4. [服务依赖图](#4-服务依赖图)
5. [认证与权限](#5-认证与权限)
6. [数据访问与缓存](#6-数据访问与缓存)
7. [事件驱动架构](#7-事件驱动架构)
8. [跨服务协作](#8-跨服务协作)
9. [核心业务流程](#9-核心业务流程)
10. [可观测性](#10-可观测性)
11. [契约与 API 架构](#11-契约与-api-架构)
12. [架构约束](#12-架构约束)
13. [ADR 记录](#13-adr-记录)
14. [附录6 阶段路线图](#14-附录6-阶段路线图)
15. [实施状态索引v2.0 新增)](#15-实施状态索引v20-新增)
16. [v2.1 实施状态索引v2.1 新增)](#16-v21-实施状态索引v21-新增)
---
## 1. 项目概述
### 1.1a 技术分层视角(系统边界)
> 本图展示**部署分层结构**(自上而下:用户 → 微前端 → 网关 → Apollo Router → 业务服务 → 总线 → 数据)。
> v2.1 关键变化4 个 MF portal 由 portal-shell 单容器替代3 个 BFF 由 apollo-router 联邦替代push-gateway 改名 realtime-gatewaySSE 优先);新增 config-service新增 TemporalOutbox 轮询废弃,改由 Debezium 监听 binlog。
> 业务领域视角见 [1.1b](#11b-业务领域视角)。端口标注见 [infra/port-allocation.md](../../infra/port-allocation.md)(唯一源)。
```mermaid
graph TB
subgraph Users["用户层(场景域用户)"]
Teacher["教学场景域用户<br/>(教师 / 教导主任 / 教研组长 共用)"]
Student["学习场景域用户<br/>(学生)"]
Parent["家长场景域用户<br/>(家长)"]
Admin["管理场景域用户<br/>(系统管理员 / 校管理员)"]
end
subgraph MFE["微前端层v2.1:单容器 Modular Monolith<br/>Next.js 15 App Router + RSC + Zustand + SWR"]
PortalShell["portal-shell :4010<br/>Shell 宿主 + 插件化仪表盘<br/>替代 4 个旧 portal"]
end
subgraph Gateway["边缘网关层Go 1.25 / Gin"]
APIGateway["api-gateway :8080<br/>JWT RS256 + JWKS + 限流 + 熔断"]
RealtimeGw["realtime-gateway :8081<br/>SSE 优先 + WS 仅监考<br/>Redis Pub/Sub 订阅(不直挂 Kafka"]
end
subgraph Federation["GraphQL 联邦层v2.1 新增)<br/>Apollo RouterRust 官方二进制)"]
ApolloRouter["apollo-router :3000<br/>自动查询计划 + @requires DataScope<br/>替代 3 个手写 BFF"]
end
subgraph Services["业务微服务NestJS + FastAPI全部暴露 GraphQL 子图)"]
IAM["iam :3002 / gRPC :50052<br/>认证 + RBAC + JWKS + DataScope拆分后"]
ConfigSvc["config-service :3011 / gRPC :50059<br/>插件配置 + 布局 + 用户偏好v2.1 新增)"]
CoreEdu["core-edu :3004 / gRPC :50053<br/>教学核心 + 组织(含 DataScope @requires"]
Content["content :3005 / gRPC :50054<br/>CQRSMySQL 写 + Neo4j/ES 读投影"]
DataAna["data-ana :3006 / gRPC :50055<br/>FastAPI + CDC consumer + ClickHouse"]
Msg["msg :3007 / gRPC :50056<br/>4 模块 + 4 渠道 + msg workerRedis Pub/Sub"]
AI["ai :3008 / gRPC :50058<br/>FastAPI 无状态化 + Temporal Worker"]
end
subgraph Workflow["工作流层v2.1 新增)"]
Temporal["Temporal Server :7233<br/>AI 工作流 + Saga严格边界<br/>PostgreSQL 持久化 + UI :8085"]
end
subgraph Bus["事件总线"]
Kafka[("Kafka<br/>双 listener 29092/9092")]
Debezium["Debezium Connect 2.7<br/>监听 outbox 表 binlog → Kafka<br/>Transaction Log Tailing"]
end
subgraph Data["数据层"]
MySQL[("MySQL 8.0<br/>每服务独占 schema")]
Redis[("Redis 7<br/>缓存/会话/Pub/Sub/ScopeToken/ai workflow")]
ClickHouse[("ClickHouse 24.3<br/>读模型宽表")]
Neo4j[("Neo4j 5.20<br/>知识图谱content 读投影)")]
ES[("Elasticsearch 8.13<br/>题库检索content 读投影)")]
TemporalPG[("PostgreSQL<br/>Temporal 持久化")]
end
Teacher --> PortalShell
Student --> PortalShell
Parent --> PortalShell
Admin --> PortalShell
PortalShell --> APIGateway
PortalShell -.SSE 推送.-> RealtimeGw
APIGateway --> ApolloRouter
APIGateway -.REST 透传.-> IAM
APIGateway -.REST 透传.-> Msg
RealtimeGw -.Redis Pub/Sub 订阅 user:userId:notify.-> Msg
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
DataAna <--> Kafka
Msg <--> Kafka
IAM <--> Kafka
AI <--> Kafka
ConfigSvc <--> Kafka
MySQL --> Debezium
Debezium --> Kafka
IAM --> MySQL
ConfigSvc --> MySQL
CoreEdu --> MySQL
Content --> MySQL
Msg --> MySQL
Content -.Kafka 投影器.-> Neo4j
Content -.Kafka 投影器.-> ES
DataAna --> ClickHouse
Temporal --> TemporalPG
IAM --> Redis
CoreEdu --> Redis
ConfigSvc --> Redis
Content --> Redis
Msg --> Redis
DataAna --> Redis
AI --> Redis
RealtimeGw --> Redis
PortalShell -.SWR 客户端轮询.-> ConfigSvc
```
### 1.1b 业务领域视角
> 本图按 **DDD 限界上下文**展示 7 个业务领域及其依赖关系v2.1 新增 D7 配置域,从 iam 拆出)。同一服务可横跨多个领域(如 core-edu 同时承载"教学组织"与"教学核心")。
> 技术分层视角见 [1.1a](#11a-技术分层视角系统边界)。
```mermaid
graph TB
subgraph D1["D1 身份认证领域iam 服务v2.1 拆分后)"]
IAM[iam 服务]
IAM_M["认证 + RBAC + JWKS + 审计 + DataScope<br/>users / roles / permissions / refresh_tokens / sessions<br/>totp / audit_logs / user_school_role<br/>子图暴露 visibleClassIds / visibleStudentIds@requires"]
end
subgraph D7["D7 配置域config-service 服务v2.1 新增)"]
CONFIG[config-service 服务]
CONFIG_M["插件配置 + 布局 + 用户偏好<br/>plugin_registry / role_plugin_mapping / role_layout_default<br/>layout_templates / user_layout_override / plugin_packages<br/>三层合并:系统默认 < 角色默认 < 用户调整"]
end
subgraph D2["D2 教学组织领域core-edu 服务)"]
ORG["core-edu 服务<br/>classes + scheduling + leave-requests"]
ORG_M["classes / teacher-associations / subjects<br/>schedule / leave-requests"]
end
subgraph D3["D3 教学核心领域core-edu 服务)"]
TEACH["core-edu 服务<br/>exams + homework + grades + attendance"]
TEACH_M["exams / exam-extensions / homework / grades<br/>attendance / dashboard / admin / iam-consumer<br/>(含 state machine + datascope-injector + @requires ScopeToken"]
end
subgraph D4["D4 内容资源领域content 服务v2.1 CQRS 改造)"]
CONTENT["content 服务"]
CONTENT_M["7 模块: textbooks / chapters / knowledge-points<br/>questions / electives / lesson-plans / course-plans<br/>MySQL 写 + Outbox → Kafka 投影器 → Neo4j/ES 读<br/>Eager Invalidation + 乐观锁版本号"]
end
subgraph D5["D5 沟通通知领域msg 服务)"]
MSG["msg 服务"]
MSG_M["4 模块: notifications / preferences / templates / announcements<br/>4 渠道: email / sms / push / in-app<br/>msg worker 消费 Kafka → Redis Pub/Sub → realtime-gateway<br/>v2.1:不再让边缘网关直挂 Kafka"]
end
subgraph D6["D6 智能洞察领域data-ana + ai 服务v2.1 ai 无状态化)"]
DATA["data-ana 服务"]
AI["ai 服务"]
DATA_M["FastAPI HTTP /analytics + gRPC 50055<br/>CDC consumer + ClickHouse 宽表<br/>analytics / dashboard / diagnostic / warnings / mastery"]
AI_M["FastAPI HTTP /v1/ai + gRPC 50058<br/>无状态 LLM 推理引擎WorkflowStateStore → Redis TTL 1h<br/>Temporal WorkerAI 耗时工作流 + Saga<br/>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
CONTENT --> DATA
TEACH --> DATA
AI -.gRPC.-> CONTENT
AI -.gRPC.-> DATA
AI -.gRPC.-> IAM
AI -.Temporal Worker.-> D6
```
**双图并存说明**
- **1.1a 技术分层**:描述部署、流量路径、网络边界,关注"如何部署与调用"
- **1.1b 业务领域**:描述 DDD 限界上下文、聚合根、领域依赖,关注"业务边界与归属"
- 两图互补,分别服务于运维/SRE 与产品/架构视角
### 1.2 服务清单
> 端口、阶段、实施状态基于 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 端口 | 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-gatewaySSE 优先 |
| 联邦 | 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 | ✅ 子图 | 内容资源CQRSMySQL 写 + 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-030UI :8085PG :5433 |
| 微前端 | portal-shell | TS (Next.js 15 App Router) | 4010 | — | — | 教师端 ShellModular 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 Hooksauth / 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-eduC1 裁决),目录保留作历史参考,不再构建部署。
### 1.3 CICD → Edu 模块映射
| CICD 模块(旧) | Edu 服务(新) | 迁移阶段 |
| ----------------------------------------- | -------------- | -------- |
| auth + users + rbac | iam | P2 |
| classes + subjects + enrollment | core-edu | P3 |
| courses + lessons + schedule + attendance | core-edu | P3 |
| assignments + grades + exams | core-edu | P3 |
| textbooks + knowledge-points | content | P4 |
| questions + grading | content | P4 |
| messaging + notifications | msg | P5 |
| analytics + dashboard + diagnostic | data-ana | P4 |
| ai + lesson-preparation | ai | P5 |
| search | content (ES) | P4 |
---
## 2. 技术栈
### 2.1 多语言技术栈矩阵
| 层级 | 语言 | 框架/版本 | 用途 |
| -------- | --------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ |
| 网关层 | 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、aiai 含 Temporal Worker |
| 工作流 | Go / TS | Temporal 1.23 SDK | temporal server仅 AI 耗时工作流 + SagaCRUD 短事务禁用) |
| 微前端 | TypeScript 5.6+ | Next.js 15 App Router + RSC + Zustand + SWR + Tailwind + shadcn 风格 | portal-shell单容器 Modular Monolith + Micro-kernel |
| 契约 | protobuf | buf v2FILE 级 breaking+ proto → GraphQL 生成器 | 跨语言契约定义与生成M0 工具链扩展 GraphQL 输出) |
| 包管理 | — | pnpm 11 / go.work / uv workspace | 多语言 monorepo |
> **v2.1 变更**
>
> - 新增 Apollo Federation 2@key/@requires/@extends 指令)+ Apollo RouterRust官方二进制
> - 新增 Temporal 1.23(仅 AI 工作流 + SagaADR-030
> - 新增 SSEServer-Sent Events替代 WebSocket 单向推送场景)
> - 新增 Debezium Connect Outbox connectorTransaction Log Tailing废弃轮询线程
> - 新增 Zustand + SWRportal-shell 状态管理 + 静默刷新)
> - 新增 StrawberryPython GraphQL 子图库)+ @nestjs/graphqlTS GraphQL 子图库)
> - **下线** Module Federation@module-federation/nextjs-mf
> - **下线** 3 个 BFF 的 GraphQL Yoga
> - **下线** WebSocket 单向推送场景(仅保留监考双向场景)
### 2.2 多存储矩阵
| 存储 | 版本 | 用途 | 使用服务 |
| ------------- | -------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| 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 + 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 存储 + UIOTLP 4317/4318 |
| Loki | 3.2.1 | 日志聚合 |
| Promtail | 3.2.1 | 日志采集 |
| Prometheus | v2.51.0 | 指标采集(--web.enable-lifecycle15d 保留) |
| 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.1 修正**Temporal 已落地M6.5ADR-030严格立规矩——仅限 AI 耗时工作流 + Saga 分布式事务补偿CRUD 短事务绝对禁止。Trace 存储使用 **Jaeger all-in-one**OTLP 接收),原 v1.0 文档误写为 Tempo。
---
## 3. 分层架构
### 3.1 六层架构v2.1 修订)
```mermaid
graph TB
subgraph L1["L1 客户端层"]
Browser[浏览器]
Mobile[移动端]
end
subgraph L2["L2 微前端层v2.1:单容器)"]
MFE[portal-shell :4010<br/>Next.js 15 App Router + RSC 预取<br/>Modular Monolith + Micro-kernel]
end
subgraph L3["L3 边缘网关层Go 1.25 / Gin"]
GW[api-gateway :8080<br/>HTTP 反向代理 + JWT RS256 + 限流 + 熔断]
RTG[realtime-gateway :8081<br/>SSE 优先 + WS 仅监考<br/>Redis Pub/Sub 订阅(不直挂 Kafka]
end
subgraph L4["L4 GraphQL 联邦层v2.1 新增,替代 BFF"]
ROUTER[apollo-router :3000<br/>Apollo Federation 2<br/>自动查询计划 + @requires DataScope]
end
subgraph L5["L5 业务微服务层(全部暴露 GraphQL 子图)"]
SVC[7 业务服务<br/>iam / config-service / core-edu / content / msg / data-ana / ai<br/>DDD + CQRS + Outbox + DataLoader + RouterAuthGuard]
TEMPORAL_SVC[Temporal Worker<br/>仅 ai 服务AI 工作流 + Saga]
end
subgraph L6["L6 数据与总线层"]
DB[(MySQL / ClickHouse / Neo4j / ES / PostgreSQL)]
REDIS[(Redis 7)]
KAFKA[(Kafka + Debezium Outbox Transaction Log Tailing)]
TEMPORAL_DB[(Temporal PostgreSQL :5433)]
end
Browser --> MFE
Mobile --> MFE
MFE --> GW
MFE -.SSE 推送.-> RTG
GW -- HTTP 代理REST --> SVC
GW -- HTTP 代理GraphQL --> ROUTER
ROUTER -- 子图查询(@requires ScopeToken --> SVC
SVC -- gRPC内部 RPC禁止 GraphQL 子图互调) --> SVC
SVC --> DB
SVC --> REDIS
ROUTER --> REDIS
SVC <--> KAFKA
RTG -.Redis Pub/Sub 订阅.-> REDIS
SVC -.Outbox 表 + Debezium 监听.-> KAFKA
TEMPORAL_SVC -- gRPC :7233 --> TEMPORAL_DB
```
### 3.2 依赖方向
```
L1 客户端 → L2 微前端portal-shell → L3 网关 → L4 Apollo Router → L5 业务服务 → L6 数据/总线
L5 内部互调走 gRPC禁止子图互调
```
**严格规则**v2.1 新增 ADR-037
1. L3 网关层只做路由、鉴权、限流、熔断,**不写业务逻辑**;只做 HTTP 反向代理,不做协议转换
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
---
## 4. 服务依赖图
```mermaid
graph TB
subgraph Gateway["边缘网关层"]
APIGW[api-gateway :8080]
RTGW[realtime-gateway :8081]
end
subgraph Federation["GraphQL 联邦层v2.1 新增)"]
ROUTER[apollo-router :3000<br/>Rust 官方二进制]
end
subgraph Portal["微前端层v2.1:单容器)"]
SHELL[portal-shell :4010]
end
subgraph Services["业务服务(全部暴露 GraphQL 子图)"]
IAM[iam :3002 / gRPC :50052]
CONFIG[config-service :3011 / gRPC :50059<br/>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<br/>AI 工作流 + Saga]
end
subgraph Shared["共享包"]
Proto[shared-proto<br/>8 proto + GraphQL 生成器]
SharedTS[shared-ts<br/>outbox/dataloader/guards]
SharedGo[shared-go<br/>env/jwks/logger/tracer]
SharedPy[shared-py]
Contracts[contracts<br/>权限点]
Hooks[hooks<br/>React Hooks]
UIComps[ui-components<br/>含 PluginCard 系列]
UITokens[ui-tokens]
end
%% 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
%% realtime-gateway → Redis Pub/Sub订阅 msg worker 推送)
RTGW -. Redis Pub/Sub user:userId:notify .-> Msg
%% AI → Temporal
AI -- gRPC Worker :7233 --> TEMP
%% 业务服务间事件Outbox + Debezium → Kafka
CoreEdu -. Kafka 事件 .-> Content
CoreEdu -. Kafka 事件 .-> DataAna
CoreEdu -. Kafka 事件 .-> Msg
Content -. Kafka 事件 .-> DataAna
IAM -. Kafka 事件 .-> CoreEdu
IAM -. Kafka 事件 .-> Msg
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
RTGW --> SharedGo
IAM --> SharedTS
CONFIG --> SharedTS
CoreEdu --> SharedTS
Content --> SharedTS
Msg --> SharedTS
SHELL --> UIComps
SHELL --> UITokens
SHELL --> Hooks
SHELL --> Contracts
```
### 4.1 服务间通信矩阵v2.1 修订)
| 调用方 → 被调用方 | 协议 | 场景 |
| ------------------------------- | ---------------- | ----------------------------------------------------- |
| 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 ChannelADR-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 耗时工作流 + SagaWorker 注册) |
| AI → Kafka | Kafka 生产 | AI 用量事件发布(`edu.ai.usage.recorded` |
### 4.2 Apollo Router 子图清单v2.1 替代 BFF 下游矩阵)
> 7 个业务服务暴露 GraphQL 子图,由 apollo-router 自动组装 Supergraph。每个子图必须实现 RouterAuthGuardADR-036+ DataLoaderADR-035
| 子图 | 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)已废弃,由本节替代。
---
## 5. 认证与权限
### 5.1 JWT RS256 认证流程
> 实施细节api-gateway 通过 JWKS Fetchershared-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 (Go)
participant IAM as iam 服务
participant SVC as 下游服务 (NestJS / FastAPI)
rect rgb(240, 248, 255)
Note over U,IAM: 阶段 1: 登录签发
U->>GW: POST /api/v1/iam/login {username, password}
GW->>IAM: HTTP 反向代理(无鉴权路由)
IAM->>IAM: bcrypt 校验 + 查询权限 (PermissionCacheService)
IAM->>IAM: 生成 JWTRS256 私钥签发)+ refresh_token
IAM->>IAM: 写 iam_outboxUSER_EVENTS
IAM-->>GW: {accessToken, refreshToken, userInfo}
GW-->>U: 200 Set-Cookie httpOnly + ActionState 信封
end
rect rgb(240, 255, 240)
Note over U,SVC: 阶段 2: 请求鉴权
U->>GW: GET /api/v1/classes + Authorization: Bearer <token>
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
```
### 5.2 三层角色模型
| 层级 | 来源 | 示例 | 优先级 |
| -------- | ------------- | ------------------------------- | ------ |
| 系统角色 | 系统预设 | admin、teacher、student、parent | 最高 |
| 组织角色 | 学校/班级分配 | 年级组长、班主任、学科组长 | 中 |
| 临时角色 | 临时授权 | 代课教师、临时代理 | 最低 |
**规则**:权限取三层角色权限的并集,拒绝权限取交集(任一层拒绝则拒绝)。
### 5.3 DataScope 6 级数据范围
| 级别 | 名称 | 数据范围 | 典型角色 |
| ---- | -------- | ---------- | ------------ |
| L0 | SELF | 仅本人数据 | 学生、家长 |
| L1 | CLASS | 本班数据 | 班主任、学生 |
| L2 | GRADE | 本年级数据 | 年级组长 |
| L3 | SCHOOL | 本校数据 | 校管理员 |
| L4 | DISTRICT | 本区数据 | 区教研员 |
| L5 | ALL | 全部数据 | 系统管理员 |
**实现**:业务服务在 Repository 层根据 dataScope 级别动态注入 WHERE 条件。
### 5.4 视口四层模型
视口Viewport是用户在特定场景域下的可见范围。视口既可独立配置RoleViewport 表),
也可由权限推导permission → viewport 默认映射)。新角色只需配置权限集,视口自动推导;
需要差异化时再显式配置视口。
| 层级 | 含义 | 配置载体 | 示例 |
| ------- | ---------------- | ---------------------------------- | -------------------------------------------- |
| L1 导航 | 侧边栏菜单项 | navigation_config 表 | 教导主任看到"全校成绩分析"菜单 |
| L2 路由 | 可访问路由 | route_permission 表 + Gateway 校验 | 教导主任可访问 /admin/grade-analysis |
| L3 组件 | 页面内组件可见性 | usePermission().hasPermission() | 教导主任看到"导出全校报表"按钮 |
| L4 数据 | 数据行级过滤 | DataScope 枚举 | 教导主任 DataScope=grade_managed管辖年级 |
**v2.1 场景域复用策略**(替代 v1 的 BFF 复用):
按"使用场景域"在 portal-shell 内通过插件配置差异化admin 在 config-service 配置角色插件集 + 角色 Layout 默认 + 角色级 props而非按角色分 BFF。新角色复用 portal-shell + apollo-router通过插件配置 + DataScope 差异化。
| 场景域 | 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 等) | 系统管理员、校管理员 |
**实现**:教导主任归入教学场景 + 额外管理视口L1 导航增加管理菜单项L4 数据范围扩大到年级),通过 config-service 配置 role_plugin_mapping 实现。
**iam 服务职责**v2.1 拆分后ADR-026
- 认证:登录/登出/JWT/2FA
- RBAC角色/权限/角色-权限映射 CRUD
- 视口配置:导航/路由/组件级视口配置 CRUD
- DataScope数据范围解析all/grade_managed/class_taught/children/owned + 自定义)
- 权限解析 APIgetEffectivePermissions(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 GuardRouterAuthGuard必须拦截并校验该 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 端点
- 业务服务之间互调走 gRPCADR-037不走 GraphQL 子图
---
## 6. 数据访问与缓存
### 6.1 CQRS 读写分离v2.1 content 改造ADR-027
```mermaid
graph LR
subgraph Write["写路径content 服务仅写 MySQL"]
Cmd[Command 命令] --> App[Application Service]
App --> Domain[Domain 领域模型]
Domain --> Repo[Repository 写模型]
Repo --> mysql_w[(MySQL 主库)]
App --> Outbox[(Outbox 表<br/>同事务原子)]
end
subgraph Eager["Eager Invalidationv2.1 ADR-038"]
Outbox --> SyncDel[事务提交后同步<br/>Redis DEL 主动失效]
end
subgraph Sync["同步链路Debezium 监听 binlog"]
Outbox --> Debezium[Debezium Connect<br/>监听 outbox 表 binlog]
Debezium --> kafka_sync[(Kafka<br/>Transaction Log Tailing)]
kafka_sync --> Proj[投影器 worker]
Proj --> ch_sync[(ClickHouse 宽表)]
Proj --> redis_sync[(Redis 缓存<br/>兜底清理)]
Proj --> es_sync[(ES 索引)]
Proj --> neo4j_sync[(Neo4j 知识图谱)]
end
subgraph Read["读路径content 子图读 MySQL + Neo4j + ES"]
Query[Query 查询] --> ReadModel[Read Model]
ReadModel --> ch_read[(ClickHouse 宽表)]
ReadModel --> redis_read[(Redis 缓存)]
ReadModel --> es_read[(ES 索引<br/>含 version 字段)]
ReadModel --> neo4j_read[(Neo4j 知识图谱)]
ReadModel --> mysql_read[(MySQL 强一致穿透读)]
end
```
> **v2.1 变更**
>
> - content 服务只写 MySQL不再直接写 Neo4j/ES由投影器消费 Kafka 同步)
> - OutboxPublisher 轮询线程废弃,改由 Debezium 监听 binlog 自动投递ADR-032
> - 新增 Eager Invalidation事务提交后同步发 Redis DEL 主动失效ADR-038
> - 新增乐观锁版本号:写返回 version读携带 expectedVersionES 落后则穿透读 MySQLADR-039
### 6.2 Apollo Router 混合读策略v2.1 替代 BFF 混合读)
```mermaid
flowchart TD
Q[Apollo Router 收到 Query] --> C1{子图本地 Redis 命中?}
C1 -- 是 --> R1[返回缓存]
C1 -- 否 --> C2{需要跨子图聚合?}
C2 -- 否 --> C3[调单一子图 /graphql]
C3 --> C4[子图内 Redis 缓存]
C4 --> R2[返回]
C2 -- 是 --> C5[并发调多子图 /graphql<br/>@requires 传递 ScopeToken]
C5 --> C6[Router 内存聚合]
C6 --> R3[返回聚合结果]
R2 --> C7[子图回填 Redis]
R3 --> C7
```
### 6.3 缓存策略矩阵v2.1 修订)
| 数据类型 | 存储 | 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 InvalidationADR-038**
- 写请求在 MySQL 事务提交后的代码行,直接同步发 Redis DEL 命令删除查询缓存(毫秒级主动失效)
- Redis DEL 在事务**外**执行(事务提交后),避免 Redis 故障导致业务回滚
- 若 Redis DEL 失败,不影响业务结果(兜底由 content-cache-projector 处理)
- Kafka 的 content-cache-projector 仅作为防止网络抖动的兜底清理,不是主失效路径
**乐观锁版本号ADR-039**
- 写接口返回记录的 `updated_at``version`
- 前端携带 `expectedVersion` 发起 QueryRouter 透传到 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 拉新值,回填 RedisTTL 5 分钟兜底)
> portal-shell 侧采用 SWR 静默后台刷新revalidateOnFocus + refreshInterval 5min用户切回 Tab 时自动检测配置变化Toast 提示"发现新布局配置,点击刷新生效"。无需 Kafka + WebSocket 推送链路,简化架构。
---
## 7. 事件驱动架构
### 7.1 Outbox + Debezium Transaction Log Tailingv2.1 ADR-032
> **v2.1 关键变更**:废弃 OutboxPublisher 轮询线程,改由 Debezium 监听 binlog 自动投递 Outbox 表到 KafkaTransaction Log Tailing。CDC 与 Outbox 不再二选一而是分工结合Outbox 定义"发什么"业务语义CDC 解决"怎么发"(技术传输)。
```mermaid
graph LR
subgraph Service["业务服务"]
Cmd[Command 处理]
Domain[Domain 聚合]
Repo[Repository]
end
subgraph MySQL["MySQL 主库"]
BizTable[(业务表)]
OutboxTable[(outbox 表<br/>同事务原子)]
end
subgraph CDC["Debezium Connect监听 binlogv2.1 替代轮询)"]
Debezium[Debezium 2.7<br/>伪装成 MySQL Slave<br/>监听 outbox 表 binlog]
SMT[SMT Topic Router<br/>重写为业务语义 topic]
end
subgraph Bus["事件总线"]
kafka_bus[(Kafka topic<br/>edu.<domain>.<aggregate>.<action>)]
end
subgraph Consumers["消费者"]
Proj[投影器 worker<br/>CQRS 读模型同步]
OtherSvc[其他服务<br/>业务订阅]
RealtimeGW[realtime-gateway<br/>via msg worker + Redis Pub/Sub]
end
Cmd --> Domain
Domain --> Repo
Repo --> BizTable
Repo --> OutboxTable
OutboxTable -.binlog row event.-> Debezium
Debezium --> SMT
SMT --> kafka_bus
kafka_bus --> Proj
kafka_bus --> OtherSvc
kafka_bus --> MsgWorker[msg worker<br/>消费 edu.notify.* ]
MsgWorker --> RedisPUB[Redis Pub/Sub<br/>user:userId:notify]
RedisPUB --> RealtimeGW
Proj --> read_stores[(ClickHouse / Redis / ES / Neo4j)]
```
**关键原则**
- 业务代码只写业务表 + 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.<domain>.<aggregate>.<action>`。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 / 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 / config_outbox / core_edu_outbox / content_outbox / msg_outbox / ai_outbox由 shared-ts/outbox 模块统一管理。投递由 Debezium 自动完成。
> **必需依赖软失败标注**ARB-015 §17.6ISSUE-058 覆盖 ISSUE-055
>
> - 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 | 通知投递完成 | 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 → Kafkaedu.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 异步决策v2.1 修订)
```mermaid
flowchart TD
Req[跨服务协作需求] --> C1{需要强一致性?}
C1 -- 是 --> C2{调用方需要立即结果?}
C2 -- 是 --> C2a{是 CRUD 短事务?}
C2a -- 是 --> Sync[同步 gRPC + 分布式锁<br/>ADR-030 严禁 Temporal]
C2a -- 否 --> Saga[Saga 编排<br/>TemporalADR-030]
C1 -- 否 --> C3{需要事件最终一致?}
C3 -- 是 --> Async[异步 Kafka 事件<br/>Outbox + DebeziumADR-032]
C3 -- 否 --> C4{仅查询读取?}
C4 -- 是 --> Read[读模型冗余 / GraphQL @requires]
C4 -- 否 --> Sync
```
### 8.2 Apollo Router 同步聚合v2.1 替代 BFF 同步聚合)
```mermaid
sequenceDiagram
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->>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
R->>CoreEdu: 子图查询 @requires(fields: "classScopeToken")
CoreEdu->>CoreEdu: 从 Redis sMembers 获取实际 ID
CoreEdu-->>R: 班级列表DataScope 过滤后)
and
R->>DataAna: 子图查询
DataAna-->>R: 统计数据
end
R->>R: 内存聚合DataLoader 批量去重)
R-->>U: 仪表盘聚合数据
```
### 8.3 异步事件闭环
```mermaid
flowchart LR
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 工作流编排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 DBPostgreSQL业务数据仍在各服务 DB
```mermaid
flowchart TD
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 Setkey: `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 共享 TTL5min自动过期清理
**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_scopeTTL 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: 聚合返回客户端
```
---
## 9. 核心业务流程
### 9.1 考试生命周期
```mermaid
sequenceDiagram
participant T as 教师
participant BFF as teacher-bff
participant Core as core-edu
participant Msg as msg
participant DA as data-ana
participant S as 学生
rect rgb(240, 248, 255)
Note over T,Core: 阶段 1: 创建发布
T->>BFF: 创建考试
BFF->>Core: gRPC CreateExam
Core->>Core: 写入 MySQL + Outbox
Core-->>BFF: examId
BFF-->>T: 创建成功
Core->>Msg: ExamPublished 事件
Msg->>S: 推送考试通知
end
rect rgb(240, 255, 240)
Note over S,Core: 阶段 2: 作答提交
S->>BFF: 提交答卷
BFF->>Core: gRPC SubmitExam
Core->>Core: 写入答卷 + Outbox
Core-->>BFF: 提交成功
Core->>DA: ExamSubmitted 事件
DA->>DA: 记录提交行为
end
rect rgb(255, 240, 245)
Note over T,DA: 阶段 3: 批改出分
T->>BFF: 批改答卷
BFF->>Core: gRPC GradeExam
Core->>Core: 写入成绩 + Outbox
Core-->>BFF: 批改完成
Core->>DA: GradeRecorded 事件
DA->>DA: 更新掌握度
Core->>Msg: GradeRecorded 事件
Msg->>S: 推送成绩通知
end
```
### 9.2 高并发提交
```mermaid
flowchart TD
S[学生提交] --> GW[API Gateway]
GW --> GW1{限流检查}
GW1 -- 通过 --> BFF[BFF]
GW1 -- 拒绝 --> R1[429 限流响应]
BFF --> Core[CoreEdu]
Core --> C1{Redis 分布式锁}
C1 -- 获取锁 --> C2[写入答卷]
C2 --> C3[Outbox 事件]
C3 --> C4[释放锁]
C4 --> R2[成功]
C1 -- 锁竞争 --> C5[排队等待 500ms]
C5 --> C1
C5 --> C6{超时?}
C6 -- 是 --> R3[排队中,请稍后]
```
### 9.3 AI 辅助出题
```mermaid
sequenceDiagram
participant T as 教师
participant BFF as teacher-bff
participant AI as ai 网关
participant Content as content
participant DA as data-ana
T->>BFF: 请求 AI 出题
BFF->>AI: gRPC GenerateQuestions
AI->>Content: gRPC 查询知识点
Content-->>AI: 知识点列表
AI->>DA: gRPC 查询班级学情
DA-->>AI: 学情数据
AI->>AI: LLM 调用生成题目
AI-->>BFF: 生成的题目列表
BFF-->>T: 返回题目供教师审核
T->>BFF: 确认入库
BFF->>Content: gRPC CreateQuestions
Content-->>BFF: 入库成功
```
---
## 10. 可观测性
### 10.1 三支柱可观测性
> 实际落地Jaeger all-in-one 接收 OTLPHTTP 4318 / gRPC 4317Loki + 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-routerRust 官方二进制,内置 OTel spans + Prometheus 指标 + spec_compliant JSON 日志config-serviceNestJS复用 pino + prom-client + OTLPportal-shellNext.js复用 web-vitals + Sentry + OTel Web
```mermaid
graph TB
subgraph App["应用层"]
NESTJS[NestJS 服务<br/>pino + prom-client + OTLP]
GO[Go 网关<br/>slog + prometheus + otelgin]
PY[Python 服务<br/>structlog + prometheus-client + OTLP]
APOLLO[apollo-router :3000<br/>Rust 内置 OTel spans +<br/>Prometheus 指标 + spec_compliant 日志]
PORTAL[portal-shell :4010<br/>web-vitals + Sentry + OTel Web]
end
subgraph Collect["采集层"]
OTEL[OpenTelemetry SDK<br/>OTLP exporter]
PROM[Prometheus 抓取<br/>/metrics]
PROMTAIL[Promtail<br/>容器日志]
end
subgraph Storage["存储层"]
LOKI[(Loki 3.2.1<br/>日志)]
JAEGER[(Jaeger 1.57<br/>TraceOTLP 4317/4318)]
PROMDB[(Prometheus v2.51<br/>指标 15d 保留)]
end
subgraph Exporters["Exporters"]
NODE[node-exporter :9100<br/>host.docker.internal]
MYSQL[mysqld-exporter :9104]
REDIS[redis-exporter :9121]
end
subgraph Vis["可视化层"]
GRAFANA[Grafana 10.4<br/>统一面板 :3030]
ALERT[Alertmanager v0.27<br/>告警路由]
end
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
MYSQL --> PROM
REDIS --> PROM
LOKI --> GRAFANA
JAEGER --> GRAFANA
PROMDB --> GRAFANA
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-vitalsCLS/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<br/>otelgin 生成 traceId]
B --> C[apollo-router<br/>继承 W3C traceparent<br/>生成子图查询计划 span]
C --> D[业务服务子图 /graphql<br/>继承 HTTP traceparent]
D --> E[Kafka 事件<br/>traceId 写入 header]
E --> F[消费者服务<br/>继承 traceId]
F --> G[下游存储<br/>span 记录]
G --> H[Jaeger UI<br/>查询链路]
```
> v2.1 变更BFF 环节由 apollo-router 替代。Router 在子图查询计划中生成独立 spanquery_plan / fetch_subgraph便于在 Jaeger 中追踪每个子图的延迟贡献。
**规则**
- 所有跨服务调用必须透传 W3C Trace ContextHTTP `traceparent` 头 / gRPC metadata
- Kafka 事件必须将 traceId 写入消息 header
- 日志必须包含 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 |
| 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 自带 /healthgRPC health check |
---
## 11. 契约与 API 架构
### 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-148 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["shared-proto/proto/ (8 文件)"]
IamProto[iam.proto<br/>IamService]
CoreEduProto[core_edu.proto<br/>9 Service: Exam/Homework/Grade/<br/>Attendance/Class/Schedule/<br/>LeaveRequest/Dashboard/Admin]
ClassesProto[classes.proto<br/>ClassService历史保留]
ContentProto[content.proto<br/>Textbook/Chapter/<br/>KnowledgeGraph/Question/<br/>Elective/LessonPlan/CoursePlan]
MsgProto[msg.proto<br/>Notification/Preference/<br/>Template Service]
DataAnaProto[analytics.proto<br/>AnalyticsService]
AiProto[ai.proto<br/>AiService]
EventsProto[events.proto<br/>领域事件 schema]
end
subgraph Gen["buf generate (buf v2)"]
BufGen[buf generate]
TSGen[shared-ts/gen/proto<br/>@bufbuild/protobuf + grpc-node]
GoGen[shared-go/gen/proto<br/>protobuf-go + grpc-go]
PyGen[shared-py/gen/proto<br/>protobuf + grpc-python]
end
subgraph Services["消费服务"]
SvcTS[NestJS 服务<br/>iam/core-edu/content/msg<br/>teacher-bff/student-bff/parent-bff]
SvcGo[Go 网关<br/>api-gateway/push-gateway]
SvcPy[Python 服务<br/>data-ana/ai]
end
IamProto --> BufGen
CoreEduProto --> BufGen
ClassesProto --> BufGen
ContentProto --> BufGen
MsgProto --> BufGen
DataAnaProto --> BufGen
AiProto --> BufGen
EventsProto --> BufGen
BufGen --> TSGen
BufGen --> GoGen
BufGen --> PyGen
TSGen --> SvcTS
GoGen --> SvcGo
PyGen --> SvcPy
```
### 11.2 契约规则
| 规则 | 说明 |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| 包命名 | proto 包名采用 `next_edu_cloud.<domain>.v1` 格式(如 `next_edu_cloud.iam.v1``next_edu_cloud.core_edu.v1` |
| Topic 命名 | Kafka topic 采用 `edu.<domain>.<aggregate>.<action>` 格式(如 `edu.identity.user.created` |
| 版本化 | 破坏性变更必须升版本v1 → v2buf breaking FILE 级别 CI 强制 |
| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
| 消息命名 | PascalCase |
| 字段命名 | snake_case |
| 注释 | 每个 message 和字段必须注释 |
| CI 强制 | buf lint + buf breaking 必须通过 |
| 代码生成 | `buf generate` 生成 6 套代码go/js/python × protobuf/grpc |
### 11.3 Apollo Router 联邦聚合模式v2.1 替代 BFF 手写聚合)
> v2.1 变更:原 3 个手写 BFFteacher-bff / student-bff / parent-bff的 GraphQL Resolver + gRPC 调用聚合职责,由 apollo-router 自动查询计划Query Plan替代零手写聚合代码。详见 ADR-023 / ADR-025。
```mermaid
graph LR
subgraph Client["客户端"]
Q[GraphQL 查询]
end
subgraph Router["apollo-router 联邦层"]
Plan[查询计划 Query Plan<br/>自动拆分 + 并发调度]
Requires["@requires DataScope 传递"]
end
subgraph Subgraphs["业务子图(各服务 /graphql"]
S1[iam 子图]
S2[core-edu 子图]
S3[content 子图]
S4[msg / data-ana / ai / config 子图]
end
Q --> Plan
Plan --> Requires
Requires --> S1
Plan --> S2
Plan --> S3
Plan --> S4
S1 -.visibleClassIds via @requires.-> S2
```
**联邦 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 解析器强制 DataLoaderADR-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-035ESLint 规则强制校验,禁止 N+1 |
| 信任凭证 | Router 请求子图时携带 `Router-Authorization` header子图 `RouterAuthGuard` 校验,拒绝非 Router 的直接 GraphQL 请求ADR-036 |
| 外部/内部边界 | 外部查询(前端 → 后端)必经 Apollo Router内部调用后端 → 后端)必走 gRPCGraphQL 子图不可作为内部 RPCADR-037 |
| DataScope | 跨服务数据范围通过 `@requires` 指令运行时解析,禁止跨库 JOINADR-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_ 已移除)
| 层级 | 服务 | 前缀 | 示例 | 仲裁依据 |
| ---- | ------------ | -------------- | ------------------------------------------------------------- | -------- |
| 网关 | api-gateway | `GW_` | GW_UNAUTHORIZED / GW_RATE_LIMITED | ARB-014 |
| 网关 | push-gateway | `PUSH_` | PUSH_DEVICE_NOT_FOUND / PUSH_CHANNEL_FAILED | ARB-015 |
| BFF | teacher-bff | `BFF_TEACHER_` | BFF_TEACHER_UNAUTHORIZED / BFF_TEACHER_BAD_GATEWAY | ARB-016 |
| BFF | student-bff | `BFF_STUDENT_` | BFF_STUDENT_UNAUTHORIZED / BFF_STUDENT_VALIDATION_ERROR | ARB-017 |
| BFF | parent-bff | `BFF_PARENT_` | BFF_PARENT_CHILD_NOT_BOUND / BFF_PARENT_BAD_GATEWAY | ARB-018 |
| 业务 | iam | `IAM_` | IAM_USER_NOT_FOUND / IAM_INVALID_CREDENTIALS | ARB-003 |
| 业务 | core-edu | `CORE_EDU_` | CORE_EDU_EXAM_NOT_FOUND / CORE_EDU_GRADE_NOT_FOUND | ARB-004 |
| 业务 | content | `CONTENT_` | CONTENT_QUESTION_NOT_FOUND / CONTENT_TEXTBOOK_NOT_FOUND | ARB-005 |
| 业务 | msg | `MSG_` | MSG_NOTIFICATION_NOT_FOUND / MSG_TEMPLATE_NOT_FOUND | ARB-008 |
| 业务 | data-ana | `DATA_ANA_` | DATA_ANA_DASHBOARD_UNAVAILABLE / DATA_ANA_ANALYTICS_NOT_READY | ARB-009 |
| 业务 | ai | `AI_` | AI_GENERATION_FAILED / AI_QUOTA_EXCEEDED | ARB-010 |
**前端 i18n key 映射规则**F4 裁决):
| 服务 | i18n key 模式 | 示例 |
| ------------ | ------------------------------- | -------------------------------------- |
| api-gateway | `error.gateway.<code_snake>` | `error.gateway.unauthorized` |
| push-gateway | `error.push.<code_snake>` | `error.push.device_not_found` |
| teacher-bff | `error.bffTeacher.<code_snake>` | `error.bffTeacher.unauthorized` |
| student-bff | `error.bffStudent.<code_snake>` | `error.bffStudent.validation_error` |
| parent-bff | `error.bffParent.<code_snake>` | `error.bffParent.child_not_bound` |
| iam | `error.iam.<code_snake>` | `error.iam.user_not_found` |
| core-edu | `error.core_edu.<code_snake>` | `error.core_edu.exam_not_found` |
| content | `error.content.<code_snake>` | `error.content.question_not_found` |
| msg | `error.msg.<code_snake>` | `error.msg.notification_not_found` |
| data-ana | `error.data_ana.<code_snake>` | `error.data_ana.dashboard_unavailable` |
| ai | `error.ai.<code_snake>` | `error.ai.generation_failed` |
### 11.6 ActionState 信封规范
> 来源coord 仲裁 ARB-017 §19.6
> 适用范围:所有 BFF / Gateway HTTP 响应、GraphQL response 的 errors 扩展字段
> 设计原则:统一错误信封 + 降级模式方案 Bdegraded 放 error.details 子字段)
**信封结构**
```typescript
interface ActionState<T = unknown> {
success: boolean; // 整体成功/失败
data: T | null; // 成功时返回数据,失败时为 null
error: {
code: string; // 错误码(见 §11.4 前缀矩阵)
message: string; // 人类可读错误消息i18n key 解析后)
details?: Record<string, unknown>; // 附加详情(含 degraded 字段)
traceId: string; // 链路追踪 IDX-Request-Id
} | null; // 成功时为 null
}
```
**降级模式(方案 B**
- `success = true`(整体成功)
- `data` 内含 `extensions.degraded: true` 子字段
- `error` 仍为 null非错误
- 用于部分聚合失败场景(如 teacher-bff 聚合 iam 成功但 core-edu 失败)
**示例 1完全成功**
```json
{
"success": true,
"data": { "userId": "u001", "name": "张老师" },
"error": null
}
```
**示例 2完全失败**
```json
{
"success": false,
"data": null,
"error": {
"code": "BFF_TEACHER_BAD_GATEWAY",
"message": "下游服务不可用",
"details": { "downstream": "core-edu", "reason": "timeout" },
"traceId": "req-abc123"
}
}
```
**示例 3降级模式部分聚合成功**
```json
{
"success": true,
"data": {
"user": { "userId": "u001", "name": "张老师" },
"classes": null,
"extensions": { "degraded": true, "failedServices": ["core-edu"] }
},
"error": null
}
```
**GraphQL errors 数组扩展**
GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段:
```json
{
"errors": [
{
"message": "下游服务不可用",
"extensions": {
"code": "BFF_TEACHER_BAD_GATEWAY",
"details": { "downstream": "core-edu" },
"traceId": "req-abc123"
}
}
]
}
```
---
## 12. 架构约束
### 12.1 服务独立性约束
| 约束 | 说明 |
| ----------- | -------------------------------------- |
| 数据库独占 | 每个微服务独占自身数据库,禁止跨库联表 |
| 契约先行 | 所有跨服务通信必须先定义 protobuf 契约 |
| Outbox 强制 | 所有领域事件必须通过 Outbox 模式发布 |
| 幂等消费 | 所有事件消费者必须实现幂等性 |
| 单一职责 | 每个服务只负责一个限界上下文 |
| 无状态服务 | 业务服务不持有会话状态Redis 承载) |
### 12.2 通信约束
> v2.1 变更BFF 环节由 apollo-router 替代push-gateway 改名 realtime-gateway外部 GraphQL 必经 Router内部 RPC 必走 gRPCADR-037
| 场景 | 允许 | 禁止 |
| ------------------------ | --------------------------------------------- | --------------------------------------- |
| 客户端 → Gateway | REST + SSE | 直连业务服务 |
| Gateway → apollo-router | HTTP 反向代理(路径重写剥离前缀) | gRPC 转换、REST 业务逻辑 |
| Gateway → 业务服务 | HTTP 反向代理(剥离 `/api` 前缀,降级路径) | gRPC 转换 |
| apollo-router → 业务子图 | GraphQL /graphql携带 Router-Authorization | 直接访问 DB、绕过 RouterAuthGuard |
| 业务子图 → 业务子图 | **禁止**(外部 GraphQL 不可作内部 RPC | 子图互调 GraphQLADR-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 异步更新 |
| 缓存 | 最终一致 | 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 耗时工作流 + SagaCRUD 短事务绝对禁止用 Temporal | ADR-030 |
| SSE 优先 | 推送默认走 SSEWS 仅限监考等强双向场景 | ADR-029 |
| Eager Invalidation | 写后同步 Redis DELKafka 投影器仅作兜底 | ADR-038 |
| 乐观锁版本号回传 | 写接口返回 version读携带 expectedVersionES 落后则穿透读 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 微前端 | 独立部署、技术栈无关、渐进迁移 | ⛔ 废弃v2.1 由 ADR-033 portal-shell 替代) |
| ADR-013 | 采用 Temporal 工作流编排 | 长流程编排、可观测、可回滚 | ✅ 已采纳v2.1 ADR-030 严格边界:仅限 AI 工作流 + SagaCRUD 短事务禁用) |
| 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-gatewayADR-034 |
| ADR-017 | classes 服务合并入 core-edu | 教学组织与教学核心共享聚合根边界 | 已采纳 |
| ADR-018 | BFF 统一 GraphQL Yoga + DataLoader + opossum 熔断 | 协议统一、批量去重、降级模式 Bdegraded 子字段) | ⛔ 废弃v2.1 由 ADR-023 Apollo Federation 替代) |
| ADR-019 | admin-portal GraphQL 经 teacher-bff 命名空间 | 复用 teacher-bff resolveradmin 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.1config-service :3011/:50059 |
| ADR-027 | content CQRS 改造 | 写读分离故障隔离MySQL 写 + Neo4j/ES 读投影) | ✅ 已采纳v2.1 |
| ADR-028 | ai 无状态化 | 明确边界可扩展WorkflowStateStore → Redis-onlyTTL 1h | ✅ 已采纳v2.1 |
| ADR-029 | SSE 优先 + WS 仅监考 | K12 场景单向为主WS 心跳/握手开销过大 | ✅ 已采纳v2.1 |
| ADR-030 | Temporal 严格边界引入v2.1 修订) | AI 工作流 + Saga 用 TemporalCRUD 短事务绝对禁止,走同步调用 + 分布式锁 | ✅ 已采纳v2.1Temporal :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.1portal-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 DELKafka 投影器仅作兜底,降低读延迟 | ✅ 已采纳v2.1 |
| ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version读携带 expectedVersionES 落后则穿透读 MySQL | ✅ 已采纳v2.1 |
| ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafkamsg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳v2.1 |
| ADR-041 | ScopeToken 优化大规模 ID 列表 | 不传全量数组,传极短 token子图从 Redis SMEMBERS 获取,降低 HTTP payload 开销 | ✅ 已采纳v2.1 |
---
## 14. 附录6 阶段路线图
### 14.1 阶段总览
```mermaid
graph LR
P1[P1 地基<br/>M1-M3] --> P2[P2 身份<br/>M4-M6]
P2 --> P3[P3 核心教学<br/>M7-M10]
P3 --> P4[P4 内容分析<br/>M11-M13]
P4 --> P5[P5 沟通AI<br/>M14-M16]
P5 --> P6[P6 硬化<br/>M17-M18]
```
### 14.2 服务与阶段映射
> 状态基于代码现状2026-07-14。✅ 已落地 / 🚧 部分落地 / ⏳ 规划中。
| 阶段 | 周期 | 交付服务 | 退出标准 | 实际状态 |
| ----------- | ------- | ----------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
| 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-gatewayGo :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-gatewayGo :8081
- **WebSocket 端点**`/ws`JWT RS256 via query `?token=` 或 Authorization 头)
- **内部 HTTP API**`/internal/push``/internal/broadcast``/internal/online/:userID`X-Internal-Key 鉴权)
- **跨实例广播**Redis Pub/SubSubscribeAll + 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 iamNestJS :3002 / gRPC :50052
- **Controllers**IamController、RbacController、AuditController、JwksController、IamGrpcController
- **Services**IamService、JwksService、PermissionCacheServiceRedis、TokenBlacklistService
- **Outbox**`iam_outbox` 表,发布到 `edu.identity.user.*` topic
- **APP_GUARD**PermissionGuardDB 驱动 + Redis 缓存I3 裁决)
- **AuthMiddleware** 应用范围:`v1/iam/me``logout``change-password``viewports``permissions/effective``children``roles``permissions``users``audit``totp`
### 15.4 core-eduNestJS :3004 / gRPC :50053
- **业务模块10**
- exams含 exam-extensions、exam-state-machine
- homework含 homework-state-machine
- grades含 grade-calculator
- attendance
- classes含 teacher-associationsC1 合并自原 classes 服务)
- scheduling含 schedule-conflict 检测)
- leave-requestsP3.13
- dashboard
- admin
- iam-consumer消费 IAM 6 类事件)
- **共享组件**datascope-injectorRepository 层 WHERE 注入、outboxcore_edu_outbox、global-error.filter
- **gRPC 服务943 RPC**ExamService / HomeworkService / GradeService / AttendanceService / ClassService / ScheduleService / LeaveRequestService / DashboardService / AdminService
### 15.5 contentNestJS :3005 / gRPC :50054
- **业务模块7**textbooks、chapters、knowledge-points、questions、electives、lesson-plans、course-plans
- **gRPC controllers7**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 msgNestJS :3007 / gRPC :50056
- **业务模块4**notifications、preferences、templates、announcements
- **gRPC controllers317 RPC**NotificationService9 RPC/ NotificationPreferenceService2 RPC/ NotificationTemplateService6 RPCARB-008 裁剪)
- **渠道4**email、sms、push、in-appchannel-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.clientHTTP 调用 /internal/push、idempotency.guardRedis SETNX 去重、Outboxmsg_outbox
- **存储**MySQL + Elasticsearch消息全文检索+ Redis幂等性
### 15.7 data-anaFastAPI :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 服务**AnalyticsService18 RPC
- **CDC 消费者**:手动 commitlag 阈值 1000超过 readyz 返 503
- **/readyz 4 依赖**ClickHouse1s+ CDC consumerlag<1000+ Redis200ms+ iam gRPC2s
- **存储**ClickHouse宽表+ Redis缓存+ iam gRPC用户信息
### 15.8 aiFastAPI :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 服务**AiService9 RPC
- **核心组件**LLM FailoverChain4 适配器 + 熔断 + 故障切换、PromptTemplateServiceJinja2 + YAML、QualityGateRuleValidator + LLMJudge
- **用量与配额**UsageRecorderRedis+ QuotaEnforcer + KafkaProducer`edu.ai.usage.*`
- **限流**RateLimiterRedis 三维度令牌桶user/ip/school
- **安全**PII redactor + 输入清洗 + 输出审核
- **备课工作流**4 步编排 + WorkflowStateStoreRedis TTL
- **下游 gRPC 客户端**ContentClientGrpc / DataAnaClientGrpc / IamClientGrpc连接失败降级不阻断启动
### 15.9 teacher-bffNestJS :3003 / GraphQL Yoga
- **模块**GraphQLModuleTeacher + Admin ResolversARB-001 扁平合并)+ HealthModule + MiddlewareModule
- **下游客户端6**iam / core-edu / content / data-ana / msg / aiB8 裁决统一抽象gRPC + mock 双实现)
- **GraphQL 端点**`/graphql`(同时承载 teacher 与 admin 命名空间)
- **SSE 控制器**`ai-chat-sse.controller.ts`(透传 ai 服务流式响应)
- **Health probes6**iam-grpc / core-edu-grpc / content-grpc / data-ana-grpc / ai-grpc / msg-grpc + redis probe
### 15.10 student-bffNestJS :3009 / GraphQL Yoga
- **模块8**CacheModuleGlobal Redis+ DownstreamModuleGlobal gRPC+ CircuitBreakerModuleGlobal opossum+ HealthModule + DataLoaderModule + StudentModuleGraphQL Yoga + Resolver+ PushGatewayModuleHTTP /internal/push+ EventModuleKafka 事件订阅 + push-gateway 推送)
- **下游客户端**iam / core-edu / data-ana
### 15.11 parent-bffNestJS :3010 / GraphQL Yoga
- **模块6**HealthModule + GraphqlModuleYoga /v1/graphql+ ClientsModuleiam + core-edu + data-ana + msg + push-http+ KafkaModulecache-invalidation + notification-push handler+ AggregationModuleorchestrator + fallback-strategy + child-guard + response-mapper+ DataLoaderModuledataloader.factory + loaders 批量去重)
- **缓存**Redis + LRU cache + cache-key.builder
### 15.12 teacher-portalNext.js 15 :4000MF 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.tsbase+ graphql-p4.ts / p5.ts / p7-admin.ts / p7-advanced.ts / p7-exams.ts / p7-grades.ts / p7-insights.ts按阶段渐进扩展
- **可观测性**observability-providerOTel Web + Sentry + web-vitals + performance-dashboard
- **国际化**messages/{en,zh-CN}.json
- **认证**lib/auth.tscookie 迁移 + token 刷新 + cross-tab sync
### 15.13 student-portal / parent-portal / admin-portalNext.js 15 :4001/:4002/:4003MF Remote
- **student-portal**:路由组 leave + 主页exam-types 类型mocks/handlers + server
- **parent-portal**login + 主页 + providerschild-storeZustandmiddleware.tsPWAmanifest + sw.js
- **admin-portal**login + admin layouthooksuse-classes/files/roles/school/students/teachers/users/graphqllib/{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/`FetcherRS256 公钥缓存)、`logger/`slog JSON`tracer/`OTLP init | ✅ |
| shared-py | pyproject.tomluv 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 / deployno-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/<service>/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` | 已知问题速查(索引式场景→技术映射) |
---
## 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 不存业务数据,状态在 RedisTTL 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 BFFADR-023 | ✅ 完成 |
| config-service | 新增 | :3011/:50059 | 从 iam 拆分,插件配置 + 布局 + 用户偏好ADR-026 | ✅ 完成 |
| portal-shell | 新增 | :4010 | Modular Monolith 单容器,替代 4 portalADR-033 | ✅ 完成 |
| Temporal | 新增 | :7233/:8085/:5433 | AI 工作流 + Saga 引擎严格边界ADR-030 | ✅ 完成 |
| realtime-gateway | 改名 | :8081 | 原 push-gatewaySSE 优先 + Redis Pub/SubADR-029/034/040 | ✅ 完成 |
| iam | 拆分 | :3002/:50052 | 移除插件配置职责,专注认证+RBAC+JWKS+DataScope | ✅ 完成 |
| content | 改造 | :3005/:50054 | CQRSMySQL 写 + Neo4j/ES 读投影ADR-027 | ✅ 完成 |
| ai | 改造 | :3008/:50058 | 无状态化WorkflowStateStore → RedisADR-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 架构重设计主 specADR-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`。**