# 模块架构设计文档 — 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)) | 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 + GetTeacherDashboardStats 待 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 GetTeacherDashboardStats,待 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_*`)
| 错误码 | HTTP | 触发条件 | 当前状态 |
| ------------------------------- | ---- | ------------------------ | --------------------------------------------- |
| `BFF_TEACHER_VALIDATION_ERROR` | 400 | Zod 校验失败 | 需迁移(当前 `TEACHER_BFF_VALIDATION_ERROR`) |
| `BFF_TEACHER_UNAUTHORIZED` | 401 | 缺 x-user-id | 需迁移 |
| `BFF_TEACHER_PERMISSION_DENIED` | 403 | (保留,BFF 一般不触发) | 需迁移 |
| `BFF_TEACHER_NOT_FOUND` | 404 | 资源不存在 | 需迁移 |
| `BFF_TEACHER_CONFLICT` | 409 | 并发冲突 | 需迁移 |
| `BFF_TEACHER_BUSINESS_ERROR` | 422 | 业务规则违反 | 需迁移 |
| `BFF_TEACHER_BAD_GATEWAY` | 502 | 下游不可达 | 需迁移 |
| `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+) | **GetTeacherDashboardStats**(建议新增) | 仪表盘统计 | 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 补 GetTeacherDashboardStats 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)