docs(student-bff): 更新架构问题记录、工作排期和对接契约,对齐已裁决规则

完成已有仲裁核查,新增待仲裁问题归档,细化全阶段排期与依赖,对齐coord B1-B8和总裁裁决,更新GraphQL规范、越权防御、DownstreamClient契约
This commit is contained in:
SpecialX
2026-07-10 14:43:11 +08:00
parent 9ba368477d
commit e5ca4c6c7b
3 changed files with 1091 additions and 108 deletions

View File

@@ -1,50 +1,310 @@
# student-bff 对接契约
> 负责人ai04
> 关联:[matrix.md](./matrix.md)、[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)
> 阶段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 gRPC 接口(如有)
### 1.1 服务基础信息
无对外 gRPC。student-bff 是 GraphQL 聚合层。
| 项目 | 值 |
| ------------- | --------------------------------------------- |
| 服务名 | student-bff |
| 服务类型 | BFF 聚合层(无 DB无 Outbox |
| HTTP 端口 | 3009 |
| gRPC 端口 | 不暴露BFF 仅对下游走 gRPC不对外提供 gRPC |
| 路由前缀 | `/student/*`api-gateway 透传) |
| 部署目录 | `services/student-bff/` |
| 角色要求 | `student`JWT 必需 + student 角色) |
### 1.2 HTTP 端点(如有)
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 |
| ------ | -------- | ----------------------------------------- | ----------------------- |
| POST | /graphql | 学生 BFF GraphQL 端点 | JWT 必需 + student 角色 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 |
| GET | /healthz | 健康检查liveness | 公开 |
| GET | /readyz | 就绪检查readiness含下游 gRPC 连通性) | 公开 |
| 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 |
### 1.3 GraphQL schema如 BFF
> **B1 裁决**P2 起直接 GraphQLGraphQL Yoga + DataLoader禁止 REST → GraphQL 渐进式过渡。
> **B2 裁决**:对下游通信首次实现即 gRPC禁止 HTTP fetch → gRPC 渐进式过渡。
> **F9 裁决**:前端 student-portal 用 urql/apollo 消费 GraphQL。
GraphQL schema 文件路径:`apps/student-bff/src/schema/*.graphql`(端口 :3009
### 1.3 GraphQL schema(核心契约
核心 Query / Mutation 域:
#### 1.3.1 schema 存放路径
- **auth**currentUser聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports
- **myClasses**:我的班级(聚合 core-edu.ClassService.GetClass + ListStudentsByClass
- **myExams**:我的考试列表(聚合 core-edu.ExamService.ListExamsByClass
- **myHomework**:我的作业(聚合 core-edu.HomeworkService.ListHomeworkByClass + SubmitHomework
- **myGrades**:我的成绩(聚合 core-edu.GradeService.ListGradesByStudent
- **myAttendance**:我的考勤(聚合 core-edu.AttendanceService.ListAttendanceByStudent
- **content**textbooks / chapters / learningPath聚合 content.KnowledgeGraphService.GetLearningPath
- **dashboard**studentDashboard聚合 data-ana.AnalyticsService.GetStudentDashboard
- **weakness**myWeakness聚合 data-ana.AnalyticsService.GetStudentWeakness
- **trend**myTrend聚合 data-ana.AnalyticsService.GetLearningTrend
- **notifications**myNotifications / markAsRead聚合 msg.NotificationService
**强制路径**`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
### 1.4 Kafka 事件发布(如有)
> 依据 president-final-rulings §2.2.13 个 BFF schema 统一存放于 `packages/shared-ts/contracts/graphql/`,由各 BFF 负责 AI 起草、coord 仲裁。student-bff schema 不放在 `services/student-bff/src/` 下,避免前端 AI 难以发现。
无。student-bff 不发布事件,仅做 gRPC 聚合。
#### 1.3.2 起草与仲裁流程
### 1.5 错误码前缀
依据 president-final-rulings §2.2.6
`BFF_STUDENT_`(如 BFF_STUDENT_UPSTREAM_UNAVAILABLE、BFF_STUDENT_AGGREGATION_FAILED、BFF_STUDENT_FORBIDDEN
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 后期接入真实校验后正式上线。
---
@@ -52,79 +312,288 @@ GraphQL schema 文件路径:`apps/student-bff/src/schema/*.graphql`(端口 :
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | ----------------------------------------- | ---------------- | ------------------------------------------- |
| iam (ai06) | IamService.GetUserInfo | 获取当前学生信息 | iam 就绪前返回固定 UserInfostudent 角色) |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | iam 就绪前返回学生权限集 |
| iam (ai06) | IamService.GetViewports | 学生导航菜单 | iam 就绪前返回固定视口列表 |
| core-edu (ai08) | ClassService.GetClass | 我的班级详情 | core-edu 就绪前返回固定 ClassInfo |
| core-edu (ai08) | ClassService.ListStudentsByClass | 班级同学名单 | core-edu 就绪前返回固定 30 个 StudentInfo |
| core-edu (ai08) | ExamService.ListExamsByClass | 我的考试 | core-edu 就绪前返回固定 2 个 Exam |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | 我的作业 | core-edu 就绪前返回固定 3 个 Homework |
| core-edu (ai08) | HomeworkService.SubmitHomework | 提交作业 | core-edu 就绪前返回 success=true |
| core-edu (ai08) | GradeService.ListGradesByStudent | 我的成绩 | core-edu 就绪前返回固定 5Grade |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | 我的考勤 | core-edu 就绪前返回固定 10 条 Attendance |
| content (ai09) | TextbookService.ListTextbooks | 教材列表 | content 就绪前返回固定 5 个教材 |
| content (ai09) | ChapterService.ListChapters | 章节列表 | content 就绪前返回固定章节树 |
| content (ai09) | KnowledgeGraphService.GetLearningPath | 学习路径 | content 就绪前返回固定 8 个知识点推荐顺序 |
| data-ana (ai11) | AnalyticsService.GetStudentDashboard | 学生仪表盘 | data-ana 就绪前返回固定仪表盘 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 我的薄弱点 | data-ana 就绪前返回固定 3 个 weak_points |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | 学习趋势 | data-ana 就绪前返回固定趋势数据 |
| msg (ai10) | NotificationService.ListNotifications | 学生通知 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | msg 就绪前返回 success=true |
依据 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 就绪前返回固定 2Exam |
| 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 事件订阅(异步)
无。student-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
依据 B7 裁决:
### 2.3 HTTP 调用(如有
- **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 就绪信号
## §3 跨阶段扩展例外规则§2.3 裁决)
### 3.1 我依赖的上游就绪标志
依据 president-final-rulings §2.3
- [ ] iam gRPC 50052 启用ai06
- [ ] core-edu gRPC 50053 启用ai08
- [ ] content gRPC 50054 启用ai09
- [ ] data-ana gRPC 50055 启用ai11
- [ ] msg gRPC 50056 启用ai10
### 3.1 允许扩展(无需 coord 仲裁
### 3.2 我的就绪标志(供下游消费
1. 新增下游 gRPC 调用(新增 RPC 方法到 DownstreamClient
2. 新增下游配置gRPC endpoint 配置)
3. 新增 /readyz 探针(按阶段启用,见 §4
4. Dashboard Query Resolver 内部数据源扩展null 字段 → 真实数据)
5. AuthorizationGuard 内部实现替换DEV_MODE 放行 → 真实 gRPC 校验 + Redis 缓存)
- [ ] student-bff GraphQL :3009 启用(/healthz 返回 200
- [ ] /readyz 返回 200含 5 个下游 gRPC 连通性检查)
- [ ] GraphQL schema 可内省POST /graphql 返回 schema
- [ ] 核心 Query 可执行currentUser / myClasses / studentDashboard / myGrades
- [ ] 核心 Mutation 可执行submitHomework / markAsRead
### 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 Mock 策略
## §4 /readyz 探针按阶段扩展规则§2.4 裁决)
### 4.1 我提供的 mock
依据 president-final-rulings §2.4 + G2 裁决:
在 student-bff 真实就绪前为下游student-portal提供以下 mock
### 4.1 探针列表(按阶段)
- **GraphQL mock**:使用 Apollo Server mockProviders 或 MSW 拦截 POST /graphql
- currentUser 返回固定学生id="student-001", name="李同学", roles=["student"]
- myClasses 返回固定 1 个班级
- studentDashboard 返回固定仪表盘avg_score=85.0, class_rank=5
- myGrades 返回固定 5 个成绩
- myHomework 返回固定 3 个作业1 个待提交)
- myNotifications 返回固定 10 条通知
student-bff 无 DB探针仅检查 Redis + 下游 gRPC 可达性:
### 4.2 我消费的 mock
| 阶段 | 探针列表 | 数量 |
| ---- | ----------------------------------------------------- | ---- |
| 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 策略列):
- **iam mock**:固定 UserInfo + 学生权限 + 固定视口
- **core-edu mock**:固定班级/同学/考试/作业/成绩/考勤
- **content mock**:固定教材/章节/学习路径
- **data-ana mock**:固定仪表盘/薄弱点/趋势
- **msg mock**:固定通知列表 + MarkAsRead success
| 上游 | mock 实现 |
| -------- | ------------------------------------------------------------ |
| iam | 固定 UserInfo + 学生权限集 + 固定视口 + DataScope=SELF |
| core-edu | 固定班级/同学/考试/作业/成绩/考勤 |
| content | 固定教材/章节/学习路径P4 启用前) |
| data-ana | 固定仪表盘/薄弱点/趋势P4 启用前) |
| msg | 固定通知列表 + GetUnreadCount + MarkAsRead successP5 启用前) |
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用
> **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 |