# 模块架构设计文档 — teacher-bff > AI 标识:ai03 > 负责模块:teacher-bff(P2) > 阶段:架构设计外包 · 阶段 2(模块架构设计) > 日期:2026-07-09 > 关联文档:[01-understanding.md](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[coord-cross-review.md](../../../docs/architecture/coord-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 目标态架构图 ```mermaid graph TB subgraph Client["前端层"] TP[teacher-portal
Next.js + urql] end subgraph Gateway["网关层"] AGW[api-gateway
Go/Gin
JWT 校验/限流/熔断] end subgraph BFF["teacher-bff(本模块)P6 终态"] direction TB GQL[GraphQL Yoga Endpoint
POST /graphql
+ GET /graphql/subscriptions] SSE[SSE Endpoint
GET /ai/chat/stream] REST[REST Endpoint
/healthz /readyz /metrics] subgraph Aggregation["聚合编排层"] RES[GraphQL Resolvers
按教学场景域组织] DL[DataLoader
批量去重] ORCH[并行编排器
Promise.allSettled] FALL[降级策略
部分失败容忍] end subgraph Cache["缓存层"] RC[Redis Cache
聚合结果 5-30s
权限 5min
用户会话 30min] IC[Invalidation Coordinator
TTL + 事件驱动] end subgraph Clients["下游 Client 抽象层"] IC_IAM[IamClient] IC_CE[CoreEduClient] IC_CNT[ContentClient] IC_DA[DataAnaClient] IC_AI[AiClient
含 StreamChat 流式] IC_MSG[MsgClient] end subgraph Resilience["弹性层"] CB[Circuit Breaker
opossum] RT[Retry
gRPC interceptor] TL[Timeout
per-RPC] end subgraph Obs["可观测层"] LOG[pino Logger] MET[prom-client Metrics] TRC[OTel Tracer
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
含 StreamChat] end subgraph Infra["基础设施"] K[Kafka
权限缓存失效事件] R[(Redis
edu-redis:6379)] J[Jaeger
OTLP] P[Prometheus
: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 演进路线图 ```mermaid graph LR P2[P2 现状加固
REST + Client 抽象层
Zod + 错误码迁移
测试 + readyz] P3[P3 gRPC 切换
iam + core-edu gRPC
Redis 聚合缓存
并行编排器] P4[P4 GraphQL 演进
GraphQL Yoga 对前端
DataLoader
content + data-ana gRPC] P5[P5 AI + 通知
SSE 流式透传
ai + msg gRPC
Kafka 缓存失效] P6[P6 硬化
熔断/重试/超时
HPA + mTLS
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](../src/shared/errors/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) ```mermaid graph TB subgraph Entry["入口层 (Entry)"] GQL[GraphQL Yoga Controller
POST /graphql] SSE[SSE Controller
GET /ai/chat/stream] HEALTH[Health Controller
/healthz /readyz /metrics] end subgraph Middleware["中间件层"] CTX[Context Middleware
解析 x-user-id/x-user-roles
注入 GraphQL context] ERR[GlobalErrorFilter
统一错误兜底] end subgraph Aggregation["聚合编排层 (Aggregation)"] RES[GraphQL Resolvers
按场景域组织] DL[DataLoader Registry
per-request 实例] ORCH[Orchestrator
Promise.allSettled + 降级] MAP[Response Mapper
proto → GraphQL type] end subgraph Cache["缓存层 (Cache)"] RCLIENT[RedisClient
ioredis] CKV[CacheKey Builder
bff:teacher:*] CINV[Invalidation Coordinator
TTL + 事件驱动] end subgraph Clients["下游 Client 抽象层 (Clients)"] IAM_C[IamClient
interface + gRPC impl] CE_C[CoreEduClient
interface + gRPC impl] CNT_C[ContentClient
interface + gRPC impl] DA_C[DataAnaClient
interface + gRPC impl] AI_C[AiClient
interface + gRPC impl
含 StreamChat] MSG_C[MsgClient
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
Zod 校验] DI[DI Container
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 黄金模板一致): ```typescript // 成功 { 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 延后**:实时通知用 SSE(P5),Subscription 留待 P6+ 评估 4. **N+1 防御**:所有 list 字段必须经 DataLoader 5. **复杂度限制**:depth limit ≤ 7,query cost ≤ 1000 ### 5.2 Schema 概览 ```graphql # ============ 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 组织(按场景域) ```mermaid graph TB subgraph Dashboard["Dashboard Resolver"] DQ[dashboard
并行: IAM + classes + data-ana] DV[viewports] DM[me] end subgraph ExamResolver["Exam Resolver"] EQ[exams
DataLoader by classId] EC[createExam] EP[publishExam] end subgraph HomeworkResolver["Homework Resolver"] HQ[homework
DataLoader by classId] HA[assignHomework] end subgraph GradeResolver["Grade Resolver"] GQ[grades
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 缓存层架构 ```mermaid 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 默认) - 所有缓存依赖短 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 四层视口模型 ```mermaid graph TB subgraph L1["L1 导航层"] L1A[侧边栏菜单项
iam_role_viewports 表配置] end subgraph L2["L2 路由层"] L2A[可访问路由
route_permission 表
Gateway 校验] end subgraph L3["L3 组件层"] L3A[页面内组件可见性
usePermission().hasPermission
前端 Hook] end subgraph L4["L4 数据层"] L4A[数据行级过滤
DataScope 枚举
下游 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 视口推导数据流 ```mermaid 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 透传机制(不做解析) ```mermaid graph LR JWT[JWT payload
含 dataScope] --> GW[api-gateway
解码 JWT] GW -->|x-user-id, x-user-roles| BFF[teacher-bff] BFF -->|getEffectiveAccess(userId)| IAM[iam] IAM -->|dataScope: SCHOOL| BFF BFF -->|gRPC metadata:
x-data-scope=SCHOOL| CE[core-edu] CE -->|Repository 注入 WHERE
school_id = ?| DB[(MySQL)] ``` ### 9.3 关键约束 1. **BFF 不解析 dataScope 语义**:BFF 只透传,不根据 dataScope 做过滤 2. **gRPC metadata 透传**:BFF 调下游时,将 dataScope 作为 gRPC metadata(key: `x-data-scope`)传递 3. **下游 Repository 注入 WHERE**:下游服务在 Repository 层根据 dataScope + 用户身份动态注入 WHERE 条件 4. **缓存隔离**:缓存 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 三层流式架构 ```mermaid 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 RPC**:ai.StreamChat 返回 `Stream`,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 携带 traceparent,SSE 响应头携带 trace-id ### 10.3 降级策略 - gRPC stream 不可用 → SSE 返回 error 事件,前端提示"AI 服务暂不可用" - 不降级为非流式(AI 出题体验依赖流式打字机效果) --- ## 11. 并行 gRPC 编排与降级策略 ### 11.1 编排器设计 ```mermaid 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 存在 | 权限决策在 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](../src/shared/observability/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](../src/shared/observability/tracer.ts)) - 注入点:HTTP middleware + gRPC interceptor(P3) - traceparent 传递:HTTP header → gRPC metadata(P3) - Jaeger 端点:OTEL_EXPORTER_OTLP_ENDPOINT 环境变量 ### 15.6 /healthz 检查逻辑 ```typescript // liveness:仅返回进程存活 { status: "ok", service: "teacher-bff", timestamp: ISO8601 } ``` ### 15.7 /readyz 检查逻辑(P2 补全) ```typescript // 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 + leave(P5+) 4. Redis connection close 5. gRPC channels close(P3+) 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 | 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 假设 1. **假设 iam 提供 `GetViewports` / `GetEffectivePermissions` RPC**(coord 裁决 P2 补全),否则 P2 继续用 REST `/iam/viewports` + `/iam/me` 2. **假设 core-edu P3 启用 gRPC server**(004 §4.1),否则 teacher-bff P3 继续 REST 调 core-edu 3. **假设 api-gateway 注入 `x-user-id` / `x-user-roles` header**(project_memory 已确认),BFF 不解码 JWT 4. **假设前端 ai13(teacher-portal)配合迁移到 GraphQL**(P4),否则 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+ 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 + 下游) - [x] 错误码前缀:`BFF_TEACHER_*`(P2 迁移) - [x] Logger:pino - [x] Metrics:prom-client + `/metrics` - [x] Tracer:OTel - [x] `/healthz` + `/readyz`:P2 补下游探针 - [x] 优雅关闭:SIGTERM - [ ] 测试覆盖率 ≥ 80%:P2 引入 Vitest - [x] Dockerfile 多阶段构建 - [ ] Zod 输入验证:P2 补全 - [x] GlobalErrorFilter 统一兜底 - [x] 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](./01-understanding.md)