Files
Edu/services/teacher-bff/docs/02-architecture-design.md
SpecialX 99155a5ea1 feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
2026-07-10 19:10:07 +08:00

73 KiB
Raw Blame History

模块架构设计文档 — teacher-bff

AI 标识ai03 负责模块teacher-bffP2 阶段:架构设计外包 · 阶段 2模块架构设计 日期2026-07-09 关联文档:01-understanding.mdai-allocation.md004 架构影响地图pending-features.mdcoord-cross-review.md

设计原则

  1. 目标态先行:先描绘 P6 终态架构,再回推分阶段演进路径,避免短视设计
  2. 契约驱动:所有跨模块交互以 proto 契约为唯一源proto 缺失项显式标注并提请 coord 仲裁
  3. 平滑演进:每一阶段都能独立交付价值,不依赖未来阶段;每次升级通过抽象层隔离变更
  4. BFF 本分:只聚合、裁剪、协议转换,不持业务状态、不做权限决策、不直访业务 DB
  5. 为未来铺垫:抽象层预留 gRPC / GraphQL / SSE / Kafka / 熔断 / 多租户扩展点

0. 文档导航

章节 内容 关键性
§1 目标态架构P6 终态) ★ 设计北极星
§2 分阶段演进路线P2→P6 ★ 落地路径
§3 模块内部分层图 ★ 内部结构
§4 API 设计REST→GraphQL 演进) ★ 对前端契约
§5 GraphQL Schema 设计(目标态) ★ P4+ 契约
§6 DataLoader 策略 ★ 防 N+1
§7 缓存策略Redis 多层) ★ 性能
§8 视口推导与角色差异化L1-L4 ★ 多角色复用
§9 DataScope 透传机制 ★ 数据行级权限
§10 SSE 流式透传P5 AI ★ AI 对话
§11 并行 gRPC 编排与降级策略 ★ 弹性
§12 领域模型(聚合模型) BFF 无领域聚合
§13 数据模型(无 DB缓存/DTO BFF 无 DB
§14 事件设计(可选 Kafka 消费) P3+ 可选
§15 横切关注点对齐清单 ★ 黄金模板对齐
§16 与其他模块的交互点(契约矩阵) ★ 跨模块契约
§17 弹性设计(熔断/重试/超时) P6 硬化
§18 可观测性设计 三支柱
§19 安全设计 JWT/CORS/限流
§20 容量与性能预估 容量规划
§21 风险与假设 风险登记
§22 待 coord 仲裁的决策清单 ★ 阻塞项

1. 目标态架构P6 终态)

北极星:描绘 teacher-bff 在 P6 硬化阶段的终态,所有分阶段设计都向此对齐。

1.1 目标态架构图

graph TB
    subgraph Client["前端层"]
        TP[teacher-portal<br/>Next.js + urql]
    end

    subgraph Gateway["网关层"]
        AGW[api-gateway<br/>Go/Gin<br/>JWT 校验/限流/熔断]
    end

    subgraph BFF["teacher-bff本模块P6 终态"]
        direction TB
        GQL[GraphQL Yoga Endpoint<br/>POST /graphql<br/>+ GET /graphql/subscriptions]
        SSE[SSE Endpoint<br/>GET /ai/chat/stream]
        REST[REST Endpoint<br/>/healthz /readyz /metrics]

        subgraph Aggregation["聚合编排层"]
            RES[GraphQL Resolvers<br/>按教学场景域组织]
            DL[DataLoader<br/>批量去重]
            ORCH[并行编排器<br/>Promise.allSettled]
            FALL[降级策略<br/>部分失败容忍]
        end

        subgraph Cache["缓存层"]
            RC[Redis Cache<br/>聚合结果 5-30s<br/>权限 5min<br/>用户会话 30min]
            IC[Invalidation Coordinator<br/>TTL + 事件驱动]
        end

        subgraph Clients["下游 Client 抽象层"]
            IC_IAM[IamClient]
            IC_CE[CoreEduClient]
            IC_CNT[ContentClient]
            IC_DA[DataAnaClient]
            IC_AI[AiClient<br/>含 StreamChat 流式]
            IC_MSG[MsgClient]
        end

        subgraph Resilience["弹性层"]
            CB[Circuit Breaker<br/>opossum]
            RT[Retry<br/>gRPC interceptor]
            TL[Timeout<br/>per-RPC]
        end

        subgraph Obs["可观测层"]
            LOG[pino Logger]
            MET[prom-client Metrics]
            TRC[OTel Tracer<br/>gRPC interceptor]
        end
    end

    subgraph Downstream["下游业务服务gRPC"]
        IAM[iam:50052]
        CE[core-edu:50053]
        CNT[content:50054]
        DA[data-ana:50055]
        MSG[msg:50056]
        AI[ai:50057<br/>含 StreamChat]
    end

    subgraph Infra["基础设施"]
        K[Kafka<br/>权限缓存失效事件]
        R[(Redis<br/>edu-redis:6379)]
        J[Jaeger<br/>OTLP]
        P[Prometheus<br/>:9090]
    end

    TP -->|GraphQL/HTTP/SSE| AGW
    AGW -->|HTTP + x-user-id/x-user-roles| GQL
    AGW --> SSE
    AGW --> REST

    GQL --> RES
    SSE --> RES
    RES --> DL
    RES --> ORCH
    ORCH --> FALL
    RES --> RC
    RC --> IC

    DL --> Clients
    ORCH --> Clients
    Clients --> Resilience
    Resilience --> IAM
    Resilience --> CE
    Resilience --> CNT
    Resilience --> DA
    Resilience --> MSG
    Resilience --> AI

    RC --> R
    IC -.->|订阅 edu.identity.*| K
    IAM -.->|发布 edu.identity.*| K

    LOG --> J
    MET --> P
    TRC --> J

1.2 目标态核心特征

维度 P6 终态 设计依据
对前端协议 GraphQL YogaQuery/Mutation/Subscription+ SSEAI 流式)+ REST健康/指标) 004 §11.3、pending-features P2
对下游协议 全 gRPCiam/core-edu/content/data-ana/msg/ai 004 §4.1
缓存 Redis 三层(聚合 5-30s / 权限 5min / 会话 30min+ 事件驱动失效 004 §6.3
批量去重 DataLoader 覆盖全部 N+1 风险点 ai-allocation §5 ai03
弹性 Circuit Breaker + Retry + Timeout + 降级 P6 硬化
可观测 pino + prom-client + OTelgRPC interceptor 全链路) project_rules §12
多角色 教师/教导主任/教研组长复用同一 BFF视口 + DataScope 差异化 004 §5.4
流式 SSE 三层透传ai.StreamChat → bff → 前端 EventSource pending-features P5
事件消费 可选 Kafka consumer 订阅 edu.identity.* 精确失效权限缓存 004 §7.2
部署 K8s + HPA + mTLSIstio Service Mesh P6 硬化

2. 分阶段演进路线P2→P6

每阶段独立交付价值,通过抽象层隔离变更,避免大爆炸式重构。

2.1 演进路线图

graph LR
    P2[P2 现状加固<br/>REST + Client 抽象层<br/>Zod + 错误码迁移<br/>测试 + readyz]
    P3[P3 gRPC 切换<br/>iam + core-edu gRPC<br/>Redis 聚合缓存<br/>并行编排器]
    P4[P4 GraphQL 演进<br/>GraphQL Yoga 对前端<br/>DataLoader<br/>content + data-ana gRPC]
    P5[P5 AI + 通知<br/>SSE 流式透传<br/>ai + msg gRPC<br/>Kafka 缓存失效]
    P6[P6 硬化<br/>熔断/重试/超时<br/>HPA + mTLS<br/>99.9% 可用性]

    P2 --> P3 --> P4 --> P5 --> P6

