Files
Edu/docs/architecture/issues/contracts/student-bff_contract.md

600 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# student-bff 对接契约
> 负责人ai04
> 阶段P3批次 2 启动)
> 版本v2对齐 coord-final-decisions B1-B8 + president-final-rulings §2.2/2.3/2.4/2.6/2.7/2.8/2.9
> 日期2026-07-10
> 关联文档:
>
> - [matrix.md](../matrix.md)
> - [coord-final-decisions.md](../../coord-final-decisions.md) §2 BFF 专项裁决 B1-B8
> - [president-final-rulings.md](../../president-final-rulings.md) §2.2 GraphQL schema 仲裁机制
> - [iam.proto](../../../../packages/shared-proto/proto/iam.proto)
> - [core_edu.proto](../../../../packages/shared-proto/proto/core_edu.proto)
> - [content.proto](../../../../packages/shared-proto/proto/content.proto)
> - [analytics.proto](../../../../packages/shared-proto/proto/analytics.proto)
> - [msg.proto](../../../../packages/shared-proto/proto/msg.proto)
---
## §0 裁决对齐声明
本文件已对齐以下裁决(无遗漏):
| 裁决编号 | 内容 | 本文件章节 |
| -------- | ------------------------------------------ | -------------- |
| B1 | P2 起直接 GraphQLGraphQL Yoga + DataLoader | §1.2 / §1.3 |
| B2 | 首次实现即 gRPC 调用下游 | §2.1 / §2.4 |
| B3 | BFF 豁免 @RequirePermission | §1.6 / §1.7 |
| B4 | 全部 BFF 强制自我越权防御 | §1.7 |
| B5 | 错误码前缀 `BFF_STUDENT_` | §1.6 |
| B6 | Redis 5-30s 短缓存 | §7 |
| B7 | P2-P4 不订阅 KafkaP5 后再订阅 | §1.5 / §2.2 |
| B8 | DownstreamClient 抽象回写 teacher-bff | §2.4 |
| G14 | 错误码前缀服务名大写 | §1.6 |
| F4 | i18n key `error.bffStudent.<code_snake>` | §1.6 |
| F7 | 权限点命名 `<RESOURCE>_<ACTION>[_<SCOPE>]` | §1.3.4 |
| F9 | 前端用 urql/apollo 消费 GraphQL | §1.2 |
| §2.2 | GraphQL schema 仲裁机制 | §1.3.2 / §1.3.3 |
| §2.3 | BFF 跨阶段扩展例外 | §3 |
| §2.4 | /readyz 探针按阶段扩展 | §4 |
| §2.6 | 降级模式方案 Bdata 内 degraded 字段) | §1.4.2 |
| §2.7 | BFF 错误码语义区分3 类) | §1.6 / §1.7.3 |
| §2.8 | Dashboard Query Resolver P2 实现方式 | §1.3.5 |
| §2.9 | 越权防御 P2 实现方式DEV_MODE 放行) | §1.7.4 |
| §5.1 | admin-portal 复用 teacher-bff不占 student-bff 命名空间 | §1.3.6 |
---
## §1 我提供什么(对外接口)
### 1.1 服务基础信息
| 项目 | 值 |
| ------------- | --------------------------------------------- |
| 服务名 | student-bff |
| 服务类型 | BFF 聚合层(无 DB无 Outbox |
| HTTP 端口 | 3009 |
| gRPC 端口 | 不暴露BFF 仅对下游走 gRPC不对外提供 gRPC |
| 路由前缀 | `/student/*`api-gateway 透传) |
| 部署目录 | `services/student-bff/` |
| 角色要求 | `student`JWT 必需 + student 角色) |
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 | 实现 |
| ------ | ---------- | ------------------------------------------ | ---------------------------- | ------- |
| POST | /graphql | 学生 BFF GraphQL 端点B1 裁决GraphQL Yoga | JWT 必需 + student 角色 | P3 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 | P3 |
| GET | /healthz | 健康检查liveness | 公开 | P3 |
| GET | /readyz | 就绪检查readiness按阶段扩展探针 | 公开 | P3 |
| GET | /metrics | Prometheus 指标端点 | 公开(生产限内网) | P3 |
> **B1 裁决**P2 起直接 GraphQLGraphQL Yoga + DataLoader禁止 REST → GraphQL 渐进式过渡。
> **B2 裁决**:对下游通信首次实现即 gRPC禁止 HTTP fetch → gRPC 渐进式过渡。
> **F9 裁决**:前端 student-portal 用 urql/apollo 消费 GraphQL。
### 1.3 GraphQL schema核心契约
#### 1.3.1 schema 存放路径
**强制路径**`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
> 依据 president-final-rulings §2.2.13 个 BFF schema 统一存放于 `packages/shared-ts/contracts/graphql/`,由各 BFF 负责 AI 起草、coord 仲裁。student-bff schema 不放在 `services/student-bff/src/` 下,避免前端 AI 难以发现。
#### 1.3.2 起草与仲裁流程
依据 president-final-rulings §2.2.6
1. **起草方**ai04 在批次 1 等待期起草 student-bff GraphQL schema 草案
2. **仲裁方**coord 在**批次 2 启动前**仲裁第一版 schema
3. **消费方**ai14student-portal基于仲裁版 schema 消费
4. **变更流程**
- schema 变更需 PR + ai04BFF+ ai14前端双方 review
- 重大变更(删除字段 / 修改类型)需 coord 仲裁
- 新增字段允许,无需 coord 仲裁,但需通知 ai14
5. **版本管理**SDL-first`schema.graphql` 文件),配合 `graphql-codegen` 生成 TS 类型
#### 1.3.3 schema 设计规范
依据 president-final-rulings §2.2.5
| 规范项 | 规则 |
| -------------- | ----------------------------------------------------------------- |
| 命名风格 | Query/Mutation 用 camelCase`studentDashboard` / `myClasses` |
| 分页规范 | **Relay Cursor Connections**`{ edges, pageInfo, totalCount }` |
| 错误响应 | GraphQL errors 数组 + `extensions.code` + `extensions.traceId` |
| 权限点标注 | 注释形式 `# @permission: DASHBOARD_VIEW`F7 规范) |
| DataScope 标注 | 注释形式 `# @dataScope: OWN`(学生数据隔离 SELF |
| 字段命名 | Type 用 PascalCase字段用 camelCase枚举用 UPPER_SNAKE_CASE |
| 非空与可选 | 必填字段用 `!`,可空字段不标 `!`(避免破坏性变更) |
#### 1.3.4 核心 Query / Mutation 域
> 完整 SDL 定义见 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`ai04 起草coord 仲裁)。
**Query 域**
| Query | 用途 | 聚合下游 RPC | 权限点标注 | DataScope |
| ---------------------- | ------------------------ | ----------------------------------------------------------- | --------------------------- | --------- |
| `currentUser` | 当前学生信息 + 权限 + 视口 | iam.GetUserInfo + GetEffectivePermissions + GetViewports | `# @permission: AUTH_READ` | OWN |
| `myClasses` | 我的班级列表 | core-edu.ClassService.GetClass + ListStudentsByClass | `# @permission: CLASS_READ` | OWN |
| `myExams` | 我的考试列表 | core-edu.ExamService.ListExamsByClass | `# @permission: EXAM_READ` | OWN |
| `myHomework` | 我的作业列表 | core-edu.HomeworkService.ListHomeworkByClass | `# @permission: HOMEWORK_READ` | OWN |
| `myGrades` | 我的成绩列表 | core-edu.GradeService.ListGradesByStudent | `# @permission: GRADE_READ` | OWN |
| `myAttendance` | 我的考勤记录 | core-edu.AttendanceService.ListAttendanceByStudent | `# @permission: ATTENDANCE_READ` | OWN |
| `textbooks` | 教材列表 | content.TextbookService.ListTextbooks | `# @permission: TEXTBOOK_READ` | OWN |
| `chapters` | 章节列表 | content.ChapterService.ListChapters | `# @permission: CHAPTER_READ` | OWN |
| `learningPath` | 学习路径推荐 | content.KnowledgeGraphService.GetLearningPath | `# @permission: LEARNING_PATH_READ` | OWN |
| `studentDashboard` | 学生仪表盘 | data-ana.AnalyticsService.GetStudentDashboard | `# @permission: DASHBOARD_VIEW` | OWN |
| `myWeakness` | 我的薄弱点 | data-ana.AnalyticsService.GetStudentWeakness | `# @permission: WEAKNESS_READ` | OWN |
| `myTrend` | 学习趋势 | data-ana.AnalyticsService.GetLearningTrend | `# @permission: TREND_READ` | OWN |
| `myNotifications` | 我的通知列表 | msg.NotificationService.ListNotifications | `# @permission: NOTIFICATION_READ` | OWN |
| `myNotificationUnreadCount` | 通知未读数 | msg.NotificationService.GetUnreadCount | `# @permission: NOTIFICATION_READ` | OWN |
**Mutation 域**
| Mutation | 用途 | 聚合下游 RPC | 权限点标注 | DataScope |
| ---------------------- | ---------------- | ----------------------------------------- | --------------------------- | --------- |
| `submitHomework` | 提交作业 | core-edu.HomeworkService.SubmitHomework | `# @permission: HOMEWORK_SUBMIT` | OWN |
| `markNotificationAsRead` | 标记通知已读 | msg.NotificationService.MarkAsRead | `# @permission: NOTIFICATION_UPDATE` | OWN |
> **分页**:所有列表 QuerymyClasses / myExams / myHomework / myGrades / myAttendance / textbooks / chapters / myNotifications使用 Relay Cursor Connections 规范,返回 `{ edges, pageInfo, totalCount }`。
> **DataScope=OWN**:学生数据隔离为 SELF所有 Query 透传 `x-user-id` 给下游,下游 Repository 按 DataScope=SELF 过滤。
#### 1.3.5 Dashboard Query Resolver 实现方式§2.8 裁决)
依据 president-final-rulings §2.8
1. **GraphQL schema 设计完整**(含全部 Query/Mutation 字段定义P3 即定型,后续不重构
2. **Resolver 实现**
- **P3 阶段**`studentDashboard` Query Resolver 内部调 iam + core-edu gRPC返回学生基础信息 + 班级列表 + 考试列表 + 作业列表 + 成绩列表
- **P4 扩展**:增加 content教材/章节)+ data-ana仪表盘/薄弱点/趋势)数据源,将 null 字段替换为真实数据
- **P5 扩展**:增加 msg通知未读数+ aiAI 助教入口)数据源
- 未启用的下游字段返回 `null` + `extensions.warning = "field_unavailable_in_p3"`
3. **前端配合**ai14 student-portal 对 null 字段做 UI 降级展示(如"数据加载中"或隐藏模块)
4. **此方案不违反"不分阶段"**schema 即最终方案Resolver 内部数据源扩展属"跨阶段扩展例外"(见 §3
#### 1.3.6 admin-portal 命名空间
依据 president-final-rulings §5.1
- **admin-portal 复用 teacher-bff GraphQL endpoint**,不新建 admin-bff 服务
- **student-bff 不预留 admin schema 命名空间**admin 操作走 teacher-bff 的 `admin.*` 命名空间)
- ai04 无需为 admin-portal 做任何 schema 预留
### 1.4 GraphQL 错误响应格式
#### 1.4.1 GraphQL errors 数组 + ActionState 扩展
依据 president-final-rulings §2.2.3 + G8 裁决:
GraphQL 错误响应遵循标准 errors 数组格式,扩展 ActionState 字段:
```json
{
"errors": [
{
"message": "学生身份验证失败",
"extensions": {
"code": "BFF_STUDENT_UNAUTHORIZED",
"traceId": "abc-123-def-456",
"i18nKey": "error.bffStudent.unauthorized",
"severity": "error"
}
}
],
"data": null
}
```
| 字段 | 类型 | 说明 |
| ------------------- | ------ | ----------------------------------------------- |
| `extensions.code` | string | 错误码BFF_STUDENT_* 前缀,见 §1.6 |
| `extensions.traceId`| string | 全链路追踪 ID由 Gateway 注入 X-Request-Id |
| `extensions.i18nKey`| string | i18n keyF4 规范:`error.bffStudent.<code_snake>` |
| `extensions.severity` | string | `error` / `warning` / `info` |
#### 1.4.2 降级模式(方案 B§2.6 裁决)
依据 president-final-rulings §2.6:当下游服务不可用但需返回部分数据时,采用**方案 B**success=true + error=null + data 内 degraded 字段):
```json
{
"data": {
"studentDashboard": {
"user": { "id": "stu-001", "name": "李同学" },
"classes": [{ "id": "cls-001", "name": "高三1班" }],
"weakness": null,
"degraded": true,
"degradedReason": "data_ana_unavailable",
"degradedFields": ["weakness"]
}
}
}
```
**规则**
1. 降级时 HTTP 200GraphQL `data` 非 null
2. 降级字段返回 `null`,并在父对象内加 `degraded: true` + `degradedReason: string` + `degradedFields: string[]`
3. 前端检查 `data.degraded` 判断降级,对 `degradedFields` 内字段做 UI 降级展示
4. 降级场景示例:
- data-ana gRPC 不可用 → `studentDashboard.weakness` / `myTrend` / `myWeakness` 降级
- content gRPC 不可用 → `textbooks` / `chapters` / `learningPath` 降级
- msg gRPC 不可用 → `myNotifications` / `myNotificationUnreadCount` 降级
### 1.5 Kafka 事件发布
**无**。student-bff 是纯聚合层,不发布 Kafka 事件,不写 Outbox。
### 1.6 错误码前缀与列表
依据 B5 + G14 + F4 + §2.7 裁决:
**错误码前缀**`BFF_STUDENT_`(统一 BFF_ 前缀,服务名大写)
**i18n key 规范**`error.bffStudent.<code_snake>`F4 裁决)
**错误码清单**
| 错误码 | HTTP | 场景 | i18n key |
| ------------------------------------- | ---- | ---------------------------------------------- | --------------------------------------------- |
| `BFF_STUDENT_UNAUTHORIZED` | 401 | x-user-id 缺失或无效 | `error.bffStudent.unauthorized` |
| `BFF_STUDENT_FORBIDDEN_RESOURCE` | 403 | 学生越权访问他人数据(场景 A | `error.bffStudent.forbidden_resource` |
| `BFF_STUDENT_IDENTITY_MISMATCH` | 403 | JWT userId 与请求 body userId 不一致(场景 B | `error.bffStudent.identity_mismatch` |
| `BFF_STUDENT_BAD_GATEWAY` | 502 | 下游 gRPC 调用失败(非业务错误) | `error.bffStudent.bad_gateway` |
| `BFF_STUDENT_UPSTREAM_UNAVAILABLE` | 503 | 下游服务不可用(降级模式触发) | `error.bffStudent.upstream_unavailable` |
| `BFF_STUDENT_AGGREGATION_FAILED` | 500 | 聚合逻辑异常(未知错误) | `error.bffStudent.aggregation_failed` |
| `BFF_STUDENT_VALIDATION_ERROR` | 400 | 输入参数校验失败Zod 校验) | `error.bffStudent.validation_error` |
| `BFF_STUDENT_GRAPHQL_PARSE_ERROR` | 400 | GraphQL 语法解析错误 | `error.bffStudent.graphql_parse_error` |
| `BFF_STUDENT_GRAPHQL_VALIDATION_ERROR`| 400 | GraphQL 字段类型校验错误 | `error.bffStudent.graphql_validation_error` |
| `BFF_STUDENT_RATE_LIMITED` | 429 | 限流触发Gateway 层处理BFF 兜底) | `error.bffStudent.rate_limited` |
> **B3 裁决澄清**BFF 豁免 `@RequirePermission` 指不做"功能权限决策"(如"能否查看仪表盘"),但必须做"数据权限防御"(如"只能看自己的数据"),见 §1.7。
### 1.7 BFF 越权防御契约B4 裁决)
#### 1.7.1 越权防御场景
依据 B4 + §2.7 裁决student-bff 强制自我越权防御:
| 场景 | 描述 | 防御方式 |
| ---- | ---------------------------------------------- | --------------------------------------------------- |
| A | 学生查询他人数据(如 query 传入非自己 userId | AuthorizationGuard 比对 `x-user-id` 与查询参数 |
| B | JWT userId 与请求 body userId 不一致 | Mutation 入参校验,拒绝不一致请求 |
#### 1.7.2 AuthorizationGuard 接口
依据 §2.9 裁决:
```typescript
// services/student-bff/src/middleware/authorization.guard.ts
export interface AuthorizationGuard {
/**
* 校验学生是否有权访问指定资源
* @param currentUserId 从 x-user-id header 获取
* @param resourceUserId 查询参数中的 userId
* @returns true 允许访问false 拒绝
*/
canAccessSelfData(currentUserId: string, resourceUserId: string): Promise<boolean>;
}
```
#### 1.7.3 3 类错误码语义§2.7 裁决)
| 错误码 | HTTP | 场景 | 触发条件 |
| -------------------------------- | ---- | ---------------------------------------------------- | ----------------------------------------- |
| `BFF_STUDENT_UNAUTHORIZED` | 401 | x-user-id 缺失或无效 | header 无 x-user-id 或为空 |
| `BFF_STUDENT_FORBIDDEN_RESOURCE` | 403 | 学生越权访问他人数据(场景 A | canAccessSelfData 返回 false |
| `BFF_STUDENT_IDENTITY_MISMATCH` | 403 | JWT userId 与请求 body userId 不一致(场景 B | Mutation submitHomework 等 body userId 不匹配 |
#### 1.7.4 P3 实现方式§2.9 裁决)
依据 §2.9 裁决:
1. **P3 抽象 AuthorizationGuard 接口**`canAccessSelfData`,见 §1.7.2
2. **P3 内部实现为"DEV_MODE 放行 + 生产拒绝"**(保守策略):
- `DEV_MODE=true`:放行所有请求,仅记录 warn 日志
- `DEV_MODE=false`:严格校验,越权返回 `BFF_STUDENT_FORBIDDEN_RESOURCE`
3. **Redis 缓存**P3 后期接入):
- key: `authz:student:{userId}`
- value: 用户基础信息userId / roles / classIds
- TTL: 5min
- AuthorizationGuard 优先查缓存,缓存未命中调 iam gRPC
4. **此方案不违反"不分阶段"**Guard 接口即最终方案P3→P3 后期仅替换内部实现(属"跨阶段扩展例外",见 §3
**P3 阶段生产环境**Guard 全部拒绝时student-bff P3 端到端验证仅限 DEV_MODE。生产环境 P3 不接入流量(仅 dev 测试P3 后期接入真实校验后正式上线。
---
## §2 我消费什么(依赖上游)
### 2.1 gRPC 调用(同步)
依据 B2 裁决student-bff 对下游全部走 gRPC禁止 HTTP fetch
| 被调用方 | Service.RPC | 用途 | 启用阶段 | mock 策略 |
| --------------- | ----------------------------------------- | ---------------- | -------- | ------------------------------------------- |
| iam (ai06) | IamService.GetUserInfo | 获取当前学生信息 | P3 | iam 就绪前返回固定 UserInfostudent 角色) |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | P3 | iam 就绪前返回学生权限集 |
| iam (ai06) | IamService.GetViewports | 学生导航菜单 | P3 | iam 就绪前返回固定视口列表 |
| iam (ai06) | IamService.GetEffectiveDataScope | DataScope 透传 | P3 | iam 就绪前返回 `SELF` |
| core-edu (ai08) | ClassService.GetClass | 我的班级详情 | P3 | core-edu 就绪前返回固定 ClassInfo |
| core-edu (ai08) | ClassService.ListStudentsByClass | 班级同学名单 | P3 | core-edu 就绪前返回固定 30 个 StudentInfo |
| core-edu (ai08) | ExamService.ListExamsByClass | 我的考试 | P3 | core-edu 就绪前返回固定 2 个 Exam |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | 我的作业 | P3 | core-edu 就绪前返回固定 3 个 Homework |
| core-edu (ai08) | HomeworkService.SubmitHomework | 提交作业 | P3 | core-edu 就绪前返回 success=true |
| core-edu (ai08) | GradeService.ListGradesByStudent | 我的成绩 | P3 | core-edu 就绪前返回固定 5 个 Grade |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | 我的考勤 | P3 | core-edu 就绪前返回固定 10 条 Attendance |
| content (ai09) | TextbookService.ListTextbooks | 教材列表 | P4 | content 就绪前返回固定 5 个教材 |
| content (ai09) | ChapterService.ListChapters | 章节列表 | P4 | content 就绪前返回固定章节树 |
| content (ai09) | KnowledgeGraphService.GetLearningPath | 学习路径 | P4 | content 就绪前返回固定 8 个知识点推荐顺序 |
| data-ana (ai11) | AnalyticsService.GetStudentDashboard | 学生仪表盘 | P4 | data-ana 就绪前返回固定仪表盘 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 我的薄弱点 | P4 | data-ana 就绪前返回固定 3 个 weak_points |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | 学习趋势 | P4 | data-ana 就绪前返回固定趋势数据 |
| msg (ai10) | NotificationService.ListNotifications | 学生通知 | P5 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.GetUnreadCount | 通知未读数 | P5 | msg 就绪前返回固定 count=3 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | P5 | msg 就绪前返回 success=true |
> **命名对齐**coord §5.2
> - `AnalyticsService.GetStudentDashboard`(无 Stats 后缀,统一命名)
> - `KnowledgeGraphService.GetLearningPath`(禁用 ContentService 命名)
### 2.2 Kafka 事件订阅(异步)
依据 B7 裁决:
- **P3-P4 阶段****不订阅 Kafka**(仅同步 gRPC 聚合)
- **P5 阶段**push-gateway 落地后,评估是否订阅 Kafka`edu.identity.user.role_changed` 用于权限缓存失效)
**P5 可选订阅的 topic**(待 P5 评估):
| Topic | 用途 | 触发动作 |
| ------------------------------- | -------------------------- | ------------------------------------- |
| `edu.identity.user.role_changed` | 学生角色变更,失效权限缓存 | iam 发布student-bff 清除 Redis 缓存 |
> **P3-P4 降级方案**:权限缓存用短 TTL5min兜底不订阅 Kafka 事件。
### 2.3 HTTP 调用
**无**。依据 B2 裁决student-bff 对下游全部走 gRPC禁止 HTTP fetch。
### 2.4 DownstreamClient 抽象层B8 裁决)
依据 B8 裁决:
1. **回写 teacher-bff**DownstreamClient 作为 BFF 模式 v2 标准抽象,回写到 teacher-bff3 个 BFFteacher-bff / student-bff / parent-bff统一使用
2. **抽象位置**`packages/shared-ts/src/bff/downstream-client.ts`coord 维护)
3. **核心能力**
```typescript
export class DownstreamClient {
/**
* gRPC 调用封装
* @param service 下游服务名(如 'iam' / 'core-edu'
* @param method RPC 方法名(如 'GetUserInfo'
* @param request 请求 message
* @param options 超时 / 重试 / traceId 透传
*/
call<TRequest, TResponse>(
service: string,
method: string,
request: TRequest,
options?: CallOptions,
): Promise<TResponse>;
}
interface CallOptions {
timeoutMs?: number; // 默认 5000ms
retryCount?: number; // 默认 2
retryBackoffMs?: number; // 默认 100ms指数退避
traceId?: string; // 从 x-request-id header 获取
metadata?: Record<string, string>; // gRPC metadata含 x-user-id / x-user-roles / x-dataScope
}
```
4. **student-bff 使用方式**
```typescript
// services/student-bff/src/auth/auth.service.ts
import { DownstreamClient } from '@edu/shared-ts/bff/downstream-client';
@Injectable()
export class AuthService {
constructor(private readonly downstream: DownstreamClient) {}
async getCurrentUser(userId: string): Promise<UserInfo> {
return this.downstream.call('iam', 'GetUserInfo', { userId }, {
metadata: { 'x-user-id': userId },
});
}
}
```
5. **回写义务**ai04 在 P3 实现时,将 DownstreamClient 抽象回写到 teacher-bff替换 teacher-bff 现有的散落 fetch 调用),保证 3 个 BFF 统一。
---
## §3 跨阶段扩展例外规则§2.3 裁决)
依据 president-final-rulings §2.3
### 3.1 允许扩展(无需 coord 仲裁)
1. 新增下游 gRPC 调用(新增 RPC 方法到 DownstreamClient
2. 新增下游配置gRPC endpoint 配置)
3. 新增 /readyz 探针(按阶段启用,见 §4
4. Dashboard Query Resolver 内部数据源扩展null 字段 → 真实数据)
5. AuthorizationGuard 内部实现替换DEV_MODE 放行 → 真实 gRPC 校验 + Redis 缓存)
### 3.2 禁止变更(需 coord 仲裁)
1. 修改已有 RPC 调用的签名或返回类型
2. 删除已实现的 RPC 调用(除非下游服务下线)
3. 修改 GraphQL schema 已有字段的类型(新增字段允许,无需仲裁)
4. 修改 /readyz 已有探针的检查项(只能新增,不能修改)
### 3.3 验收标准
扩展时必须:
1. 更新 student-bff 02-architecture-design.md 下游调用矩阵§7
2. 更新 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
3. 运行 `pnpm run arch:scan` 更新 arch.db
4. coord 在批次验收时检查上述 3 项
---
## §4 /readyz 探针按阶段扩展规则§2.4 裁决)
依据 president-final-rulings §2.4 + G2 裁决:
### 4.1 探针列表(按阶段)
student-bff 无 DB探针仅检查 Redis + 下游 gRPC 可达性:
| 阶段 | 探针列表 | 数量 |
| ---- | ----------------------------------------------------- | ---- |
| P3 | Redis PING + iam gRPC 50052 + core-edu gRPC 50053 | 3 |
| P4 | + content gRPC 50054 + data-ana gRPC 50055 | 5 |
| P5 | + ai gRPC 50058 + msg gRPC 50056 | 7 |
**实现方式**`DownstreamHealthCheck` 注册表模式,每个下游注册独立探针,按阶段启用(通过 ENV 过滤):
```typescript
const checks: HealthCheck[] = [
checkRedis(),
checkGrpc('iam', 50052),
checkGrpc('core-edu', 50053),
];
if (env.CONTENT_GRPC_ENABLED) checks.push(checkGrpc('content', 50054));
if (env.DATA_ANA_GRPC_ENABLED) checks.push(checkGrpc('data-ana', 50055));
if (env.AI_GRPC_ENABLED) checks.push(checkGrpc('ai', 50058));
if (env.MSG_GRPC_ENABLED) checks.push(checkGrpc('msg', 50056));
```
### 4.2 软失败规则
依据 §2.4.3
- **必需依赖**Redis + 已启用 gRPC 下游):失败返回 503触发 Pod 重启
- **可选依赖**(未启用 gRPC 下游 / Kafka 订阅):失败仅告警,返回 200 + body `degraded: true`
**student-bff 软失败场景**
| 依赖 | 类型 | 失败行为 |
| ------------------- | ------ | ------------------------------------------- |
| Redis | 必需 | 503缓存失效会影响性能但 BFF 仍可降级运行) |
| iam gRPC 50052 | 必需 | 503无 iam 无法做身份校验) |
| core-edu gRPC 50053 | 必需 | 503核心数据源 |
| content gRPC 50054 | 可选P4 启用前) | 200 + degraded=true |
| data-ana gRPC 50055 | 可选P4 启用前) | 200 + degraded=true |
| ai gRPC 50058 | 可选P5 启用前) | 200 + degraded=true |
| msg gRPC 50056 | 可选P5 启用前) | 200 + degraded=true |
> **P3 阶段**content / data-ana / ai / msg 均为可选P3 /readyz 仅检查 Redis + iam + core-edu3 项)。
---
## §5 就绪信号
### 5.1 我依赖的上游就绪标志
| 上游依赖 | 就绪标志 | 责任方 | 完成期限 |
| ---------------- | ------------------------------------------- | ------ | ------------- |
| iam gRPC 50052 | iam /readyz 返回 200 + GetUserInfo RPC 可调 | ai06 | 批次 1 完成 |
| core-edu gRPC 50053 | core-edu /readyz 返回 200 + ListExamsByClass RPC 可调 | ai08 | 批次 2 完成 |
| content gRPC 50054 | content /readyz 返回 200 + ListTextbooks RPC 可调 | ai09 | 批次 3 完成 |
| data-ana gRPC 50055 | data-ana /readyz 返回 200 + GetStudentDashboard RPC 可调 | ai11 | 批次 3 完成 |
| msg gRPC 50056 | msg /readyz 返回 200 + ListNotifications RPC 可调 | ai10 | 批次 4 完成 |
| GraphQL schema 仲裁 | coord 仲裁 student-bff schema 第一版 | coord | 批次 2 启动前 |
| DownstreamClient 抽象 | packages/shared-ts/src/bff/downstream-client.ts 就绪 | coord | 批次 1 启动前 |
| shared-go 包 | packages/shared-go/ 骨架就绪 | coord | 批次 0.14 |
### 5.2 我的就绪标志(供下游消费)
| 就绪标志 | 完成标准 | 完成阶段 |
| ----------------------------------------- | ----------------------------------------------------------- | -------- |
| student-bff GraphQL :3009 启用 | /healthz 返回 200 | P3 |
| /readyz 返回 200P3 探针 3 项) | Redis + iam gRPC + core-edu gRPC 全部通过 | P3 |
| GraphQL schema 可内省 | POST /graphql `{ query: "{ __schema { types { name } } }" }` 返回 schema | P3 |
| 核心 Query 可执行 | currentUser / myClasses / myExams / myHomework / myGrades 返回真实数据 | P3 |
| 核心 Mutation 可执行 | submitHomework 可执行 | P3 |
| AuthorizationGuard 接口就绪 | canAccessSelfData 接口已实现DEV_MODE 放行) | P3 |
| DownstreamClient 回写 teacher-bff 完成 | teacher-bff 已切换为 DownstreamClient 抽象 | P3 |
| /readyz 扩展 P4 探针5 项) | + content gRPC + data-ana gRPC | P4 |
| /readyz 扩展 P5 探针7 项) | + ai gRPC + msg gRPC | P5 |
---
## §6 Mock 策略
### 6.1 我提供的 mock供 student-portal 消费)
在 student-bff 真实就绪前,为 ai14student-portal提供以下 mock
**GraphQL mock 实现方式**GraphQL Yoga 内置 mock 模式(`graphql-yoga``mocking` 配置)或 MSW 拦截 POST /graphql
| Query/Mutation | mock 返回 |
| --------------------------- | ------------------------------------------------------------ |
| `currentUser` | 固定学生id="student-001", name="李同学", roles=["student"] |
| `myClasses` | 固定 1 个班级id="cls-001", name="高三1班" |
| `myExams` | 固定 2 个考试 |
| `myHomework` | 固定 3 个作业1 个待提交) |
| `myGrades` | 固定 5 个成绩avg_score=85.0 |
| `myAttendance` | 固定 10 条考勤 |
| `studentDashboard` | 固定仪表盘avg_score=85.0, class_rank=5 |
| `myNotifications` | 固定 10 条通知3 条未读) |
| `myNotificationUnreadCount` | 固定 count=3 |
| `submitHomework` | 返回 success=true |
| `markNotificationAsRead` | 返回 success=true |
> **P4 字段降级**P3 阶段 `studentDashboard.weakness` / `myTrend` / `textbooks` / `chapters` / `learningPath` 返回 null + `extensions.warning = "field_unavailable_in_p3"`
### 6.2 我消费的 mock上游未就绪时
在真实上游就绪前student-bff 使用以下 mock详见 §2.1 mock 策略列):
| 上游 | mock 实现 |
| -------- | ------------------------------------------------------------ |
| iam | 固定 UserInfo + 学生权限集 + 固定视口 + DataScope=SELF |
| core-edu | 固定班级/同学/考试/作业/成绩/考勤 |
| content | 固定教材/章节/学习路径P4 启用前) |
| data-ana | 固定仪表盘/薄弱点/趋势P4 启用前) |
| msg | 固定通知列表 + GetUnreadCount + MarkAsRead successP5 启用前) |
> **mock 实现方式**:通过 DownstreamClient 的 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用。具体:在 `DownstreamClient.call()` 内部检查 `env.MOCK_UPSTREAM=true`,若为 true 则返回 mock 数据,否则走真实 gRPC。
---
## §7 缓存策略B6 裁决)
依据 B6 裁决 + 004 §6.2 BFF 混合读策略:
### 7.1 Redis 短缓存
| 缓存对象 | key 模式 | TTL | 失效策略 |
| ----------------------- | ----------------------------------- | ----- | ----------------------- |
| currentUser 聚合结果 | `bff:student:user:{userId}` | 30s | TTL 过期 |
| myClasses 聚合结果 | `bff:student:classes:{userId}` | 30s | TTL 过期 |
| studentDashboard 聚合结果 | `bff:student:dashboard:{userId}` | 5s | TTL 过期(实时性要求高)|
| 权限列表 | `authz:student:{userId}` | 5min | P5 订阅 Kafka 事件失效 |
| 视口配置 | `bff:student:viewports:{userId}` | 5min | TTL 过期 |
### 7.2 缓存规则
1. **仅缓存 Query**Mutation 不缓存
2. **缓存粒度**:按 Query + userId 维度缓存(学生数据隔离 SELF
3. **降级策略**Redis 不可用时,直接走 gRPC 调用(不缓存),返回数据 + `degraded: true`(标记缓存降级)
4. **缓存击穿防护**:使用 `ioredis``GETSET` 或单飞模式(同一 key 并发请求只发一个 gRPC 调用)
---
## §8 变更记录
| 日期 | 版本 | 变更 | 负责人 |
| ---------- | ---- | -------------------------------------------------------------------- | ------ |
| 2026-07-09 | v1 | 初始创建 | ai04 |
| 2026-07-10 | v2 | 对齐 coord B1-B8 + president §2.2-2.9 裁决schema 路径/错误响应/越权防御/DownstreamClient/跨阶段扩展/readyz 探针/降级模式/缓存策略) | ai04 |