35 KiB
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
- coord-final-decisions.md §2 BFF 专项裁决 B1-B8
- president-final-rulings.md §2.2 GraphQL schema 仲裁机制
- iam.proto
- core_edu.proto
- content.proto
- analytics.proto
- msg.proto
§0 裁决对齐声明
本文件已对齐以下裁决(无遗漏):
| 裁决编号 | 内容 | 本文件章节 |
|---|---|---|
| B1 | P2 起直接 GraphQL(GraphQL 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 不订阅 Kafka,P5 后再订阅 | §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 | 降级模式方案 B(data 内 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 起直接 GraphQL(GraphQL 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.1:3 个 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:
- 起草方:ai04 在批次 1 等待期起草 student-bff GraphQL schema 草案
- 仲裁方:coord 在批次 2 启动前仲裁第一版 schema
- 消费方:ai14(student-portal)基于仲裁版 schema 消费
- 变更流程:
- schema 变更需 PR + ai04(BFF)+ ai14(前端)双方 review
- 重大变更(删除字段 / 修改类型)需 coord 仲裁
- 新增字段允许,无需 coord 仲裁,但需通知 ai14
- 版本管理: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 |
分页:所有列表 Query(myClasses / 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:
- GraphQL schema 设计完整(含全部 Query/Mutation 字段定义),P3 即定型,后续不重构
- Resolver 实现:
- P3 阶段:
studentDashboardQuery Resolver 内部调 iam + core-edu gRPC,返回学生基础信息 + 班级列表 + 考试列表 + 作业列表 + 成绩列表 - P4 扩展:增加 content(教材/章节)+ data-ana(仪表盘/薄弱点/趋势)数据源,将 null 字段替换为真实数据
- P5 扩展:增加 msg(通知未读数)+ ai(AI 助教入口)数据源
- 未启用的下游字段返回
null+extensions.warning = "field_unavailable_in_p3"
- P3 阶段:
- 前端配合:ai14 student-portal 对 null 字段做 UI 降级展示(如"数据加载中"或隐藏模块)
- 此方案不违反"不分阶段":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 字段:
{
"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 key(F4 规范: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 字段):
{
"data": {
"studentDashboard": {
"user": { "id": "stu-001", "name": "李同学" },
"classes": [{ "id": "cls-001", "name": "高三1班" }],
"weakness": null,
"degraded": true,
"degradedReason": "data_ana_unavailable",
"degradedFields": ["weakness"]
}
}
}
规则:
- 降级时 HTTP 200,GraphQL
data非 null - 降级字段返回
null,并在父对象内加degraded: true+degradedReason: string+degradedFields: string[] - 前端检查
data.degraded判断降级,对degradedFields内字段做 UI 降级展示 - 降级场景示例:
- data-ana gRPC 不可用 →
studentDashboard.weakness/myTrend/myWeakness降级 - content gRPC 不可用 →
textbooks/chapters/learningPath降级 - msg gRPC 不可用 →
myNotifications/myNotificationUnreadCount降级
- data-ana gRPC 不可用 →
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 裁决:
// 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 裁决:
- P3 抽象 AuthorizationGuard 接口(
canAccessSelfData,见 §1.7.2) - P3 内部实现为"DEV_MODE 放行 + 生产拒绝"(保守策略):
DEV_MODE=true:放行所有请求,仅记录 warn 日志DEV_MODE=false:严格校验,越权返回BFF_STUDENT_FORBIDDEN_RESOURCE
- Redis 缓存(P3 后期接入):
- key:
authz:student:{userId} - value: 用户基础信息(userId / roles / classIds)
- TTL: 5min
- AuthorizationGuard 优先查缓存,缓存未命中调 iam gRPC
- key:
- 此方案不违反"不分阶段":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 就绪前返回固定 UserInfo(student 角色) |
| 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 降级方案:权限缓存用短 TTL(5min)兜底,不订阅 Kafka 事件。
2.3 HTTP 调用
无。依据 B2 裁决,student-bff 对下游全部走 gRPC,禁止 HTTP fetch。
2.4 DownstreamClient 抽象层(B8 裁决)
依据 B8 裁决:
- 回写 teacher-bff:DownstreamClient 作为 BFF 模式 v2 标准抽象,回写到 teacher-bff,3 个 BFF(teacher-bff / student-bff / parent-bff)统一使用
- 抽象位置:
packages/shared-ts/src/bff/downstream-client.ts(coord 维护) - 核心能力:
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)
}
- student-bff 使用方式:
// 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 },
});
}
}
- 回写义务:ai04 在 P3 实现时,将 DownstreamClient 抽象回写到 teacher-bff(替换 teacher-bff 现有的散落 fetch 调用),保证 3 个 BFF 统一。
§3 跨阶段扩展例外规则(§2.3 裁决)
依据 president-final-rulings §2.3:
3.1 允许扩展(无需 coord 仲裁)
- 新增下游 gRPC 调用(新增 RPC 方法到 DownstreamClient)
- 新增下游配置(gRPC endpoint 配置)
- 新增 /readyz 探针(按阶段启用,见 §4)
- Dashboard Query Resolver 内部数据源扩展(null 字段 → 真实数据)
- AuthorizationGuard 内部实现替换(DEV_MODE 放行 → 真实 gRPC 校验 + Redis 缓存)
3.2 禁止变更(需 coord 仲裁)
- 修改已有 RPC 调用的签名或返回类型
- 删除已实现的 RPC 调用(除非下游服务下线)
- 修改 GraphQL schema 已有字段的类型(新增字段允许,无需仲裁)
- 修改 /readyz 已有探针的检查项(只能新增,不能修改)
3.3 验收标准
扩展时必须:
- 更新 student-bff 02-architecture-design.md 下游调用矩阵(§7)
- 更新
packages/shared-ts/contracts/graphql/student-bff.schema.graphql - 运行
pnpm run arch:scan更新 arch.db - 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 过滤):
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-edu(3 项)。
§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 返回 200(P3 探针 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 真实就绪前,为 ai14(student-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 success(P5 启用前) |
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 缓存规则
- 仅缓存 Query:Mutation 不缓存
- 缓存粒度:按 Query + userId 维度缓存(学生数据隔离 SELF)
- 降级策略:Redis 不可用时,直接走 gRPC 调用(不缓存),返回数据 +
degraded: true(标记缓存降级) - 缓存击穿防护:使用
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 |