2006 lines
130 KiB
Markdown
2006 lines
130 KiB
Markdown
# 架构影响地图(微服务版)
|
||
|
||
> 版本: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 个手写 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.<domain>.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)。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
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-gateway(SSE 优先);新增 config-service;新增 Temporal;Outbox 轮询废弃,改由 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 Router(Rust 官方二进制)"]
|
||
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/>CQRS:MySQL 写 + Neo4j/ES 读投影"]
|
||
DataAna["data-ana :3006 / gRPC :50055<br/>FastAPI + CDC consumer + ClickHouse"]
|
||
Msg["msg :3007 / gRPC :50056<br/>4 模块 + 4 渠道 + msg worker(Redis 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 Worker:AI 耗时工作流 + 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-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 模块映射
|
||
|
||
| 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、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、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 存储 + 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.1 修正**:Temporal 已落地(M6.5,ADR-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 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 Apollo Router 子图清单(v2.1 替代 BFF 下游矩阵)
|
||
|
||
> 7 个业务服务暴露 GraphQL 子图,由 apollo-router 自动组装 Supergraph。每个子图必须实现 RouterAuthGuard(ADR-036)+ DataLoader(ADR-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 Fetcher(shared-go/jwks)缓存 RS256 公钥校验 JWT;
|
||
> 校验通过后注入 `x-user-id` / `x-user-roles` / `x-user-data-scope` / `x-request-id` 头部,HTTP 反向代理到下游服务;
|
||
> 下游服务(NestJS)通过 `AuthMiddleware` 解析头部、`PermissionGuard`(APP_GUARD)做权限校验。
|
||
> DEV_MODE=true 时跳过 JWT 校验,接受 `dev-token`。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant GW as API Gateway (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: 生成 JWT(RS256 私钥签发)+ refresh_token
|
||
IAM->>IAM: 写 iam_outbox(USER_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 + 自定义)
|
||
- 权限解析 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 读写分离(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 Invalidation(v2.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,读携带 expectedVersion,ES 落后则穿透读 MySQL(ADR-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 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 + 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
|
||
subgraph Service["业务服务"]
|
||
Cmd[Command 处理]
|
||
Domain[Domain 聚合]
|
||
Repo[Repository]
|
||
end
|
||
|
||
subgraph MySQL["MySQL 主库"]
|
||
BizTable[(业务表)]
|
||
OutboxTable[(outbox 表<br/>同事务原子)]
|
||
end
|
||
|
||
subgraph CDC["Debezium Connect(监听 binlog,v2.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.6,ISSUE-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 → 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 异步决策(v2.1 修订)
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Req[跨服务协作需求] --> C1{需要强一致性?}
|
||
C1 -- 是 --> C2{调用方需要立即结果?}
|
||
C2 -- 是 --> C2a{是 CRUD 短事务?}
|
||
C2a -- 是 --> Sync[同步 gRPC + 分布式锁<br/>ADR-030 严禁 Temporal]
|
||
C2a -- 否 --> Saga[Saga 编排<br/>Temporal(ADR-030)]
|
||
C1 -- 否 --> C3{需要事件最终一致?}
|
||
C3 -- 是 --> Async[异步 Kafka 事件<br/>Outbox + Debezium(ADR-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 DB(PostgreSQL),业务数据仍在各服务 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 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: 聚合返回客户端
|
||
```
|
||
|
||
---
|
||
|
||
## 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 接收 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
|
||
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/>Trace,OTLP 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-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<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 在子图查询计划中生成独立 span(query_plan / fetch_subgraph),便于在 Jaeger 中追踪每个子图的延迟贡献。
|
||
|
||
**规则**:
|
||
|
||
- 所有跨服务调用必须透传 W3C Trace Context(HTTP `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 自带 /health(gRPC 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-14):8 proto 文件 / 23 service / 305 message / 139 RPC。
|
||
> buf v2 配置:`lint STANDARD`(含 5 项 except 豁免)+ `breaking FILE` 级别。
|
||
> `buf.gen.yaml` 生成 6 套代码:protocolbuffers {go/js/python} + grpc {go/node/python},输出到 `shared-{go,ts,py}/gen/proto/`。
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Proto["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 → v2),buf breaking FILE 级别 CI 强制 |
|
||
| 字段编号 | 禁止复用已删除字段编号,使用 reserved |
|
||
| 消息命名 | PascalCase |
|
||
| 字段命名 | snake_case |
|
||
| 注释 | 每个 message 和字段必须注释 |
|
||
| CI 强制 | buf lint + buf breaking 必须通过 |
|
||
| 代码生成 | `buf generate` 生成 6 套代码(go/js/python × protobuf/grpc) |
|
||
|
||
### 11.3 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
|
||
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 解析器强制 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_ 已移除)
|
||
|
||
| 层级 | 服务 | 前缀 | 示例 | 仲裁依据 |
|
||
| ---- | ------------ | -------------- | ------------------------------------------------------------- | -------- |
|
||
| 网关 | 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 扩展字段
|
||
> 设计原则:统一错误信封 + 降级模式方案 B(degraded 放 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; // 链路追踪 ID(X-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 必走 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 异步更新 |
|
||
| 缓存 | 最终一致 | 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 微前端 | 独立部署、技术栈无关、渐进迁移 | ⛔ 废弃(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) |
|
||
|
||
---
|
||
|
||
## 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-gateway(Go :8080)
|
||
|
||
- **中间件链**:Recovery → otelgin → RequestID → CORS → SecurityHeaders → RequestBodyLimit(10MB) → RateLimit(100rps+20burst) → CircuitBreaker → AuthMiddleware(JWKS RS256) → Metrics
|
||
- **路由组**:
|
||
- `/healthz`、`/readyz`、`/metrics`(公开)
|
||
- `/api/v1/{classes,iam,exams,homework,grades,textbooks,chapters,knowledge-points,questions,notifications,messages,announcements,ai,analytics,dashboard}/*` → HTTP 反向代理
|
||
- `/api/v1/{teacher,student,parent}/*` → BFF 反向代理(剥离 `/api/v1/{bff}` 前缀)
|
||
- `/api/admin/graphql` → teacher-bff `/graphql`(AdminRoleMiddleware 强制)
|
||
- **关键依赖**:`shared-go/jwks`(公钥缓存)、`shared-go/env`、`shared-go/logger`、`shared-go/tracer`
|
||
|
||
### 15.2 push-gateway(Go :8081)
|
||
|
||
- **WebSocket 端点**:`/ws`(JWT RS256 via query `?token=` 或 Authorization 头)
|
||
- **内部 HTTP API**:`/internal/push`、`/internal/broadcast`、`/internal/online/:userID`(X-Internal-Key 鉴权)
|
||
- **跨实例广播**:Redis Pub/Sub(SubscribeAll + RebuildPresenceOnStartup)
|
||
- **Kafka 消费**:`edu.notify.notification.sent`(ARB-013)
|
||
- **健康检查**:`/healthz`(liveness)、`/readyz`(Redis 软失败 + Kafka consumer 状态)
|
||
- **依赖**:`hub`(in-memory connection registry)、`redisclient`、`kafkaconsumer`、`ws`、`observability`
|
||
|
||
### 15.3 iam(NestJS :3002 / gRPC :50052)
|
||
|
||
- **Controllers**:IamController、RbacController、AuditController、JwksController、IamGrpcController
|
||
- **Services**:IamService、JwksService、PermissionCacheService(Redis)、TokenBlacklistService
|
||
- **Outbox**:`iam_outbox` 表,发布到 `edu.identity.user.*` topic
|
||
- **APP_GUARD**:PermissionGuard(DB 驱动 + Redis 缓存,I3 裁决)
|
||
- **AuthMiddleware** 应用范围:`v1/iam/me`、`logout`、`change-password`、`viewports`、`permissions/effective`、`children`、`roles`、`permissions`、`users`、`audit`、`totp`
|
||
|
||
### 15.4 core-edu(NestJS :3004 / gRPC :50053)
|
||
|
||
- **业务模块(10)**:
|
||
- exams(含 exam-extensions、exam-state-machine)
|
||
- homework(含 homework-state-machine)
|
||
- grades(含 grade-calculator)
|
||
- attendance
|
||
- classes(含 teacher-associations,C1 合并自原 classes 服务)
|
||
- scheduling(含 schedule-conflict 检测)
|
||
- leave-requests(P3.13)
|
||
- dashboard
|
||
- admin
|
||
- iam-consumer(消费 IAM 6 类事件)
|
||
- **共享组件**:datascope-injector(Repository 层 WHERE 注入)、outbox(core_edu_outbox)、global-error.filter
|
||
- **gRPC 服务(9,43 RPC)**:ExamService / HomeworkService / GradeService / AttendanceService / ClassService / ScheduleService / LeaveRequestService / DashboardService / AdminService
|
||
|
||
### 15.5 content(NestJS :3005 / gRPC :50054)
|
||
|
||
- **业务模块(7)**:textbooks、chapters、knowledge-points、questions、electives、lesson-plans、course-plans
|
||
- **gRPC controllers(7)**:Textbook / Chapter / KnowledgeGraph / Question / Elective / LessonPlan / CoursePlan
|
||
- **同步 worker**:es-sync.worker(题库 ES 索引)、neo4j-sync.worker(知识图谱节点)
|
||
- **Outbox**:content_outbox,发布 `edu.content.*` 事件
|
||
- **存储**:MySQL(业务表)+ Neo4j(知识图谱)+ Elasticsearch(题库检索)
|
||
|
||
### 15.6 msg(NestJS :3007 / gRPC :50056)
|
||
|
||
- **业务模块(4)**:notifications、preferences、templates、announcements
|
||
- **gRPC controllers(3,17 RPC)**:NotificationService(9 RPC)/ NotificationPreferenceService(2 RPC)/ NotificationTemplateService(6 RPC,ARB-008 裁剪)
|
||
- **渠道(4)**:email、sms、push、in-app(channel-dispatcher 路由)
|
||
- **Kafka 消费**:16 类事件(iam 6 + core-edu 9 + data-ana 1),见 `shared/kafka/topic-map.ts`
|
||
- **Kafka 生产**:4 类 `edu.notify.notification.*` 事件 + 1 兜底 topic
|
||
- **辅助组件**:push-gateway.client(HTTP 调用 /internal/push)、idempotency.guard(Redis SETNX 去重)、Outbox(msg_outbox)
|
||
- **存储**:MySQL + Elasticsearch(消息全文检索)+ Redis(幂等性)
|
||
|
||
### 15.7 data-ana(FastAPI :3006 / gRPC :50055)
|
||
|
||
- **HTTP 端点(3 基础 + 11 业务 = 14)**:
|
||
- 基础:`/`、`/healthz`、`/readyz`
|
||
- 业务(全部 ActionState 信封):
|
||
- `/analytics/class/{class_id}/performance`
|
||
- `/analytics/student/{student_id}/{weakness,trend,errorbook,mastery}`
|
||
- `/analytics/{teacher,student,parent,admin}/dashboard`
|
||
- `/analytics/warnings` + `/analytics/warnings/trigger`
|
||
- `/analytics/class/{class_id}/mastery-distribution`
|
||
- **gRPC 服务**:AnalyticsService(18 RPC)
|
||
- **CDC 消费者**:手动 commit,lag 阈值 1000(超过 readyz 返 503)
|
||
- **/readyz 4 依赖**:ClickHouse(1s)+ CDC consumer(lag<1000)+ Redis(200ms)+ iam gRPC(2s)
|
||
- **存储**:ClickHouse(宽表)+ Redis(缓存)+ iam gRPC(用户信息)
|
||
|
||
### 15.8 ai(FastAPI :3008 / gRPC :50058)
|
||
|
||
- **HTTP 端点(11)**:
|
||
- `/healthz`、`/readyz`
|
||
- `/v1/ai/chat`(非流式)+ `/v1/ai/chat/stream`(SSE)
|
||
- `/v1/ai/generate/question` + `/v1/ai/generate/question/stream`(SSE)
|
||
- `/v1/ai/optimize/expression`
|
||
- `/v1/ai/lesson-plan/generate` + `/v1/ai/lesson-plan/status/{workflow_id}` + `/v1/ai/lesson-plan/confirm/{workflow_id}`
|
||
- `/v1/ai/generate/report`(class_summary / student_detail / exam_analysis)
|
||
- **gRPC 服务**:AiService(9 RPC)
|
||
- **核心组件**:LLM FailoverChain(4 适配器 + 熔断 + 故障切换)、PromptTemplateService(Jinja2 + YAML)、QualityGate(RuleValidator + LLMJudge)
|
||
- **用量与配额**:UsageRecorder(Redis)+ QuotaEnforcer + KafkaProducer(`edu.ai.usage.*`)
|
||
- **限流**:RateLimiter(Redis 三维度令牌桶:user/ip/school)
|
||
- **安全**:PII redactor + 输入清洗 + 输出审核
|
||
- **备课工作流**:4 步编排 + WorkflowStateStore(Redis TTL)
|
||
- **下游 gRPC 客户端**:ContentClientGrpc / DataAnaClientGrpc / IamClientGrpc(连接失败降级,不阻断启动)
|
||
|
||
### 15.9 teacher-bff(NestJS :3003 / GraphQL Yoga)
|
||
|
||
- **模块**:GraphQLModule(Teacher + Admin Resolvers,ARB-001 扁平合并)+ HealthModule + MiddlewareModule
|
||
- **下游客户端(6)**:iam / core-edu / content / data-ana / msg / ai(B8 裁决统一抽象,gRPC + mock 双实现)
|
||
- **GraphQL 端点**:`/graphql`(同时承载 teacher 与 admin 命名空间)
|
||
- **SSE 控制器**:`ai-chat-sse.controller.ts`(透传 ai 服务流式响应)
|
||
- **Health probes(6)**:iam-grpc / core-edu-grpc / content-grpc / data-ana-grpc / ai-grpc / msg-grpc + redis probe
|
||
|
||
### 15.10 student-bff(NestJS :3009 / GraphQL Yoga)
|
||
|
||
- **模块(8)**:CacheModule(Global Redis)+ DownstreamModule(Global gRPC)+ CircuitBreakerModule(Global opossum)+ HealthModule + DataLoaderModule + StudentModule(GraphQL Yoga + Resolver)+ PushGatewayModule(HTTP /internal/push)+ EventModule(Kafka 事件订阅 + push-gateway 推送)
|
||
- **下游客户端**:iam / core-edu / data-ana
|
||
|
||
### 15.11 parent-bff(NestJS :3010 / GraphQL Yoga)
|
||
|
||
- **模块(6)**:HealthModule + GraphqlModule(Yoga /v1/graphql)+ ClientsModule(iam + core-edu + data-ana + msg + push-http)+ KafkaModule(cache-invalidation + notification-push handler)+ AggregationModule(orchestrator + fallback-strategy + child-guard + response-mapper)+ DataLoaderModule(dataloader.factory + loaders 批量去重)
|
||
- **缓存**:Redis + LRU cache + cache-key.builder
|
||
|
||
### 15.12 teacher-portal(Next.js 15 :4000,MF Shell)
|
||
|
||
- **路由组**(25+ 业务页面):dashboard、classes、schedule、exams、homework、grades、attendance、analytics、knowledge-graph、lesson-plans、course-plans、textbooks、questions、notifications、students、ai-assist、ai-lesson-plan、ai-report、diagnostic、error-book、practice、leave、elective、schedule-changes、settings
|
||
- **MF Remote 加载**:ParentPortalRemote(在 teacher-portal 内嵌入家长端视图)
|
||
- **GraphQL 客户端文件**:graphql.ts(base)+ graphql-p4.ts / p5.ts / p7-admin.ts / p7-advanced.ts / p7-exams.ts / p7-grades.ts / p7-insights.ts(按阶段渐进扩展)
|
||
- **可观测性**:observability-provider(OTel Web + Sentry + web-vitals + performance-dashboard)
|
||
- **国际化**:messages/{en,zh-CN}.json
|
||
- **认证**:lib/auth.ts(cookie 迁移 + token 刷新 + cross-tab sync)
|
||
|
||
### 15.13 student-portal / parent-portal / admin-portal(Next.js 15 :4001/:4002/:4003,MF Remote)
|
||
|
||
- **student-portal**:路由组 leave + 主页;exam-types 类型;mocks/handlers + server
|
||
- **parent-portal**:login + 主页 + providers;child-store(Zustand);middleware.ts;PWA(manifest + sw.js)
|
||
- **admin-portal**:login + admin layout;hooks(use-classes/files/roles/school/students/teachers/users/graphql);lib/{auth,i18n,permissions,web-vitals}
|
||
|
||
### 15.14 共享包实施状态
|
||
|
||
| 包 | 关键导出 | 状态 |
|
||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---- |
|
||
| shared-proto | 8 proto 文件 + buf.yaml + buf.gen.yaml(生成 6 套代码) | ✅ |
|
||
| shared-ts | `bff/`(logger)、`outbox/`(OutboxModule + publisher + schema + types,被 iam/core-edu/content/msg/BFF 共享) | ✅ |
|
||
| shared-go | `env/`(config.Load)、`jwks/`(Fetcher,RS256 公钥缓存)、`logger/`(slog JSON)、`tracer/`(OTLP init) | ✅ |
|
||
| shared-py | pyproject.toml(uv workspace member) | 🚧 |
|
||
| contracts | `STUDENT_PERMISSIONS` 常量(12 权限点) | 🚧 |
|
||
| hooks | use-auth / use-permission / use-viewports / use-graphql-client / use-trace-id / use-a11y-id / use-aria-live / use-api / use-toast | ✅ |
|
||
| ui-components | data-table / filter-bar / form / modal / chart / calendar / status-badge / empty / loading + utils/cn | ✅ |
|
||
| ui-tokens | primitive.css / semantic-light.css / semantic-dark.css / tailwind-theme.css / all.css + colors/shadows/spacing/typography (.ts) | ✅ |
|
||
|
||
### 15.15 部署与运维实施状态
|
||
|
||
- **本地开发**:`infra/docker-compose.yml`(基础设施)+ pnpm dev / go run / uv run
|
||
- **最小化部署**:`infra/docker-compose.minimal.yml`
|
||
- **生产部署**:`infra/docker-compose.deploy.yml`(build: 替代 image:)
|
||
- **测试部署**:`infra/docker-compose.test.yml`
|
||
- **监控栈**:`infra/docker-compose.monitoring.yml`(observability profile)
|
||
- **K8s Helm chart**:`infra/k8s/helm/`(edu-platform umbrella + 各子 chart)
|
||
- **CI/CD**:`.github/workflows/ci.yml`(quality-ts / quality-go / quality-proto / deploy,no-push 本地构建模式)
|
||
- **备份**:`infra/backup/backup-mysql.sh`(每服务独立,保留 7 天)
|
||
- **混沌工程**:`infra/chaos/experiments.yaml`
|
||
- **WAF**:`infra/security/waf-rules.conf`
|
||
- **端口分配**:`infra/port-allocation.md`(唯一源)
|
||
|
||
### 15.16 文档体系实施状态
|
||
|
||
| 路径 | 用途 |
|
||
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `docs/architecture/004_architecture_impact_map.md` | 本文件(架构影响地图,设计意图唯一源) |
|
||
| `docs/architecture/0010_architecture.md` | 理想蓝图(目标态) |
|
||
| `docs/architecture/004-p6-addendum.md` | P6 硬化附录 |
|
||
| `docs/architecture/roadmap/` | tech-debt / pending-features / integration-test-phase |
|
||
| `docs/architecture/runbooks/` | p6-hardening / post-p6-followup / incident-response |
|
||
| `docs/architecture/issues/` | 协调记录(coord / matrix / workline)+ contracts/ + objections/ + worklines/ |
|
||
| `docs/architecture/ai-allocation.md` | AI 模块分配 |
|
||
| `docs/architecture/ai-work-orchestration.md` | AI 工作编排 |
|
||
| `docs/architecture/coord-cross-review.md` | coord 交叉审查 |
|
||
| `docs/architecture/coord-final-decisions.md` | coord 最终裁决(ARB-001~022+) |
|
||
| `docs/architecture/president-final-rulings.md` | president 最终裁决(batch 0.9+) |
|
||
| `docs/modules/<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 不存业务数据,状态在 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`。**
|