Files
Edu/services/teacher-bff/docs/02-architecture-design.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md)

2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration)

3.各服务 01/02 文档补全

4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens)

5.Proto 契约补全

6.004 架构影响地图更新

7.端口分配表

8.设计规格文档
2026-07-10 12:58:22 +08:00

1514 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — teacher-bff
> AI 标识ai03
> 负责模块teacher-bffP2
> 阶段:架构设计外包 · 阶段 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<br/>Next.js + urql]
end
subgraph Gateway["网关层"]
AGW[api-gateway<br/>Go/Gin<br/>JWT 校验/限流/熔断]
end
subgraph BFF["teacher-bff本模块P6 终态"]
direction TB
GQL[GraphQL Yoga Endpoint<br/>POST /graphql<br/>+ GET /graphql/subscriptions]
SSE[SSE Endpoint<br/>GET /ai/chat/stream]
REST[REST Endpoint<br/>/healthz /readyz /metrics]
subgraph Aggregation["聚合编排层"]
RES[GraphQL Resolvers<br/>按教学场景域组织]
DL[DataLoader<br/>批量去重]
ORCH[并行编排器<br/>Promise.allSettled]
FALL[降级策略<br/>部分失败容忍]
end
subgraph Cache["缓存层"]
RC[Redis Cache<br/>聚合结果 5-30s<br/>权限 5min<br/>用户会话 30min]
IC[Invalidation Coordinator<br/>TTL + 事件驱动]
end
subgraph Clients["下游 Client 抽象层"]
IC_IAM[IamClient]
IC_CE[CoreEduClient]
IC_CNT[ContentClient]
IC_DA[DataAnaClient]
IC_AI[AiClient<br/>含 StreamChat 流式]
IC_MSG[MsgClient]
end
subgraph Resilience["弹性层"]
CB[Circuit Breaker<br/>opossum]
RT[Retry<br/>gRPC interceptor]
TL[Timeout<br/>per-RPC]
end
subgraph Obs["可观测层"]
LOG[pino Logger]
MET[prom-client Metrics]
TRC[OTel Tracer<br/>gRPC interceptor]
end
end
subgraph Downstream["下游业务服务gRPC"]
IAM[iam:50052]
CE[core-edu:50053]
CNT[content:50054]
DA[data-ana:50055]
MSG[msg:50056]
AI[ai:50057<br/>含 StreamChat]
end
subgraph Infra["基础设施"]
K[Kafka<br/>权限缓存失效事件]
R[(Redis<br/>edu-redis:6379)]
J[Jaeger<br/>OTLP]
P[Prometheus<br/>:9090]
end
TP -->|GraphQL/HTTP/SSE| AGW
AGW -->|HTTP + x-user-id/x-user-roles| GQL
AGW --> SSE
AGW --> REST
GQL --> RES
SSE --> RES
RES --> DL
RES --> ORCH
ORCH --> FALL
RES --> RC
RC --> IC
DL --> Clients
ORCH --> Clients
Clients --> Resilience
Resilience --> IAM
Resilience --> CE
Resilience --> CNT
Resilience --> DA
Resilience --> MSG
Resilience --> AI
RC --> R
IC -.->|订阅 edu.identity.*| K
IAM -.->|发布 edu.identity.*| K
LOG --> J
MET --> P
TRC --> J
```
### 1.2 目标态核心特征
| 维度 | P6 终态 | 设计依据 |
| ---------- | ------------------------------------------------------------------------------ | ------------------------------ |
| 对前端协议 | GraphQL YogaQuery/Mutation/Subscription+ SSEAI 流式)+ REST健康/指标) | 004 §11.3、pending-features P2 |
| 对下游协议 | 全 gRPCiam/core-edu/content/data-ana/msg/ai | 004 §4.1 |
| 缓存 | Redis 三层(聚合 5-30s / 权限 5min / 会话 30min+ 事件驱动失效 | 004 §6.3 |
| 批量去重 | DataLoader 覆盖全部 N+1 风险点 | ai-allocation §5 ai03 |
| 弹性 | Circuit Breaker + Retry + Timeout + 降级 | P6 硬化 |
| 可观测 | pino + prom-client + OTelgRPC interceptor 全链路) | project_rules §12 |
| 多角色 | 教师/教导主任/教研组长复用同一 BFF视口 + DataScope 差异化 | 004 §5.4 |
| 流式 | SSE 三层透传ai.StreamChat → bff → 前端 EventSource | pending-features P5 |
| 事件消费 | 可选 Kafka consumer 订阅 `edu.identity.*` 精确失效权限缓存 | 004 §7.2 |
| 部署 | K8s + HPA + mTLSIstio Service Mesh | P6 硬化 |
---
## 2. 分阶段演进路线P2→P6
> 每阶段独立交付价值,通过抽象层隔离变更,避免大爆炸式重构。
### 2.1 演进路线图
```mermaid
graph LR
P2[P2 现状加固<br/>REST + Client 抽象层<br/>Zod + 错误码迁移<br/>测试 + readyz]
P3[P3 gRPC 切换<br/>iam + core-edu gRPC<br/>Redis 聚合缓存<br/>并行编排器]
P4[P4 GraphQL 演进<br/>GraphQL Yoga 对前端<br/>DataLoader<br/>content + data-ana gRPC]
P5[P5 AI + 通知<br/>SSE 流式透传<br/>ai + msg gRPC<br/>Kafka 缓存失效]
P6[P6 硬化<br/>熔断/重试/超时<br/>HPA + mTLS<br/>99.9% 可用性]
P2 --> P3 --> P4 --> P5 --> P6
```
### 2.2 各阶段交付清单
#### P2 — 现状加固(当前阶段)
**目标**:在不改变对外 REST 契约的前提下,补齐黄金模板差距,为后续 gRPC/GraphQL 切换铺路。
| 工作项 | 详情 | 优先级 |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| 引入 Client 抽象层 | 新增 `src/clients/` 目录,定义 `IamClient`/`ClassesClient`/`CoreEduClient` 接口,当前实现为 REST fetch adapterP3 替换为 gRPC adapter | P0 |
| 错误码迁移 | `TEACHER_BFF_*``BFF_TEACHER_*`(全量替换 [application-error.ts](../src/shared/errors/application-error.ts) | P0 |
| Zod 输入验证 | Controller 层引入 Zod schema 解析 path/bodyclassId/examId 等) | P0 |
| `/readyz` 下游探针 | 并行 ping iam + classes + core-edu 的 `/healthz`,全部 ok 才返回 ok | P0 |
| Vitest 测试框架 | 引入 Vitest覆盖 Service 聚合逻辑 + Controller + 错误处理,目标 ≥ 80% | P1 |
| env 重构 | `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 serverteacher-bff 通过 Client 抽象层切换为 gRPC 调用;引入 Redis 聚合缓存。
| 工作项 | 详情 |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| IamClient gRPC adapter | 实现 `@grpc/grpc-js` + `@bufbuild/protobuf` client调用 iam:50052 |
| CoreEduClient gRPC adapter | 调用 core-edu:50053ExamService/HomeworkService/GradeService + AttendanceService 待 ai08 补) |
| ClassesClient 整合 | classes 合并入 core-eduClassesClient 退役,调用走 CoreEduClient.ListClasses |
| 并行编排器 | 抽取 `src/aggregation/orchestrator.ts`,封装 `Promise.allSettled` + 降级策略 |
| Redis 聚合缓存 | 聚合结果 5-30s TTLkey = `bff:teacher:{userId}:{queryHash}` |
| gRPC interceptor | 注入 trace contexttraceparent+ x-user-id metadata + metrics |
| 优雅降级 | dashboard 聚合iam 失败返回 user=nullclasses 失败返回 classes=null当前已实现抽象到编排器 |
**P3 退出标准**iam + core-edu 全部 gRPC 调用;聚合缓存命中率 ≥ 60%;并行编排器 + 降级策略单元测试覆盖。
#### P4 — GraphQL 演进 + DataLoader
**目标**:对前端引入 GraphQL YogaDataLoader 防 N+1content + data-ana 切 gRPC。
| 工作项 | 详情 |
| -------------------------- | --------------------------------------------------------------------------------------- |
| GraphQL Yoga 接入 | 新增 `POST /graphql` 端点graphql-yoga + NestJS 集成 |
| Schema 设计 | 按教学场景域组织 Query/Mutation见 §5 |
| DataLoader 接入 | Resolver 内调用 DataLoader覆盖全部 N+1 风险点(见 §6 |
| ContentClient gRPC adapter | 调用 content:50054KnowledgeGraphService + ChapterService/QuestionService 待 ai09 补) |
| DataAnaClient gRPC adapter | 调用 data-ana:50055AnalyticsService + 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.StreamChatgRPC stream→ BFF → 前端 EventSource见 §10 |
| MsgClient gRPC adapter | 调用 msg:50056NotificationService |
| 通知聚合 Query | GraphQL Query.notificationsListNotifications + MarkAsRead mutation |
| Kafka consumer可选 | 订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`,精确失效 Redis 权限缓存(见 §7.4 |
| AI 用量计费 | 透传 ai 服务的 token 用量 metricsBFF 记录 `bff_teacher_ai_usage_total` |
**P5 退出标准**SSE 流式透传端到端通;通知聚合 QueryKafka consumer可选上线或显式决策用短 TTL 兜底。
#### P6 — 硬化
**目标**99.9% 可用性,全链路可观测,独立扩缩容。
| 工作项 | 详情 |
| --------------- | ------------------------------------------------------------------ |
| Circuit Breaker | opossumper-downstream-service错误率/延迟阈值 |
| Retry | gRPC interceptor指数退避仅幂等 RPC |
| Timeout | per-RPC timeout默认 3s聚合总超时 5s |
| HPA | K8s HorizontalPodAutoscaler基于 CPU + 自定义 metrics请求速率 |
| mTLS | Istio Service Mesh服务间双向 TLS |
| 全链路 trace | OTel + JaegergRPC interceptor 全链路 traceparent 传递 |
| SLO | 99.9% 可用性 + P99 延迟 < 500ms + 错误率 < 0.1% |
**P6 退出标准**SLO 达标;全链路 trace 可视化HPA 生效mTLS 全覆盖。
---
## 3. 模块内部分层图
### 3.1 目标态分层P6
```mermaid
graph TB
subgraph Entry["入口层 (Entry)"]
GQL[GraphQL Yoga Controller<br/>POST /graphql]
SSE[SSE Controller<br/>GET /ai/chat/stream]
HEALTH[Health Controller<br/>/healthz /readyz /metrics]
end
subgraph Middleware["中间件层"]
CTX[Context Middleware<br/>解析 x-user-id/x-user-roles<br/>注入 GraphQL context]
ERR[GlobalErrorFilter<br/>统一错误兜底]
end
subgraph Aggregation["聚合编排层 (Aggregation)"]
RES[GraphQL Resolvers<br/>按场景域组织]
DL[DataLoader Registry<br/>per-request 实例]
ORCH[Orchestrator<br/>Promise.allSettled + 降级]
MAP[Response Mapper<br/>proto → GraphQL type]
end
subgraph Cache["缓存层 (Cache)"]
RCLIENT[RedisClient<br/>ioredis]
CKV[CacheKey Builder<br/>bff:teacher:*]
CINV[Invalidation Coordinator<br/>TTL + 事件驱动]
end
subgraph Clients["下游 Client 抽象层 (Clients)"]
IAM_C[IamClient<br/>interface + gRPC impl]
CE_C[CoreEduClient<br/>interface + gRPC impl]
CNT_C[ContentClient<br/>interface + gRPC impl]
DA_C[DataAnaClient<br/>interface + gRPC impl]
AI_C[AiClient<br/>interface + gRPC impl<br/>含 StreamChat]
MSG_C[MsgClient<br/>interface + gRPC impl]
end
subgraph Resilience["弹性层"]
CB[CircuitBreaker]
RT[Retry]
TL[Timeout]
end
subgraph Obs["可观测层"]
LOG[pino]
MET[prom-client]
TRC[OTel Tracer]
end
subgraph Config["配置层"]
ENV[env.ts<br/>Zod 校验]
DI[DI Container<br/>NestJS]
end
GQL --> CTX
SSE --> CTX
CTX --> RES
RES --> DL
RES --> ORCH
RES --> RCLIENT
DL --> Clients
ORCH --> Clients
Clients --> Resilience
Resilience --> Downstream
RCLIENT --> CINV
ERR -.-> GQL
ERR -.-> SSE
```
### 3.2 目录结构(目标态)
```
services/teacher-bff/src/
├─ config/
│ └─ env.ts # Zod 校验环境变量
├─ entry/ # 入口层
│ ├─ graphql.controller.ts # GraphQL Yoga 端点P4
│ ├─ sse.controller.ts # SSE 流式端点P5
│ └─ context.middleware.ts # 解析 x-user-id 注入 context
├─ teacher/ # 教学场景域P2 REST 端点P4 迁移 GraphQL
│ ├─ teacher.controller.ts # REST ControllerP2P4 后逐步 deprecate
│ ├─ teacher.service.ts # 聚合 Service
│ └─ teacher.schema.ts # Zod 输入校验
├─ aggregation/ # 聚合编排层
│ ├─ orchestrator.ts # Promise.allSettled + 降级
│ ├─ response-mapper.ts # proto → GraphQL/REST type
│ └─ fallback-strategy.ts # 降级策略
├─ dataloader/ # DataLoaderP4
│ ├─ dataloader.module.ts # per-request DataLoader 注册器
│ ├─ user.dataloader.ts # 按 userId 批量取用户
│ ├─ class.dataloader.ts # 按 classId 批量取班级
│ ├─ exam.dataloader.ts # 按 classId 批量取考试
│ ├─ homework.dataloader.ts # 按 classId 批量取作业
│ └─ grade.dataloader.ts # 按 examId 批量取成绩
├─ graphql/ # GraphQL SchemaP4
│ ├─ schema.ts # typeDefs + resolvers
│ ├─ types/ # GraphQL type 定义
│ │ ├─ user.type.ts
│ │ ├─ class.type.ts
│ │ ├─ exam.type.ts
│ │ ├─ homework.type.ts
│ │ ├─ grade.type.ts
│ │ └─ analytics.type.ts
│ └─ resolvers/ # 按场景域组织
│ ├─ dashboard.resolver.ts
│ ├─ exam.resolver.ts
│ ├─ homework.resolver.ts
│ ├─ grade.resolver.ts
│ ├─ analytics.resolver.ts
│ └─ notification.resolver.ts
├─ clients/ # 下游 Client 抽象层
│ ├─ iam.client.ts # interface + gRPC impl
│ ├─ core-edu.client.ts
│ ├─ content.client.ts # P4
│ ├─ data-ana.client.ts # P4
│ ├─ ai.client.ts # P5含 StreamChat
│ ├─ msg.client.ts # P5
│ ├─ grpc/ # gRPC adapterP3
│ │ ├─ grpc.factory.ts # Channel + Client 工厂
│ │ └─ interceptors.ts # trace/metrics/retry interceptor
│ └─ rest/ # REST adapterP2过渡期
│ └─ rest-fetch.adapter.ts
├─ shared/
│ ├─ errors/
│ │ ├─ application-error.ts # BFF_TEACHER_* 错误码
│ │ └─ global-error.filter.ts
│ ├─ health/
│ │ ├─ health.controller.ts # /healthz + /readyz
│ │ └─ readiness.probe.ts # 下游就绪探针P2 补)
│ ├─ cache/ # 缓存层
│ │ ├─ redis.client.ts # ioredis 客户端
│ │ ├─ cache-key.builder.ts
│ │ └─ invalidation.coordinator.ts # TTL + 事件驱动失效
│ ├─ observability/
│ │ ├─ logger.ts # pino
│ │ ├─ metrics.ts # prom-client + 指标定义
│ │ └─ tracer.ts # OTel
│ ├─ resilience/ # 弹性层P6
│ │ ├─ circuit-breaker.ts # opossum
│ │ └─ retry.policy.ts
│ └─ kafka/ # Kafka consumerP5 可选)
│ ├─ kafka.consumer.ts
│ └─ handlers/
│ └─ permission-invalidation.handler.ts
├─ app.module.ts
└─ main.ts
```
### 3.3 分层职责契约
| 层 | 职责 | 禁止 |
| ------------- | ---------------------------------------------------- | --------------------- |
| Entry | 协议入口GraphQL/REST/SSE解析请求注入 context | 业务逻辑、下游调用 |
| Middleware | 通用横切context 注入、错误兜底) | 业务逻辑 |
| Aggregation | 编排多下游调用、聚合裁剪、降级 | 直接访问 DB、权限决策 |
| Cache | 缓存读写、失效协调 | 业务逻辑 |
| Clients | 下游协议适配REST/gRPC、interceptor | 业务聚合逻辑 |
| Resilience | 熔断/重试/超时 | 业务逻辑 |
| Observability | 日志/指标/链路 | 业务逻辑 |
**依赖方向**Entry → Aggregation → Clients → Resilience → DownstreamCache 横向被 Aggregation 调用Observability 横向被所有层调用。禁止反向依赖。
---
## 4. API 设计REST→GraphQL 演进)
### 4.1 API 风格仲裁说明
> **冲突**pending-features P2 退出标准要求 "GraphQL Yoga + DataLoader"coord-cross-review §2.5 裁决 "P2-P3 用 RESTP4 起若 BFF 切 GraphQL 再引入 urql"。
>
> **ai03 设计决策**(提请 coord 仲裁):
>
> - **方案 A推荐**P2 保持 REST 对前端(不阻塞 P2 退出,因 P2 退出标准核心是"教师登录→侧边栏→Dashboard"REST 足以支撑P4 引入 GraphQL Yoga + DataLoader。理由P2 引入 GraphQL 会大幅增加 P2 工作量且与 coord 裁决冲突GraphQL 价值在 P3+ 复杂聚合场景才显现(考试/作业/成绩/学情多源聚合)。
> - **方案 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: {...} }` | readinessP2 补下游探针) |
| GET | /metrics | 无 | — | Prometheus text | Prometheus 抓取 |
**P2 加固项**
- 全部 Controller 方法补 Zod 输入校验classId/examId 必须是 cuid/uuid 格式)
- 错误码统一 `BFF_TEACHER_*`
- `/readyz` 补下游就绪探针
### 4.3 P3 REST 扩展
| method | path | 说明 |
| ------ | ------------------------------------ | ----------------------------------------------------------------- |
| POST | /teacher/exams | 创建考试(透传 core-edu CreateExam |
| POST | /teacher/homework | 布置作业(透传 core-edu AssignHomework |
| POST | /teacher/grades | 录入成绩(透传 core-edu RecordGrade |
| GET | /teacher/classes/:classId/attendance | 出勤列表core-edu AttendanceService待 ai08 补 proto |
| GET | /teacher/dashboard/stats | 仪表盘统计data-ana 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 延后**:实时通知用 SSEP5Subscription 留待 P6+ 评估
4. **N+1 防御**:所有 list 字段必须经 DataLoader
5. **复杂度限制**depth limit ≤ 7query 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<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 缓存层架构
```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 默认)
- 所有缓存依赖短 TTL5-30s自然过期
- 优点:简单,无需 Kafka consumer
- 缺点:权限变更最长 5min 才生效(权限列表 TTL
- 适用P2-P3权限变更频率低
#### 策略 B事件驱动精确失效P5+ 可选)
- teacher-bff 订阅 Kafka topic
- `edu.identity.user.role_changed` → 删除 `perms:{userId}` + `viewports:{userId}`
- `edu.identity.role.updated` → 批量删除该角色所有用户的 `perms:*` + `viewports:*`
- `edu.org.class.created` → 删除 `classes:{userId}`
- 优点:权限变更秒级生效
- 缺点:引入 Kafka consumer 复杂度 + 幂等性处理
- 适用P5+,权限变更频率高或安全要求严格
**ai03 决策**P2-P3 用策略 A短 TTL 兜底P5 评估是否引入策略 B取决于权限变更频率。提请 coord 仲裁。
---
## 8. 视口推导与角色差异化L1-L4
### 8.1 四层视口模型
```mermaid
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 视口推导数据流
```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<br/>含 dataScope] --> GW[api-gateway<br/>解码 JWT]
GW -->|x-user-id, x-user-roles| BFF[teacher-bff]
BFF -->|getEffectiveAccess(userId)| IAM[iam]
IAM -->|dataScope: SCHOOL| BFF
BFF -->|gRPC metadata:<br/>x-data-scope=SCHOOL| CE[core-edu]
CE -->|Repository 注入 WHERE<br/>school_id = ?| DB[(MySQL)]
```
### 9.3 关键约束
1. **BFF 不解析 dataScope 语义**BFF 只透传,不根据 dataScope 做过滤
2. **gRPC metadata 透传**BFF 调下游时,将 dataScope 作为 gRPC metadatakey: `x-data-scope`)传递
3. **下游 Repository 注入 WHERE**:下游服务在 Repository 层根据 dataScope + 用户身份动态注入 WHERE 条件
4. **缓存隔离**:缓存 key 必须含 dataScope避免不同 dataScope 用户读到同一缓存(如 `grades:{examId}:{dataScope}:{userId}`
### 9.4 DataScope 获取方式P2 vs P3+
- **P2REST**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<ChatChunk>`BFF 用 `@grpc/grpc-js` 的 streaming client
2. **SSE 响应**BFF 用 Express res.write() 逐 chunk 写入 SSE 格式(`data: {json}\n\n`
3. **背压处理**BFF 监听 res 的 drain 事件gRPC stream 的 pause/resume
4. **超时与取消**客户端断开连接时BFF 调用 gRPC call.cancel() 取消下游流
5. **错误透传**gRPC stream error → SSE event: error
6. **trace 传递**gRPC metadata 携带 traceparentSSE 响应头携带 trace-id
### 10.3 降级策略
- gRPC stream 不可用 → SSE 返回 error 事件,前端提示"AI 服务暂不可用"
- 不降级为非流式AI 出题体验依赖流式打字机效果)
---
## 11. 并行 gRPC 编排与降级策略
### 11.1 编排器设计
```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 存在 | 权限决策在 GatewayJWT+ 下游服务(@RequirePermission |
| /healthz, /readyz, /metrics | 无 | 白名单放行 |
| /graphqlP4 | 校验 x-user-id + 注入 context | 同上 |
| /ai/chat/streamP5 | 校验 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)
- 配置:结构化 JSONLOG_LEVEL 环境变量控制
- 注入字段traceId、userId、service=teacher-bff
- 禁止 `console.log`
### 15.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 描述 |
| ----------------------------------------- | --------------- | -------------------- | ---------------- |
| `bff_teacher_http_requests_total` | Counter | method, path, status | HTTP 请求总数 |
| `bff_teacher_http_duration_seconds` | Histogram | method, path | HTTP 请求延迟 |
| `bff_teacher_downstream_calls_total` | Counter | service, rpc, status | 下游调用总数 |
| `bff_teacher_downstream_duration_seconds` | Histogram | service, rpc | 下游调用延迟 |
| `bff_teacher_cache_hits_total` | Counter | cache_type | 缓存命中数 |
| `bff_teacher_cache_misses_total` | Counter | cache_type | 缓存未命中数 |
| `bff_teacher_graphql_query_total` | CounterP4 | operation, status | GraphQL 查询总数 |
| `bff_teacher_graphql_duration_seconds` | HistogramP4 | operation | GraphQL 查询延迟 |
| `bff_teacher_ai_usage_total` | CounterP5 | user, model | AI token 用量 |
### 15.5 Tracer
-OpenTelemetry SDK + OTLP exporter已具备见 [tracer.ts](../src/shared/observability/tracer.ts)
- 注入点HTTP middleware + gRPC interceptorP3
- traceparent 传递HTTP header → gRPC metadataP3
- 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 + leaveP5+
4. Redis connection close
5. gRPC channels closeP3+
6. Tracer flush + shutdown
当前 main.ts 已处理 SIGTERM + app.close(),需在 P3+ 补充 Redis/gRPC/Kafka 的关闭顺序。
---
## 16. 与其他模块的交互点(契约矩阵)
### 16.1 完整交互矩阵P2-P6
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 | proto 状态 |
| ------------ | ------------------ | ----------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------ | ---- | ---------------- |
| 被调用 | api-gateway | HTTP | `POST /graphql` + `GET /teacher/*` + `GET /ai/chat/stream` + `/healthz` `/readyz` `/metrics` | Gateway 转发 + 注入 x-user-id | P2 | ✅ |
| 调用 | iam | HTTPP2/ gRPC 50052P3+ | GetUserInfo | 拉取用户信息 | P2 | ✅ 已有 |
| 调用 | iam | gRPC 50052P3+ | **GetViewports** | 拉取视口配置 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052P3+ | **GetEffectivePermissions** | 拉取权限列表 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052P4+ | **GetEffectiveAccess**(建议聚合 RPC | 一次拉取 {perms, viewports, dataScope} | P4 | ❌ 待 coord 仲裁 |
| 调用 | iam | gRPC 50052P3+ | **Logout** | 登出 | P2 | ❌ 待 ai06 补 |
| 调用 | iam | gRPC 50052P3+ | **GetPublicKey / JWKS** | 公钥BFF 不校验 JWT但可缓存公钥给前端 | P3 | ❌ 待 ai06 补 |
| 调用 | classes / core-edu | HTTPP2/ gRPC 50053P3+ | ListClasses / GetClass | 班级列表 | P2 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053P3+ | ExamService.*CreateExam/GetExam/ListExamsByClass 等) | 考试聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053P3+ | HomeworkService.* | 作业聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053P3+ | GradeService.* | 成绩聚合 | P3 | ✅ 已有 |
| 调用 | core-edu | gRPC 50053P3+ | **AttendanceService.*** | 出勤聚合 | P3 | ❌ 待 ai08 补 |
| 调用 | core-edu | gRPC 50053P3+ | **GetClassesByTeacher**(建议新增) | 按教师拉班级 | P3 | ❌ 待 coord 仲裁 |
| 调用 | content | gRPC 50054P4+ | KnowledgeGraphService.GetPrerequisites / GetLearningPath | 知识图谱 | P4 | ✅ 已有 |
| 调用 | content | gRPC 50054P4+ | _*ChapterService.* / QuestionService._** | 章节/题库 | P4 | ❌ 待 ai09 补 |
| 调用 | data-ana | gRPC 50055P4+ | AnalyticsService.GetClassPerformance / GetStudentWeakness / GetLearningTrend | 学情诊断 | P4 | ✅ 已有 |
| 调用 | data-ana | gRPC 50055P4+ | **GetTeacherDashboardStats**(建议新增) | 仪表盘统计 | P4 | ❌ 待 coord 仲裁 |
| 调用 | ai | gRPC 50057P5+ | AiService.GenerateQuestion / Chat / OptimizeExpression | AI 辅助 | P5 | ✅ 已有 |
| 调用 | ai | gRPC 50057 streamingP5+ | **AiService.StreamChat** | SSE 流式对话 | P5 | ✅ 已有 |
| 调用 | msg | gRPC 50056P5+ | NotificationService.ListNotifications / SearchNotifications / MarkAsRead | 通知聚合 | P5 | ✅ 已有 |
| 消费(可选) | Kafka | 异步 | `edu.identity.user.role_changed` | 权限缓存失效 | P5+ | ✅ topic 已定义 |
| 消费(可选) | Kafka | 异步 | `edu.identity.role.updated` | 权限缓存失效 | P5+ | ✅ topic 已定义 |
### 16.2 端口分配
| 服务 | HTTP 端口 | gRPC 端口 |
| --------------------- | --------- | -------------------------------- |
| teacher-bff本模块 | 3003 | 不暴露(只被 Gateway HTTP 调用) |
| iam | 3002 | 50052 |
| classes / core-edu | 3004 | 50053 |
| content | 3005 | 50054 |
| data-ana | 3006 | 50055 |
| msg | 3007 | 50056 |
| ai | 3008 | 50057 |
---
## 17. 弹性设计(熔断/重试/超时P6 硬化)
### 17.1 Circuit Breaker 配置
| 下游服务 | 错误率阈值 | 熔断时长 | 半开请求数 |
| -------- | --------------- | -------- | ---------- |
| iam | 50% | 30s | 3 |
| core-edu | 50% | 30s | 3 |
| content | 50% | 60s | 2 |
| data-ana | 50% | 60s | 2 |
| ai | 70%(容忍度高) | 30s | 5 |
| msg | 50% | 60s | 2 |
### 17.2 Retry 策略
- 仅对幂等 RPC 重试GET 类查询)
- Mutation 类 RPC 不重试(避免重复写入)
- 最大重试次数2
- 退避策略指数退避100ms, 400ms
- 仅对 transient 错误重试UNAVAILABLE / DEADLINE_EXCEEDED不重试 INVALID_ARGUMENT / NOT_FOUND
### 17.3 超时层级
| 层级 | 超时 |
| -------------- | ------------------------- |
| 单个 gRPC RPC | 3s |
| 聚合编排总超时 | 5s |
| GraphQL 查询 | 10s |
| SSE 流式 | 无(靠心跳 + 客户端断开) |
| Redis 操作 | 100ms |
---
## 18. 可观测性设计
### 18.1 三支柱project_rules §12
| 支柱 | 实现 | 端点 | 采样率 |
| ---- | ------------------------ | ------------ | -------------- |
| 日志 | pino结构化 JSON | stdout | 100% |
| 指标 | prom-client | GET /metrics | 100% |
| 链路 | OpenTelemetry SDK + OTLP | Jaeger | 10%P6 全量) |
### 18.2 关键 trace span
```
HTTP /graphql (root span)
├─ graphql.resolve (operation)
│ ├─ dataloader.load (batch key)
│ ├─ grpc.call iam.GetUser (child span)
│ ├─ grpc.call core-edu.ListExams (child span)
│ └─ redis.get cache (child span)
└─ http.response (serialize)
```
### 18.3 告警规则(提给 SRE
| 告警 | 条件 | 严重度 |
| ------------------------ | --------------------------------------------------------------------- | ------ |
| teacher-bff 5xx 错误率高 | `rate(bff_teacher_http_requests_total{status=~"5.."}[5m]) > 0.01` | P1 |
| 下游调用失败率高 | `rate(bff_teacher_downstream_calls_total{status="error"}[5m]) > 0.05` | P1 |
| 缓存命中率低 | `cache_misses / (hits + misses) > 0.5` 持续 10min | P2 |
| P99 延迟高 | `histogram_quantile(0.99, bff_teacher_http_duration_seconds) > 1` | P2 |
| /readyz 失败 | `readyz_status != "ok"` 持续 1min | P0 |
---
## 19. 安全设计
### 19.1 JWT 与身份传递
- **BFF 不校验 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 输入校验
- RESTController 层 Zod schema 解析P2 补全)
- GraphQLgraphql-yoga 内置参数校验 + 自定义 scalarDateTime、ID
- 查询复杂度限制P4depth ≤ 7cost ≤ 1000
### 19.5 敏感数据
- 日志禁止记录 JWT token、密码、用户敏感信息pino redact 配置)
- Redis 缓存不存储密码、token仅存 userId/roles/dataScope/perms/viewports
---
## 20. 容量与性能预估
### 20.1 容量预估(假设 1000 教师 / 100 学校)
| 指标 | 预估值 | 依据 |
| ---------- | ---------------- | ------------------------ |
| 日活教师 | 1000 | 100 校 × 10 教师/校 |
| 峰值 QPS | 100 | 1000 教师 × 0.1 请求/秒 |
| 峰值并发 | 50 | QPS × 平均延迟 0.5s |
| 单实例容量 | 500 QPS | NestJS Node.js 单进程 |
| 建议实例数 | 2P6 HPA 2-10 | 峰值 QPS / 单实例 + 冗余 |
### 20.2 性能目标
| 指标 | 目标 | 阶段 |
| ---------- | ------- | ---- |
| P50 延迟 | < 100ms | P3 |
| P99 延迟 | < 500ms | P3 |
| 缓存命中率 | > 60% | P3 |
| 错误率 | < 0.1% | P6 |
| 可用性 | 99.9% | P6 |
### 20.3 Redis 容量预估
| 数据类型 | 数量 | 单条大小 | 总量 |
| -------- | ---- | -------- | --------- |
| 用户会话 | 1000 | 200B | 200KB |
| 权限列表 | 1000 | 500B | 500KB |
| 视口配置 | 1000 | 1KB | 1MB |
| 班级列表 | 1000 | 5KB | 5MB |
| 聚合结果 | 1000 | 10KB | 10MB |
| **合计** | — | — | **~17MB** |
Redis 单实例 256MB 足够,无需集群。
---
## 21. 风险与假设
### 21.1 技术风险
| 风险 | 影响 | 概率 | 缓解措施 |
| --------------------------- | ---- | ---- | ------------------------------------------------ |
| gRPC 切换引入回归 | 高 | 中 | Client 抽象层 + feature flag 灰度 + 完整测试 |
| GraphQL Schema 演进破坏前端 | 高 | 中 | Schema 版本化 + deprecation 先行 + 协调前端 ai13 |
| Redis 缓存穿透 | 中 | 低 | 短 TTL + 空结果缓存null cache 5s |
| 下游服务不可用 | 中 | 中 | 降级策略 + 熔断 |
| SSE 流式连接堆积 | 中 | 低 | 超时 + 心跳 + 连接数限制 |
| Kafka consumer 引入复杂度 | 低 | 中 | P5 评估,可继续用短 TTL 兜底 |
### 21.2 假设
1. **假设 iam 提供 `GetViewports` / `GetEffectivePermissions` 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. **假设前端 ai13teacher-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+ GraphQLcoord §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] Loggerpino
- [x] Metricsprom-client + `/metrics`
- [x] TracerOTel
- [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 AutoscalerK8s 水平自动扩缩容 |
| mTLS | mutual TLS双向 TLS 认证 |
---
**AI Agent**: ai03 (teacher-bff)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main
**关联阶段 1 文档**: [01-understanding.md](./01-understanding.md)