Files
Edu/docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md
SpecialX 62682b9d61 docs(docs): 004 arch.db 二次校验 + 新增 v2 架构重设计 spec
004 修正 7 处与代码不符描述:

- proto 统计 / 包名 / core-edu gRPC service 数

- msg RPC 数 / data-ana RPC 数

- student-bff 模块数 / parent-bff 模块数

新增 v2 架构重设计 spec(996 行):

- Apollo Federation BFF 联邦

- DataScope @requires 运行时解析

- iam 拆分 config-service

- content CQRS / ai 无状态化

- SSE 优先 / Temporal 不引入

Spec 自审修复 6 处问题:

- apollo-router 端口冲突 4000→4011

- Kafka topic 命名一致性

- CDC/Outbox 投影器职责分工

- 改动点数字 / 服务数 / 容器数计算
2026-07-14 21:25:37 +08:00

50 KiB
Raw Blame History

Edu 平台架构 v2 重设计

版本v2.0 日期2026-07-14 状态:待评审 关联:


1. 背景与目标

1.1 v1 架构痛点诊断

当前 Edu 微服务架构v1/v2.0 of 004存在以下核心痛点

  1. BFF 样板代码灾难3 个手写 BFFteacher-bff / student-bff / parent-bff重复实现 GraphQL Resolver + gRPC 调用,每个字段改动需改 3 处proto → schema → resolver×3 灾难
  2. DataScope 跨库 JOIN 悖论core-edu 要过滤"管理员可见班级",但用户-学校关系在 iam 库,微服务禁止跨库 JOIN运行时传 ID 列表性能崩溃
  3. CDC 与 Outbox 文档矛盾§1.1a 画 CDCMySQL→Debezium→Kafka§7.1 画 Outbox业务表→Relay Worker→Kafka看起来像两条路径做同一件事
  4. WebSocket 性能开销push-gateway 同时支持 WS/SSE但 K12 场景以单向通知为主WS 心跳/握手开销过大
  5. Temporal 滥用边界:规划引入 Temporal 但未明确边界,短事务若走 Temporal 会被入库开销拖垮吞吐
  6. iam 职责过载iam 承载认证 + RBAC + 审计 + JWKS + 插件配置6 张表)+ DataScope成为"上帝服务"
  7. content 多存储耦合content 同时写 MySQL + Neo4j + ES事务一致性、同步延迟、故障域都是问题
  8. ai 有状态化ai 服务内嵌 WorkflowStateStore边界不清难以水平扩展
  9. 配置散落:配置散落在 env / DB / code无统一配置中心
  10. 前端 4 portal 重复4 个独立 Next.js portal 重复实现 Dockerfile / i18n / auth / layout已在 portal-shell v2.1 设计稿中解决)

1.2 v2 设计目标

  1. BFF 联邦化:采用 Apollo Federation消灭手写 BFF 样板代码
  2. DataScope 运行时解析:通过 @requires 指令在子图间传递可见范围,解决跨库悖论
  3. CDC/Outbox 职责分工Outbox 负责领域事件CDC 负责读模型投影
  4. SSE 优先:推送默认走 SSEWS 仅限强双向场景
  5. Temporal 不引入:长工作流用 Redis 状态机 + Outbox 事件
  6. 服务边界清晰iam 拆分、content CQRS、ai 无状态化
  7. 配置中心化config-service 统一管理动态配置
  8. 单 portal-shell:前端单容器部署(引用 portal-shell v2.1 设计稿)

1.3 约束条件

  • 多 AI 协作模式不变coord + dev + sre模块单一负责制
  • 技术栈全开放:可换 DB / MQ / 框架 / 语言
  • 数据可清零:无迁移包袱,允许重建测试数据
  • 微服务架构保留:不为当前规模妥协,为未来扩展性预留
  • AI 禁止切换分支:分支创建/切换由人类决策者负责

1.4 非目标

  • 不重写已有业务逻辑(仅调整架构边界)
  • 不替换 MySQL / Redis / Kafka / ClickHouse / Neo4j / Elasticsearch
  • 不引入 etcd / ConsulK12 场景过重)
  • 不引入 Temporal / Cadence短事务禁用长工作流用轻量状态机

2. 整体架构

2.1 6 层分层架构

L1 用户层
  └─ 教师 / 学生 / 家长 / 管理员

L2 微前端层(单容器)
  └─ portal-shellModular Monolith + Micro-kernelNext.js 15 App Router
     · RSC 服务端预取 Config + initialData
     · dynamic import 懒加载插件
     · URL Params + Zustand 状态共享
     · SWR 静默刷新配置
     · 详见 portal-shell v2.1 设计稿

L3 边缘网关层Go 1.25 / Gin
  ├─ api-gatewayJWT 校验 + 限流 + 熔断 + CORS
  └─ realtime-gatewaySSE 优先 + WS 仅监考 + Redis Pub/Sub

L4 GraphQL 联邦层Apollo Router
  └─ apollo-router自动查询计划 + @requires DataScope 传递)
     · 替代 3 个 BFF 的聚合职责
     · 子图自主暴露 GraphQL