2.2 各阶段交付清单

P2 — 现状加固(当前阶段)

目标:在不改变对外 REST 契约的前提下,补齐黄金模板差距,为后续 gRPC/GraphQL 切换铺路。

工作项 详情 优先级
引入 Client 抽象层 新增 src/clients/ 目录,定义 IamClient/ClassesClient/CoreEduClient 接口,当前实现为 REST fetch adapterP3 替换为 gRPC adapter P0
错误码迁移 TEACHER_BFF_*BFF_TEACHER_*(已完成,见 application-error.ts+ 3 类越权错误码president §2.7 P0
Zod 输入验证 Controller 层引入 Zod schema 解析 path/bodyclassId/examId 等) P0
/readyz 下游探针 并行 ping iam + classes + core-edu 的 /healthz,全部 ok 才返回 ok P0
Vitest 测试框架 引入 Vitest覆盖 Service 聚合逻辑 + Controller + 错误处理,目标 ≥ 80% P1
env 重构 IamServiceUrlIamServiceTarget(兼容 REST URL 和 gRPC target为 P3 切换铺路 P1
DataLoader 骨架 新增 src/dataloader/ 目录,定义 DataLoader 注册器骨架P2 不启用P4 接入 GraphQL P2
Redis 客户端引入 新增 src/shared/cache/redis.client.tsP2 仅用于会话/权限缓存P3 接入聚合缓存 P2

P2 退出标准lint + typecheck 零错误;测试覆盖率 ≥ 80%/readyz 真实探针;错误码全部 BFF_TEACHER_*Zod 校验全 Controller 覆盖。

P3 — gRPC 切换 + 聚合缓存

目标iam + core-edu 启用 gRPC serverteacher-bff 通过 Client 抽象层切换为 gRPC 调用;引入 Redis 聚合缓存。

工作项 详情
IamClient gRPC adapter 实现 @grpc/grpc-js + @bufbuild/protobuf client调用 iam:50052
CoreEduClient gRPC adapter 调用 core-edu:50053ExamService/HomeworkService/GradeService + AttendanceService 待 ai08 补)
ClassesClient 整合 classes 合并入 core-eduClassesClient 退役,调用走 CoreEduClient.ListClasses
并行编排器 抽取 src/aggregation/orchestrator.ts,封装 Promise.allSettled + 降级策略
Redis 聚合缓存 聚合结果 5-30s TTLkey = bff:teacher:{userId}:{queryHash}
gRPC interceptor 注入 trace contexttraceparent+ x-user-id metadata + metrics
优雅降级 dashboard 聚合iam 失败返回 user=nullclasses 失败返回 classes=null当前已实现抽象到编排器

P3 退出标准iam + core-edu 全部 gRPC 调用;聚合缓存命中率 ≥ 60%;并行编排器 + 降级策略单元测试覆盖。

P4 — GraphQL 演进 + DataLoader

目标:对前端引入 GraphQL YogaDataLoader 防 N+1content + data-ana 切 gRPC。

工作项 详情
GraphQL Yoga 接入 新增 POST /graphql 端点graphql-yoga + NestJS 集成
Schema 设计 按教学场景域组织 Query/Mutation见 §5
DataLoader 接入 Resolver 内调用 DataLoader覆盖全部 N+1 风险点(见 §6
ContentClient gRPC adapter 调用 content:50054KnowledgeGraphService + ChapterService/QuestionService 待 ai09 补)
DataAnaClient gRPC adapter 调用 data-ana:50055AnalyticsService + GetTeacherDashboard 待 ai11 补)
双轨读支持 data-ana 学情查询支持实时查主库 + 聚合查 ClickHouse 宽表
REST 兼容期 REST 端点保留deprecation header前端逐步迁移到 GraphQL
查询复杂度限制 GraphQL depth limit + query cost analysis防恶意查询

P4 退出标准GraphQL Yoga 上线DataLoader 覆盖全部列表查询content + data-ana gRPC 调用REST 进入 deprecation。

P5 — AI 流式 + 通知

目标AI 辅助出题SSE 流式透传)+ 通知查询聚合ai + msg 切 gRPC可选 Kafka 缓存失效。

