233 lines
14 KiB
Markdown
233 lines
14 KiB
Markdown
# 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 仲裁 |
|