L5 业务服务层NestJS / FastAPI按 DDD 限界上下文)
  ├─ iam认证 + RBAC + JWKS + DataScope— 拆分后
  ├─ config-service插件配置 + 布局 + 用户偏好)— 新拆分
  ├─ core-edu教学核心 + 教学组织)
  ├─ content内容资源CQRSMySQL 写 + Neo4j/ES 读投影)
  ├─ msg消息通知
  ├─ data-ana数据分析有状态
  └─ aiLLM 推理,无状态)

L6 数据层
  ├─ MySQL 8.0(写模型,每服务独占 schema
  ├─ Redis 7缓存 + 会话 + Pub/Sub + 限流 + ai workflow 状态)
  ├─ ClickHouse 24.3(读模型宽表)
  ├─ Neo4j 5.20(知识图谱读模型)
  ├─ Elasticsearch 8.13(题库检索读模型)
  └─ 配置中心config-service + Redis 缓存)

2.2 服务清单v2

类别 服务名 语言/框架 HTTP gRPC GraphQL 限界上下文 变更说明
边缘 api-gateway Go 1.25 / Gin 8080 网关 保留,移除 BFF 代理路由
边缘 realtime-gateway Go 1.25 / Gin 8081 推送 改名SSE 优先
联邦 apollo-router Rust (Apollo Router) 4011 GraphQL 联邦 新增(避开 portal 4000-4009 段位,与 v1 并行运行不冲突)
业务 iam NestJS 3002 50052 子图 认证+RBAC+JWKS+DataScope 拆分,移除插件配置
业务 config-service NestJS 3011 50059 子图 插件+布局+偏好 新增,从 iam 拆出
业务 core-edu NestJS 3004 50053 子图 教学核心+组织 保留
业务 content NestJS 3005 50054 子图 内容资源 CQRS 改造
业务 msg NestJS 3007 50056 子图 沟通通知 保留
业务 data-ana FastAPI 3006 50055 子图 数据分析 保留
业务 ai FastAPI 3008 50058 子图 LLM 推理 无状态化
微前端 portal-shell Next.js 15 4010 教师端 Shell 新增,替代 4 portal

移除的服务

  • teacher-bff / student-bff / parent-bff → 由 apollo-router 替代
  • teacher-portal / student-portal / parent-portal / admin-portal → 由 portal-shell 替代

移除的组件

  • Debezium Connect → v2 默认不启用,投影器走 Outbox 事件(见 §5.4 / §6.1);仅当 Outbox 事件无法覆盖投影需求时,作为备选方案启用
  • Temporal → 不引入ai 保留 WorkflowStateStore 作为轻量替代)

2.3 核心数据流

graph TB
    User[用户] --> PortalShell[portal-shell :4010<br/>RSC 预取]
    PortalShell --> ApiGateway[api-gateway :8080<br/>JWT + 限流]
    ApiGateway --> ApolloRouter[apollo-router :4011<br/>GraphQL 联邦]

    ApolloRouter -->|子图查询| Iam[iam 子图]
    ApolloRouter -->|子图查询| Config[config-service 子图]
    ApolloRouter -->|子图查询| CoreEdu[core-edu 子图]
    ApolloRouter -->|子图查询| Content[content 子图]
    ApolloRouter -->|子图查询| Msg[msg 子图]
    ApolloRouter -->|子图查询| DataAna[data-ana 子图]
    ApolloRouter -->|子图查询| Ai[ai 子图]

    ApolloRouter -.@requires DataScope.-> Iam
    CoreEdu -.接收 visibleClassIds.-> CoreEdu

    PortalShell -.SSE 推送.-> RealtimeGw[realtime-gateway :8081]
    RealtimeGw -.Kafka 消费.-> Msg

    Iam <-->|Outbox| Kafka[(Kafka)]
    CoreEdu <-->|Outbox| Kafka
    Content <-->|Outbox + 投影器消费| Kafka
    Msg <-->|Outbox| Kafka
    DataAna <-->|Outbox 消费| Kafka
    Ai <-->|Outbox| Kafka

    Iam --> MySQL[(MySQL)]
    Config --> MySQL
    CoreEdu --> MySQL
    Content --> MySQL
    Content -.Outbox 投影器.-> Neo4j[(Neo4j)]
    Content -.Outbox 投影器.-> ES[(Elasticsearch)]
    Msg --> MySQL
    DataAna --> ClickHouse[(ClickHouse)]

2.4 关键架构决策摘要

决策点 v1 方案 v2 方案 理由
前端 4 portal + MF 单 portal-shell Modular Monolith减少重复
BFF 3 个手写 BFF Apollo Federation 工业标准,零样板代码
DataScope 网关透传 + 服务注入 @requires 运行时解析 解决跨库悖论
Push WS + SSE SSE 优先 + WS 仅监考 K12 场景单向为主
Temporal 规划引入 不引入,保留 WorkflowStateStore 短事务禁用,长工作流轻量替代
iam 职责 认证+RBAC+插件配置 拆分 iam + config-service 避免上帝服务
content 存储 MySQL+Neo4j+ES 混写 MySQL 写 + Outbox 投影器读模型 CQRS 分离
ai 状态 有状态工作流 无状态推理 + WorkflowStateStore 明确边界
配置 env + DB 散落 config-service 统一 配置中心化

