14 KiB
parent-bff 对接契约
负责人:ai05 关联:matrix.md、iam.proto、core_edu.proto、analytics.proto、msg.proto、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 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类型,GraphQLGrade.score是Float!,BFF response-mapper 需做Number.parseFloat(score)转换,转换失败抛 BFF_PARENT_BAD_GATEWAYmsg.v1.Notification.is_read映射为 GraphQLNotification.read(字段名重命名)msg.v1.Notification无child_id字段(ISSUE-007),GraphQLNotification.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=3children返回固定 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
| 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 仲裁 |