工作项 详情
AiClient gRPC adapter 调用 ai:50057含 StreamChat streaming RPC
SSE 流式透传 GET /ai/chat/streamai.StreamChatgRPC stream→ BFF → 前端 EventSource见 §10
MsgClient gRPC adapter 调用 msg:50056NotificationService
通知聚合 Query GraphQL Query.notificationsListNotifications + MarkAsRead mutation
Kafka consumer可选 订阅 edu.identity.user.role_changed / edu.identity.role.updated,精确失效 Redis 权限缓存(见 §7.4
AI 用量计费 透传 ai 服务的 token 用量 metricsBFF 记录 bff_teacher_ai_usage_total

P5 退出标准SSE 流式透传端到端通;通知聚合 QueryKafka consumer可选上线或显式决策用短 TTL 兜底。

P6 — 硬化

目标99.9% 可用性,全链路可观测,独立扩缩容。

工作项 详情
Circuit Breaker opossumper-downstream-service错误率/延迟阈值
Retry gRPC interceptor指数退避仅幂等 RPC
Timeout per-RPC timeout默认 3s聚合总超时 5s
HPA K8s HorizontalPodAutoscaler基于 CPU + 自定义 metrics请求速率
mTLS Istio Service Mesh服务间双向 TLS
全链路 trace OTel + JaegergRPC interceptor 全链路 traceparent 传递
SLO 99.9% 可用性 + P99 延迟 < 500ms + 错误率 < 0.1%

P6 退出标准SLO 达标;全链路 trace 可视化HPA 生效mTLS 全覆盖。


3. 模块内部分层图

3.1 目标态分层P6

graph TB
    subgraph Entry["入口层 (Entry)"]
        GQL[GraphQL Yoga Controller<br/>POST /graphql]
        SSE[SSE Controller<br/>GET /ai/chat/stream]
        HEALTH[Health Controller<br/>/healthz /readyz /metrics]
    end

    subgraph Middleware["中间件层"]
        CTX[Context Middleware<br/>解析 x-user-id/x-user-roles<br/>注入 GraphQL context]
        ERR[GlobalErrorFilter<br/>统一错误兜底]
    end

    subgraph Aggregation["聚合编排层 (Aggregation)"]
        RES[GraphQL Resolvers<br/>按场景域组织]
        DL[DataLoader Registry<br/>per-request 实例]
        ORCH[Orchestrator<br/>Promise.allSettled + 降级]
        MAP[Response Mapper<br/>proto → GraphQL type]
    end

    subgraph Cache["缓存层 (Cache)"]
        RCLIENT[RedisClient<br/>ioredis]
        CKV[CacheKey Builder<br/>bff:teacher:*]
        CINV[Invalidation Coordinator<br/>TTL + 事件驱动]
    end

    subgraph Clients["下游 Client 抽象层 (Clients)"]
        IAM_C[IamClient<br/>interface + gRPC impl]
        CE_C[CoreEduClient<br/>interface + gRPC impl]
        CNT_C[ContentClient<br/>interface + gRPC impl]
        DA_C[DataAnaClient<br/>interface + gRPC impl]
        AI_C[AiClient<br/>interface + gRPC impl<br/>含 StreamChat]
        MSG_C[MsgClient<br/>interface + gRPC impl]
    end

    subgraph Resilience["弹性层"]
        CB[CircuitBreaker]
        RT[Retry]
        TL[Timeout]
    end

    subgraph Obs["可观测层"]
        LOG[pino]
        MET[prom-client]
        TRC[OTel Tracer]
    end

    subgraph Config["配置层"]
        ENV[env.ts<br/>Zod 校验]
        DI[DI Container<br/>NestJS]
    end

    GQL --> CTX
    SSE --> CTX
    CTX --> RES
    RES --> DL
    RES --> ORCH
    RES --> RCLIENT
    DL --> Clients
    ORCH --> Clients
    Clients --> Resilience
    Resilience --> Downstream

    RCLIENT --> CINV
    ERR -.-> GQL
    ERR -.-> SSE

3.2 目录结构(目标态)

services/teacher-bff/src/
├─ config/
│  └─ env.ts                          # Zod 校验环境变量
├─ entry/                             # 入口层
│  ├─ graphql.controller.ts           # GraphQL Yoga 端点P4
│  ├─ sse.controller.ts               # SSE 流式端点P5
│  └─ context.middleware.ts           # 解析 x-user-id 注入 context
├─ teacher/                           # 教学场景域P2 REST 端点P4 迁移 GraphQL
│  ├─ teacher.controller.ts           # REST ControllerP2P4 后逐步 deprecate
│  ├─ teacher.service.ts              # 聚合 Service
│  └─ teacher.schema.ts               # Zod 输入校验
├─ aggregation/                       # 聚合编排层
│  ├─ orchestrator.ts                 # Promise.allSettled + 降级
│  ├─ response-mapper.ts              # proto → GraphQL/REST type
│  └─ fallback-strategy.ts            # 降级策略
├─ dataloader/                        # DataLoaderP4
│  ├─ dataloader.module.ts            # per-request DataLoader 注册器
│  ├─ user.dataloader.ts              # 按 userId 批量取用户
│  ├─ class.dataloader.ts             # 按 classId 批量取班级
│  ├─ exam.dataloader.ts              # 按 classId 批量取考试
│  ├─ homework.dataloader.ts          # 按 classId 批量取作业
│  └─ grade.dataloader.ts             # 按 examId 批量取成绩
├─ graphql/                           # GraphQL SchemaP4
│  ├─ schema.ts                       # typeDefs + resolvers
│  ├─ types/                          # GraphQL type 定义
│  │  ├─ user.type.ts
│  │  ├─ class.type.ts
│  │  ├─ exam.type.ts
│  │  ├─ homework.type.ts
│  │  ├─ grade.type.ts
│  │  └─ analytics.type.ts
│  └─ resolvers/                      # 按场景域组织
│     ├─ dashboard.resolver.ts
│     ├─ exam.resolver.ts
│     ├─ homework.resolver.ts
│     ├─ grade.resolver.ts
│     ├─ analytics.resolver.ts
│     └─ notification.resolver.ts
├─ clients/                           # 下游 Client 抽象层
│  ├─ iam.client.ts                   # interface + gRPC impl
│  ├─ core-edu.client.ts
│  ├─ content.client.ts               # P4
│  ├─ data-ana.client.ts              # P4
│  ├─ ai.client.ts                    # P5含 StreamChat
│  ├─ msg.client.ts                   # P5
│  ├─ grpc/                           # gRPC adapterP3
│  │  ├─ grpc.factory.ts              # Channel + Client 工厂
│  │  └─ interceptors.ts              # trace/metrics/retry interceptor
│  └─ rest/                           # REST adapterP2过渡期
│     └─ rest-fetch.adapter.ts
├─ shared/
│  ├─ errors/
│  │  ├─ application-error.ts         # BFF_TEACHER_* 错误码
│  │  └─ global-error.filter.ts
│  ├─ health/
│  │  ├─ health.controller.ts         # /healthz + /readyz
│  │  └─ readiness.probe.ts           # 下游就绪探针P2 补)
│  ├─ cache/                          # 缓存层
│  │  ├─ redis.client.ts              # ioredis 客户端
│  │  ├─ cache-key.builder.ts
│  │  └─ invalidation.coordinator.ts  # TTL + 事件驱动失效
│  ├─ observability/
│  │  ├─ logger.ts                    # pino
│  │  ├─ metrics.ts                   # prom-client + 指标定义
│  │  └─ tracer.ts                    # OTel
│  ├─ resilience/                     # 弹性层P6
│  │  ├─ circuit-breaker.ts           # opossum
│  │  └─ retry.policy.ts
│  └─ kafka/                          # Kafka consumerP5 可选)
│     ├─ kafka.consumer.ts
│     └─ handlers/
│        └─ permission-invalidation.handler.ts
├─ app.module.ts
└─ main.ts

3.3 分层职责契约

职责 禁止
Entry 协议入口GraphQL/REST/SSE解析请求注入 context 业务逻辑、下游调用
Middleware 通用横切context 注入、错误兜底) 业务逻辑
Aggregation 编排多下游调用、聚合裁剪、降级 直接访问 DB、权限决策
Cache 缓存读写、失效协调 业务逻辑
Clients 下游协议适配REST/gRPC、interceptor 业务聚合逻辑
Resilience 熔断/重试/超时 业务逻辑
Observability 日志/指标/链路 业务逻辑

依赖方向Entry → Aggregation → Clients → Resilience → DownstreamCache 横向被 Aggregation 调用Observability 横向被所有层调用。禁止反向依赖。


4. API 设计REST→GraphQL 演进)

4.1 API 风格仲裁说明

冲突pending-features P2 退出标准要求 "GraphQL Yoga + DataLoader"coord-cross-review §2.5 裁决 "P2-P3 用 RESTP4 起若 BFF 切 GraphQL 再引入 urql"。

ai03 设计决策(提请 coord 仲裁):

  • 方案 A推荐P2 保持 REST 对前端(不阻塞 P2 退出,因 P2 退出标准核心是"教师登录→侧边栏→Dashboard"REST 足以支撑P4 引入 GraphQL Yoga + DataLoader。理由P2 引入 GraphQL 会大幅增加 P2 工作量且与 coord 裁决冲突GraphQL 价值在 P3+ 复杂聚合场景才显现(考试/作业/成绩/学情多源聚合)。
  • 方案 BP2 即引入 GraphQL Yoga + DataLoader。理由严格遵循 pending-features 字面要求,但需 coord 撤销 §2.5 裁决。

本设计文档后续以方案 A 为基准P2 REST、P4 GraphQL若 coord 仲裁为方案 B则将 §5 GraphQL Schema 前移到 P2 实施。

4.2 P2 REST API当前 + 加固)

method path 权限 请求 响应 说明
GET /teacher/dashboard 仅校验 x-user-id { success, data: { user, classes } } 聚合 IAM 用户 + classes 列表
GET /teacher/viewports 仅校验 x-user-id { success, data: ViewportItem[] } 拉取 IAM 视口L1 导航)
GET /teacher/classes/:classId/exams 仅校验 x-user-id classId: string { success, data: Exam[] } 聚合 core-edu 考试列表
GET /teacher/classes/:classId/homework 仅校验 x-user-id classId: string { success, data: Homework[] } 聚合 core-edu 作业列表
GET /teacher/exams/:examId/grades 仅校验 x-user-id examId: string { success, data: Grade[] } 聚合 core-edu 成绩列表
GET /healthz { status, service, timestamp } liveness
GET /readyz { status, service, timestamp, checks: {...} } readinessP2 补下游探针)
GET /metrics Prometheus text Prometheus 抓取

P2 加固项

  • 全部 Controller 方法补 Zod 输入校验classId/examId 必须是 cuid/uuid 格式)
  • 错误码统一 BFF_TEACHER_*
  • /readyz 补下游就绪探针

4.3 P3 REST 扩展