3. Apollo Federation BFF 层

3.1 设计原理

痛点v1 的 3 个手写 BFF 重复实现 GraphQL Resolver + gRPC 调用,每个字段改动需改 3 处proto → schema → resolver3 个 BFF = ×3 灾难。

Apollo Federation 工业标准方案

  • 每个业务服务自主暴露 GraphQL 子图Subgraph
  • Apollo Router 作为唯一入口自动拆分查询计划Query Plan
  • 客户端发一个 QueryRouter 自动决定调用哪些子图、以什么顺序、如何聚合
  • 零手写聚合代码

3.2 子图暴露方式proto → GraphQL 自动生成

从 proto 自动生成 GraphQL schema业务服务只需实现 Resolver。

proto 扩展

// core_edu.proto
service ExamService {
  rpc GetExam(GetExamRequest) returns (Exam) {
    option (graphql.query) = "exam";
  }
  rpc ListExams(ListExamsRequest) returns (ListExamsResponse) {
    option (graphql.query) = "exams";
  }
}

message Exam {
  string exam_id = 1;
  string title = 2;
  repeated string class_ids = 3;
}

生成的 GraphQL 子图core-edu 子图):

type Query {
  exam(examId: ID!): Exam
  exams(classIds: [ID!]!): [Exam!]!
}

type Exam @key(fields: "examId") {
  examId: ID!
  title: String
  classIds: [String!]!
}

3.3 各服务子图清单

服务 子图关键类型 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(个人级)

3.4 Apollo Router 部署架构

┌─────────────────────────────────────────────────┐
│  api-gateway :8080Go / Gin                  │
│  · JWT RS256 校验                                │
│  · 限流 / 熔断 / CORS                            │
│  · 注入 x-user-id / x-user-role / x-dataScope   │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  apollo-router :4011Rust官方二进制         │
│  · 接收 GraphQL Query                            │
│  · 解析查询计划Query Plan                    │
│  · 并发调用子图,@requires 传递 DataScope        │
│  · 聚合响应返回客户端                            │
│  · 内置缓存 / 持续查询 / 遥测                    │
└────────────────┬────────────────────────────────┘
                 │
       ┌─────────┼─────────┬─────────┬─────────┐
       ▼         ▼         ▼         ▼         ▼
   iam:3002  core-edu  content    msg     data-ana
   /graphql  :3004     :3005      :3007   :3006
             /graphql  /graphql   /graphql  /graphql

Router 配置router.yaml

supergraph:
  listen: 0.0.0.0:4011
  path: /graphql
  introspection: true

homepage:
  enabled: true

health_check:
  listen: 0.0.0.0:8088

override_subgraph_url:
  iam: http://iam:3002/graphql
  config-service: http://config-service:3011/graphql
  core-edu: http://core-edu:3004/graphql
  content: http://content:3005/graphql
  msg: http://msg:3007/graphql
  data-ana: http://data-ana:3006/graphql
  ai: http://ai:3008/graphql

headers:
  all:
    request:
      - propagate:
          named: "authorization"
      - propagate:
          named: "x-user-id"
      - propagate:
          named: "x-user-role"
      - propagate:
          named: "x-dataScope"
      - propagate:
          named: "x-request-id"

3.5 BFF 样板代码消灭对比

v1 流程(新增字段 exam.duration,手动改动点 6 处)

  1. core_edu.proto,加 int32 duration = 5;(手动)
  2. 重新生成 proto 代码(自动)
  3. 改 teacher-bff 的 schema.graphql + resolver手动
  4. 改 student-bff 的 schema.graphql + resolver手动
  5. 改 parent-bff 的 schema.graphql + resolver手动
  6. 改 portal 的 GraphQL 查询(手动)

v2 流程(手动改动点 2 处)

  1. core_edu.proto,加 int32 duration = 5;(手动)
  2. 运行 buf generate,自动生成 GraphQL schema自动
  3. core-edu 服务自动暴露 duration 字段(自动)
  4. Apollo Router 自动拉取新 schema自动
  5. 改 portal-shell 的 GraphQL 查询(手动)

手动改动点从 6 处 → 2 处(仅改 proto + 改前端查询)。


4. 业务服务层调整

4.1 iam 拆分iam + config-service

问题iam 承载认证 + RBAC + 审计 + JWKS + 插件配置v2.1 新增 6 张表)+ DataScope 解析,成为"上帝服务"。

拆分方案

服务 职责 gRPC Service
iam瘦身 认证 + RBAC + JWKS + 审计 + DataScope users / roles / permissions / refresh_tokens / sessions / totp / audit_logs / user_school_role IamService / RbacService / JwksService / AuditService
config-service新增 插件配置 + 布局配置 + 用户偏好 plugin_registry / role_plugin_mapping / role_layout_default / layout_templates / user_layout_override / plugin_packages ConfigService

