# teacher-bff 工作排期

> 负责人：ai03
> 关联：[workline.md](../workline.md)、[coord.md §1 ARB-001](../coord.md)、[contracts/teacher-bff_contract.md](../contracts/teacher-bff_contract.md)、[president-final-rulings.md §3.1/§7.3](../../president-final-rulings.md)、[coord-final-decisions.md §2 B1-B8](../../coord-final-decisions.md)
> 模式：全并行（各 AI 一口气完成 P2-P6 全部代码，最后统一集成测试）
> 裁决依据：B1 P2 即 GraphQL / B2 首次实现即 gRPC / B3 豁免 @RequirePermission / B4 越权防御 / B5 BFF_TEACHER_ 前缀 / B6 Redis 短缓存 / B7 P2-P4 不订阅 Kafka / B8 DownstreamClient 抽象

---

## §1 总览

teacher-bff 是教学场景域聚合层（BFF），全阶段目标：

| 阶段 | 核心交付 | 裁决依据 |
| ---- | -------- | -------- |
| P2 | GraphQL schema 第一版（5 Query + admin 预留）+ DownstreamClient 抽象 + iam gRPC + AuthorizationGuard + ActionState 信封 | B1/B2/B3/B4/B5/B8 + ARB-001 + §3.1 |
| P3 | core-edu gRPC 扩展（exams/homework/grades Query + Mutation）+ AuthorizationGuard 接入 core-edu + Redis 聚合缓存 | §2.3 跨阶段扩展例外 |
| P4 | content + data-ana gRPC 扩展（学情分析 Query）+ DataLoader 全量接入 | §2.3 + §2.8 |
| P5 | ai + msg gRPC 扩展（SSE 流式 + notifications Query）+ Kafka consumer（push-gateway 落地后） | B7 |
| P6 | admin 命名空间实现 + 硬化（熔断/重试/超时/HPA/mTLS） | §5.1 + P6 硬化 |

---

## §2 全阶段甘特图（P2-P6）

```mermaid
gantt
    title ai03 teacher-bff 全阶段排期
    dateFormat YYYY-MM-DD
    axisFormat %m-%d

    section P2 GraphQL 基础（8d）
    schema第一版+admin预留(ARB-001) :crit, a3a, 2026-07-10, 2d
    Yoga endpoint+5 Query Resolver :crit, a3b, after a3a, 3d
    DownstreamClient+iam gRPC :crit, a3c, after a3a, 2d
    AuthorizationGuard越权防御(B4) :crit, a3d, after a3b, 1d
    ActionState信封+降级模式B :a3e, after a3b, 1d
    /readyz探针注册表+iam探针 :a3f, after a3d, 1d
    错误码迁移BFF_TEACHER_+回写02 :a3g, after a3e, 1d

    section P3 core-edu 扩展（5d）
    CoreEduClient gRPC(exams/homework/grades) :crit, a3h, after a3g, 2d
    Mutation(createExam/assignHomework/recordGrade) :a3i, after a3h, 1d
    AuthorizationGuard接入core-edu+Redis缓存 :a3j, after a3h, 1d
    /readyz+core-edu探针 :a3k, after a3j, 1d

    section P4 content+data-ana 扩展（4d）
    ContentClient+DataAnaClient gRPC :a3l, after a3k, 2d
    学情分析Query+DataLoader全量 :a3m, after a3l, 1d
    /readyz+content+data-ana探针 :a3n, after a3m, 1d

    section P5 ai+msg 扩展（8d）
    AiClient+MsgClient gRPC :a3o, after a3n, 2d
    SSE流式透传+notifications Query :a3p, after a3o, 2d
    Kafka consumer(push-gateway落地后) :a3q, after a3p, 2d
    /readyz+ai+msg探针 :a3r, after a3q, 1d
    Should Have补全(OTel/metrics/DataLoader/Redis) :a3s, after a3r, 1d

    section P6 admin+硬化（3d）
    admin命名空间实现 :a3t, after a3s, 1d
    熔断/重试/超时 :a3u, after a3t, 1d
    Nice to Have补全(Zod全量/优雅关闭) :a3v, after a3u, 1d
```

