# parent-bff 对接契约

> 负责人：ai05
> 关联：[matrix.md](../matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[02-architecture-design.md](../../../services/parent-bff/docs/02-architecture-design.md)
> 修订：2026-07-10 ai05 复审（修正仲裁引用、proto 包名、契约缺口标注）

---

## §0 契约基线说明

| 维度 | 内容 |
| --- | --- |
| proto 包名前缀 | 所有 proto 包名统一为 `next_edu_cloud.<domain>.v1`（如 `next_edu_cloud.iam.v1`），引用 proto message 时须带完整包名 |
| GraphQL schema 文件 | `packages/shared-ts/contracts/graphql/parent-bff.graphql`（SDL-first 集中管理，对齐 coord ARB-001 模式） |
| 错误码前缀 | `BFF_PARENT_`（C1 仲裁，BFF 在前） |
| 端口 | HTTP 3010，不暴露 gRPC（C2 仲裁） |
| API 风格 | GraphQL Yoga（U3 仲裁，P4 直接 GraphQL，不走 REST 过渡） |
| 权限校验 | BFF 豁免 @RequirePermission（U4 仲裁），仅校验 x-user-id + ChildGuard 越权防御 |

---

## §1 我提供什么（对外接口）

### 1.1 gRPC 接口

无对外 gRPC。parent-bff 是 GraphQL 聚合层，对上游仅暴露 HTTP/GraphQL（C2 仲裁）。

### 1.2 HTTP 端点

| Method | Path | 用途 | 认证 | 阶段 |
| --- | --- | --- | --- | --- |
| POST | /graphql | 家长 BFF GraphQL 端点（U3 仲裁） | JWT 必需 + parent 角色 | P4 |
| GET | /graphql | GraphQL Playground（仅开发环境） | 开发环境公开 | P4 |
| GET | /healthz | 健康检查（liveness，直接返回 ok） | 公开 | P4 |
| GET | /readyz | 就绪检查（readiness，含下游 gRPC 连通性，02 §9 #7） | 公开 | P4 |
| GET | /metrics | Prometheus 指标（parent_bff_*） | 公开 | P4 |

> 网关路径：`/api/v1/parent/*` → api-gateway 剥离 `/api/v1` 后代理到 parent-bff:3010
> **不实现 REST 业务端点**（02 §4.1，仅保留 /healthz /readyz /metrics 基础端点）

### 1.3 GraphQL schema

**schema 文件**：`packages/shared-ts/contracts/graphql/parent-bff.graphql`（SDL-first）
**完整 schema 定义**：见 [02-architecture-design.md §4.2](../../../services/parent-bff/docs/02-architecture-design.md) GraphQL Schema 完整定义

核心 Query / Mutation 域（按阶段分级）：

| 类型 | 字段 | 聚合下游 | ChildGuard | 阶段 |
| --- | --- | --- | --- | --- |
| Query | dashboard | iam + core-edu | 否（聚合所有孩子） | P4 |
| Query | viewports | iam | 否 | P4 |
| Query | me | iam | 否 | P4 |
| Query | children | iam | 否 | P4 |
| Query | child | iam + core-edu | 是 | P4 |
| Query | childGrades | core-edu | 是 | P4 |
| Query | childHomework | core-edu | 是 | P4 |
| Query | childExams | core-edu | 是 | P4 |
| Query | childAnalytics | data-ana | 是 | P4 |
| Query | notifications | msg | 否 | P5 |
| Query | notificationPreferences | msg | 否 | P5 |
| Mutation | selectChild | （BFF 内部审计） | 是 | P4 |
| Mutation | markNotificationRead | msg | 否 | P5 |
| Mutation | updateNotificationPreferences | msg | 否 | P5 |

**统一响应信封**：GraphQL 规范（data/errors），错误扩展字段携带 `extensions.code = "BFF_PARENT_*"`（02 §4.5）

### 1.4 Kafka 事件发布

无。parent-bff 不发布领域事件（BFF 聚合层无业务状态变更，02 §5.6）。

### 1.5 错误码前缀

`BFF_PARENT_`（C1 仲裁，BFF 在前；非 PARENT_BFF_）

错误码清单（完整见 02 §6.2）：

| 错误码 | HTTP | 触发条件 |
| --- | --- | --- |
| BFF_PARENT_VALIDATION_ERROR | 400 | Zod / GraphQL input 校验失败 |
| BFF_PARENT_UNAUTHORIZED | 401 | 缺失 x-user-id 头 |
| BFF_PARENT_CHILD_NOT_BOUND | 403 | ChildGuard 拦截：childId 不在家长绑定列表 |
| BFF_PARENT_NOT_FOUND | 404 | 资源不存在 |
| BFF_PARENT_BAD_GATEWAY | 502 | 下游服务返回非 ok 或 gRPC rejected |
| BFF_PARENT_GATEWAY_TIMEOUT | 504 | 下游调用超时 |
| BFF_PARENT_SERVICE_UNAVAILABLE | 503 | 熔断器开启（P6） |
| BFF_PARENT_INTERNAL_ERROR | 500 | 未捕获异常 |

**下游错误透传**（C3 仲裁：core-edu 统一 CORE_EDU_*）：

| 下游服务 | 错误码前缀 | 示例 |
| --- | --- | --- |
| iam | IAM_ | IAM_USER_NOT_FOUND |
| core-edu | CORE_EDU_ | CORE_EDU_GRADE_NOT_FOUND |
| data-ana | DATA_ANA_ | DATA_ANA_ANALYTICS_NOT_READY |
| msg | MSG_ | MSG_NOTIFICATION_NOT_FOUND |

---

## §2 我消费什么（依赖上游）

### 2.1 gRPC 调用（同步）

> proto 包名均为 `next_edu_cloud.<domain>.v1`
> **状态标注**：✅ 已有 = proto 已定义；❌ 待补 = proto 缺失；⚠️ 待仲裁 = ai05 提请 coord 仲裁中

| 被调用方 | Service.RPC | proto message 包名 | 用途 | mock 策略 | 状态 |
| --- | --- | --- | --- | --- | --- |
| iam (ai06) | IamService.GetUserInfo | next_edu_cloud.iam.v1.UserInfo | 获取当前家长信息 | 返回固定 UserInfo（parent 角色） | ✅ 已有 |
| iam (ai06) | IamService.GetViewports | next_edu_cloud.iam.v1.Viewport[] | 家长导航菜单 | 返回固定视口列表 | ❌ 待 ai06 补（coord-cross-review #1） |
| iam (ai06) | IamService.GetEffectivePermissions | next_edu_cloud.iam.v1.EffectivePermissions | 权限校验 | 返回家长权限集 | ❌ 待 ai06 补 |
| iam (ai06) | **IamService.GetChildrenByParent** | next_edu_cloud.iam.v1.Child[] | 查询关联孩子列表（**P0 核心依赖**） | 返回固定 2 个 ChildInfo | ❌ 待 ai06 补（**I6 裁决**，P0 阻塞） |
| core-edu (ai08) | GradeService.ListGradesByStudent | next_edu_cloud.core_edu.v1.Grade[] | 孩子成绩 | 返回固定 5 个 Grade | ✅ 已有 |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | next_edu_cloud.core_edu.v1.Homework[] | 孩子作业 | 返回固定 3 个 Homework | ✅ 已有 |
| core-edu (ai08) | ExamService.ListExamsByClass | next_edu_cloud.core_edu.v1.Exam[] | 孩子考试 | 返回固定 3 个 Exam | ✅ 已有 |
| core-edu (ai08) | **ClassService.GetClass** | next_edu_cloud.core_edu.v1.Class | 孩子班级信息 | 返回固定 ClassInfo | ❌ 待补（**ISSUE-008**：proto 缺 ClassService，待 coord 仲裁归属） |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | next_edu_cloud.core_edu.v1.Attendance[] | 孩子考勤（P5+） | 返回固定 10 条 | ❌ 待 ai08 补（coord-cross-review #5，P3 补全） |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | next_edu_cloud.analytics.v1.StudentWeakness | 孩子薄弱点 | 返回固定 3 个 weak_points | ✅ 已有 |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | next_edu_cloud.analytics.v1.LearningTrend | 孩子学习趋势 | 返回固定趋势数据 | ✅ 已有 |
| data-ana (ai11) | AnalyticsService.GetClassPerformance | next_edu_cloud.analytics.v1.ClassPerformance | 班级学情对比 | 返回固定班级数据 | ✅ 已有 |
| data-ana (ai11) | **AnalyticsService.GetParentDashboard** | — | 家长仪表盘聚合（多子女防 N+1） | 返回固定仪表盘 | ⚠️ 待仲裁（02 §14 #3，ai05 提请，proto 未定义） |
| msg (ai10) | NotificationService.ListNotifications | next_edu_cloud.msg.v1.Notification[] | 家长通知 | 返回固定 10 条通知 | ✅ 已有 |
| msg (ai10) | NotificationService.MarkAsRead | next_edu_cloud.msg.v1.Empty | 标记已读 | 返回 success | ✅ 已有 |
| msg (ai10) | **NotificationPreferenceService.*** | — | 通知偏好配置 | 返回固定偏好 | ❌ 待 ai10 补（proto + 实现均缺失，P5） |

**类型映射注意**（ISSUE-008）：

- `core_edu.v1.Grade.score` 是 `string` 类型，GraphQL `Grade.score` 是 `Float!`，BFF response-mapper 需做 `Number.parseFloat(score)` 转换，转换失败抛 BFF_PARENT_BAD_GATEWAY
- `msg.v1.Notification.is_read` 映射为 GraphQL `Notification.read`（字段名重命名）
- `msg.v1.Notification` 无 `child_id` 字段（ISSUE-007），GraphQL `Notification.childId` 暂从 notification.type+content 解析或置 null，待 ai10 补 proto 字段

### 2.2 Kafka 事件订阅（异步，P5 可选）

> P4 阶段不订阅事件（02 §5.1）。P5 阶段可选订阅以下 topic 用于实时推送 + 缓存失效（02 §5.2）。

| Topic | 事件 | 发布方 | 消费动作 | 幂等性 | 阶段 |
| --- | --- | --- | --- | --- | --- |
| edu.notification.sent | 通知发送 | msg | 推送给家长（push-gateway HTTP） | event_id SETNX | P5 |
| edu.notification.read | 通知已读 | msg | 失效 bff:parent:notifications:* | event_id SETNX | P5 |
| edu.notification.recalled | 通知撤回 | msg | 失效通知缓存 + 推送撤回 | event_id SETNX | P5 |
| edu.notification.failed | 通知失败 | msg | 记录日志 + 告警 | event_id SETNX | P5 |
| edu.teaching.grade.recorded | 成绩录入 | core-edu | 失效 bff:parent:grades:{childId} + 推送 | event_id SETNX | P5 |
| edu.teaching.homework.graded | 作业批改 | core-edu | 失效 bff:parent:homework:{childId} + 推送 | event_id SETNX | P5 |
| edu.teaching.exam.published | 考试发布 | core-edu | 失效 bff:parent:exams:{childId} + 推送 | event_id SETNX | P5 |

**消费者组**：`parent-bff-event-subscriber`（02 §5.5）
**DLQ**：`edu.parent-bff.dlq`
**提交策略**：manual commit

### 2.3 HTTP 调用（非 gRPC）

| 被调用方 | Method | Path | 用途 | 认证 | 阶段 |
| --- | --- | --- | --- | --- | --- |
| push-gateway (ai02) | POST | /internal/push | 推送给在线家长（**U2 仲裁**：push-gateway 豁免 gRPC） | X-Internal-Key | P5 |

> push-gateway HTTP 调用在 P5 阶段启用，P4 不涉及。

---

## §3 就绪信号

### 3.1 我依赖的上游就绪标志

- [ ] iam gRPC 50052 启用（ai06）—— **核心依赖 GetChildrenByParent（I6 裁决，P0 阻塞）**
- [ ] iam 补 GetViewports / GetEffectivePermissions RPC（coord-cross-review #1）
- [ ] iam_student_guardians 表已建立（I6 裁决）
- [ ] core-edu gRPC 50053 启用（ai08）
- [ ] core-edu ClassService.GetClass proto 补全（ISSUE-008 待仲裁）
- [ ] data-ana gRPC 50055 启用（ai11）
- [ ] msg gRPC 50056 启用（ai10）—— P5
- [ ] msg.proto Notification 补 child_id 字段（ISSUE-007 待仲裁）—— P5
- [ ] msg NotificationPreferenceService 补全 —— P5
- [ ] push-gateway /internal/push 启用（ai02）—— P5
- [ ] api-gateway /parent 路由注册（ai01）
- [ ] Kafka topic 已创建（C5 仲裁）—— P5
- [ ] Redis 已部署且网络可达
- [ ] buf.gen.yaml 补 gRPC TS 插件（coord）

### 3.2 我的就绪标志（供下游消费）

- [ ] parent-bff GraphQL :3010 启用（/healthz 返回 200）
- [ ] /readyz 返回 200（含 4 个下游 gRPC 连通性检查：iam + core-edu + data-ana + Redis，02 §9 #7）
- [ ] GraphQL schema 可内省（POST /graphql 返回 schema）
- [ ] 核心 Query 可执行：dashboard / children / childGrades / childAnalytics
- [ ] 核心 Mutation 可执行：selectChild（P4）/ markNotificationRead（P5）
- [ ] DataScope=CHILDREN 校验生效（家长只能查自己孩子的数据，ChildGuard 基于 iam.GetChildrenByParent 返回的列表校验）
- [ ] /metrics 暴露 parent_bff_* 指标

---

## §4 Mock 策略

### 4.1 我提供的 mock（供下游 parent-portal ai15）

在 parent-bff 真实就绪前，为 parent-portal 提供以下 mock：

- **GraphQL mock**：使用 MSW 拦截 POST /graphql 或 Apollo Server mockProviders
  - `dashboard` 返回固定家长（id="parent-001", name="王家长", roles=["parent"]）+ 2 个孩子 + unreadNotifications=3
  - `children` 返回固定 2 个孩子（id="student-001" 李同学 + id="student-002" 李妹妹）
  - `childGrades` 返回固定 5 个成绩（含 score 字段，Float 类型）
  - `childAnalytics` 返回固定学情（child_avg_score=85.0, class_rank=5）
  - `childHomework` 返回固定 3 个作业
  - `childExams` 返回固定 3 个考试
  - `notifications`（P5）返回固定 10 条通知（含 childId 字段）

### 4.2 我消费的 mock（上游未就绪时）

在真实上游就绪前，parent-bff 使用以下 mock（详见 §2.1 mock 策略列）：

- **iam mock**：固定 UserInfo + 家长权限 + 固定视口 + 固定 2 个 ChildInfo（家长-学生关联核心数据）
- **core-edu mock**：固定孩子成绩/作业/考试/班级信息
- **data-ana mock**：固定孩子薄弱点/趋势/班级对比
- **msg mock**（P5）：固定通知列表（含 childId）+ MarkAsRead success + 固定偏好配置
- **push-gateway mock**（P5）：fetch mock 返回 success
- **Redis**：Testcontainers 真实 Redis 实例（不用 mock）
- **Kafka**（P5）：kafkajs mock + jest.mock

> **关键约束**：iam.GetChildrenByParent 的 mock 必须返回与 core-edu mock 数据一致的 student_id（student-001 + student-002），否则 ChildGuard 越权校验会失败。parent-bff 启动时校验 myChildren 返回的 user_id 与下游查询的 student_id 一致性。

---

## §5 跨模块契约冲突跟踪

> 以下为 ai05 复审发现的跨模块契约问题，详见 [objections/parent-bff_issue.md](../objections/parent-bff_issue.md)

| ISSUE | 问题 | 影响 | 状态 |
| --- | --- | --- | --- |
| ISSUE-003 | contract.md 仲裁引用编号错误（I3 → I6） | 引用勘误，已修正本文档 | 待 coord 确认 |
| ISSUE-004 | 004 §4 + matrix.md §1 未同步 C6 仲裁（缺 DataAna + Msg） | 新 AI 误判依赖 | 待 coord 仲裁 |
| ISSUE-005 | parent-portal 01 文档仍按 REST 消费 parent-bff | 跨模块契约冲突 | 待 coord 仲裁 |
| ISSUE-006 | proto 包名引用缺 next_edu_cloud 前缀 | gRPC 代码生成错误，已修正本文档 | 待 coord 确认 |
| ISSUE-007 | msg.proto Notification 缺 child_id 字段 | 按孩子过滤通知失效 | 待 coord 仲裁 |
| ISSUE-008 | core_edu.proto 缺 ClassService + Grade.score 类型不一致 | 班级信息查询 + 类型转换 | 待 coord 仲裁 |