拆分理由

  • 认证是"高频低变"(每次请求校验 JWT
  • 配置是"低频高变"admin 改配置、用户改偏好)
  • 两者耦合会导致:配置表锁竞争影响认证、配置 Schema 变更需重启认证服务

DataScope 归属

  • iam 保留 user_school_role 表(用户-学校-角色关系)
  • iam 子图暴露 visibleClassIds / visibleStudentIds 查询DataScope 解析在 iam
  • config-service 不涉及 DataScope

端口分配

  • iam: 3002 / gRPC 50052不变
  • config-service: 3011 / gRPC 50059新增

4.2 content CQRS 改造

问题content 当前同时写 MySQL + Neo4j + ES三存储事务一致性、同步延迟、故障域都是问题。

CQRS 改造方案

写侧Command
  content 服务 → MySQL唯一写模型
                 ↓ Outbox 事件
                 Kafkaedu.content.* topic
                 ↓
读侧投影器Query
  ├── neo4j-projector消费事件 → 写 Neo4j 知识图谱)
  ├── es-projector消费事件 → 写 ES 题库索引)
  └── content-cache-projector消费事件 → 失效 Redis 缓存)

改造点

  • content 服务只写 MySQL不再直接写 Neo4j/ES
  • 新增 neo4j-projector / es-projector 作为 content 服务内 worker
  • content 服务的 GraphQL 子图读 MySQL强一致+ 读 Neo4j/ES最终一致

GraphQL 子图设计

type Query {
  textbook(textbookId: ID!): Textbook # 读 MySQL
  textbooks: [Textbook!]! # 读 MySQL
  searchQuestions(keyword: String!): [Question!]! # 读 ES
  knowledgeGraph(subjectId: ID!): KnowledgeGraph # 读 Neo4j
}

好处

  • 写路径简单(单库事务)
  • 读路径可独立扩展ES/Neo4j 可单独扩容)
  • 故障隔离ES 挂了不影响写入)
  • 数据一致性通过 Outbox + 投影器保证

4.3 ai 服务无状态化

问题ai 当前有 WorkflowStateStorelesson_plan_workflow是有状态的。

v2 定位

  • ai 是无状态 LLM 推理引擎
  • 不存业务数据,不存会话历史
  • 会话历史存 Redis短期或 data-ana长期分析
  • 工作流状态存 Rediskey: ai:workflow:{workflowId}TTL 1 小时)

改造点

  • 移除 ai 内部 WorkflowStateStore 的持久化(改为 Redis
  • lesson_plan_workflow 状态存 Redis
  • ai 用量事件通过 Outbox 发布到 Kafkaedu.ai.usage.recorded topic
  • data-ana 消费 ai 用量事件做统计

4.4 data-ana 与 ai 边界

维度 data-ana ai
定位 有状态分析引擎 无状态推理引擎
存储 ClickHouse宽表+ Redis缓存 无业务存储(仅 Redis 存 workflow 状态)
输入 业务事件Outbox 消费) 用户请求 + data-ana 数据
输出 统计结果 + 预警 + 掌握度 LLM 生成内容(聊天/题目/报告)
交互 ai 调 data-ana 获取分析数据 data-ana 不调 ai

调用关系

  • ai → data-anagRPC获取学生掌握度、班级表现等数据作为 LLM prompt 上下文)
  • data-ana → aidata-ana 不依赖 ai
  • ai 用量事件 → Kafka → data-ana 消费(统计 token 使用量)

5. 数据层 + DataScope

5.1 存储矩阵v2

存储 版本 用途 使用服务 变更
MySQL 8.0 写模型主库(每服务独占 schema iam / config-service / core-edu / content / msg 新增 config-service schema
Redis 7 缓存 / 会话 / 限流 / Pub/Sub / ai workflow 状态 全部服务 新增 ai workflow key
ClickHouse 24.3 读模型宽表 / 分析聚合 data-ana 不变
Neo4j 5.20 知识图谱content 读模型投影) content 改为投影器写入
Elasticsearch 8.13 题库检索 / 消息检索(读模型投影) content / msg content 改为投影器写入
配置中心 动态配置(插件/布局/特性开关) config-service 新增

5.2 DataScope @requires 实现

K12 场景规模分析

角色 可见范围 ID 量级 性能评估
学生 1 个班 无压力
教师 5-10 个班 IN 查询可扛
教研组长 20-50 个班 IN 查询可扛
管理员 100-500 个班 需索引优化
超管 全校 走 DataScope=ALL不传 ID

结论K12 场景 ID 列表通常 < 500@requires 运行时解析完全可行。

