# 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..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..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 仲裁 |