73 KiB
模块架构设计文档 — teacher-bff
AI 标识:ai03 负责模块:teacher-bff(P2) 阶段:架构设计外包 · 阶段 2(模块架构设计) 日期:2026-07-09 关联文档:01-understanding.md、ai-allocation.md、004 架构影响地图、pending-features.md、coord-cross-review.md
设计原则:
- 目标态先行:先描绘 P6 终态架构,再回推分阶段演进路径,避免短视设计
- 契约驱动:所有跨模块交互以 proto 契约为唯一源,proto 缺失项显式标注并提请 coord 仲裁
- 平滑演进:每一阶段都能独立交付价值,不依赖未来阶段;每次升级通过抽象层隔离变更
- BFF 本分:只聚合、裁剪、协议转换,不持业务状态、不做权限决策、不直访业务 DB
- 为未来铺垫:抽象层预留 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 Yoga(Query/Mutation/Subscription)+ SSE(AI 流式)+ REST(健康/指标) | 004 §11.3、pending-features P2 |
| 对下游协议 | 全 gRPC(iam/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 + OTel(gRPC 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 + mTLS(Istio 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 adapter,P3 替换为 gRPC adapter |
P0 |
| 错误码迁移 | TEACHER_BFF_* → BFF_TEACHER_*(已完成,见 application-error.ts)+ 3 类越权错误码(president §2.7) |
P0 |
| Zod 输入验证 | Controller 层引入 Zod schema 解析 path/body(classId/examId 等) | P0 |
/readyz 下游探针 |
并行 ping iam + classes + core-edu 的 /healthz,全部 ok 才返回 ok |
P0 |
| Vitest 测试框架 | 引入 Vitest,覆盖 Service 聚合逻辑 + Controller + 错误处理,目标 ≥ 80% | P1 |
| env 重构 | IamServiceUrl → IamServiceTarget(兼容 REST URL 和 gRPC target),为 P3 切换铺路 |
P1 |
| DataLoader 骨架 | 新增 src/dataloader/ 目录,定义 DataLoader 注册器骨架(P2 不启用,P4 接入 GraphQL) |
P2 |
| Redis 客户端引入 | 新增 src/shared/cache/redis.client.ts,P2 仅用于会话/权限缓存,P3 接入聚合缓存 |
P2 |
P2 退出标准:lint + typecheck 零错误;测试覆盖率 ≥ 80%;/readyz 真实探针;错误码全部 BFF_TEACHER_*;Zod 校验全 Controller 覆盖。
P3 — gRPC 切换 + 聚合缓存
目标:iam + core-edu 启用 gRPC server,teacher-bff 通过 Client 抽象层切换为 gRPC 调用;引入 Redis 聚合缓存。
| 工作项 | 详情 |
|---|---|
| IamClient gRPC adapter | 实现 @grpc/grpc-js + @bufbuild/protobuf client,调用 iam:50052 |
| CoreEduClient gRPC adapter | 调用 core-edu:50053(ExamService/HomeworkService/GradeService + AttendanceService 待 ai08 补) |
| ClassesClient 整合 | classes 合并入 core-edu,ClassesClient 退役,调用走 CoreEduClient.ListClasses |
| 并行编排器 | 抽取 src/aggregation/orchestrator.ts,封装 Promise.allSettled + 降级策略 |
| Redis 聚合缓存 | 聚合结果 5-30s TTL,key = bff:teacher:{userId}:{queryHash} |
| gRPC interceptor | 注入 trace context(traceparent)+ x-user-id metadata + metrics |
| 优雅降级 | dashboard 聚合:iam 失败返回 user=null,classes 失败返回 classes=null(当前已实现,抽象到编排器) |
P3 退出标准:iam + core-edu 全部 gRPC 调用;聚合缓存命中率 ≥ 60%;并行编排器 + 降级策略单元测试覆盖。
P4 — GraphQL 演进 + DataLoader
目标:对前端引入 GraphQL Yoga,DataLoader 防 N+1;content + data-ana 切 gRPC。
| 工作项 | 详情 |
|---|---|
| GraphQL Yoga 接入 | 新增 POST /graphql 端点,graphql-yoga + NestJS 集成 |
| Schema 设计 | 按教学场景域组织 Query/Mutation(见 §5) |
| DataLoader 接入 | Resolver 内调用 DataLoader,覆盖全部 N+1 风险点(见 §6) |
| ContentClient gRPC adapter | 调用 content:50054(KnowledgeGraphService + ChapterService/QuestionService 待 ai09 补) |
| DataAnaClient gRPC adapter | 调用 data-ana:50055(AnalyticsService + 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/stream:ai.StreamChat(gRPC stream)→ BFF → 前端 EventSource(见 §10) |
| MsgClient gRPC adapter | 调用 msg:50056(NotificationService) |
| 通知聚合 Query | GraphQL Query.notifications(ListNotifications + MarkAsRead mutation) |
| Kafka consumer(可选) | 订阅 edu.identity.user.role_changed / edu.identity.role.updated,精确失效 Redis 权限缓存(见 §7.4) |
| AI 用量计费 | 透传 ai 服务的 token 用量 metrics,BFF 记录 bff_teacher_ai_usage_total |
P5 退出标准:SSE 流式透传端到端通;通知聚合 Query;Kafka consumer(可选)上线或显式决策用短 TTL 兜底。
P6 — 硬化
目标:99.9% 可用性,全链路可观测,独立扩缩容。
| 工作项 | 详情 |
|---|---|
| Circuit Breaker | opossum,per-downstream-service,错误率/延迟阈值 |
| Retry | gRPC interceptor,指数退避,仅幂等 RPC |
| Timeout | per-RPC timeout(默认 3s,聚合总超时 5s) |
| HPA | K8s HorizontalPodAutoscaler,基于 CPU + 自定义 metrics(请求速率) |
| mTLS | Istio Service Mesh,服务间双向 TLS |
| 全链路 trace | OTel + Jaeger,gRPC 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 Controller(P2,P4 后逐步 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/ # DataLoader(P4)
│ ├─ 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 Schema(P4)
│ ├─ 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 adapter(P3)
│ │ ├─ grpc.factory.ts # Channel + Client 工厂
│ │ └─ interceptors.ts # trace/metrics/retry interceptor
│ └─ rest/ # REST adapter(P2,过渡期)
│ └─ 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 consumer(P5 可选)
│ ├─ 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 → Downstream;Cache 横向被 Aggregation 调用;Observability 横向被所有层调用。禁止反向依赖。
4. API 设计(REST→GraphQL 演进)
4.1 API 风格仲裁说明
冲突:pending-features P2 退出标准要求 "GraphQL Yoga + DataLoader";coord-cross-review §2.5 裁决 "P2-P3 用 REST,P4 起若 BFF 切 GraphQL 再引入 urql"。
ai03 设计决策(提请 coord 仲裁):
- 方案 A(推荐):P2 保持 REST 对前端(不阻塞 P2 退出,因 P2 退出标准核心是"教师登录→侧边栏→Dashboard",REST 足以支撑),P4 引入 GraphQL Yoga + DataLoader。理由:P2 引入 GraphQL 会大幅增加 P2 工作量且与 coord 裁决冲突;GraphQL 价值在 P3+ 复杂聚合场景才显现(考试/作业/成绩/学情多源聚合)。
- 方案 B:P2 即引入 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: {...} } |
readiness(P2 补下游探针) |
| 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 设计原则
- 按教学场景域组织(不按角色分),教导主任/教研组长复用同一 schema,靠 viewports + dataScope 驱动前端差异化
- Query 为主,Mutation 谨慎:BFF 偏读多写少,Mutation 仅透传下游命令
- Subscription 延后:实时通知用 SSE(P5),Subscription 留待 P6+ 评估
- N+1 防御:所有 list 字段必须经 DataLoader
- 复杂度限制:depth limit ≤ 7,query 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 设计原则
- 命名空间:全部 key 以
bff:teacher:前缀,便于运维清理 - 版本号:key 内含 schema 版本(如
bff:teacher:v1:dashboard:{userId}),schema 变更时 bump 版本 - 用户隔离:所有 key 必须含 userId,避免跨用户数据泄漏
- 不缓存 Mutation 结果:仅缓存 Query 结果
7.4 缓存失效策略
策略 A:短 TTL 兜底(P2-P3 默认)
- 所有缓存依赖短 TTL(5-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 返回的 viewports(L1)+ dataScope(L4)驱动前端差异化渲染 + 下游数据过滤。
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 关键约束
- BFF 不解析 dataScope 语义:BFF 只透传,不根据 dataScope 做过滤
- gRPC metadata 透传:BFF 调下游时,将 dataScope 作为 gRPC metadata(key:
x-data-scope)传递 - 下游 Repository 注入 WHERE:下游服务在 Repository 层根据 dataScope + 用户身份动态注入 WHERE 条件
- 缓存隔离:缓存 key 必须含 dataScope,避免不同 dataScope 用户读到同一缓存(如
grades:{examId}:{dataScope}:{userId})
9.4 DataScope 获取方式(P2 vs P3+)
- P2(REST):teacher-bff 调 IAM
/iam/me获取 user.dataScope(当前 iam TS 接口含 dataScope 字段) - P3+(gRPC):teacher-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 实现要点
- gRPC streaming RPC:ai.StreamChat 返回
Stream<ChatChunk>,BFF 用@grpc/grpc-js的 streaming client - SSE 响应:BFF 用 Express res.write() 逐 chunk 写入 SSE 格式(
data: {json}\n\n) - 背压处理:BFF 监听 res 的 drain 事件,gRPC stream 的 pause/resume
- 超时与取消:客户端断开连接时,BFF 调用 gRPC call.cancel() 取消下游流
- 错误透传:gRPC stream error → SSE event: error
- trace 传递:gRPC metadata 携带 traceparent,SSE 响应头携带 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 SETNXevent: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 存在 | 权限决策在 Gateway(JWT)+ 下游服务(@RequirePermission) |
| /healthz, /readyz, /metrics | 无 | 白名单放行 |
| /graphql(P4) | 校验 x-user-id + 注入 context | 同上 |
| /ai/chat/stream(P5) | 校验 x-user-id | 同上 |
15.2 错误码清单(BFF_TEACHER_* 前缀,B5 + G14 裁决)
越权防御 3 类错误码(president §2.7):UNAUTHORIZED / 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)
- 配置:结构化 JSON,LOG_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 |
Counter(P4) | operation, status | GraphQL 查询总数 |
bff_teacher_graphql_duration_seconds |
Histogram(P4) | operation | GraphQL 查询延迟 |
bff_teacher_ai_usage_total |
Counter(P5) | user, model | AI token 用量 |
15.5 Tracer
- 库:OpenTelemetry SDK + OTLP exporter(已具备,见 tracer.ts)
- 注入点:HTTP middleware + gRPC interceptor(P3)
- traceparent 传递:HTTP header → gRPC metadata(P3)
- 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 优雅关闭顺序
- HTTP server stop accepting new connections
- 等待 in-flight 请求完成(最长 10s)
- Kafka consumer commit + leave(P5+)
- Redis connection close
- gRPC channels close(P3+)
- 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 | HTTP(P2)/ gRPC 50052(P3+) | GetUserInfo | 拉取用户信息 | P2 | ✅ 已有 |
| 调用 | iam | gRPC 50052(P3+) | GetViewports | 拉取视口配置 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052(P3+) | GetEffectivePermissions | 拉取权限列表 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052(P4+) | GetEffectiveAccess(建议聚合 RPC) | 一次拉取 {perms, viewports, dataScope} | P4 | ❌ 待 coord 仲裁 |
| 调用 | iam | gRPC 50052(P3+) | Logout | 登出 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052(P3+) | GetPublicKey / JWKS | 公钥(BFF 不校验 JWT,但可缓存公钥给前端) | P3 | ❌ 待 ai06 补 |
| 调用 | classes / core-edu | HTTP(P2)/ gRPC 50053(P3+) | ListClasses / GetClass | 班级列表 | P2 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053(P3+) | ExamService.*(CreateExam/GetExam/ListExamsByClass 等) | 考试聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053(P3+) | HomeworkService.* | 作业聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053(P3+) | GradeService.* | 成绩聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053(P3+) | AttendanceService.* | 出勤聚合 | P3 | ❌ 待 ai08 补 |
| 调用 | core-edu | gRPC 50053(P3+) | GetClassesByTeacher(建议新增) | 按教师拉班级 | P3 | ❌ 待 coord 仲裁 |
| 调用 | content | gRPC 50054(P4+) | KnowledgeGraphService.GetPrerequisites / GetLearningPath | 知识图谱 | P4 | ✅ 已有 |
| 调用 | content | gRPC 50054(P4+) | ChapterService. / QuestionService.** | 章节/题库 | P4 | ❌ 待 ai09 补 |
| 调用 | data-ana | gRPC 50055(P4+) | AnalyticsService.GetClassPerformance / GetStudentWeakness / GetLearningTrend | 学情诊断 | P4 | ✅ 已有 |
| 调用 | data-ana | gRPC 50055(P4+) | GetTeacherDashboard(建议新增) | 仪表盘统计 | P4 | ❌ 待 coord 仲裁 |
| 调用 | ai | gRPC 50057(P5+) | AiService.GenerateQuestion / Chat / OptimizeExpression | AI 辅助 | P5 | ✅ 已有 |
| 调用 | ai | gRPC 50057 streaming(P5+) | AiService.StreamChat | SSE 流式对话 | P5 | ✅ 已有 |
| 调用 | msg | gRPC 50056(P5+) | 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 不校验 JWT:JWT 校验在 api-gateway 层(project_rules §4)
- BFF 接收 header:
x-user-id、x-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 输入校验
- REST:Controller 层 Zod schema 解析(P2 补全)
- GraphQL:graphql-yoga 内置参数校验 + 自定义 scalar(DateTime、ID)
- 查询复杂度限制(P4):depth ≤ 7,cost ≤ 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 单进程 |
| 建议实例数 | 2(P6 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 假设
- 假设 iam 提供
GetViewports/GetEffectivePermissionsRPC(coord 裁决 P2 补全),否则 P2 继续用 REST/iam/viewports+/iam/me - 假设 core-edu P3 启用 gRPC server(004 §4.1),否则 teacher-bff P3 继续 REST 调 core-edu
- 假设 api-gateway 注入
x-user-id/x-user-rolesheader(project_memory 已确认),BFF 不解码 JWT - 假设前端 ai13(teacher-portal)配合迁移到 GraphQL(P4),否则 REST 兼容期延长
- 假设 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+ GraphQL(coord §2.5 裁决) | B(P2 引入 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 迁移) - Logger:pino
- Metrics:prom-client +
/metrics - Tracer:OTel
/healthz+/readyz:P2 补下游探针- 优雅关闭: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 Autoscaler,K8s 水平自动扩缩容 |
| mTLS | mutual TLS,双向 TLS 认证 |
AI Agent: ai03 (teacher-bff) Coordinator: coord-ai Branch: 单仓库并行模式(直接 push main) 关联阶段 1 文档: 01-understanding.md