DataScope 6 级(保留 v1

Level 含义 实现
L1 SELF 仅自己 WHERE user_id = ?
L2 CLASS 本班 WHERE class_id IN (visibleClassIds)
L3 GRADE 本年级 WHERE grade_id IN (visibleGradeIds)
L4 SCHOOL 本校 WHERE school_id IN (visibleSchoolIds)
L5 REGION 本区域 WHERE region_id IN (visibleRegionIds)
L6 ALL 全部 无 WHERE

@requires 查询计划示例

场景:教研组长查"本组所有班级的考试成绩"

客户端 Query

query GetGradesForMyClasses($termId: ID!) {
  gradesForCurrentUser(termId: $termId) {
    examId
    title
    classId
    avgScore
  }
}

Apollo Router 查询计划(自动生成):

步骤 1: 调 iam 子图
  visibleClassIds(userId=x-user-id)
  → iam 查 Redis 缓存key: iam:datascope:class:{userId}TTL 5min
  → 未命中则查 user_school_role 表
  → 返回 ["class-001", ..., "class-050"]

步骤 2: 调 core-edu 子图
  gradesForCurrentUser(
    visibleClassIds: ["class-001", ..., "class-050"],
    termId: "2024-spring"
  )
  @requires(fields: "visibleClassIds")
  → core-edu 查 MySQL: WHERE class_id IN (...) AND term_id = ?
  → 返回 [Grade, Grade, ...]

步骤 3: 聚合返回客户端

iam 子图 DataScope Resolver

@Resolver(() => User)
export class DataScopeResolver {
  constructor(
    private iamService: IamService,
    @Inject("REDIS") private redis: Redis,
  ) {}

  @ResolveField(() => [String])
  async visibleClassIds(
    @Parent() user: User,
    @Context() ctx: { userId: string; dataScope: string },
  ): Promise<string[]> {
    if (ctx.dataScope === "ALL") return [];

    const cacheKey = `iam:datascope:class:${ctx.userId}`;
    const cached = await this.redis.get(cacheKey);
    if (cached) return JSON.parse(cached);

    const classIds = await this.iamService.getVisibleClassIds(ctx.userId);
    await this.redis.setex(cacheKey, 300, JSON.stringify(classIds));
    return classIds;
  }
}

core-edu 子图 @requires Resolver

@Resolver(() => Grade)
export class GradeResolver {
  constructor(private gradeService: GradeService) {}

  @Query(() => [Grade])
  @RequirePermission("GRADES_READ")
  async gradesForCurrentUser(
    @Context()
    ctx: {
      userId: string;
      dataScope: string;
      visibleClassIds?: string[];
    },
    @Args("termId") termId: string,
  ): Promise<Grade[]> {
    if (ctx.dataScope === "ALL") {
      return this.gradeService.findByTerm(termId);
    }
    return this.gradeService.findByClassIds(ctx.visibleClassIds ?? [], termId);
  }
}

5.3 配置中心config-service + Redis

不引入 etcd/ConsulK12 场景过重),用 config-service + Redis 实现轻量配置中心。

Admin 改配置
  ↓
config-service 写 MySQLplugin_registry 等)
  ↓
config-service 发 Kafka 事件edu.config.entry.changed
  ↓
各服务消费事件,失效本地 Redis 缓存
  ↓
下次查询从 config-service 拉新值,回填 Redis

缓存策略

  • Redis key: config:{type}:{key}(如 config:plugin:grades-widget
  • TTL: 5 分钟(兜底失效)
  • 事件驱动失效Kafka edu.config.entry.changed

配置类型

  • 插件配置plugin_registry / role_plugin_mapping
  • 布局配置layout_templates / role_layout_default / user_layout_override
  • 特性开关feature_flagsenable_ai_tutor
  • 系统配置system_configmax_exam_duration

5.4 Outbox 投影器设计content CQRS

投影器架构(默认走 Outbox 事件CDC/Debezium 仅作备选,当 Outbox 事件粒度不够时启用):

content 服务写 MySQL
  ↓ Outbox 事件
  ↓
Kafkaedu.content.* topic
  ↓
  ├── neo4j-projector消费 → 写 Neo4j 知识图谱)
  │   · 监听 KnowledgePointCreated / KnowledgePointLinked
  │   · 写 Neo4j 节点 + 关系
  │
  ├── es-projector消费 → 写 ES 题库索引)
  │   · 监听 QuestionCreated / QuestionUpdated / QuestionDeleted
  │   · 写 ES 索引(题干、选项、知识点标签)
  │
  └── content-cache-projector消费 → 失效 Redis 缓存)
      · 监听所有 content 事件
      · 失效 content 查询缓存

投影器实现位置:作为 content 服务内的独立 workercontent/src/workers/

content/src/
├── textbooks/
├── questions/
├── knowledge-points/
├── graphql/            # GraphQL 子图(读 MySQL + Neo4j + ES
└── workers/            # 投影器(消费 Kafka → 写读模型)
    ├── neo4j-projector.worker.ts
    ├── es-projector.worker.ts
    └── cache-projector.worker.ts

6. 事件驱动架构

6.1 Outbox vs CDC 职责分工

写侧Outbox 模式(领域事件发布)
  业务服务iam/core-edu/content/msg/ai
    ① 业务事务内写业务表 + outbox 表(原子)
    ② OutboxPublisher 轮询 outbox 表 → 投递 Kafka
    ③ 保证 at-least-once 语义
  用途发布业务语义事件ExamCreated / HomeworkSubmitted