> **总工期**：28d（P2 8d + P3 5d + P4 4d + P5 8d + P6 3d），与 workline.md §1 总时间线对齐（批次1 8d + 批次2 5d + 批次4 8d + P4/P6 合并 7d）。

---

## §3 详细任务

### P2：GraphQL 基础 + DownstreamClient + 越权防御（批次 1，8d）

> 裁决依据：president §3.1 Must Have 13 项（teacher-bff 无 DB，G10/G11 不适用，实际 11 项 + DownstreamClient + admin 预留）

#### 3.1 GraphQL schema 第一版 + admin 预留（2d，P0 阻塞 ai13）

- **负责人**：ai03
- **依赖**：coord 仲裁 ARB-001（已裁决）
- **交付物**：
  - `packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql` — P2 schema（5 Query: dashboard/viewports/me/classes/class + admin 命名空间占位）
  - admin 命名空间预留：schema 中声明 `admin` Query/Mutation 类型骨架（无实际 Resolver），P6 实现
- **验收标准**：5 Query SDL 定义完整 + admin 占位类型声明 + depth ≤ 7 + cost ≤ 1000
- **裁决引用**：ARB-001 §1.2/§1.3 + president §5.1/§2.17

#### 3.2 Yoga endpoint + 5 Query Resolver（3d，P0 阻塞 ai13）

- **负责人**：ai03
- **依赖**：3.1 schema + iam gRPC 50052（ai06 P2.1）
- **交付物**：
  - `POST /graphql` Yoga GraphQL endpoint
  - 5 Query Resolver：dashboard / viewports / me / classes / class
  - Dashboard Resolver P2 实现方式（ISSUE-032）：P2 仅调 iam gRPC，未启用字段返回 null + `extensions.warning = "field_unavailable_in_p2"`
- **验收标准**：5 Query 可执行 + ActionState 信封 + 降级模式 B（success=true + error=null + data 内 degraded 字段）
- **裁决引用**：B1 + ARB-001 §1.4 + president §2.6/§2.8

#### 3.3 DownstreamClient 抽象 + iam gRPC（2d，P0 阻塞 ai04/ai05）

- **负责人**：ai03
- **依赖**：iam gRPC 50052（ai06 P2.1）
- **交付物**：
  - `src/clients/` DownstreamClient 抽象层（B8：BFF 模式 v2 标准抽象，3 个 BFF 统一使用，回写 teacher-bff）
  - IamClient gRPC 实现：`@grpc/grpc-js` + `@bufbuild/protobuf`，调 iam:50052
  - gRPC interceptor：注入 trace context（traceparent）+ x-user-id metadata + metrics
- **验收标准**：IamClient gRPC 调 iam GetUserInfo/GetViewports/GetEffectiveAccess 成功
- **裁决引用**：B2（首次实现即 gRPC）+ B8（DownstreamClient 抽象）

#### 3.4 AuthorizationGuard 越权防御（1d，P0）

- **负责人**：ai03
- **依赖**：3.2 Resolver
- **交付物**：
  - `src/middleware/authorization.guard.ts` — AuthorizationGuard 接口（`canAccessClass(userId, classId): Promise<boolean>`）
  - P2 内部实现：DEV_MODE 放行 + 生产拒绝（保守策略）
  - 错误码：`BFF_TEACHER_FORBIDDEN_RESOURCE`（teacherId 与资源无归属）+ `BFF_TEACHER_IDENTITY_MISMATCH`（JWT teacherId 与 body 不一致）
- **验收标准**：Guard 接口定义 + DEV_MODE 放行 + 生产拒绝 + 2 个越权错误码
- **裁决引用**：B4 + president §2.7（错误码语义）+ §2.9（越权防御 P2 实现）

#### 3.5 ActionState 信封 + 降级模式 B（1d）

- **负责人**：ai03
- **依赖**：3.2 Resolver
- **交付物**：GlobalErrorFilter + ActionState 信封（success/errors/data）+ 降级模式 B
- **验收标准**：GraphQL errors 数组扩展 ActionState 字段，`extensions.code = BFF_TEACHER_*`
- **裁决引用**：G8 + president §2.6

