# 模块架构设计文档 — 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)）                        | P0     |
| Zod 输入验证       | Controller 层引入 Zod schema 解析 path/body（classId/examId 等）                                                                       | P0     |
| `/readyz` 下游探针 | 并行 ping iam + classes + core-edu 的 `/healthz`，全部 ok 才返回 ok                                                                    | P0     |
| Vitest 测试框架    | 引入 Vitest，覆盖 Service 聚合逻辑 + Controller + 错误处理，目标 ≥ 80%                                                                 | P1     |
| env 重构           | `IamServiceUrl` → `IamServiceTarget`（兼容 REST URL 和 gRPC target），为 P3 切换铺路                                                   | P1     |
| DataLoader 骨架    | 新增 `src/dataloader/` 目录，定义 DataLoader 注册器骨架（P2 不启用，P4 接入 GraphQL）                                                  | P2     |
| Redis 客户端引入   | 新增 `src/shared/cache/redis.client.ts`，P2 仅用于会话/权限缓存，P3 接入聚合缓存                                                       | P2     |

**P2 退出标准**：lint + typecheck 零错误；测试覆盖率 ≥ 80%；`/readyz` 真实探针；错误码全部 `BFF_TEACHER_*`；Zod 校验全 Controller 覆盖。

#### P3 — gRPC 切换 + 聚合缓存

**目标**：iam + core-edu 启用 gRPC server，teacher-bff 通过 Client 抽象层切换为 gRPC 调用；引入 Redis 聚合缓存。

| 工作项                     | 详情                                                                                              |
| -------------------------- | ------------------------------------------------------------------------------------------------- |
| IamClient gRPC adapter     | 实现 `@grpc/grpc-js` + `@bufbuild/protobuf` client，调用 iam:50052                                |
| CoreEduClient gRPC adapter | 调用 core-edu:50053（ExamService/HomeworkService/GradeService + AttendanceService 待 ai08 补）    |
| ClassesClient 整合         | classes 合并入 core-edu，ClassesClient 退役，调用走 CoreEduClient.ListClasses                     |
| 并行编排器                 | 抽取 `src/aggregation/orchestrator.ts`，封装 `Promise.allSettled` + 降级策略                      |
| Redis 聚合缓存             | 聚合结果 5-30s TTL，key = `bff:teacher:{userId}:{queryHash}`                                      |
| gRPC interceptor           | 注入 trace context（traceparent）+ x-user-id metadata + metrics                                   |
| 优雅降级                   | dashboard 聚合：iam 失败返回 user=null，classes 失败返回 classes=null（当前已实现，抽象到编排器） |

**P3 退出标准**：iam + core-edu 全部 gRPC 调用；聚合缓存命中率 ≥ 60%；并行编排器 + 降级策略单元测试覆盖。

#### P4 — GraphQL 演进 + DataLoader

**目标**：对前端引入 GraphQL Yoga，DataLoader 防 N+1；content + data-ana 切 gRPC。

| 工作项                     | 详情                                                                                    |
| -------------------------- | --------------------------------------------------------------------------------------- |
| GraphQL Yoga 接入          | 新增 `POST /graphql` 端点，graphql-yoga + NestJS 集成                                   |
| Schema 设计                | 按教学场景域组织 Query/Mutation（见 §5）                                                |
| DataLoader 接入            | Resolver 内调用 DataLoader，覆盖全部 N+1 风险点（见 §6）                                |
| ContentClient gRPC adapter | 调用 content:50054（KnowledgeGraphService + ChapterService/QuestionService 待 ai09 补） |
| DataAnaClient gRPC adapter | 调用 data-ana:50055（AnalyticsService + GetTeacherDashboardStats 待 ai11 补）           |
| 双轨读支持                 | data-ana 学情查询支持实时查主库 + 聚合查 ClickHouse 宽表                                |
| REST 兼容期                | REST 端点保留（deprecation header），前端逐步迁移到 GraphQL                             |
| 查询复杂度限制             | GraphQL depth limit + query cost analysis（防恶意查询）                                 |

**P4 退出标准**：GraphQL Yoga 上线；DataLoader 覆盖全部列表查询；content + data-ana gRPC 调用；REST 进入 deprecation。

#### P5 — AI 流式 + 通知