读侧投影器CQRS 读模型 / 跨服务物化视图)
  方式 ADebezium → Kafka用于跨服务物化视图同步备选
  方式 BOutbox 事件 → 投影器(用于读模型投影,推荐)
  用途CQRS 读模型投影,非业务事件发布

关键原则

  • 领域事件必须走 Outbox业务语义由业务代码显式发布
  • 数据投影走 CDC 或 Outbox 消费(技术同步,无需业务语义)
  • CDC 不替代 OutboxCDC 是 binlog 级Outbox 是业务语义级)
  • v2 简化:优先用 Outbox 事件做投影器,减少 Debezium 依赖

6.2 Kafka Topic 命名规范

格式edu.<domain>.<aggregate>.<action>

Topic 生产者 消费者 用途
identity edu.identity.user.created iam msg / core-edu / content 用户创建
identity edu.identity.user.updated iam msg / core-edu / content 用户更新
identity edu.identity.user.role_changed iam msg / data-ana 角色变更
identity edu.identity.role.created iam msg 角色创建
teaching edu.teaching.exam.published core-edu msg / data-ana 考试发布
teaching edu.teaching.exam.extended core-edu msg 考试延时
teaching edu.teaching.homework.assigned core-edu msg / data-ana 作业布置
teaching edu.teaching.assignment.submitted core-edu msg / data-ana 作业提交
teaching edu.teaching.assignment.graded core-edu msg / data-ana 作业批改
teaching edu.teaching.grade.recorded core-edu msg / data-ana 成绩录入
teaching edu.teaching.attendance.recorded core-edu msg / data-ana 考勤记录
content edu.content.question.created content es-projector / neo4j-projector 题目创建
content edu.content.question.updated content es-projector / neo4j-projector 题目更新
content edu.content.knowledge_point.linked content neo4j-projector 知识点关联
notify edu.notify.notification.sent msg realtime-gateway 通知发送
notify edu.notify.notification.read msg 通知已读
notify edu.notify.notification.recalled msg realtime-gateway 通知撤回
insight edu.insight.mastery.updated data-ana msg 掌握度更新
ai edu.ai.usage.recorded ai data-ana AI 用量记录
config edu.config.entry.changed config-service 全部服务 配置变更(失效缓存)

新增 topic

  • edu.ai.usage.recordedai 用量事件(回流 data-ana 统计)
  • edu.config.entry.changed:配置变更通知(失效各服务本地缓存)

6.3 事件版本化

  • Schema Registry用 Kafka 的 schema registry或简化为 proto 版本号)
  • 版本后缀:v1 / v2(如 edu.teaching.exam.published.v2
  • 向后兼容:新增字段用 optional删除字段用 reserved
  • 破坏性变更:升版本号,新旧 topic 并行,消费者逐步迁移

6.4 幂等性

组件 幂等机制
Kafka Producer idempotent=true + transactionalId
Outbox Publisher 基于 event_id 去重Redis SETNX
Consumer 基于 event_id 去重DB 唯一索引或 Redis SETNX
投影器 基于 event_id + aggregate_id 去重

7. 认证与权限

7.1 JWT 流程v2

用户登录
  ↓
api-gateway → iam /auth/login
  ↓
iam 校验密码 → 签发 JWT RS256含 userId / role / dataScope
  ↓
api-gateway 设置 httpOnly CookieJWT
  ↓
portal-shell 后续请求携带 Cookie
  ↓
api-gateway 校验 JWT → 注入 x-user-id / x-user-role / x-dataScope / x-request-id
  ↓
apollo-router 透传 x-user-* 头到子图
  ↓
子图 Resolver 从 context 读取 userId / dataScope
  ↓
iam 子图 DataScope Resolver 查 visibleClassIdsRedis 缓存)
  ↓
core-edu 子图 @requires 接收 visibleClassIds注入 WHERE

7.2 权限校验分层

层级 职责 实现
api-gateway JWT 校验 + 限流 Go 中间件
apollo-router 透传 x-user-* 头 配置 headers.propagate
子图 Resolver @RequirePermission 装饰器 NestJS Guard
子图 DataScope @requires 注入 visibleClassIds Apollo Federation 指令
Repository WHERE 注入 visibleClassIds DataScopeInjector保留 v1

7.3 DEV_MODE

  • DEV_MODE=true 跳过 JWT 校验,接受 dev-token
  • 保留现有 dev-token 预定义角色机制

8. 可观测性 + 工作流 + 推送

8.1 可观测性

支柱 组件 v2 变更
日志 pino / zap / structlog 不变
指标 prom-client / prometheus / prometheus-client 新增 apollo-router 指标
链路 OpenTelemetry + Jaeger 1.57 不变
健康检查 /healthz + /readyz 新增 apollo-router /health

apollo-router 可观测性

  • 内置 Prometheus 指标(请求量/延迟/错误率/子图延迟)
  • 内置 OpenTelemetry trace
  • 内置持续查询Persisted Queries统计