#### 3.6 /readyz 探针注册表 + iam 探针（1d）

- **负责人**：ai03
- **依赖**：3.3 IamClient
- **交付物**：
  - `src/shared/health/readiness.probe.ts` — DownstreamHealthCheck 注册表模式
  - P2 探针：Redis + iam gRPC 50052（teacher-bff 无 DB，2 项）
- **验收标准**：/readyz 返回 2 项检查结果 + 必需依赖失败返回 503
- **裁决引用**：G2 + president §2.4（探针按阶段扩展）

#### 3.7 错误码迁移 + 回写 02 文档（1d）

- **负责人**：ai03
- **依赖**：3.2-3.6
- **交付物**：
  - `application-error.ts` 全量迁移 `TEACHER_BFF_*` → `BFF_TEACHER_*`
  - 回写 02 文档：4 处 `GetTeacherDashboardStats` → `GetTeacherDashboard`（ISSUE-035）+ B1/B2/B4/B8 裁决对齐
- **验收标准**：源码零 `TEACHER_BFF_*` + 02 文档与裁决一致
- **裁决引用**：B5 + G14 + president §3.4 回写义务

#### P2 横切项（贯穿 3.1-3.7）

| 项 | 状态 | 说明 |
| -- | ---- | ---- |
| pino 结构化日志（G4） | ✅ 已具备 | logger.ts |
| /healthz liveness（G3） | ✅ 已具备 | health.controller.ts |
| GlobalErrorFilter（G8） | ✅ 已具备 | global-error.filter.ts |
| ESM import .js 后缀（G12） | ✅ 已具备 | 源码已用 .js |
| import type（G13） | ✅ 已具备 | 源码已用 import type |
| OTel tracer（G6） | ⚠️ Should Have | P2 可降级为 logger-only，P5 补全 |
| /metrics 业务指标（G5） | ⚠️ Should Have | P2 仅暴露 process metrics |
| DataLoader（B1） | ⚠️ Should Have | P2 可先用普通 resolver，P4 全量接入 |
| Redis 5-30s 短缓存（B6） | ⚠️ Should Have | P3 接入聚合缓存 |
| Zod 全量验证（G7） | ⚠️ Nice to Have | P2 先校验核心 Query，P6 全量 |
| 优雅关闭 SIGTERM（G9） | ⚠️ Nice to Have | P2 已有基础，P6 补全关闭顺序 |

**P2 退出标准**：POST /graphql 可用 + 5 Query Resolver + DownstreamClient + AuthorizationGuard + ActionState 信封 + /readyz 2 项探针 + 错误码 BFF_TEACHER_* + admin 命名空间预留。

---

### P3：core-edu gRPC 扩展（批次 2，5d）

> 裁决依据：§2.3 跨阶段扩展例外（新增下游 gRPC 调用 + AuthorizationGuard 内部实现替换 + /readyz 探针扩展）

#### 3.8 CoreEduClient gRPC（2d，P0）

- **负责人**：ai03
- **依赖**：core-edu gRPC 50053（ai08 P3）
- **交付物**：
  - CoreEduClient gRPC 实现：ExamService / HomeworkService / GradeService
  - GraphQL Query 扩展：exams(classId) / homework(classId) / grades(examId)
  - Dashboard Resolver 扩展：null 字段替换为 core-edu 真实数据
- **验收标准**：3 个 Query 返回 core-edu 数据 + Dashboard null 字段消除
- **裁决引用**：§2.3 跨阶段扩展例外 + §2.8 Dashboard Resolver 扩展

#### 3.9 Mutation 透传（1d）

- **交付物**：createExam / assignHomework / recordGrade Mutation（透传 core-edu gRPC）
- **验收标准**：3 个 Mutation 可执行 + 返回 ActionState 信封

#### 3.10 AuthorizationGuard 接入 core-edu + Redis 缓存（1d）

- **交付物**：
  - AuthorizationGuard 内部实现替换：DEV_MODE 放行 → 真实 gRPC 校验
  - Redis 缓存：`GetClassesByTeacher` 结果缓存（key: `authz:teacher:{teacherId}:classes`，TTL 5min）