method path 说明
POST /teacher/exams 创建考试(透传 core-edu CreateExam
POST /teacher/homework 布置作业(透传 core-edu AssignHomework
POST /teacher/grades 录入成绩(透传 core-edu RecordGrade
GET /teacher/classes/:classId/attendance 出勤列表core-edu AttendanceService待 ai08 补 proto
GET /teacher/dashboard/stats 仪表盘统计data-ana GetTeacherDashboard待 ai11 补 proto

4.4 P4 GraphQL API目标态

见 §5 详细 Schema 设计。REST 端点进入 deprecation响应头加 Deprecation: true),前端逐步迁移。

4.5 P5 SSE 流式 API

method path 说明
GET /ai/chat/stream SSE 流式 AI 对话ai.StreamChat 透传,见 §10
POST /ai/generate/question AI 出题(透传 ai.GenerateQuestion

4.6 统一响应信封

所有 REST 端点遵循 ActionState 信封(与 classes 黄金模板一致):

// 成功
{ success: true, data: T }
// 失败GlobalErrorFilter 兜底)
{ success: false, error: { code: "BFF_TEACHER_*", message, details, traceId } }

GraphQL 端点遵循 GraphQL 规范data/errors错误扩展字段携带 extensions.code = "BFF_TEACHER_*"


5. GraphQL Schema 设计(目标态 P4

5.1 设计原则

  1. 按教学场景域组织(不按角色分),教导主任/教研组长复用同一 schema靠 viewports + dataScope 驱动前端差异化
  2. Query 为主Mutation 谨慎BFF 偏读多写少Mutation 仅透传下游命令
  3. Subscription 延后:实时通知用 SSEP5Subscription 留待 P6+ 评估
  4. N+1 防御:所有 list 字段必须经 DataLoader
  5. 复杂度限制depth limit ≤ 7query cost ≤ 1000

5.2 Schema 概览

# ============ Types ============

type User {
  id: ID!
  email: String!
  name: String!
  roles: [String!]!
  dataScope: DataScope!
}

enum DataScope {
  SELF # L0 本人
  CLASS # L1 本班
  GRADE # L2 本年级
  SCHOOL # L3 本校
  DISTRICT # L4 本区
  ALL # L5 全部
}

type ViewportItem {
  key: String!
  label: String!
  route: String!
  icon: String
  sortOrder: String!
  requiredPermission: String
}

type Class {
  id: ID!
  name: String!
  gradeId: String!
  # 延迟加载:班级下的考试
  exams: [Exam!]!
  # 延迟加载:班级下的作业
  homework: [Homework!]!
}

type Exam {
  id: ID!
  title: String!
  classId: ID!
  status: ExamStatus!
  publishedAt: DateTime
  # 延迟加载:考试下的成绩
  grades: [Grade!]!
}

enum ExamStatus {
  DRAFT
  PUBLISHED
  IN_PROGRESS
  GRADING
  SCORED
  ARCHIVED
}

type Homework {
  id: ID!
  title: String!
  classId: ID!
  dueDate: DateTime!
  submissions: [Submission!]!
}

type Submission {
  id: ID!
  studentId: ID!
  submittedAt: DateTime!
  status: SubmissionStatus!
}

enum SubmissionStatus {
  NOT_SUBMITTED
  SUBMITTED
  GRADED
}

type Grade {
  id: ID!
  examId: ID!
  studentId: ID!
  score: Float!
  rank: Int
  submittedAt: DateTime!
}

type KnowledgePath {
  knowledgePointId: ID!
  name: String!
  prerequisites: [KnowledgePoint!]!
}

type KnowledgePoint {
  id: ID!
  name: String!
  subject: String!
}

type ClassPerformance {
  classId: ID!
  averageScore: Float!
  passRate: Float!
  weaknessTopics: [String!]!
}

type StudentWeakness {
  studentId: ID!
  weakTopics: [String!]!
  recommendedExercises: [String!]!
}

type LearningTrend {
  studentId: ID!
  trend: [TrendPoint!]!
}

type TrendPoint {
  date: DateTime!
  score: Float!
}

type Notification {
  id: ID!
  type: NotificationType!
  title: String!
  content: String!
  read: Boolean!
  createdAt: DateTime!
}

enum NotificationType {
  SYSTEM
  EXAM
  HOMEWORK
  GRADE
  ATTENDANCE
}

type DashboardData {
  user: User
  classes: [Class!]!
  viewports: [ViewportItem!]!
  stats: DashboardStats
}

type DashboardStats {
  totalExams: Int!
  pendingGrading: Int!
  todayHomework: Int!
}

# ============ Query ============

type Query {
  # 仪表盘聚合(并行拉取 IAM + classes + data-ana
  dashboard: DashboardData!

  # 视口配置L1 导航)
  viewports: [ViewportItem!]!

  # 当前用户信息
  me: User!

  # 班级相关
  classes: [Class!]!
  class(id: ID!): Class

  # 考试/作业/成绩
  exams(classId: ID!): [Exam!]!
  homework(classId: ID!): [Homework!]!
  grades(examId: ID!): [Grade!]!

  # 学情分析P4
  classPerformance(classId: ID!): ClassPerformance!
  studentWeakness(studentId: ID!): StudentWeakness!
  learningTrend(studentId: ID!, dateRange: DateRangeInput): LearningTrend!

  # 知识图谱P4
  knowledgePath(knowledgePointId: ID!): KnowledgePath!

  # 通知P5
  notifications(unreadOnly: Boolean): [Notification!]!
}

input DateRangeInput {
  start: DateTime!
  end: DateTime!
}

# ============ Mutation ============

type Mutation {
  # 考试管理
  createExam(input: CreateExamInput!): Exam!
  publishExam(examId: ID!): Exam!

  # 作业管理
  assignHomework(input: AssignHomeworkInput!): Homework!

  # 成绩管理
  recordGrade(input: RecordGradeInput!): Grade!

  # AI 辅助P5
  generateQuestion(input: GenerateQuestionInput!): GeneratedQuestion!

  # 通知P5
  markNotificationAsRead(id: ID!): Notification!
}

input CreateExamInput {
  classId: ID!
  title: String!
  subject: String!
  scheduledAt: DateTime!
}

input AssignHomeworkInput {
  classId: ID!
  title: String!
  dueDate: DateTime!
}

input RecordGradeInput {
  examId: ID!
  studentId: ID!
  score: Float!
}

input GenerateQuestionInput {
  subject: String!
  gradeLevel: String!
  difficulty: String!
  knowledgePointIds: [ID!]!
}

5.3 Resolver 组织(按场景域)

graph TB
    subgraph Dashboard["Dashboard Resolver"]
        DQ[dashboard<br/>并行: IAM + classes + data-ana]
        DV[viewports]
        DM[me]
    end

    subgraph ExamResolver["Exam Resolver"]
        EQ[exams<br/>DataLoader by classId]
        EC[createExam]
        EP[publishExam]
    end

    subgraph HomeworkResolver["Homework Resolver"]
        HQ[homework<br/>DataLoader by classId]
        HA[assignHomework]
    end

    subgraph GradeResolver["Grade Resolver"]
        GQ[grades<br/>DataLoader by examId]
        GR[recordGrade]
    end

    subgraph AnalyticsResolver["Analytics Resolver (P4)"]
        CP[classPerformance]
        SW[studentWeakness]
        LT[learningTrend]
    end

    subgraph NotificationResolver["Notification Resolver (P5)"]
        NQ[notifications]
        NM[markNotificationAsRead]
    end

6. DataLoader 策略

6.1 N+1 风险点与 DataLoader 覆盖

风险点 场景 DataLoader 批量键 下游 RPC
用户信息 dashboard 拉 user多个 Resolver 都要 user UserLoader userId[] iam.GetUserInfo待补批量 RPC BatchGetUsers
班级信息 class.exam → 需拉 class 详情 ClassLoader classId[] core-edu.GetClass
考试列表 classes → class.exams ExamLoader classId[] core-edu.ListExamsByClass
作业列表 classes → class.homework HomeworkLoader classId[] core-edu.ListHomeworkByClass
成绩列表 exams → exam.grades GradeLoader examId[] core-edu.ListGradesByExam

6.2 DataLoader 生命周期

  • per-request 实例:每个 GraphQL 请求创建独立 DataLoader 实例,请求结束销毁(避免跨请求缓存污染)
  • 批量化窗口:默认 16ms 内的请求合并为一次批量调用
  • 缓存DataLoader 自身缓存per-request配合 Redis 跨请求缓存§7

6.3 批量 RPC 缺口(提请 coord 仲裁)

当前 proto 多数 service 仅提供单个 ID 查询,缺批量 RPC

service 现有 RPC 缺失批量 RPC 用途
iam.IamService GetUserInfo BatchGetUsers UserLoader 批量化
core-edu.ClassService GetClass BatchGetClasses ClassLoader 批量化
core-edu.ExamService ListExamsByClass 已支持classId 入参) ExamLoader
core-edu.GradeService ListGradesByExam 已支持 GradeLoader

ai03 提请iam 补 BatchGetUsers(userIds[])、core-edu 补 BatchGetClasses(classIds[]),否则 DataLoader 只能退化为一对一调用 + per-request 去重(仍有价值,但批量化收益受限)。


7. 缓存策略Redis 多层)

7.1 缓存层架构

graph TB
    REQ[请求] --> CK{Redis 命中?}
    CK -->|是| RET1[返回缓存]
    CK -->|否| AGG{需要聚合多服务?}
    AGG -->|否| SINGLE[直接调单一服务]
    AGG -->|是| PARALLEL[并行 gRPC 调多服务]
    SINGLE --> MERGE[内存聚合裁剪]
    PARALLEL --> MERGE
    MERGE --> WRITE[写入 Redis 短缓存]
    WRITE --> RET2[返回]

7.2 缓存策略矩阵

数据类型 Redis Key Pattern TTL 失效策略 阶段
用户会话 session:{userId} 30 min 滑动过期 P3
权限列表 perms:{userId} 5 min TTL + 事件驱动(edu.identity.user.role_changed P3
视口配置 viewports:{userId} 5 min TTL + 事件驱动(edu.identity.role.updated P3
班级列表 classes:{userId} 5 min TTL + 事件驱动(edu.org.class.created P3
考试/作业/成绩列表 exams:{classId} / homework:{classId} / grades:{examId} 30 s 短 TTL P3
聚合结果dashboard bff:teacher:dashboard:{userId} 5-30 s 短 TTL P3
学情宽表 ClickHouse 实时 CDC 同步teacher-bff 经 data-ana 读) P4
题库检索 ES 实时 CDC 同步teacher-bff 经 content 读) P4

7.3 缓存 Key 设计原则

  1. 命名空间:全部 key 以 bff:teacher: 前缀,便于运维清理
  2. 版本号key 内含 schema 版本(如 bff:teacher:v1:dashboard:{userId}schema 变更时 bump 版本
  3. 用户隔离:所有 key 必须含 userId避免跨用户数据泄漏
  4. 不缓存 Mutation 结果:仅缓存 Query 结果

7.4 缓存失效策略

策略 A短 TTL 兜底P2-P3 默认)

  • 所有缓存依赖短 TTL5-30s自然过期
  • 优点:简单,无需 Kafka consumer
  • 缺点:权限变更最长 5min 才生效(权限列表 TTL
  • 适用P2-P3权限变更频率低

策略 B事件驱动精确失效P5+ 可选)

  • teacher-bff 订阅 Kafka topic
    • edu.identity.user.role_changed → 删除 perms:{userId} + viewports:{userId}
    • edu.identity.role.updated → 批量删除该角色所有用户的 perms:* + viewports:*
    • edu.org.class.created → 删除 classes:{userId}
  • 优点:权限变更秒级生效
  • 缺点:引入 Kafka consumer 复杂度 + 幂等性处理
  • 适用P5+,权限变更频率高或安全要求严格

ai03 决策P2-P3 用策略 A短 TTL 兜底P5 评估是否引入策略 B取决于权限变更频率。提请 coord 仲裁。


8. 视口推导与角色差异化L1-L4

8.1 四层视口模型

graph TB
    subgraph L1["L1 导航层"]
        L1A[侧边栏菜单项<br/>iam_role_viewports 表配置]
    end

    subgraph L2["L2 路由层"]
        L2A[可访问路由<br/>route_permission 表<br/>Gateway 校验]
    end

    subgraph L3["L3 组件层"]
        L3A[页面内组件可见性<br/>usePermission().hasPermission<br/>前端 Hook]
    end

    subgraph L4["L4 数据层"]
        L4A[数据行级过滤<br/>DataScope 枚举<br/>下游 Repository 注入 WHERE]
    end

    L1 --> L2 --> L3 --> L4

8.2 多角色复用策略004 §5.4

角色 BFF L1 导航 L4 DataScope
教师 teacher-bff 标准教学菜单 SELF / CLASS
教导主任 teacher-bff 标准教学菜单 + 管理菜单(全校成绩分析等) GRADE / SCHOOL
教研组长 teacher-bff 标准教学菜单 + 教研菜单(教研成果对比等) GRADE

关键teacher-bff 的 GraphQL schema / REST 端点不为不同角色设计差异化接口,靠 IAM 返回的 viewportsL1+ dataScopeL4驱动前端差异化渲染 + 下游数据过滤。

8.3 teacher-bff 的视口职责

层级 teacher-bff 职责 实现方
L1 导航 透传 IAM getUserViewports 结果给前端 teacher-bff /teacher/viewports 端点(已实现)
L2 路由 不参与Gateway 层校验) api-gateway
L3 组件 不参与(前端 Hook teacher-portal
L4 数据 透传 dataScope 给下游 gRPC metadata teacher-bff Clients 层

8.4 视口推导数据流

sequenceDiagram
    participant FE as teacher-portal
    participant GW as api-gateway
    participant BFF as teacher-bff
    participant IAM as iam

    FE->>GW: GET /teacher/viewports (JWT)
    GW->>BFF: GET /teacher/viewports (x-user-id, x-user-roles)
    BFF->>IAM: GetViewports(userId) 或 GetEffectiveAccess(userId)
    IAM-->>BFF: ViewportItem[] (过滤掉 requiredPermission 不匹配的项)
    BFF-->>GW: { success, data: ViewportItem[] }
    GW-->>FE: { success, data: ViewportItem[] }
    FE->>FE: 按 sortOrder 渲染侧边栏

9. DataScope 透传机制

9.1 DataScope 6 级定义

级别 名称 数据范围 典型角色
L0 SELF 仅本人数据 学生、家长
L1 CLASS 本班数据 班主任、学生
L2 GRADE 本年级数据 年级组长、教研组长
L3 SCHOOL 本校数据 校管理员、教导主任
L4 DISTRICT 本区数据 区教研员
L5 ALL 全部数据 系统管理员

9.2 teacher-bff 透传机制(不做解析)

graph LR
    JWT[JWT payload<br/>含 dataScope] --> GW[api-gateway<br/>解码 JWT]
    GW -->|x-user-id, x-user-roles| BFF[teacher-bff]
    BFF -->|getEffectiveAccess(userId)| IAM[iam]
    IAM -->|dataScope: SCHOOL| BFF
    BFF -->|gRPC metadata:<br/>x-data-scope=SCHOOL| CE[core-edu]
    CE -->|Repository 注入 WHERE<br/>school_id = ?| DB[(MySQL)]

9.3 关键约束

  1. BFF 不解析 dataScope 语义BFF 只透传,不根据 dataScope 做过滤
  2. gRPC metadata 透传BFF 调下游时,将 dataScope 作为 gRPC metadatakey: x-data-scope)传递
  3. 下游 Repository 注入 WHERE:下游服务在 Repository 层根据 dataScope + 用户身份动态注入 WHERE 条件
  4. 缓存隔离:缓存 key 必须含 dataScope避免不同 dataScope 用户读到同一缓存(如 grades:{examId}:{dataScope}:{userId}

9.4 DataScope 获取方式P2 vs P3+

  • P2RESTteacher-bff 调 IAM /iam/me 获取 user.dataScope当前 iam TS 接口含 dataScope 字段)
  • P3+gRPCteacher-bff 调 iam GetEffectiveAccess(userId) 一次获取 {permissions, viewports, dataScope} 聚合结果(待 ai06 补 proto RPC

10. SSE 流式透传P5 AI 对话)

10.1 三层流式架构

sequenceDiagram
    participant FE as teacher-portal
    participant GW as api-gateway
    participant BFF as teacher-bff
    participant AI as ai (gRPC stream)

    FE->>GW: GET /ai/chat/stream?prompt=... (EventSource)
    GW->>BFF: GET /ai/chat/stream (x-user-id)
    BFF->>AI: StreamChat(req) [gRPC streaming RPC]
    loop 逐 chunk
        AI-->>BFF: ChatChunk { delta }
        BFF-->>GW: SSE: data: { delta }
        GW-->>FE: SSE: data: { delta }
    end
    AI-->>BFF: [stream end]
    BFF-->>GW: SSE: event: done
    GW-->>FE: SSE: event: done

10.2 实现要点

  1. gRPC streaming RPCai.StreamChat 返回 Stream<ChatChunk>BFF 用 @grpc/grpc-js 的 streaming client
  2. SSE 响应BFF 用 Express res.write() 逐 chunk 写入 SSE 格式(data: {json}\n\n
  3. 背压处理BFF 监听 res 的 drain 事件gRPC stream 的 pause/resume
  4. 超时与取消客户端断开连接时BFF 调用 gRPC call.cancel() 取消下游流
  5. 错误透传gRPC stream error → SSE event: error
  6. trace 传递gRPC metadata 携带 traceparentSSE 响应头携带 trace-id

10.3 降级策略

  • gRPC stream 不可用 → SSE 返回 error 事件,前端提示"AI 服务暂不可用"
  • 不降级为非流式AI 出题体验依赖流式打字机效果)

11. 并行 gRPC 编排与降级策略

11.1 编排器设计

graph TB
    REQ[Resolver 请求] --> ORCH[Orchestrator]
    ORCH --> PARALLEL[Promise.allSettled]
    PARALLEL --> C1[IamClient.getUser]
    PARALLEL --> C2[CoreEduClient.getClasses]
    PARALLEL --> C3[DataAnaClient.getStats]
    C1 --> R1[结果1]
    C2 --> R2[结果2]
    C3 --> R3[结果3]
    R1 --> FALL[降级策略]
    R2 --> FALL
    R3 --> FALL
    FALL --> MAP[Response Mapper]
    MAP --> RESP[返回聚合结果]

11.2 降级策略矩阵

场景 失败的下游 降级行为 示例
Dashboard 聚合 iam user=null返回 classes + stats 教师看不到自己信息但能看到班级
Dashboard 聚合 classes classes=[],返回 user + stats 教师看不到班级但能看到自己信息
Dashboard 聚合 data-ana stats=null返回 user + classes 教师看不到统计但能看到基础信息
Dashboard 聚合 全部失败 BFF_TEACHER_BAD_GATEWAY 整体 502
单一查询 任意 直接抛错 grades 查询失败 → 502

11.3 超时策略

层级 超时 说明
单个 gRPC RPC 3s 默认
聚合编排总超时 5s Promise.allSettled + Promise.race(timeout)
SSE 流式 无超时 流式连接保持,靠心跳
GraphQL 查询 10s GraphQL Yoga 内置超时

12. 领域模型(聚合模型)

teacher-bff 是 BFF 聚合层,不持有业务领域聚合根。本节描述 BFF 内部的聚合模型DTO/VO

12.1 聚合模型清单

模型 来源 用途
DashboardData 聚合 iam.UserInfo + classes.Class[] + data-ana.DashboardStats dashboard 端点响应
ViewportItem 透传 iam.ViewportItem viewports 端点响应
ExamListResult 透传 core-edu.Exam[] exams 端点响应
HomeworkListResult 透传 core-edu.Homework[] homework 端点响应
GradeListResult 透传 core-edu.Grade[] grades 端点响应
AggregationContext 内部 编排器上下文userId, dataScope, traceId

12.2 模型间通信

  • BFF 内部聚合模型不跨服务通信(仅内存聚合)
  • 跨服务通信全部通过 Clients 层 gRPC 调用
  • 聚合模型不可变immutable通过 Response Mapper 从 proto message 映射

13. 数据模型(无 DB缓存/DTO

teacher-bff 无数据库,本节描述缓存 schema 和 DTO 结构。

13.1 Redis 缓存 Schema

# 用户会话P3
session:{userId} → JSON { userId, roles, dataScope, exp }

# 权限列表P3
perms:{userId} → JSON string[] (permission names)

# 视口配置P3
viewports:{userId} → JSON ViewportItem[]

# 班级列表P3
classes:{userId} → JSON Class[]

# 聚合结果P3
bff:teacher:v1:dashboard:{userId} → JSON DashboardData

# 考试/作业/成绩列表P3
exams:{classId} → JSON Exam[]
homework:{classId} → JSON Homework[]
grades:{examId} → JSON Grade[]

13.2 DTO 与 proto message 映射

BFF DTO proto message 映射规则
ViewportItem iam.v1.ViewportItem(待补) 字段一一映射
DashboardData.user iam.v1.UserInfo 字段一一映射
DashboardData.classes classes.v1.Class[] 字段一一映射
ExamListResult core_edu.v1.Exam[] 时间字段毫秒数转换

14. 事件设计(可选 Kafka 消费)

14.1 teacher-bff 消费的事件P5+ 可选)

Topic 发布方 消费动作 阶段
edu.identity.user.role_changed iam 删除 perms:{userId} + viewports:{userId} P5+
edu.identity.role.updated iam 批量删除该角色所有用户的 perms:* + viewports:* P5+
edu.org.class.created core-edu 删除 classes:{userId} P5+

14.2 幂等性设计

  • 基于 event_id 去重Redis SETNX event:processed:{eventId}TTL 24h
  • Consumer group: teacher-bff-cache-invalidation
  • 失败重试3 次指数退避,超过后进死信 topic

14.3 teacher-bff 不发布事件

BFF 是聚合层,不发布领域事件无业务状态变更。BFF 自身的 metrics/audit log 走可观测性通道,不走 Kafka。


15. 横切关注点对齐清单

15.1 权限装饰器

BFF 不做权限决策,不加 @RequirePermission Guard。BFF 仅校验 x-user-id header 存在(当前已实现)。

端点 权限校验 说明
/teacher/* 仅校验 x-user-id 存在 权限决策在 GatewayJWT+ 下游服务(@RequirePermission
/healthz, /readyz, /metrics 白名单放行
/graphqlP4 校验 x-user-id + 注入 context 同上
/ai/chat/streamP5 校验 x-user-id 同上

15.2 错误码清单(BFF_TEACHER_* 前缀B5 + G14 裁决)

越权防御 3 类错误码president §2.7UNAUTHORIZED / FORBIDDEN_RESOURCE / IDENTITY_MISMATCH

错误码 HTTP 触发条件 状态
BFF_TEACHER_VALIDATION_FAILED 400 Zod 校验失败 已实现
BFF_TEACHER_UNAUTHORIZED 401 x-user-id 缺失或无效president §2.7 已实现
BFF_TEACHER_FORBIDDEN_RESOURCE 403 teacherId 与资源无归属关系(场景 A§2.7 已实现
BFF_TEACHER_IDENTITY_MISMATCH 403 JWT teacherId 与 body teacherId 不一致(场景 B§2.7 已实现
BFF_TEACHER_NOT_FOUND 404 资源不存在 已实现
BFF_TEACHER_CONFLICT 409 并发冲突 已实现
BFF_TEACHER_BUSINESS_ERROR 422 业务规则违反(含 P2 未就绪字段) 已实现
BFF_TEACHER_UPSTREAM_UNAVAILABLE 502 下游 gRPC 不可达 已实现
BFF_TEACHER_AGGREGATION_FAILED 500 聚合多下游部分失败且无降级数据president §2.6 已实现
BFF_TEACHER_INTERNAL_ERROR 500 未知异常 已实现

下游业务错误透传iam 抛 IAM_*、core-edu 抛 CORE_EDU_*、classes 抛 CLASSES_*BFF 不改写错误码,直接透传给前端。

15.3 Logger

  • pino已具备logger.ts
  • 配置:结构化 JSONLOG_LEVEL 环境变量控制
  • 注入字段traceId、userId、service=teacher-bff
  • 禁止 console.log

15.4 Metrics 指标清单

指标名 类型 标签 描述
bff_teacher_http_requests_total Counter method, path, status HTTP 请求总数
bff_teacher_http_duration_seconds Histogram method, path HTTP 请求延迟
bff_teacher_downstream_calls_total Counter service, rpc, status 下游调用总数
bff_teacher_downstream_duration_seconds Histogram service, rpc 下游调用延迟
bff_teacher_cache_hits_total Counter cache_type 缓存命中数
bff_teacher_cache_misses_total Counter cache_type 缓存未命中数
bff_teacher_graphql_query_total CounterP4 operation, status GraphQL 查询总数
bff_teacher_graphql_duration_seconds HistogramP4 operation GraphQL 查询延迟
bff_teacher_ai_usage_total CounterP5 user, model AI token 用量

15.5 Tracer

  • OpenTelemetry SDK + OTLP exporter已具备tracer.ts
  • 注入点HTTP middleware + gRPC interceptorP3
  • traceparent 传递HTTP header → gRPC metadataP3
  • Jaeger 端点OTEL_EXPORTER_OTLP_ENDPOINT 环境变量

15.6 /healthz 检查逻辑

// liveness仅返回进程存活
{ status: "ok", service: "teacher-bff", timestamp: ISO8601 }

15.7 /readyz 检查逻辑P2 补全)

// readiness并行 ping 下游服务 /healthz
async readiness() {
  const checks = await Promise.allSettled([
    fetch(`${env.IamServiceTarget}/healthz`),
    fetch(`${env.ClassesServiceTarget}/healthz`),
    fetch(`${env.CoreEduServiceTarget}/healthz`),
    redisClient.ping(),  // P3 引入 Redis 后
  ]);
  const status = checks.every(r => r.status === 'fulfilled') ? 'ok' : 'degraded';
  return { status, service: 'teacher-bff', timestamp, checks: {...} };
}

15.8 优雅关闭顺序

  1. HTTP server stop accepting new connections
  2. 等待 in-flight 请求完成(最长 10s
  3. Kafka consumer commit + leaveP5+
  4. Redis connection close
  5. gRPC channels closeP3+
  6. Tracer flush + shutdown

当前 main.ts 已处理 SIGTERM + app.close(),需在 P3+ 补充 Redis/gRPC/Kafka 的关闭顺序。


16. 与其他模块的交互点(契约矩阵)

16.1 完整交互矩阵P2-P6

方向 对方服务 协议 接口/事件 用途 阶段 proto 状态
被调用 api-gateway HTTP POST /graphql + GET /teacher/* + GET /ai/chat/stream + /healthz /readyz /metrics Gateway 转发 + 注入 x-user-id P2
调用 iam HTTPP2/ gRPC 50052P3+ GetUserInfo 拉取用户信息 P2 已有
调用 iam gRPC 50052P3+ GetViewports 拉取视口配置 P2 待 ai06 补
调用 iam gRPC 50052P3+ GetEffectivePermissions 拉取权限列表 P2 待 ai06 补
调用 iam gRPC 50052P4+ GetEffectiveAccess(建议聚合 RPC 一次拉取 {perms, viewports, dataScope} P4 待 coord 仲裁
调用 iam gRPC 50052P3+ Logout 登出 P2 待 ai06 补
调用 iam gRPC 50052P3+ GetPublicKey / JWKS 公钥BFF 不校验 JWT但可缓存公钥给前端 P3 待 ai06 补
调用 classes / core-edu HTTPP2/ gRPC 50053P3+ ListClasses / GetClass 班级列表 P2 已有
调用 core-edu gRPC 50053P3+ ExamService.*CreateExam/GetExam/ListExamsByClass 等) 考试聚合 P3 已有
调用 core-edu gRPC 50053P3+ HomeworkService.* 作业聚合 P3 已有
调用 core-edu gRPC 50053P3+ GradeService.* 成绩聚合 P3 已有
调用 core-edu gRPC 50053P3+ AttendanceService.* 出勤聚合 P3 待 ai08 补
调用 core-edu gRPC 50053P3+ GetClassesByTeacher(建议新增) 按教师拉班级 P3 待 coord 仲裁
调用 content gRPC 50054P4+ KnowledgeGraphService.GetPrerequisites / GetLearningPath 知识图谱 P4 已有
调用 content gRPC 50054P4+ ChapterService. / QuestionService.** 章节/题库 P4 待 ai09 补
调用 data-ana gRPC 50055P4+ AnalyticsService.GetClassPerformance / GetStudentWeakness / GetLearningTrend 学情诊断 P4 已有
调用 data-ana gRPC 50055P4+ GetTeacherDashboard(建议新增) 仪表盘统计 P4 待 coord 仲裁
调用 ai gRPC 50057P5+ AiService.GenerateQuestion / Chat / OptimizeExpression AI 辅助 P5 已有
调用 ai gRPC 50057 streamingP5+ AiService.StreamChat SSE 流式对话 P5 已有
调用 msg gRPC 50056P5+ NotificationService.ListNotifications / SearchNotifications / MarkAsRead 通知聚合 P5 已有
消费(可选) Kafka 异步 edu.identity.user.role_changed 权限缓存失效 P5+ topic 已定义
消费(可选) Kafka 异步 edu.identity.role.updated 权限缓存失效 P5+ topic 已定义

16.2 端口分配

服务 HTTP 端口 gRPC 端口
teacher-bff本模块 3003 不暴露(只被 Gateway HTTP 调用)
iam 3002 50052
classes / core-edu 3004 50053
content 3005 50054
data-ana 3006 50055
msg 3007 50056
ai 3008 50057

17. 弹性设计(熔断/重试/超时P6 硬化)

17.1 Circuit Breaker 配置

下游服务 错误率阈值 熔断时长 半开请求数
iam 50% 30s 3
core-edu 50% 30s 3
content 50% 60s 2
data-ana 50% 60s 2
ai 70%(容忍度高) 30s 5
msg 50% 60s 2

17.2 Retry 策略

  • 仅对幂等 RPC 重试GET 类查询)
  • Mutation 类 RPC 不重试(避免重复写入)
  • 最大重试次数2
  • 退避策略指数退避100ms, 400ms
  • 仅对 transient 错误重试UNAVAILABLE / DEADLINE_EXCEEDED不重试 INVALID_ARGUMENT / NOT_FOUND

17.3 超时层级

层级 超时
单个 gRPC RPC 3s
聚合编排总超时 5s
GraphQL 查询 10s
SSE 流式 无(靠心跳 + 客户端断开)
Redis 操作 100ms

18. 可观测性设计

18.1 三支柱project_rules §12

支柱 实现 端点 采样率
日志 pino结构化 JSON stdout 100%
指标 prom-client GET /metrics 100%
链路 OpenTelemetry SDK + OTLP Jaeger 10%P6 全量)

18.2 关键 trace span

HTTP /graphql (root span)
├─ graphql.resolve (operation)
│  ├─ dataloader.load (batch key)
│  ├─ grpc.call iam.GetUser (child span)
│  ├─ grpc.call core-edu.ListExams (child span)
│  └─ redis.get cache (child span)
└─ http.response (serialize)

18.3 告警规则(提给 SRE

告警 条件 严重度
teacher-bff 5xx 错误率高 rate(bff_teacher_http_requests_total{status=~"5.."}[5m]) > 0.01 P1
下游调用失败率高 rate(bff_teacher_downstream_calls_total{status="error"}[5m]) > 0.05 P1
缓存命中率低 cache_misses / (hits + misses) > 0.5 持续 10min P2
P99 延迟高 histogram_quantile(0.99, bff_teacher_http_duration_seconds) > 1 P2
/readyz 失败 readyz_status != "ok" 持续 1min P0

19. 安全设计

19.1 JWT 与身份传递

  • BFF 不校验 JWTJWT 校验在 api-gateway 层project_rules §4
  • BFF 接收 headerx-user-idx-user-roles(由 Gateway 解码 JWT 后注入)
  • BFF 透传:调下游 gRPC 时,将 userId/roles/dataScope 作为 metadata 传递

19.2 CORS

  • 由 api-gateway 统一处理BFF 不直接暴露给前端
  • BFF 仅接受来自 api-gateway 的请求(内网通信)

19.3 限流

  • 由 api-gateway 统一处理IP 级令牌桶 + 用户级)
  • BFF 自身不做限流(避免双重限流逻辑)

19.4 输入校验

  • RESTController 层 Zod schema 解析P2 补全)
  • GraphQLgraphql-yoga 内置参数校验 + 自定义 scalarDateTime、ID
  • 查询复杂度限制P4depth ≤ 7cost ≤ 1000

19.5 敏感数据

  • 日志禁止记录 JWT token、密码、用户敏感信息pino redact 配置)
  • Redis 缓存不存储密码、token仅存 userId/roles/dataScope/perms/viewports

20. 容量与性能预估

20.1 容量预估(假设 1000 教师 / 100 学校)

指标 预估值 依据
日活教师 1000 100 校 × 10 教师/校
峰值 QPS 100 1000 教师 × 0.1 请求/秒
峰值并发 50 QPS × 平均延迟 0.5s
单实例容量 500 QPS NestJS Node.js 单进程
建议实例数 2P6 HPA 2-10 峰值 QPS / 单实例 + 冗余

20.2 性能目标

指标 目标 阶段
P50 延迟 < 100ms P3
P99 延迟 < 500ms P3
缓存命中率 > 60% P3
错误率 < 0.1% P6
可用性 99.9% P6

20.3 Redis 容量预估

数据类型 数量 单条大小 总量
用户会话 1000 200B 200KB
权限列表 1000 500B 500KB
视口配置 1000 1KB 1MB
班级列表 1000 5KB 5MB
聚合结果 1000 10KB 10MB
合计 ~17MB

Redis 单实例 256MB 足够,无需集群。


21. 风险与假设

21.1 技术风险

风险 影响 概率 缓解措施
gRPC 切换引入回归 Client 抽象层 + feature flag 灰度 + 完整测试
GraphQL Schema 演进破坏前端 Schema 版本化 + deprecation 先行 + 协调前端 ai13
Redis 缓存穿透 短 TTL + 空结果缓存null cache 5s
下游服务不可用 降级策略 + 熔断
SSE 流式连接堆积 超时 + 心跳 + 连接数限制
Kafka consumer 引入复杂度 P5 评估,可继续用短 TTL 兜底

21.2 假设

  1. 假设 iam 提供 GetViewports / GetEffectivePermissions RPCcoord 裁决 P2 补全),否则 P2 继续用 REST /iam/viewports + /iam/me
  2. 假设 core-edu P3 启用 gRPC server004 §4.1),否则 teacher-bff P3 继续 REST 调 core-edu
  3. 假设 api-gateway 注入 x-user-id / x-user-roles headerproject_memory 已确认BFF 不解码 JWT
  4. 假设前端 ai13teacher-portal配合迁移到 GraphQLP4否则 REST 兼容期延长
  5. 假设 Redis 共用 edu-redis 实例project_memory 已确认),不独立部署

21.3 未决设计决策(提请 coord 仲裁,见 §22

见 §22 待 coord 仲裁的决策清单。


22. 待 coord 仲裁的决策清单

以下决策阻塞 P2-P5 实施,需 coord 仲裁后 ai03 据此调整设计。

# 决策项 选项 ai03 建议 阻塞阶段
1 GraphQL 引入时机 A. P2 即引入pending-features 字面)/ B. P2-P3 REST、P4+ GraphQLcoord §2.5 裁决) BP2 引入 GraphQL 工作量大P4 复杂聚合场景才显现价值) P2
2 iam 聚合 API A. 新增 GetEffectiveAccess(userId) → {perms, viewports, dataScope} / B. 三次独立 RPC A(减少 3 次 RTT仪表盘场景受益 P2-P3
3 iam 补 4 个 RPC GetViewports / GetEffectivePermissions / Logout / GetPublicKey 强烈建议 P2 补全teacher-bff 直接依赖) P2
4 core-edu 补 AttendanceService service 级新增 建议 P3 补全 P3
5 core-edu 补 GetClassesByTeacher RPC 按教师拉班级 建议新增(当前 ListClasses 仅按 gradeId P3
6 data-ana 补 GetTeacherDashboard RPC 仪表盘统计聚合 建议新增 P4
7 iam 补 BatchGetUsers RPC DataLoader 批量化 建议新增(否则 DataLoader 退化为一对一) P4
8 core-edu 补 BatchGetClasses RPC DataLoader 批量化 建议新增 P4
9 Kafka consumer 引入时机 A. P5 引入精确失效 / B. 全程短 TTL 兜底 P5 评估(取决于权限变更频率) P5
10 错误码迁移 TEACHER_BFF_*BFF_TEACHER_* 全量替换 已是 coord P0 整改 #3本设计确认执行 P2
11 REST deprecation 时间表 P4 引入 GraphQL 后 REST 保留多久 建议 P4-P5 兼容期P6 下线 P4

附录 A与黄金模板classes对齐 Checklist

供 coord 交叉审查 ai03 对齐情况。

  • 权限装饰器BFF 不加(设计决策,依赖 Gateway + 下游)
  • 错误码前缀:BFF_TEACHER_*P2 迁移)
  • Loggerpino
  • Metricsprom-client + /metrics
  • TracerOTel
  • /healthz + /readyzP2 补下游探针
  • 优雅关闭SIGTERM
  • 测试覆盖率 ≥ 80%P2 引入 Vitest
  • Dockerfile 多阶段构建
  • Zod 输入验证P2 补全
  • GlobalErrorFilter 统一兜底
  • env Zod 校验env.ts 已有)

附录 B术语表

术语 含义
BFF Backend For Frontend面向前端的后端聚合层
DataLoader Facebook 出品的批量化 + 缓存工具,防 N+1
DataScope 数据范围枚举6 级),控制数据行级可见性
视口Viewport 角色可见的导航/路由/组件/数据四层配置
SSE Server-Sent Events服务器推送事件
Outbox 事务内写业务表 + outbox 表的事件发布模式
CDC Change Data Capture数据库变更捕获
HPA Horizontal Pod AutoscalerK8s 水平自动扩缩容
mTLS mutual TLS双向 TLS 认证

AI Agent: ai03 (teacher-bff) Coordinator: coord-ai Branch: 单仓库并行模式(直接 push main 关联阶段 1 文档: 01-understanding.md