8.2 工作流边界Temporal 约束)

v2 决策:不引入 Temporal保留 ai 服务的 WorkflowStateStore 作为轻量替代。

架构约束

工作流类型 时长 推荐方案 示例
长工作流 分钟~天 Redis 状态机 + Outbox 事件 备课工作流、报告生成
短事务 毫秒~秒 同步调用 + 分布式锁 作业提交、成绩录入
批处理 小时 Cron + Batch Job 成绩统计、掌握度计算

禁止

  • 短事务用工作流引擎(入库开销拖垮吞吐)
  • 纯读操作走工作流(直接走缓存)

允许

  • 跨天长流程用状态机 + 事件驱动
  • ai 备课/报告生成用 Redis 状态机WorkflowStateStore

8.3 推送架构SSE 优先)

msg 服务发布通知
  ↓ Outbox → Kafka
  ↓
realtime-gateway 消费 Kafkaedu.notify.notification.*
  ↓
  ├── SSE 推送(默认,单向)
  │   · 客户端 GET /sse?token=JWT保持长连接
  │   · 服务端 push 事件流
  │   · 适合 99% 场景(考试发布/成绩推送/通知)
  │
  └── WebSocket 推送(仅监考场景)
      · 客户端 WS /ws双向通信
      · 用于在线监考(心跳/防作弊/实时指令)
      · MVP 可不实现,二期按需

realtime-gateway 端点

  • GET /sse — SSE 推送(默认)
  • GET /ws — WebSocket 升级(监考专用)
  • POST /internal/push — msg 服务 HTTP 调用推送
  • GET /online/:userId — 查询在线状态
  • GET /healthz / GET /readyz / GET /metrics

性能对比

  • SSE单连接内存 ~10KB可扛 10 万连接/节点
  • WS单连接内存 ~100KB可扛 1 万连接/节点

9. 架构约束v2 新增)

约束 说明
BFF 联邦化 禁止手写 BFF 聚合层,所有 GraphQL 查询走 Apollo Router
子图自主 每个业务服务必须暴露 GraphQL 子图proto → GraphQL 自动生成
DataScope @requires 禁止跨库 JOINDataScope 通过 @requires 指令运行时解析
Outbox 领域事件 领域事件必须走 Outbox禁止直接调 Kafka producer
CDC 仅备选投影 CDC/Debezium 默认不启用,仅当 Outbox 事件粒度不够时作为投影器备选;禁止用于领域事件发布
SSE 优先 推送默认走 SSEWS 仅限强双向场景(监考)
Temporal 禁用 不引入 Temporal长工作流用 Redis 状态机 + Outbox
iam 职责限定 iam 仅负责认证/RBAC/JWKS/审计/DataScope禁止承载配置
config 统一 动态配置必须走 config-service禁止散落 env/code
content CQRS content 仅写 MySQLNeo4j/ES 通过投影器同步
ai 无状态 ai 不存业务数据,会话/工作流状态存 Redis
单 portal-shell 前端单容器部署,禁止新增独立 portal

保留约束v1

  • 四层分层Gateway → Router → Services → Data
  • 依赖方向单向,禁止反向依赖
  • 服务间通信GraphQL@requires+ gRPC服务间+ Kafka事件
  • 每服务独占 DB schema
  • JWT RS256 + JWKS
  • Cookie: httpOnly + Secure + SameSite=Strict
  • buf v2 + FILE 级 breaking 检查

10. 迁移路径