- **验收标准**：生产环境越权防御生效 + Redis 缓存命中
- **裁决引用**：president §2.9（P3 接入 core-edu 后替换 Guard 实现）

#### 3.11 /readyz + core-edu 探针（1d）

- **交付物**：/readyz 探针注册表扩展 core-edu gRPC 50053 探针（3 项：Redis + iam + core-edu）
- **验收标准**：/readyz 返回 3 项检查结果

**P3 退出标准**：core-edu gRPC 3 Query + 3 Mutation + AuthorizationGuard 生产生效 + Redis 缓存 + /readyz 3 项探针。

---

### P4：content + data-ana gRPC 扩展（4d）

> 裁决依据：§2.3 跨阶段扩展例外

#### 3.12 ContentClient + DataAnaClient gRPC（2d）

- **依赖**：content gRPC 50054（ai09 P4）+ data-ana gRPC 50055（ai11 P4）
- **交付物**：
  - ContentClient gRPC：KnowledgeGraphService（GetPrerequisites / GetLearningPath）
  - DataAnaClient gRPC：AnalyticsService（GetClassPerformance / GetStudentWeakness / GetLearningTrend / GetTeacherDashboard）
  - GraphQL Query 扩展：knowledgePath / classPerformance / studentWeakness / learningTrend / teacherDashboard
- **验收标准**：5 个 Query 返回真实数据

#### 3.13 DataLoader 全量接入（1d）

- **交付物**：DataLoader 覆盖全部 N+1 风险点（UserLoader / ClassLoader / ExamLoader / HomeworkLoader / GradeLoader）
- **验收标准**：DataLoader per-request 实例 + 批量化窗口 16ms
- **裁决引用**：B1 Should Have → P4 全量接入

#### 3.14 /readyz + content + data-ana 探针（1d）

- **交付物**：/readyz 探针扩展 content + data-ana（5 项：Redis + iam + core-edu + content + data-ana）

**P4 退出标准**：content + data-ana gRPC + 5 Query + DataLoader 全量 + /readyz 5 项探针。

---

### P5：ai + msg gRPC 扩展 + SSE + Kafka（批次 4，8d）

> 裁决依据：B7（P5 push-gateway 落地后再订阅 Kafka）

#### 3.15 AiClient + MsgClient gRPC（2d）

- **依赖**：ai gRPC 50057（ai12 P5）+ msg gRPC 50056（ai10 P5）
- **交付物**：
  - AiClient gRPC：AiService（Chat / StreamChat streaming / GenerateQuestion / OptimizeExpression）
  - MsgClient gRPC：NotificationService（ListNotifications / SearchNotifications / MarkAsRead）
  - GraphQL Query 扩展：notifications + Mutation：generateQuestion / markNotificationAsRead

#### 3.16 SSE 流式透传（2d）

- **交付物**：`GET /ai/chat/stream` SSE 端点（ai.StreamChat gRPC stream → BFF → 前端 EventSource）
- **验收标准**：SSE 三层透传端到端通 + 背压处理 + 超时取消
- **裁决引用**：02 文档 §10

#### 3.17 Kafka consumer（2d，push-gateway 落地后）

