1518 lines
73 KiB
Markdown
1518 lines
73 KiB
Markdown
# 模块架构设计文档 — 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<br/>Next.js + urql]
|
||
end
|
||
|
||
subgraph Gateway["网关层"]
|
||
AGW[api-gateway<br/>Go/Gin<br/>JWT 校验/限流/熔断]
|
||
end
|
||
|
||
subgraph BFF["teacher-bff(本模块)P6 终态"]
|
||
direction TB
|
||
GQL[GraphQL Yoga Endpoint<br/>POST /graphql<br/>+ GET /graphql/subscriptions]
|
||
SSE[SSE Endpoint<br/>GET /ai/chat/stream]
|
||
REST[REST Endpoint<br/>/healthz /readyz /metrics]
|
||
|
||
subgraph Aggregation["聚合编排层"]
|
||
RES[GraphQL Resolvers<br/>按教学场景域组织]
|
||
DL[DataLoader<br/>批量去重]
|
||
ORCH[并行编排器<br/>Promise.allSettled]
|
||
FALL[降级策略<br/>部分失败容忍]
|
||
end
|
||
|
||
subgraph Cache["缓存层"]
|
||
RC[Redis Cache<br/>聚合结果 5-30s<br/>权限 5min<br/>用户会话 30min]
|
||
IC[Invalidation Coordinator<br/>TTL + 事件驱动]
|
||
end
|
||
|
||
subgraph Clients["下游 Client 抽象层"]
|
||
IC_IAM[IamClient]
|
||
IC_CE[CoreEduClient]
|
||
IC_CNT[ContentClient]
|
||
IC_DA[DataAnaClient]
|
||
IC_AI[AiClient<br/>含 StreamChat 流式]
|
||
IC_MSG[MsgClient]
|
||
end
|
||
|
||
subgraph Resilience["弹性层"]
|
||
CB[Circuit Breaker<br/>opossum]
|
||
RT[Retry<br/>gRPC interceptor]
|
||
TL[Timeout<br/>per-RPC]
|
||
end
|
||
|
||
subgraph Obs["可观测层"]
|
||
LOG[pino Logger]
|
||
MET[prom-client Metrics]
|
||
TRC[OTel Tracer<br/>gRPC interceptor]
|
||
end
|
||
end
|
||
|
||
subgraph Downstream["下游业务服务(gRPC)"]
|
||
IAM[iam:50052]
|
||
CE[core-edu:50053]
|
||
CNT[content:50054]
|
||
DA[data-ana:50055]
|
||
MSG[msg:50056]
|
||
AI[ai:50057<br/>含 StreamChat]
|
||
end
|
||
|
||
subgraph Infra["基础设施"]
|
||
K[Kafka<br/>权限缓存失效事件]
|
||
R[(Redis<br/>edu-redis:6379)]
|
||
J[Jaeger<br/>OTLP]
|
||
P[Prometheus<br/>:9090]
|
||
end
|
||
|
||
TP -->|GraphQL/HTTP/SSE| AGW
|
||
AGW -->|HTTP + x-user-id/x-user-roles| GQL
|
||
AGW --> SSE
|
||
AGW --> REST
|
||
|
||
GQL --> RES
|
||
SSE --> RES
|
||
RES --> DL
|
||
RES --> ORCH
|
||
ORCH --> FALL
|
||
RES --> RC
|
||
RC --> IC
|
||
|
||
DL --> Clients
|
||
ORCH --> Clients
|
||
Clients --> Resilience
|
||
Resilience --> IAM
|
||
Resilience --> CE
|
||
Resilience --> CNT
|
||
Resilience --> DA
|
||
Resilience --> MSG
|
||
Resilience --> AI
|
||
|
||
RC --> R
|
||
IC -.->|订阅 edu.identity.*| K
|
||
IAM -.->|发布 edu.identity.*| K
|
||
|
||
LOG --> J
|
||
MET --> P
|
||
TRC --> J
|
||
```
|
||
|
||
### 1.2 目标态核心特征
|
||
|
||
| 维度 | P6 终态 | 设计依据 |
|
||
| ---------- | ------------------------------------------------------------------------------ | ------------------------------ |
|
||
| 对前端协议 | GraphQL Yoga(Query/Mutation/Subscription)+ SSE(AI 流式)+ REST(健康/指标) | 004 §11.3、pending-features P2 |
|
||
| 对下游协议 | 全 gRPC(iam/core-edu/content/data-ana/msg/ai) | 004 §4.1 |
|
||
| 缓存 | Redis 三层(聚合 5-30s / 权限 5min / 会话 30min)+ 事件驱动失效 | 004 §6.3 |
|
||
| 批量去重 | DataLoader 覆盖全部 N+1 风险点 | ai-allocation §5 ai03 |
|
||
| 弹性 | Circuit Breaker + Retry + Timeout + 降级 | P6 硬化 |
|
||
| 可观测 | pino + prom-client + OTel(gRPC interceptor 全链路) | project_rules §12 |
|
||
| 多角色 | 教师/教导主任/教研组长复用同一 BFF,视口 + DataScope 差异化 | 004 §5.4 |
|
||
| 流式 | SSE 三层透传(ai.StreamChat → bff → 前端 EventSource) | pending-features P5 |
|
||
| 事件消费 | 可选 Kafka consumer 订阅 `edu.identity.*` 精确失效权限缓存 | 004 §7.2 |
|
||
| 部署 | K8s + HPA + mTLS(Istio Service Mesh) | P6 硬化 |
|
||
|
||
---
|
||
|
||
## 2. 分阶段演进路线(P2→P6)
|
||
|
||
> 每阶段独立交付价值,通过抽象层隔离变更,避免大爆炸式重构。
|
||
|
||
### 2.1 演进路线图
|
||
|
||
```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 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<br/>POST /graphql]
|
||
SSE[SSE Controller<br/>GET /ai/chat/stream]
|
||
HEALTH[Health Controller<br/>/healthz /readyz /metrics]
|
||
end
|
||
|
||
subgraph Middleware["中间件层"]
|
||
CTX[Context Middleware<br/>解析 x-user-id/x-user-roles<br/>注入 GraphQL context]
|
||
ERR[GlobalErrorFilter<br/>统一错误兜底]
|
||
end
|
||
|
||
subgraph Aggregation["聚合编排层 (Aggregation)"]
|
||
RES[GraphQL Resolvers<br/>按场景域组织]
|
||
DL[DataLoader Registry<br/>per-request 实例]
|
||
ORCH[Orchestrator<br/>Promise.allSettled + 降级]
|
||
MAP[Response Mapper<br/>proto → GraphQL type]
|
||
end
|
||
|
||
subgraph Cache["缓存层 (Cache)"]
|
||
RCLIENT[RedisClient<br/>ioredis]
|
||
CKV[CacheKey Builder<br/>bff:teacher:*]
|
||
CINV[Invalidation Coordinator<br/>TTL + 事件驱动]
|
||
end
|
||
|
||
subgraph Clients["下游 Client 抽象层 (Clients)"]
|
||
IAM_C[IamClient<br/>interface + gRPC impl]
|
||
CE_C[CoreEduClient<br/>interface + gRPC impl]
|
||
CNT_C[ContentClient<br/>interface + gRPC impl]
|
||
DA_C[DataAnaClient<br/>interface + gRPC impl]
|
||
AI_C[AiClient<br/>interface + gRPC impl<br/>含 StreamChat]
|
||
MSG_C[MsgClient<br/>interface + gRPC impl]
|
||
end
|
||
|
||
subgraph Resilience["弹性层"]
|
||
CB[CircuitBreaker]
|
||
RT[Retry]
|
||
TL[Timeout]
|
||
end
|
||
|
||
subgraph Obs["可观测层"]
|
||
LOG[pino]
|
||
MET[prom-client]
|
||
TRC[OTel Tracer]
|
||
end
|
||
|
||
subgraph Config["配置层"]
|
||
ENV[env.ts<br/>Zod 校验]
|
||
DI[DI Container<br/>NestJS]
|
||
end
|
||
|
||
GQL --> CTX
|
||
SSE --> CTX
|
||
CTX --> RES
|
||
RES --> DL
|
||
RES --> ORCH
|
||
RES --> RCLIENT
|
||
DL --> Clients
|
||
ORCH --> Clients
|
||
Clients --> Resilience
|
||
Resilience --> Downstream
|
||
|
||
RCLIENT --> CINV
|
||
ERR -.-> GQL
|
||
ERR -.-> SSE
|
||
```
|
||
|
||
### 3.2 目录结构(目标态)
|
||
|
||
```
|
||
services/teacher-bff/src/
|
||
├─ config/
|
||
│ └─ env.ts # Zod 校验环境变量
|
||
├─ entry/ # 入口层
|
||
│ ├─ graphql.controller.ts # GraphQL Yoga 端点(P4)
|
||
│ ├─ sse.controller.ts # SSE 流式端点(P5)
|
||
│ └─ context.middleware.ts # 解析 x-user-id 注入 context
|
||
├─ teacher/ # 教学场景域(P2 REST 端点,P4 迁移 GraphQL)
|
||
│ ├─ teacher.controller.ts # REST Controller(P2,P4 后逐步 deprecate)
|
||
│ ├─ teacher.service.ts # 聚合 Service
|
||
│ └─ teacher.schema.ts # Zod 输入校验
|
||
├─ aggregation/ # 聚合编排层
|
||
│ ├─ orchestrator.ts # Promise.allSettled + 降级
|
||
│ ├─ response-mapper.ts # proto → GraphQL/REST type
|
||
│ └─ fallback-strategy.ts # 降级策略
|
||
├─ dataloader/ # DataLoader(P4)
|
||
│ ├─ dataloader.module.ts # per-request DataLoader 注册器
|
||
│ ├─ user.dataloader.ts # 按 userId 批量取用户
|
||
│ ├─ class.dataloader.ts # 按 classId 批量取班级
|
||
│ ├─ exam.dataloader.ts # 按 classId 批量取考试
|
||
│ ├─ homework.dataloader.ts # 按 classId 批量取作业
|
||
│ └─ grade.dataloader.ts # 按 examId 批量取成绩
|
||
├─ graphql/ # GraphQL Schema(P4)
|
||
│ ├─ schema.ts # typeDefs + resolvers
|
||
│ ├─ types/ # GraphQL type 定义
|
||
│ │ ├─ user.type.ts
|
||
│ │ ├─ class.type.ts
|
||
│ │ ├─ exam.type.ts
|
||
│ │ ├─ homework.type.ts
|
||
│ │ ├─ grade.type.ts
|
||
│ │ └─ analytics.type.ts
|
||
│ └─ resolvers/ # 按场景域组织
|
||
│ ├─ dashboard.resolver.ts
|
||
│ ├─ exam.resolver.ts
|
||
│ ├─ homework.resolver.ts
|
||
│ ├─ grade.resolver.ts
|
||
│ ├─ analytics.resolver.ts
|
||
│ └─ notification.resolver.ts
|
||
├─ clients/ # 下游 Client 抽象层
|
||
│ ├─ iam.client.ts # interface + gRPC impl
|
||
│ ├─ core-edu.client.ts
|
||
│ ├─ content.client.ts # P4
|
||
│ ├─ data-ana.client.ts # P4
|
||
│ ├─ ai.client.ts # P5(含 StreamChat)
|
||
│ ├─ msg.client.ts # P5
|
||
│ ├─ grpc/ # gRPC adapter(P3)
|
||
│ │ ├─ grpc.factory.ts # Channel + Client 工厂
|
||
│ │ └─ interceptors.ts # trace/metrics/retry interceptor
|
||
│ └─ rest/ # REST adapter(P2,过渡期)
|
||
│ └─ rest-fetch.adapter.ts
|
||
├─ shared/
|
||
│ ├─ errors/
|
||
│ │ ├─ application-error.ts # BFF_TEACHER_* 错误码
|
||
│ │ └─ global-error.filter.ts
|
||
│ ├─ health/
|
||
│ │ ├─ health.controller.ts # /healthz + /readyz
|
||
│ │ └─ readiness.probe.ts # 下游就绪探针(P2 补)
|
||
│ ├─ cache/ # 缓存层
|
||
│ │ ├─ redis.client.ts # ioredis 客户端
|
||
│ │ ├─ cache-key.builder.ts
|
||
│ │ └─ invalidation.coordinator.ts # TTL + 事件驱动失效
|
||
│ ├─ observability/
|
||
│ │ ├─ logger.ts # pino
|
||
│ │ ├─ metrics.ts # prom-client + 指标定义
|
||
│ │ └─ tracer.ts # OTel
|
||
│ ├─ resilience/ # 弹性层(P6)
|
||
│ │ ├─ circuit-breaker.ts # opossum
|
||
│ │ └─ retry.policy.ts
|
||
│ └─ kafka/ # Kafka consumer(P5 可选)
|
||
│ ├─ kafka.consumer.ts
|
||
│ └─ handlers/
|
||
│ └─ permission-invalidation.handler.ts
|
||
├─ app.module.ts
|
||
└─ main.ts
|
||
```
|
||
|
||
### 3.3 分层职责契约
|
||
|
||
| 层 | 职责 | 禁止 |
|
||
| ------------- | ---------------------------------------------------- | --------------------- |
|
||
| Entry | 协议入口(GraphQL/REST/SSE),解析请求,注入 context | 业务逻辑、下游调用 |
|
||
| Middleware | 通用横切(context 注入、错误兜底) | 业务逻辑 |
|
||
| Aggregation | 编排多下游调用、聚合裁剪、降级 | 直接访问 DB、权限决策 |
|
||
| Cache | 缓存读写、失效协调 | 业务逻辑 |
|
||
| Clients | 下游协议适配(REST/gRPC)、interceptor | 业务聚合逻辑 |
|
||
| Resilience | 熔断/重试/超时 | 业务逻辑 |
|
||
| Observability | 日志/指标/链路 | 业务逻辑 |
|
||
|
||
**依赖方向**:Entry → Aggregation → Clients → Resilience → Downstream;Cache 横向被 Aggregation 调用;Observability 横向被所有层调用。禁止反向依赖。
|
||
|
||
---
|
||
|
||
## 4. API 设计(REST→GraphQL 演进)
|
||
|
||
### 4.1 API 风格仲裁说明
|
||
|
||
> **冲突**:pending-features P2 退出标准要求 "GraphQL Yoga + DataLoader";coord-cross-review §2.5 裁决 "P2-P3 用 REST,P4 起若 BFF 切 GraphQL 再引入 urql"。
|
||
>
|
||
> **ai03 设计决策**(提请 coord 仲裁):
|
||
>
|
||
> - **方案 A(推荐)**:P2 保持 REST 对前端(不阻塞 P2 退出,因 P2 退出标准核心是"教师登录→侧边栏→Dashboard",REST 足以支撑),P4 引入 GraphQL Yoga + DataLoader。理由:P2 引入 GraphQL 会大幅增加 P2 工作量且与 coord 裁决冲突;GraphQL 价值在 P3+ 复杂聚合场景才显现(考试/作业/成绩/学情多源聚合)。
|
||
> - **方案 B**:P2 即引入 GraphQL Yoga + DataLoader。理由:严格遵循 pending-features 字面要求,但需 coord 撤销 §2.5 裁决。
|
||
>
|
||
> **本设计文档后续以方案 A 为基准**(P2 REST、P4 GraphQL),若 coord 仲裁为方案 B,则将 §5 GraphQL Schema 前移到 P2 实施。
|
||
|
||
### 4.2 P2 REST API(当前 + 加固)
|
||
|
||
| method | path | 权限 | 请求 | 响应 | 说明 |
|
||
| ------ | ---------------------------------- | ---------------- | --------------- | ----------------------------------------------- | ---------------------------- |
|
||
| GET | /teacher/dashboard | 仅校验 x-user-id | — | `{ success, data: { user, classes } }` | 聚合 IAM 用户 + classes 列表 |
|
||
| GET | /teacher/viewports | 仅校验 x-user-id | — | `{ success, data: ViewportItem[] }` | 拉取 IAM 视口(L1 导航) |
|
||
| GET | /teacher/classes/:classId/exams | 仅校验 x-user-id | classId: string | `{ success, data: Exam[] }` | 聚合 core-edu 考试列表 |
|
||
| GET | /teacher/classes/:classId/homework | 仅校验 x-user-id | classId: string | `{ success, data: Homework[] }` | 聚合 core-edu 作业列表 |
|
||
| GET | /teacher/exams/:examId/grades | 仅校验 x-user-id | examId: string | `{ success, data: Grade[] }` | 聚合 core-edu 成绩列表 |
|
||
| GET | /healthz | 无 | — | `{ status, service, timestamp }` | liveness |
|
||
| GET | /readyz | 无 | — | `{ status, service, timestamp, checks: {...} }` | readiness(P2 补下游探针) |
|
||
| GET | /metrics | 无 | — | Prometheus text | Prometheus 抓取 |
|
||
|
||
**P2 加固项**:
|
||
|
||
- 全部 Controller 方法补 Zod 输入校验(classId/examId 必须是 cuid/uuid 格式)
|
||
- 错误码统一 `BFF_TEACHER_*`
|
||
- `/readyz` 补下游就绪探针
|
||
|
||
### 4.3 P3 REST 扩展
|
||
|
||
| method | path | 说明 |
|
||
| ------ | ------------------------------------ | ------------------------------------------------------------ |
|
||
| POST | /teacher/exams | 创建考试(透传 core-edu CreateExam) |
|
||
| POST | /teacher/homework | 布置作业(透传 core-edu AssignHomework) |
|
||
| POST | /teacher/grades | 录入成绩(透传 core-edu RecordGrade) |
|
||
| GET | /teacher/classes/:classId/attendance | 出勤列表(core-edu AttendanceService,待 ai08 补 proto) |
|
||
| GET | /teacher/dashboard/stats | 仪表盘统计(data-ana GetTeacherDashboard,待 ai11 补 proto) |
|
||
|
||
### 4.4 P4 GraphQL API(目标态)
|
||
|
||
见 §5 详细 Schema 设计。REST 端点进入 deprecation(响应头加 `Deprecation: true`),前端逐步迁移。
|
||
|
||
### 4.5 P5 SSE 流式 API
|
||
|
||
| method | path | 说明 |
|
||
| ------ | --------------------- | ---------------------------------------------- |
|
||
| GET | /ai/chat/stream | SSE 流式 AI 对话(ai.StreamChat 透传,见 §10) |
|
||
| POST | /ai/generate/question | AI 出题(透传 ai.GenerateQuestion) |
|
||
|
||
### 4.6 统一响应信封
|
||
|
||
所有 REST 端点遵循 ActionState 信封(与 classes 黄金模板一致):
|
||
|
||
```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<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 默认)
|
||
|
||
- 所有缓存依赖短 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[侧边栏菜单项<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 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<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 携带 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)
|