10.1 阶段划分(从 v1 到 v2

阶段 内容 验收标准 依赖
M0 proto → GraphQL 代码生成工具链 buf generate 输出 GraphQL schema
M1 各服务暴露 GraphQL 子图 /graphql 端点可查询 M0
M2 apollo-router 部署 + Supergraph 组装 Router 可聚合查询 M1
M3 iam 拆分 config-service config-service 独立运行 M1
M4 DataScope @requires 实现 教师查询可见班级成绩正确 M2
M5 content CQRS 改造(投影器) Neo4j/ES 通过投影器同步 M1
M6 ai 无状态化 ai 不存业务数据,状态在 Redis M1
M7 realtime-gateway SSE 优先 SSE 推送可用
M8 portal-shell 接入 apollo-router portal-shell 查询走 Router M2/M4
M9 旧 BFF 下线teacher/student/parent-bff 流量为 0 M8
M10 旧 portal 下线 流量为 0 M8
M11 arch.db + 004 文档同步 arch:scan 通过 全部

10.2 并行运行策略

  • v1 的 3 个 BFF + 4 个 portal 保留并行运行
  • v2 的 apollo-router + portal-shell 新建
  • 用户通过路由前缀切换(/shell/* 走新,其他走旧)
  • 逐步迁移用户,流量切完后下线旧服务

10.3 回滚策略

  • 任意阶段失败,回滚到上一阶段
  • v1 服务始终可用v2 失败不影响现有用户
  • iam 拆分 config-service 时config 表与现有表无外键依赖,可独立回滚

11. ADR 记录

ADR 决策 理由
ADR-023 采用 Apollo Federation 替代 3 个手写 BFF 工业标准,消灭样板代码
ADR-024 DataScope 通过 @requires 运行时解析 解决跨库 JOIN 悖论
ADR-025 proto → GraphQL 自动生成 单一数据源,字段变更只改 proto
ADR-026 iam 拆分 config-service 避免上帝服务
ADR-027 content CQRS 改造 写读分离,故障隔离
ADR-028 ai 无状态化 明确边界,可扩展
ADR-029 SSE 优先 + WS 仅监考 K12 场景单向为主
ADR-030 不引入 Temporal 短事务禁用,长工作流用轻量状态机
ADR-031 config-service + Redis 轻量配置中心 K12 场景,不引入 etcd
ADR-032 CDC 默认禁用,投影器首选 Outbox 事件 与 Outbox 职责分工,简化依赖
ADR-033 portal-shell 单容器替代 4 portal Modular Monolith
ADR-034 realtime-gateway 改名 SSE 优先定位

12. 对接清单

12.1 新增服务

路径 职责 动作
services/apollo-router/ Apollo Router 部署配置router.yaml + Dockerfile 新建
services/config-service/ 配置服务NestJS从 iam 拆出) 新建
apps/portal-shell/ Portal Shell 前端(详见 portal-shell v2.1 设计稿) 新建

12.2 改造服务

路径 变更 动作
services/iam/ 移除插件配置相关表和逻辑到 config-service新增 GraphQL 子图 改造
services/core-edu/ 新增 GraphQL 子图;实现 @requires DataScope 改造
services/content/ CQRS 改造:移除直接写 Neo4j/ES新增投影器 worker 改造
services/msg/ 新增 GraphQL 子图 改造
services/data-ana/ 新增 GraphQL 子图 改造
services/ai/ 无状态化WorkflowStateStore 改为 Redis新增 GraphQL 子图 改造
services/push-gateway/ 改名 realtime-gatewaySSE 优先 改造
services/api-gateway/ 移除 BFF 代理路由;新增 apollo-router 路由 改造

12.3 下线服务

路径 处理 时机
services/teacher-bff/ 流量切到 apollo-router 后下线 M9
services/student-bff/ 流量切到 apollo-router 后下线 M9
services/parent-bff/ 流量切到 apollo-router 后下线 M9
apps/teacher-portal/ 流量切到 portal-shell 后下线 M10
apps/student-portal/ 流量切到 portal-shell 后下线 M10
apps/parent-portal/ 流量切到 portal-shell 后下线 M10
apps/admin-portal/ 流量切到 portal-shell 后下线 M10

12.4 新增工具链

路径 职责 动作
packages/shared-proto/gen-graphql.ts proto → GraphQL schema 生成器 新建
packages/shared-proto/buf.gen.yaml 新增 GraphQL 输出配置 修改

12.5 基础设施

路径 变更 动作
infra/docker-compose.deploy.yml 新增 apollo-router / config-service / portal-shell移除 3 BFF + 4 portal 修改
infra/port-allocation.md 新增 3011 / 4000 / 4010 端口 修改
infra/init-sql/ 新增 config-service schema DDL 新增

13. 验收标准

13.1 功能验收

  • Apollo Router 可聚合 7 个子图查询
  • DataScope @requires 正确传递 visibleClassIds
  • iam 拆分后 config-service 独立运行
  • content CQRS 投影器正确同步 Neo4j/ES
  • ai 无状态化,工作流状态在 Redis
  • realtime-gateway SSE 推送可用
  • portal-shell 接入 apollo-router 查询正常
  • 配置变更通过 Kafka 事件失效各服务缓存

13.2 非功能验收

  • pnpm run lint + pnpm run typecheck 零错误
  • Apollo Router 查询延迟 P99 < 100ms不含子图
  • DataScope @requires 查询延迟 P99 < 500ms含子图
  • SSE 单节点可扛 10 万连接
  • arch.db 更新004 文档同步
  • 所有 ADR 记录完整

14. 参考资料


15. 版本演进对比

维度 v1当前 v2本设计
前端 4 portal + MF 单 portal-shell
BFF 3 个手写 BFF Apollo Federation
DataScope 网关透传 + 服务注入 @requires 运行时解析
Push WS + SSE SSE 优先 + WS 仅监考
Temporal 规划引入 不引入
iam 认证+RBAC+插件配置 拆分 iam + config-service
content MySQL+Neo4j+ES 混写 CQRSMySQL 写 + 投影器
ai 有状态工作流 无状态推理
配置 env + DB 散落 config-service 统一
服务数(业务+网关+前端) 15 个1 gw + 1 push + 3 BFF + 6 services + 4 portal 11 个1 gw + 1 realtime + 1 router + 7 services + 1 shell替换 3 BFF + 4 portal 为 1 Router + 1 Shell + 1 config
容器数(含数据层 6 个MySQL/Redis/Kafka/ClickHouse/Neo4j/ES 21 17