**目标**：AI 辅助出题（SSE 流式透传）+ 通知查询聚合；ai + msg 切 gRPC；可选 Kafka 缓存失效。

| 工作项                 | 详情                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| AiClient gRPC adapter  | 调用 ai:50057（含 StreamChat streaming RPC）                                                            |
| SSE 流式透传           | `GET /ai/chat/stream`：ai.StreamChat（gRPC stream）→ BFF → 前端 EventSource（见 §10）                   |
| MsgClient gRPC adapter | 调用 msg:50056（NotificationService）                                                                   |
| 通知聚合 Query         | GraphQL Query.notifications（ListNotifications + MarkAsRead mutation）                                  |
| Kafka consumer（可选） | 订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`，精确失效 Redis 权限缓存（见 §7.4） |
| AI 用量计费            | 透传 ai 服务的 token 用量 metrics，BFF 记录 `bff_teacher_ai_usage_total`                                |

**P5 退出标准**：SSE 流式透传端到端通；通知聚合 Query；Kafka consumer（可选）上线或显式决策用短 TTL 兜底。

#### P6 — 硬化

**目标**：99.9% 可用性，全链路可观测，独立扩缩容。

| 工作项          | 详情                                                               |
| --------------- | ------------------------------------------------------------------ |
| Circuit Breaker | opossum，per-downstream-service，错误率/延迟阈值                   |
| Retry           | gRPC interceptor，指数退避，仅幂等 RPC                             |
| Timeout         | per-RPC timeout（默认 3s，聚合总超时 5s）                          |
| HPA             | K8s HorizontalPodAutoscaler，基于 CPU + 自定义 metrics（请求速率） |
| mTLS            | Istio Service Mesh，服务间双向 TLS                                 |
| 全链路 trace    | OTel + Jaeger，gRPC interceptor 全链路 traceparent 传递            |
| SLO             | 99.9% 可用性 + P99 延迟 < 500ms + 错误率 < 0.1%                    |

**P6 退出标准**：SLO 达标；全链路 trace 可视化；HPA 生效；mTLS 全覆盖。

---

## 3. 模块内部分层图

### 3.1 目标态分层（P6）

```mermaid
graph TB
    subgraph Entry["入口层 (Entry)"]
        GQL[GraphQL Yoga Controller<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 GetTeacherDashboardStats，待 ai11 补 proto） |

### 4.4 P4 GraphQL API（目标态）

见 §5 详细 Schema 设计。REST 端点进入 deprecation（响应头加 `Deprecation: true`），前端逐步迁移。

### 4.5 P5 SSE 流式 API

| method | path                  | 说明                                           |
| ------ | --------------------- | ---------------------------------------------- |
| GET    | /ai/chat/stream       | SSE 流式 AI 对话（ai.StreamChat 透传，见 §10） |
| POST   | /ai/generate/question | AI 出题（透传 ai.GenerateQuestion）            |

### 4.6 统一响应信封

所有 REST 端点遵循 ActionState 信封（与 classes 黄金模板一致）：

```typescript
// 成功
{ success: true, data: T }
// 失败（GlobalErrorFilter 兜底）
{ success: false, error: { code: "BFF_TEACHER_*", message, details, traceId } }
```

GraphQL 端点遵循 GraphQL 规范（data/errors），错误扩展字段携带 `extensions.code = "BFF_TEACHER_*"`。

---

## 5. GraphQL Schema 设计（目标态 P4）

### 5.1 设计原则

1. **按教学场景域组织**（不按角色分），教导主任/教研组长复用同一 schema，靠 viewports + dataScope 驱动前端差异化
2. **Query 为主，Mutation 谨慎**：BFF 偏读多写少，Mutation 仅透传下游命令
3. **Subscription 延后**：实时通知用 SSE（P5），Subscription 留待 P6+ 评估
4. **N+1 防御**：所有 list 字段必须经 DataLoader
5. **复杂度限制**：depth limit ≤ 7，query cost ≤ 1000

### 5.2 Schema 概览

```graphql
# ============ Types ============

type User {
  id: ID!
  email: String!
  name: String!
  roles: [String!]!
  dataScope: DataScope!
}

enum DataScope {
  SELF # L0 本人
  CLASS # L1 本班
  GRADE # L2 本年级
  SCHOOL # L3 本校
  DISTRICT # L4 本区
  ALL # L5 全部
}

type ViewportItem {
  key: String!
  label: String!
  route: String!
  icon: String
  sortOrder: String!
  requiredPermission: String
}

type Class {
  id: ID!
  name: String!
  gradeId: String!
  # 延迟加载：班级下的考试
  exams: [Exam!]!
  # 延迟加载：班级下的作业
  homework: [Homework!]!
}

type Exam {
  id: ID!
  title: String!
  classId: ID!
  status: ExamStatus!
  publishedAt: DateTime
  # 延迟加载：考试下的成绩
  grades: [Grade!]!
}

enum ExamStatus {
  DRAFT
  PUBLISHED
  IN_PROGRESS
  GRADING
  SCORED
  ARCHIVED
}

type Homework {
  id: ID!
  title: String!
  classId: ID!
  dueDate: DateTime!
  submissions: [Submission!]!
}

type Submission {
  id: ID!
  studentId: ID!
  submittedAt: DateTime!
  status: SubmissionStatus!
}

enum SubmissionStatus {
  NOT_SUBMITTED
  SUBMITTED
  GRADED
}

type Grade {
  id: ID!
  examId: ID!
  studentId: ID!
  score: Float!
  rank: Int
  submittedAt: DateTime!
}

type KnowledgePath {
  knowledgePointId: ID!
  name: String!
  prerequisites: [KnowledgePoint!]!
}

type KnowledgePoint {
  id: ID!
  name: String!
  subject: String!
}

type ClassPerformance {
  classId: ID!
  averageScore: Float!
  passRate: Float!
  weaknessTopics: [String!]!
}

type StudentWeakness {
  studentId: ID!
  weakTopics: [String!]!
  recommendedExercises: [String!]!
}

type LearningTrend {
  studentId: ID!
  trend: [TrendPoint!]!
}

type TrendPoint {
  date: DateTime!
  score: Float!
}

type Notification {
  id: ID!
  type: NotificationType!
  title: String!
  content: String!
  read: Boolean!
  createdAt: DateTime!
}

enum NotificationType {
  SYSTEM
  EXAM
  HOMEWORK
  GRADE
  ATTENDANCE
}

type DashboardData {
  user: User
  classes: [Class!]!
  viewports: [ViewportItem!]!
  stats: DashboardStats
}

type DashboardStats {
  totalExams: Int!
  pendingGrading: Int!
  todayHomework: Int!
}

# ============ Query ============

type Query {
  # 仪表盘聚合（并行拉取 IAM + classes + data-ana）
  dashboard: DashboardData!

  # 视口配置（L1 导航）
  viewports: [ViewportItem!]!

  # 当前用户信息
  me: User!

  # 班级相关
  classes: [Class!]!
  class(id: ID!): Class

  # 考试/作业/成绩
  exams(classId: ID!): [Exam!]!
  homework(classId: ID!): [Homework!]!
  grades(examId: ID!): [Grade!]!

  # 学情分析（P4）
  classPerformance(classId: ID!): ClassPerformance!
  studentWeakness(studentId: ID!): StudentWeakness!
  learningTrend(studentId: ID!, dateRange: DateRangeInput): LearningTrend!

  # 知识图谱（P4）
  knowledgePath(knowledgePointId: ID!): KnowledgePath!

  # 通知（P5）
  notifications(unreadOnly: Boolean): [Notification!]!
}

input DateRangeInput {
  start: DateTime!
  end: DateTime!
}

# ============ Mutation ============

type Mutation {
  # 考试管理
  createExam(input: CreateExamInput!): Exam!
  publishExam(examId: ID!): Exam!

  # 作业管理
  assignHomework(input: AssignHomeworkInput!): Homework!

  # 成绩管理
  recordGrade(input: RecordGradeInput!): Grade!

  # AI 辅助（P5）
  generateQuestion(input: GenerateQuestionInput!): GeneratedQuestion!

  # 通知（P5）
  markNotificationAsRead(id: ID!): Notification!
}

input CreateExamInput {
  classId: ID!
  title: String!
  subject: String!
  scheduledAt: DateTime!
}

input AssignHomeworkInput {
  classId: ID!
  title: String!
  dueDate: DateTime!
}

input RecordGradeInput {
  examId: ID!
  studentId: ID!
  score: Float!
}

input GenerateQuestionInput {
  subject: String!
  gradeLevel: String!
  difficulty: String!
  knowledgePointIds: [ID!]!
}
```

### 5.3 Resolver 组织（按场景域）

```mermaid
graph TB
    subgraph Dashboard["Dashboard Resolver"]
        DQ[dashboard<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_*`）

| 错误码                          | HTTP | 触发条件                 | 当前状态                                      |
| ------------------------------- | ---- | ------------------------ | --------------------------------------------- |
| `BFF_TEACHER_VALIDATION_ERROR`  | 400  | Zod 校验失败             | 需迁移（当前 `TEACHER_BFF_VALIDATION_ERROR`） |
| `BFF_TEACHER_UNAUTHORIZED`      | 401  | 缺 x-user-id             | 需迁移                                        |
| `BFF_TEACHER_PERMISSION_DENIED` | 403  | （保留，BFF 一般不触发） | 需迁移                                        |
| `BFF_TEACHER_NOT_FOUND`         | 404  | 资源不存在               | 需迁移                                        |
| `BFF_TEACHER_CONFLICT`          | 409  | 并发冲突                 | 需迁移                                        |
| `BFF_TEACHER_BUSINESS_ERROR`    | 422  | 业务规则违反             | 需迁移                                        |
| `BFF_TEACHER_BAD_GATEWAY`       | 502  | 下游不可达               | 需迁移                                        |
| `BFF_TEACHER_INTERNAL_ERROR`    | 500  | 未知异常                 | 需迁移                                        |

**下游业务错误透传**：iam 抛 `IAM_*`、core-edu 抛 `CORE_EDU_*`、classes 抛 `CLASSES_*`，BFF 不改写错误码，直接透传给前端。

### 15.3 Logger

- 库：pino（已具备，见 [logger.ts](../src/shared/observability/logger.ts)）
- 配置：结构化 JSON，LOG_LEVEL 环境变量控制
- 注入字段：traceId、userId、service=teacher-bff
- 禁止 `console.log`

### 15.4 Metrics 指标清单

| 指标名                                    | 类型            | 标签                 | 描述             |
| ----------------------------------------- | --------------- | -------------------- | ---------------- |
| `bff_teacher_http_requests_total`         | Counter         | method, path, status | HTTP 请求总数    |
| `bff_teacher_http_duration_seconds`       | Histogram       | method, path         | HTTP 请求延迟    |
| `bff_teacher_downstream_calls_total`      | Counter         | service, rpc, status | 下游调用总数     |
| `bff_teacher_downstream_duration_seconds` | Histogram       | service, rpc         | 下游调用延迟     |
| `bff_teacher_cache_hits_total`            | Counter         | cache_type           | 缓存命中数       |
| `bff_teacher_cache_misses_total`          | Counter         | cache_type           | 缓存未命中数     |
| `bff_teacher_graphql_query_total`         | Counter（P4）   | operation, status    | GraphQL 查询总数 |
| `bff_teacher_graphql_duration_seconds`    | Histogram（P4） | operation            | GraphQL 查询延迟 |
| `bff_teacher_ai_usage_total`              | Counter（P5）   | user, model          | AI token 用量    |

### 15.5 Tracer

- 库：OpenTelemetry SDK + OTLP exporter（已具备，见 [tracer.ts](../src/shared/observability/tracer.ts)）
- 注入点：HTTP middleware + gRPC interceptor（P3）
- traceparent 传递：HTTP header → gRPC metadata（P3）
- Jaeger 端点：OTEL_EXPORTER_OTLP_ENDPOINT 环境变量

### 15.6 /healthz 检查逻辑

```typescript
// liveness：仅返回进程存活
{ status: "ok", service: "teacher-bff", timestamp: ISO8601 }
```

### 15.7 /readyz 检查逻辑（P2 补全）

```typescript
// readiness：并行 ping 下游服务 /healthz
async readiness() {
  const checks = await Promise.allSettled([
    fetch(`${env.IamServiceTarget}/healthz`),
    fetch(`${env.ClassesServiceTarget}/healthz`),
    fetch(`${env.CoreEduServiceTarget}/healthz`),
    redisClient.ping(),  // P3 引入 Redis 后
  ]);
  const status = checks.every(r => r.status === 'fulfilled') ? 'ok' : 'degraded';
  return { status, service: 'teacher-bff', timestamp, checks: {...} };
}
```

### 15.8 优雅关闭顺序

1. HTTP server stop accepting new connections
2. 等待 in-flight 请求完成（最长 10s）
3. Kafka consumer commit + leave（P5+）
4. Redis connection close
5. gRPC channels close（P3+）
6. Tracer flush + shutdown

当前 main.ts 已处理 SIGTERM + app.close()，需在 P3+ 补充 Redis/gRPC/Kafka 的关闭顺序。

---

## 16. 与其他模块的交互点（契约矩阵）

### 16.1 完整交互矩阵（P2-P6）

| 方向         | 对方服务           | 协议                          | 接口/事件                                                                                    | 用途                                       | 阶段 | proto 状态       |
| ------------ | ------------------ | ----------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------ | ---- | ---------------- |
| 被调用       | api-gateway        | HTTP                          | `POST /graphql` + `GET /teacher/*` + `GET /ai/chat/stream` + `/healthz` `/readyz` `/metrics` | Gateway 转发 + 注入 x-user-id              | P2   | ✅               |
| 调用         | iam                | HTTP（P2）/ gRPC 50052（P3+） | GetUserInfo                                                                                  | 拉取用户信息                               | P2   | ✅ 已有          |
| 调用         | iam                | gRPC 50052（P3+）             | **GetViewports**                                                                             | 拉取视口配置                               | P2   | ❌ 待 ai06 补    |
| 调用         | iam                | gRPC 50052（P3+）             | **GetEffectivePermissions**                                                                  | 拉取权限列表                               | P2   | ❌ 待 ai06 补    |
| 调用         | iam                | gRPC 50052（P4+）             | **GetEffectiveAccess**（建议聚合 RPC）                                                       | 一次拉取 {perms, viewports, dataScope}     | P4   | ❌ 待 coord 仲裁 |
| 调用         | iam                | gRPC 50052（P3+）             | **Logout**                                                                                   | 登出                                       | P2   | ❌ 待 ai06 补    |
| 调用         | iam                | gRPC 50052（P3+）             | **GetPublicKey / JWKS**                                                                      | 公钥（BFF 不校验 JWT，但可缓存公钥给前端） | P3   | ❌ 待 ai06 补    |
| 调用         | classes / core-edu | HTTP（P2）/ gRPC 50053（P3+） | ListClasses / GetClass                                                                       | 班级列表                                   | P2   | ✅ 已有          |
| 调用         | core-edu           | gRPC 50053（P3+）             | ExamService.*（CreateExam/GetExam/ListExamsByClass 等）                                      | 考试聚合                                   | P3   | ✅ 已有          |
| 调用         | core-edu           | gRPC 50053（P3+）             | HomeworkService.*                                                                            | 作业聚合                                   | P3   | ✅ 已有          |
| 调用         | core-edu           | gRPC 50053（P3+）             | GradeService.*                                                                               | 成绩聚合                                   | P3   | ✅ 已有          |
| 调用         | core-edu           | gRPC 50053（P3+）             | **AttendanceService.***                                                                      | 出勤聚合                                   | P3   | ❌ 待 ai08 补    |
| 调用         | core-edu           | gRPC 50053（P3+）             | **GetClassesByTeacher**（建议新增）                                                          | 按教师拉班级                               | P3   | ❌ 待 coord 仲裁 |
| 调用         | content            | gRPC 50054（P4+）             | KnowledgeGraphService.GetPrerequisites / GetLearningPath                                     | 知识图谱                                   | P4   | ✅ 已有          |
| 调用         | content            | gRPC 50054（P4+）             | _*ChapterService.* / QuestionService._**                                                     | 章节/题库                                  | P4   | ❌ 待 ai09 补    |
| 调用         | data-ana           | gRPC 50055（P4+）             | AnalyticsService.GetClassPerformance / GetStudentWeakness / GetLearningTrend                 | 学情诊断                                   | P4   | ✅ 已有          |
| 调用         | data-ana           | gRPC 50055（P4+）             | **GetTeacherDashboardStats**（建议新增）                                                     | 仪表盘统计                                 | P4   | ❌ 待 coord 仲裁 |
| 调用         | ai                 | gRPC 50057（P5+）             | AiService.GenerateQuestion / Chat / OptimizeExpression                                       | AI 辅助                                    | P5   | ✅ 已有          |
| 调用         | ai                 | gRPC 50057 streaming（P5+）   | **AiService.StreamChat**                                                                     | SSE 流式对话                               | P5   | ✅ 已有          |
| 调用         | msg                | gRPC 50056（P5+）             | NotificationService.ListNotifications / SearchNotifications / MarkAsRead                     | 通知聚合                                   | P5   | ✅ 已有          |
| 消费（可选） | Kafka              | 异步                          | `edu.identity.user.role_changed`                                                             | 权限缓存失效                               | P5+  | ✅ topic 已定义  |
| 消费（可选） | Kafka              | 异步                          | `edu.identity.role.updated`                                                                  | 权限缓存失效                               | P5+  | ✅ topic 已定义  |

### 16.2 端口分配

| 服务                  | HTTP 端口 | gRPC 端口                        |
| --------------------- | --------- | -------------------------------- |
| teacher-bff（本模块） | 3003      | 不暴露（只被 Gateway HTTP 调用） |
| iam                   | 3002      | 50052                            |
| classes / core-edu    | 3004      | 50053                            |
| content               | 3005      | 50054                            |
| data-ana              | 3006      | 50055                            |
| msg                   | 3007      | 50056                            |
| ai                    | 3008      | 50057                            |

---

## 17. 弹性设计（熔断/重试/超时）（P6 硬化）

### 17.1 Circuit Breaker 配置

| 下游服务 | 错误率阈值      | 熔断时长 | 半开请求数 |
| -------- | --------------- | -------- | ---------- |
| iam      | 50%             | 30s      | 3          |
| core-edu | 50%             | 30s      | 3          |
| content  | 50%             | 60s      | 2          |
| data-ana | 50%             | 60s      | 2          |
| ai       | 70%（容忍度高） | 30s      | 5          |
| msg      | 50%             | 60s      | 2          |

### 17.2 Retry 策略

- 仅对幂等 RPC 重试（GET 类查询）
- Mutation 类 RPC 不重试（避免重复写入）
- 最大重试次数：2
- 退避策略：指数退避（100ms, 400ms）
- 仅对 transient 错误重试（UNAVAILABLE / DEADLINE_EXCEEDED），不重试 INVALID_ARGUMENT / NOT_FOUND

### 17.3 超时层级

| 层级           | 超时                      |
| -------------- | ------------------------- |
| 单个 gRPC RPC  | 3s                        |
| 聚合编排总超时 | 5s                        |
| GraphQL 查询   | 10s                       |
| SSE 流式       | 无（靠心跳 + 客户端断开） |
| Redis 操作     | 100ms                     |

---

## 18. 可观测性设计

### 18.1 三支柱（project_rules §12）

| 支柱 | 实现                     | 端点         | 采样率         |
| ---- | ------------------------ | ------------ | -------------- |
| 日志 | pino（结构化 JSON）      | stdout       | 100%           |
| 指标 | prom-client              | GET /metrics | 100%           |
| 链路 | OpenTelemetry SDK + OTLP | Jaeger       | 10%（P6 全量） |

### 18.2 关键 trace span

```
HTTP /graphql (root span)
├─ graphql.resolve (operation)
│  ├─ dataloader.load (batch key)
│  ├─ grpc.call iam.GetUser (child span)
│  ├─ grpc.call core-edu.ListExams (child span)
│  └─ redis.get cache (child span)
└─ http.response (serialize)
```

### 18.3 告警规则（提给 SRE）

| 告警                     | 条件                                                                  | 严重度 |
| ------------------------ | --------------------------------------------------------------------- | ------ |
| teacher-bff 5xx 错误率高 | `rate(bff_teacher_http_requests_total{status=~"5.."}[5m]) > 0.01`     | P1     |
| 下游调用失败率高         | `rate(bff_teacher_downstream_calls_total{status="error"}[5m]) > 0.05` | P1     |
| 缓存命中率低             | `cache_misses / (hits + misses) > 0.5` 持续 10min                     | P2     |
| P99 延迟高               | `histogram_quantile(0.99, bff_teacher_http_duration_seconds) > 1`     | P2     |
| /readyz 失败             | `readyz_status != "ok"` 持续 1min                                     | P0     |

---

## 19. 安全设计

### 19.1 JWT 与身份传递

- **BFF 不校验 JWT**：JWT 校验在 api-gateway 层（project_rules §4）
- **BFF 接收 header**：`x-user-id`、`x-user-roles`（由 Gateway 解码 JWT 后注入）
- **BFF 透传**：调下游 gRPC 时，将 userId/roles/dataScope 作为 metadata 传递

### 19.2 CORS

- 由 api-gateway 统一处理，BFF 不直接暴露给前端
- BFF 仅接受来自 api-gateway 的请求（内网通信）

### 19.3 限流

- 由 api-gateway 统一处理（IP 级令牌桶 + 用户级）
- BFF 自身不做限流（避免双重限流逻辑）

### 19.4 输入校验

- REST：Controller 层 Zod schema 解析（P2 补全）
- GraphQL：graphql-yoga 内置参数校验 + 自定义 scalar（DateTime、ID）
- 查询复杂度限制（P4）：depth ≤ 7，cost ≤ 1000

### 19.5 敏感数据

- 日志禁止记录 JWT token、密码、用户敏感信息（pino redact 配置）
- Redis 缓存不存储密码、token（仅存 userId/roles/dataScope/perms/viewports）

---

## 20. 容量与性能预估

### 20.1 容量预估（假设 1000 教师 / 100 学校）

| 指标       | 预估值           | 依据                     |
| ---------- | ---------------- | ------------------------ |
| 日活教师   | 1000             | 100 校 × 10 教师/校      |
| 峰值 QPS   | 100              | 1000 教师 × 0.1 请求/秒  |
| 峰值并发   | 50               | QPS × 平均延迟 0.5s      |
| 单实例容量 | 500 QPS          | NestJS Node.js 单进程    |
| 建议实例数 | 2（P6 HPA 2-10） | 峰值 QPS / 单实例 + 冗余 |

### 20.2 性能目标

| 指标       | 目标    | 阶段 |
| ---------- | ------- | ---- |
| P50 延迟   | < 100ms | P3   |
| P99 延迟   | < 500ms | P3   |
| 缓存命中率 | > 60%   | P3   |
| 错误率     | < 0.1%  | P6   |
| 可用性     | 99.9%   | P6   |

### 20.3 Redis 容量预估

| 数据类型 | 数量 | 单条大小 | 总量      |
| -------- | ---- | -------- | --------- |
| 用户会话 | 1000 | 200B     | 200KB     |
| 权限列表 | 1000 | 500B     | 500KB     |
| 视口配置 | 1000 | 1KB      | 1MB       |
| 班级列表 | 1000 | 5KB      | 5MB       |
| 聚合结果 | 1000 | 10KB     | 10MB      |
| **合计** | —    | —        | **~17MB** |

Redis 单实例 256MB 足够，无需集群。

---

## 21. 风险与假设

### 21.1 技术风险

| 风险                        | 影响 | 概率 | 缓解措施                                         |
| --------------------------- | ---- | ---- | ------------------------------------------------ |
| gRPC 切换引入回归           | 高   | 中   | Client 抽象层 + feature flag 灰度 + 完整测试     |
| GraphQL Schema 演进破坏前端 | 高   | 中   | Schema 版本化 + deprecation 先行 + 协调前端 ai13 |
| Redis 缓存穿透              | 中   | 低   | 短 TTL + 空结果缓存（null cache 5s）             |
| 下游服务不可用              | 中   | 中   | 降级策略 + 熔断                                  |
| SSE 流式连接堆积            | 中   | 低   | 超时 + 心跳 + 连接数限制                         |
| Kafka consumer 引入复杂度   | 低   | 中   | P5 评估，可继续用短 TTL 兜底                     |

### 21.2 假设

1. **假设 iam 提供 `GetViewports` / `GetEffectivePermissions` RPC**（coord 裁决 P2 补全），否则 P2 继续用 REST `/iam/viewports` + `/iam/me`
2. **假设 core-edu P3 启用 gRPC server**（004 §4.1），否则 teacher-bff P3 继续 REST 调 core-edu
3. **假设 api-gateway 注入 `x-user-id` / `x-user-roles` header**（project_memory 已确认），BFF 不解码 JWT
4. **假设前端 ai13（teacher-portal）配合迁移到 GraphQL**（P4），否则 REST 兼容期延长
5. **假设 Redis 共用 edu-redis 实例**（project_memory 已确认），不独立部署

### 21.3 未决设计决策（提请 coord 仲裁，见 §22）

见 §22 待 coord 仲裁的决策清单。

---

## 22. 待 coord 仲裁的决策清单

> 以下决策阻塞 P2-P5 实施，需 coord 仲裁后 ai03 据此调整设计。

| #   | 决策项                                           | 选项                                                                                   | ai03 建议                                                    | 阻塞阶段 |
| --- | ------------------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------- |
| 1   | **GraphQL 引入时机**                             | A. P2 即引入（pending-features 字面）/ B. P2-P3 REST、P4+ GraphQL（coord §2.5 裁决）   | **B**（P2 引入 GraphQL 工作量大，P4 复杂聚合场景才显现价值） | P2       |
| 2   | **iam 聚合 API**                                 | A. 新增 `GetEffectiveAccess(userId) → {perms, viewports, dataScope}` / B. 三次独立 RPC | **A**（减少 3 次 RTT，仪表盘场景受益）                       | P2-P3    |
| 3   | **iam 补 4 个 RPC**                              | GetViewports / GetEffectivePermissions / Logout / GetPublicKey                         | 强烈建议 P2 补全（teacher-bff 直接依赖）                     | P2       |
| 4   | **core-edu 补 AttendanceService**                | service 级新增                                                                         | 建议 P3 补全                                                 | P3       |
| 5   | **core-edu 补 GetClassesByTeacher RPC**          | 按教师拉班级                                                                           | 建议新增（当前 ListClasses 仅按 gradeId）                    | P3       |
| 6   | **data-ana 补 GetTeacherDashboardStats RPC**     | 仪表盘统计聚合                                                                         | 建议新增                                                     | P4       |
| 7   | **iam 补 BatchGetUsers RPC**                     | DataLoader 批量化                                                                      | 建议新增（否则 DataLoader 退化为一对一）                     | P4       |
| 8   | **core-edu 补 BatchGetClasses RPC**              | DataLoader 批量化                                                                      | 建议新增                                                     | P4       |
| 9   | **Kafka consumer 引入时机**                      | A. P5 引入精确失效 / B. 全程短 TTL 兜底                                                | **P5 评估**（取决于权限变更频率）                            | P5       |
| 10  | **错误码迁移 `TEACHER_BFF_*` → `BFF_TEACHER_*`** | 全量替换                                                                               | 已是 coord P0 整改 #3，本设计确认执行                        | P2       |
| 11  | **REST deprecation 时间表**                      | P4 引入 GraphQL 后 REST 保留多久                                                       | 建议 P4-P5 兼容期，P6 下线                                   | P4       |

---

## 附录 A：与黄金模板（classes）对齐 Checklist

> 供 coord 交叉审查 ai03 对齐情况。

- [ ] 权限装饰器：BFF 不加（设计决策，依赖 Gateway + 下游）
- [x] 错误码前缀：`BFF_TEACHER_*`（P2 迁移）
- [x] Logger：pino
- [x] Metrics：prom-client + `/metrics`
- [x] Tracer：OTel
- [x] `/healthz` + `/readyz`：P2 补下游探针
- [x] 优雅关闭：SIGTERM
- [ ] 测试覆盖率 ≥ 80%：P2 引入 Vitest
- [x] Dockerfile 多阶段构建
- [ ] Zod 输入验证：P2 补全
- [x] GlobalErrorFilter 统一兜底
- [x] env Zod 校验（env.ts 已有）

---

## 附录 B：术语表

| 术语             | 含义                                          |
| ---------------- | --------------------------------------------- |
| BFF              | Backend For Frontend，面向前端的后端聚合层    |
| DataLoader       | Facebook 出品的批量化 + 缓存工具，防 N+1      |
| DataScope        | 数据范围枚举（6 级），控制数据行级可见性      |
| 视口（Viewport） | 角色可见的导航/路由/组件/数据四层配置         |
| SSE              | Server-Sent Events，服务器推送事件            |
| Outbox           | 事务内写业务表 + outbox 表的事件发布模式      |
| CDC              | Change Data Capture，数据库变更捕获           |
| HPA              | Horizontal Pod Autoscaler，K8s 水平自动扩缩容 |
| mTLS             | mutual TLS，双向 TLS 认证                     |

---

**AI Agent**: ai03 (teacher-bff)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式（直接 push main）
**关联阶段 1 文档**: [01-understanding.md](./01-understanding.md)