- **交付物**：
  - Kafka consumer 订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`，精确失效 Redis 权限缓存
  - 幂等性：基于 event_id 去重（Redis SETNX）
- **验收标准**：权限变更秒级缓存失效 + 幂等消费
- **裁决引用**：B7（P5 push-gateway 落地后再订阅）

#### 3.18 /readyz + ai + msg 探针 + Should Have 补全（2d）

- **交付物**：
  - /readyz 探针扩展 ai + msg（7 项：Redis + iam + core-edu + content + data-ana + ai + msg）
  - Should Have 补全：OTel tracer 全链路 + /metrics 业务指标 + Redis 聚合缓存 5-30s
- **验收标准**：/readyz 7 项 + OTel 全链路 trace + 缓存命中率 ≥ 60%

**P5 退出标准**：ai + msg gRPC + SSE 流式 + notifications Query + Kafka consumer + /readyz 7 项探针 + Should Have 补全。

---

### P6：admin 命名空间 + 硬化（3d）

> 裁决依据：president §5.1（admin-portal 复用 teacher-bff）+ P6 硬化

#### 3.19 admin 命名空间实现（1d）

- **依赖**：admin-portal（ai16 P6）
- **交付物**：
  - admin schema 命名空间 Resolver 实现（P2 预留的占位类型填充实际 Resolver）
  - admin Query/Mutation：用户管理 / 角色权限管理 / 学校设置 / 组织管理 / 审计日志查询
- **验收标准**：admin namespace 可内省 + admin-portal 可消费
- **裁决引用**：president §5.1（admin-portal 复用 teacher-bff GraphQL endpoint）

#### 3.20 熔断 / 重试 / 超时（1d）

- **交付物**：
  - Circuit Breaker（opossum，per-downstream-service）
  - Retry（gRPC interceptor，仅幂等 RPC，指数退避）
  - Timeout（per-RPC 3s，聚合总超时 5s）
- **验收标准**：熔断/重试/超时生效 + 降级策略覆盖

#### 3.21 Nice to Have 补全（1d）

- **交付物**：Zod 全量验证 + 优雅关闭顺序（HTTP→Redis→gRPC→Kafka→Tracer）+ 测试覆盖率 ≥ 80%
- **验收标准**：Zod 全 Controller 覆盖 + SIGTERM 顺序关闭 + Vitest 覆盖率 ≥ 80%

**P6 退出标准**：admin namespace 实现 + 熔断/重试/超时 + Zod 全量 + 测试 ≥ 80% + SLO 99.9%。

---

## §4 依赖与就绪信号

### 4.1 我依赖的上游就绪信号

| 上游 | 就绪信号 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| iam（ai06） | gRPC 50052 + 8 RPC（GetUserInfo/GetViewports/GetEffectivePermissions/GetEffectiveAccess/Logout/GetPublicKey/BatchGetUsers/GetChildrenByParent） | P2 | ⏳ |
| core-edu（ai08） | gRPC 50053 + ExamService/HomeworkService/GradeService | P3 | ⏳ |
| content（ai09） | gRPC 50054 + KnowledgeGraphService | P4 | ⏳ |
| data-ana（ai11） | gRPC 50055 + AnalyticsService（含 GetTeacherDashboard，ISSUE-027 补全） | P4 | ⏳ |
| ai（ai12） | gRPC 50057 + AiService（含 StreamChat） | P5 | ⏳ |
| msg（ai10） | gRPC 50056 + NotificationService | P5 | ⏳ |
| push-gateway（ai02） | /internal/push 落地（B7 Kafka 订阅前提） | P5 | ⏳ |
| admin-portal（ai16） | admin schema 需求确认 | P6 | ⏳ |

### 4.2 我的就绪信号（供下游消费）

| 阶段 | 就绪信号 | 消费方 |
| ---- | -------- | ------ |
| P2 | POST /graphql 可用 + 5 Query + admin 预留 | teacher-portal（ai13） |
| P2 | DownstreamClient 抽象（B8 回写） | student-bff（ai04）/ parent-bff（ai05） |
| P3 | exams/homework/grades Query + Mutation | teacher-portal（ai13） |
| P4 | 学情分析 Query + DataLoader | teacher-portal（ai13） |
| P5 | SSE 流式 + notifications Query | teacher-portal（ai13） |
| P6 | admin namespace 可用 | admin-portal（ai16） |

---

## §5 跨阶段扩展例外验收清单

> 裁决依据：president §2.3（ISSUE-020）。每次跨阶段扩展时对照检查。

- [ ] 扩展时更新 02-architecture-design.md 下游调用矩阵
- [ ] 扩展时更新 packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql
- [ ] 扩展时运行 `pnpm run arch:scan` 更新 arch.db
- [ ] 未修改已有 RPC 调用签名或返回类型
- [ ] 未删除已实现的 RPC 调用
- [ ] 未修改 GraphQL schema 已有字段类型（仅新增字段）
- [ ] 未修改 /readyz 已有探针检查项（仅新增）
