feat(student-bff): 完整实现 student-bff 聚合层
包含 src 全部实现、Dockerfile、shared-ts/bff 包等
This commit is contained in:
@@ -3,8 +3,15 @@
|
||||
> AI 标识:ai04
|
||||
> 阶段:阶段 1(全局理解)
|
||||
> 日期:2026-07-09
|
||||
> 状态:待 coord 审核
|
||||
> 关联文档:[ai-allocation §6 模板](../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md)、[pending-features P3](../../docs/architecture/roadmap/pending-features.md)
|
||||
> 状态:已对齐仲裁裁决(coord-final-decisions §2 B1-B8 + president-final-rulings §2.2-2.9)
|
||||
> 关联文档:[ai-allocation §6 模板](../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md)、[pending-features P3](../../docs/architecture/roadmap/pending-features.md)、[coord-final-decisions](../../docs/architecture/coord-final-decisions.md)、[president-final-rulings](../../docs/architecture/president-final-rulings.md)
|
||||
>
|
||||
> **仲裁对齐说明(ISSUE-STU-001 修复)**:
|
||||
> - B1: API 风格 = GraphQL Yoga + DataLoader(P2 起直接 GraphQL,禁止 REST 渐进)
|
||||
> - B2: 下游通信 = gRPC 首次实现即用(@grpc/grpc-js + @grpc/proto-loader,禁止 HTTP fetch)
|
||||
> - B5: 错误码前缀 = `BFF_STUDENT_`(BFF 在前,非 `STUDENT_BFF_`)
|
||||
> - B8: DownstreamClient 抽象复用 shared-ts(回写 teacher-bff,3 BFF 统一)
|
||||
> - 本文档中早期将通信方式写为 HTTP REST/fetch、错误码前缀写为 `STUDENT_BFF_` 的部分已修正,以本对齐说明为准。
|
||||
|
||||
---
|
||||
|
||||
@@ -15,8 +22,8 @@
|
||||
| 层级 | **L4 BFF 聚合层**(004 §3.1 六层架构) |
|
||||
| 上游调用方 | api-gateway(Go Gin,反向代理 `/api/v1/student/*` → student-bff:3009) |
|
||||
| 下游被调用方 | iam、core-edu、content、data-ana(按 004 §4 服务依赖图) |
|
||||
| 通信方式(入) | HTTP REST(api-gateway → student-bff,当前阶段);设计意图为 gRPC(004 §4.1) |
|
||||
| 通信方式(出) | HTTP fetch(当前阶段,对齐 teacher-bff 模式);设计意图为 gRPC(004 §4.1) |
|
||||
| 通信方式(入) | **GraphQL Yoga over HTTP**(api-gateway → student-bff:3009,B1 裁决:P2 起直接 GraphQL,禁止 REST 渐进) |
|
||||
| 通信方式(出) | **gRPC**(@grpc/grpc-js + @grpc/proto-loader,B2 裁决:首次实现即用 gRPC,禁止 HTTP fetch) |
|
||||
| 微前端对接 | student-portal(ai07 负责,P3 阶段)通过 api-gateway 调用 student-bff |
|
||||
| 推送通道 | push-gateway(P5 阶段,WebSocket/SSE 推送考试通知、成绩发布等) |
|
||||
|
||||
@@ -73,70 +80,107 @@ student-bff 是**纯聚合层**,不持有业务状态、不直接访问 DB(
|
||||
|
||||
### 3.1 我消费的 proto message / 下游接口
|
||||
|
||||
> ⚠️ **重要差距**:当前阶段 BFF→Service 走 HTTP fetch(对齐 teacher-bff 现状),proto 仅作"契约文档"。gRPC 落地需 coord 在 buf.gen.yaml 补 gRPC 插件。
|
||||
> ✅ **B2 裁决落地**:BFF→Service 首次实现即用 gRPC(@grpc/grpc-js + @grpc/proto-loader),通过 `DownstreamClient` 抽象(B8 裁决,复用 shared-ts,3 个 BFF 统一)。proto 即契约,不再是"文档"。
|
||||
|
||||
| 下游服务 | proto service(设计意图) | 当前 REST 端点(实际可用) | 用途 |
|
||||
| ------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------ |
|
||||
| iam | `IamService.GetUserInfo` | `GET /iam/me` | 获取学生个人信息 + roles + dataScope |
|
||||
| iam | `IamService.GetViewports`(proto 缺失) | `GET /iam/viewports` | 获取学生端导航视口 |
|
||||
| iam | `IamService.GetEffectivePermissions`(proto 缺失) | `GET /iam/permissions/effective` | 获取有效权限列表 |
|
||||
| classes(core-edu) | `ClassService.GetClass` / `ListClasses` | `GET /classes` / `GET /classes/:id` | 查自己所在班级 |
|
||||
| core-edu | `ExamService.GetExam` / `ListExamsByClass` | `GET /exams/class/:classId` | 查班级考试 |
|
||||
| core-edu | `HomeworkService.GetHomework` / `ListHomeworkByClass` / `SubmitHomework` | `GET /homework/class/:classId` / `POST /homework/:id/submit` | 查作业 + 提交 |
|
||||
| core-edu | `GradeService.GetGrade` / `ListGradesByStudent` | `GET /grades/student/:studentId` | 查自己成绩 |
|
||||
| content | `TextbookService.GetTextbook` / `ListTextbooks` | `GET /textbooks` | 查教材 |
|
||||
| content | `ChapterService`(proto 缺失) | `GET /chapters` / `GET /chapters/:id` | 查章节 |
|
||||
| content | `QuestionService`(proto 缺失) | `GET /questions` | 查题库 |
|
||||
| content | `KnowledgeGraphService.GetLearningPath` | `GET /knowledge-points/:id/learning-path` | 学习路径 |
|
||||
| msg | `NotificationService.ListNotifications` / `MarkAsRead` / `SearchNotifications` | `GET /notifications` / `POST /notifications/:id/read` | 消息中心 |
|
||||
| data-ana | `AnalyticsService.GetStudentWeakness` / `GetLearningTrend` | **REST 未实现** | 学情分析 |
|
||||
| ai | `AiService.Chat` / `StreamChat` / `GenerateQuestion` | **REST 未实现** | AI 答疑 |
|
||||
| 下游服务 | proto service | gRPC method(B2 裁决) | 用途 |
|
||||
| ------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| iam | `IamService.GetUserInfo` | `iam.GetUserInfo` | 获取学生个人信息 + roles + dataScope |
|
||||
| iam | `IamService.GetViewports`(proto 缺失) | `iam.GetViewports` | 获取学生端导航视口 |
|
||||
| iam | `IamService.GetEffectivePermissions`(proto 缺失) | `iam.GetEffectivePermissions` | 获取有效权限列表 |
|
||||
| classes(core-edu) | `ClassService.GetClass` / `ListClasses` | `classes.GetClass` / `classes.ListClasses` | 查自己所在班级 |
|
||||
| core-edu | `ExamService.GetExam` / `ListExamsByClass` | `core-edu.GetExam` / `core-edu.ListExamsByClass` | 查班级考试 |
|
||||
| core-edu | `HomeworkService.GetHomework` / `ListHomeworkByClass` / `SubmitHomework` | `core-edu.ListHomeworkByStudent` / `core-edu.SubmitHomework` | 查作业 + 提交 |
|
||||
| core-edu | `GradeService.GetGrade` / `ListGradesByStudent` | `core-edu.ListGradesByStudent` | 查自己成绩 |
|
||||
| content | `TextbookService.GetTextbook` / `ListTextbooks` | `content.ListTextbooks` | 查教材 |
|
||||
| content | `ChapterService`(proto 缺失) | `content.ListChapters` | 查章节 |
|
||||
| content | `QuestionService`(proto 缺失) | `content.ListQuestions` | 查题库 |
|
||||
| content | `KnowledgeGraphService.GetLearningPath` | `content.GetLearningPath` | 学习路径 |
|
||||
| msg | `NotificationService.ListNotifications` / `MarkAsRead` / `SearchNotifications` | `msg.ListNotifications` / `msg.MarkAsRead` | 消息中心 |
|
||||
| data-ana | `AnalyticsService.GetStudentWeakness` / `GetLearningTrend` | `data-ana.GetStudentWeakness` / `data-ana.GetLearningTrend` | 学情分析 |
|
||||
| ai | `AiService.Chat` / `StreamChat` / `GenerateQuestion` | `ai.Chat`(同步) / `ai.StreamChat`(server-streaming,B2 裁决) | AI 答疑 |
|
||||
|
||||
### 3.2 我暴露的 API 端点(student-bff 对外)
|
||||
### 3.2 我暴露的 GraphQL API(student-bff 对外)
|
||||
|
||||
> 路由前缀:`/student`(对齐 teacher-bff 用 `/teacher` 的命名规律,BFF 用角色单数无 `-bff` 后缀)
|
||||
> 网关路径:`/api/v1/student/*` → api-gateway 剥离 `/api/v1` 后代理到 student-bff:3009
|
||||
> ✅ **B1 裁决落地**:P2 起直接 GraphQL Yoga(禁止 REST 渐进),schema 存放于 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`(president §2.2.1)。
|
||||
> 网关路径:`/api/v1/student/*` → api-gateway 剥离 `/api/v1` 后代理到 student-bff:3009 GraphQL endpoint。
|
||||
> 分页采用 Relay Cursor Connections 规范(`{ edges, pageInfo, totalCount }`)。
|
||||
> 权限点标注于 schema 注释 `# @permission:`,DataScope 固定 `OWN`(学生数据隔离 SELF)。
|
||||
|
||||
| method | path | 聚合下游 | 权限(透传给下游校验) | 说明 |
|
||||
| ------ | --------------------------------- | -------------------- | ------------------------- | -------------------- |
|
||||
| GET | `/student/dashboard` | iam + core-edu + msg | STUDENT_DASHBOARD_READ | 学生首页聚合 |
|
||||
| GET | `/student/exams` | core-edu | STUDENT_EXAM_READ | 即将到来的考试 |
|
||||
| GET | `/student/homework` | core-edu | STUDENT_HOMEWORK_READ | 我的作业列表 |
|
||||
| POST | `/student/homework/:id/submit` | core-edu | STUDENT_HOMEWORK_SUBMIT | 提交作业 |
|
||||
| GET | `/student/grades` | core-edu | STUDENT_GRADE_READ | 我的成绩 |
|
||||
| GET | `/student/notifications` | msg | STUDENT_NOTIFICATION_READ | 消息列表 |
|
||||
| POST | `/student/notifications/:id/read` | msg | STUDENT_NOTIFICATION_READ | 标记已读 |
|
||||
| GET | `/student/textbooks` | content | STUDENT_CONTENT_READ | 教材列表 |
|
||||
| GET | `/student/chapters/:textbookId` | content | STUDENT_CONTENT_READ | 章节树 |
|
||||
| GET | `/student/questions` | content | STUDENT_CONTENT_READ | 题库(按知识点过滤) |
|
||||
| GET | `/student/analytics/weakness` | data-ana | STUDENT_ANALYTICS_READ | 学情诊断 |
|
||||
| GET | `/student/analytics/trend` | data-ana | STUDENT_ANALYTICS_READ | 学习趋势 |
|
||||
| POST | `/student/ai/chat` | ai | STUDENT_AI_CHAT | AI 答疑(同步) |
|
||||
| POST | `/student/ai/stream-chat` | ai | STUDENT_AI_CHAT | AI 答疑(SSE 流式) |
|
||||
#### Query(14 个字段)
|
||||
|
||||
| Query 字段 | 聚合下游 | 权限点(注释标注) | 说明 |
|
||||
| ------------------------- | -------------------- | ----------------------- | -------------------- |
|
||||
| `currentUser` | iam | AUTH_READ | 学生信息 + 权限 + 视口 |
|
||||
| `myClasses` | core-edu | CLASS_READ | 我的班级列表 |
|
||||
| `myExams` | core-edu | EXAM_READ | 即将到来的考试 |
|
||||
| `myHomework` | core-edu | HOMEWORK_READ | 我的作业列表 |
|
||||
| `myGrades` | core-edu | GRADE_READ | 我的成绩(B4 比对) |
|
||||
| `myAttendance` | core-edu | ATTENDANCE_READ | 我的考勤记录 |
|
||||
| `textbooks` | content | TEXTBOOK_READ | 教材列表 |
|
||||
| `chapters` | content | CHAPTER_READ | 章节树 |
|
||||
| `learningPath` | content | LEARNING_PATH_READ | 学习路径推荐 |
|
||||
| `studentDashboard` | data-ana | DASHBOARD_VIEW | 学生仪表盘聚合 |
|
||||
| `myWeakness` | data-ana | WEAKNESS_READ | 学情诊断(薄弱点) |
|
||||
| `myTrend` | data-ana | TREND_READ | 学习趋势 |
|
||||
| `myNotifications` | msg | NOTIFICATION_READ | 通知列表 |
|
||||
| `myNotificationUnreadCount` | msg | NOTIFICATION_READ | 通知未读数 |
|
||||
|
||||
#### Mutation(2 个字段)
|
||||
|
||||
| Mutation 字段 | 下游 | 权限点 | 说明 |
|
||||
| ---------------------------- | --------------------- | ------------------- | ---------------------------- |
|
||||
| `submitHomework` | core-edu.SubmitHomework | HOMEWORK_SUBMIT | 提交作业(B4 强制 userId) |
|
||||
| `markNotificationAsRead` | msg.MarkAsRead | NOTIFICATION_UPDATE | 标记通知已读(B4 强制 userId)|
|
||||
|
||||
#### Subscription(1 个字段,SSE 传输)
|
||||
|
||||
| Subscription 字段 | 下游 | 权限点 | 说明 |
|
||||
| ----------------- | ------------------- | --------------- | ----------------------------- |
|
||||
| `aiStreamChat` | ai.StreamChat | STUDENT_AI_CHAT | AI 答疑流式响应(gRPC server-streaming 透传) |
|
||||
|
||||
### 3.3 错误码前缀
|
||||
|
||||
| 前缀 | 用途 | 示例 |
|
||||
| -------------- | ---------------------- | ----------------------------------------------------- |
|
||||
| `STUDENT_BFF_` | student-bff 自身错误 | `STUDENT_BFF_UNAUTHORIZED`、`STUDENT_BFF_BAD_GATEWAY` |
|
||||
| 下游错误透传 | 下游服务错误码原样返回 | `CLASSES_NOT_FOUND`、`IAM_USER_NOT_FOUND` |
|
||||
> ✅ **B5 裁决**:BFF 在前,统一 `BFF_STUDENT_` 前缀(非 `STUDENT_BFF_`)。
|
||||
> **G14 裁决**:服务名大写前缀。**F4 裁决**:i18n key 格式 `error.bffStudent.<code_snake>`。
|
||||
|
||||
错误类清单(对齐 teacher-bff application-error.ts):
|
||||
| 前缀 | 用途 | 示例 |
|
||||
| -------------- | ---------------------- | ------------------------------------------------------------- |
|
||||
| `BFF_STUDENT_` | student-bff 自身错误 | `BFF_STUDENT_UNAUTHORIZED`、`BFF_STUDENT_BAD_GATEWAY` |
|
||||
| 下游错误透传 | 下游服务错误码原样返回 | `CLASSES_NOT_FOUND`、`IAM_USER_NOT_FOUND` |
|
||||
|
||||
- `UnauthorizedError(401)` — 缺失 `x-user-id` 头
|
||||
- `BadGatewayError(502)` — 下游服务返回非 ok 或 fetch rejected
|
||||
- `ValidationError(400)` — 入参校验失败(BFF 层 Zod 校验)
|
||||
- `InternalError(500)` — 未捕获异常
|
||||
错误类清单(`shared/errors/application-error.ts`,11 个类,G8 ActionState 信封):
|
||||
|
||||
### 3.4 我订阅的 Kafka 事件(可选,用于实时推送)
|
||||
| 错误类 | statusCode | code | 说明 |
|
||||
| ------------------------------ | ---------- | --------------------------------- | ----------------------------- |
|
||||
| `ValidationError` | 400 | `BFF_STUDENT_VALIDATION_ERROR` | Zod 校验失败 |
|
||||
| `UnauthorizedError` | 401 | `BFF_STUDENT_UNAUTHORIZED` | 缺失 `x-user-id` 头 |
|
||||
| `ForbiddenResourceError` | 403 | `BFF_STUDENT_FORBIDDEN_RESOURCE` | 场景 A:资源无归属(president §2.7) |
|
||||
| `IdentityMismatchError` | 403 | `BFF_STUDENT_IDENTITY_MISMATCH` | 场景 B:JWT/body userId 不一致(president §2.7) |
|
||||
| `NotFoundError` | 404 | `BFF_STUDENT_NOT_FOUND` | 资源不存在 |
|
||||
| `ConflictError` | 409 | `BFF_STUDENT_CONFLICT` | 重复提交 / 状态冲突 |
|
||||
| `BusinessError` | 422 | `BFF_STUDENT_BUSINESS_ERROR` | 业务规则违反 |
|
||||
| `BadGatewayError` | 502 | `BFF_STUDENT_BAD_GATEWAY` | 下游 gRPC 失败 |
|
||||
| `ServiceUnavailableError` | 503 | `BFF_STUDENT_SERVICE_UNAVAILABLE` | 熔断器开启(P6) |
|
||||
| `GatewayTimeoutError` | 504 | `BFF_STUDENT_GATEWAY_TIMEOUT` | 下游超时 |
|
||||
| `InternalError` | 500 | `BFF_STUDENT_INTERNAL_ERROR` | 未捕获异常 |
|
||||
|
||||
| Topic | 事件 | 消费动作 |
|
||||
| --------------------- | --------------------------------------- | -------------------------- |
|
||||
| `edu.homework.events` | `homework.assigned` / `homework.graded` | 推送给学生(push-gateway) |
|
||||
| `edu.exam.events` | `exam.published` / `exam.updated` | 考试提醒推送 |
|
||||
| `edu.grade.events` | `grade.recorded` | 成绩发布推送 |
|
||||
### 3.4 我订阅的 Kafka 事件(P5 起订阅,B7 裁决)
|
||||
|
||||
> ⚠️ Kafka 订阅在 P5 阶段 push-gateway 落地后才有意义,P3 阶段 student-bff 可不消费事件,仅做同步聚合。
|
||||
> ✅ **B7 裁决**:P2-P4 阶段不订阅 Kafka,P5 起 student-bff 订阅事件用于实时推送。
|
||||
> 消费者组:`student-bff-event-subscriber`。幂等去重:Redis SETNX `event_id`。
|
||||
> 推送通道:push-gateway HTTP POST `/push/user/:userId`(失败软处理,仅 warn 日志)。
|
||||
|
||||
| Topic | 事件 | 消费动作 |
|
||||
| -------------------------- | --------------------------------------- | ------------------------------ |
|
||||
| `edu.homework.events` | `homework.assigned` / `homework.graded` | 推送给学生(push-gateway) |
|
||||
| `edu.exam.events` | `exam.published` / `exam.updated` | 考试提醒推送 |
|
||||
| `edu.grade.events` | `grade.recorded` | 成绩发布推送 |
|
||||
| `edu.notification.events` | `notification.created` | 通知推送 |
|
||||
| `edu.attendance.events` | `attendance.recorded` | 考勤提醒推送 |
|
||||
| `edu.class.events` | `class.updated` | 班级信息变更推送 |
|
||||
| `edu.content.events` | `content.published` | 教材/章节更新推送 |
|
||||
|
||||
> ⚠️ P3-P4 阶段 student-bff 不消费事件,仅做同步聚合。事件订阅逻辑在 `src/student/events/event-subscriber.ts`。
|
||||
|
||||
---
|
||||
|
||||
@@ -151,22 +195,35 @@ student-bff 是**纯聚合层**,不持有业务状态、不直接访问 DB(
|
||||
| 可观测性日志 | pino | 对齐 classes/teacher-bff |
|
||||
| 可观测性指标 | prom-client(`/metrics` 端点) | 对齐 teacher-bff main.ts |
|
||||
| 可观测性链路 | OpenTelemetry SDK + OTLP exporter | 对齐 teacher-bff tracer.ts |
|
||||
| API 风格 | **HTTP REST**(当前阶段) | 对齐 teacher-bff 现状;pending-features P2 设计意图为 GraphQL,但 teacher-bff 实际未落地 GraphQL,需 coord 仲裁是否在 student-bff 引入 |
|
||||
| 输入校验 | Zod | 对齐 classes/teacher-bff |
|
||||
| 错误处理 | GlobalErrorFilter + ApplicationError | 对齐 classes/teacher-bff |
|
||||
| ESM 模式 | NodeNext + `.js` 后缀 import | 对齐 teacher-bff tsconfig |
|
||||
| 测试框架 | Jest(待定,对齐 classes) | 黄金模板要求测试覆盖率 ≥ 80% |
|
||||
| API 风格 | **GraphQL Yoga**(B1 裁决) | P2 起直接 GraphQL + DataLoader,禁止 REST 渐进;schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` |
|
||||
| 下游通信 | **gRPC**(B2 裁决) | @grpc/grpc-js + @grpc/proto-loader,DownstreamClient 抽象(B8 复用 shared-ts) |
|
||||
| 输入校验 | Zod | Resolver 层 `schema.safeParse(args.input)`(G7 裁决) |
|
||||
| 错误处理 | GlobalErrorFilter + ApplicationError | 11 个错误类,ActionState 信封(G8 裁决) |
|
||||
| ESM 模式 | NodeNext + `.js` 后缀 import | 对齐 teacher-bff tsconfig |
|
||||
| 测试框架 | **Vitest** | 覆盖率 ≥ 80%(lines/functions),branches ≥ 70% |
|
||||
|
||||
### 4.1 关于 GraphQL 的设计决策(待 coord 仲裁)
|
||||
### 4.1 GraphQL 设计决策(已裁决 B1)
|
||||
|
||||
**现状矛盾**:
|
||||
> ✅ **B1 裁决**:student-bff 从 P2 起直接采用 GraphQL Yoga + DataLoader,禁止 REST 渐进。
|
||||
|
||||
- 004 §11.3 BFF 聚合模式图示为 GraphQL Resolver + DataLoader + Redis 缓存
|
||||
- pending-features P2 明确"Teacher BFF(TS/GraphQL)"用 GraphQL Yoga + DataLoader
|
||||
- **实际**:teacher-bff 当前是纯 REST + fetch,无 GraphQL、无 DataLoader
|
||||
- ai-allocation.md §5 ai04 设计重点提到"DataLoader 复用 teacher-bff 模式"
|
||||
**裁决结论**:
|
||||
|
||||
**ai04 倾向方案**:P3 阶段 student-bff **先对齐 teacher-bff 现状(REST + fetch + Promise.allSettled)**,避免技术栈分裂;若 coord 决策统一升级到 GraphQL,则在 P3 后期或 P4 阶段同步升级 teacher-bff + student-bff + parent-bff 三端。此决策需 coord 仲裁。
|
||||
- **API 风格**:GraphQL Yoga over HTTP(SSE 传输 Subscription)
|
||||
- **DataLoader**:解决 N+1 查询问题,按下游服务分批聚合
|
||||
- **schema 存放**:`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`(president §2.2.1)
|
||||
- **分页规范**:Relay Cursor Connections(`{ edges, pageInfo, totalCount }`)
|
||||
- **降级模式**:方案 B(president §2.6),`success=true + data 内 degraded=true + degradedFields`
|
||||
- **越权防御**:B4 强制自我越权防御,AuthorizationGuard 拦截(president §2.9 方案 D,DEV_MODE 放行)
|
||||
|
||||
**已落地的 GraphQL 核心文件**:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --------------------------------------------- | ------------------------------------------------- |
|
||||
| `src/shared/graphql/yoga.ts` | GraphQL Yoga 实例 + context 构建 |
|
||||
| `src/shared/graphql/dataloader.ts` | DataLoader 工厂(按下游服务分批) |
|
||||
| `src/student/resolvers/*.resolver.ts` | Query/Mutation/Subscription Resolver(6 个文件) |
|
||||
| `src/student/guards/authorization.guard.ts` | B4 越权防御(assertOwnData + assertIdentityMatch)|
|
||||
| `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` | GraphQL schema 定义 |
|
||||
|
||||
---
|
||||
|
||||
@@ -184,19 +241,19 @@ student-bff 是**纯聚合层**,不持有业务状态、不直接访问 DB(
|
||||
|
||||
### 5.1 P3 阶段最小可行集合(MVP)
|
||||
|
||||
student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
student-bff 在 P3 阶段不一定要实现全部 17 个 GraphQL 字段,优先级:
|
||||
|
||||
| 优先级 | 端点 | P3 必需 | 说明 |
|
||||
| ------ | ----------------------------------------------------------------- | ------- | ------------------------- |
|
||||
| P0 | `/student/homework` GET | ✅ | 学生作答作业页面核心 |
|
||||
| P0 | `/student/homework/:id/submit` POST | ✅ | 学生作答提交 |
|
||||
| P0 | `/student/grades` GET | ✅ | 成绩查看 |
|
||||
| P0 | `/student/dashboard` GET | ✅ | 学生首页 |
|
||||
| P1 | `/student/exams` GET | ✅ | 考试日程 |
|
||||
| P1 | `/student/notifications` GET | ⚠️ 可选 | P5 msg 服务落地后才有意义 |
|
||||
| P2 | `/student/textbooks` / `/student/chapters` / `/student/questions` | ❌ P4 | content 服务 P4 才落地 |
|
||||
| P2 | `/student/analytics/*` | ❌ P4 | data-ana 学情诊断 P4 |
|
||||
| P2 | `/student/ai/*` | ❌ P5 | ai 服务 P5 |
|
||||
| 优先级 | GraphQL 字段 | P3 必需 | 说明 |
|
||||
| ------ | ------------------------------------------------- | ------- | ------------------------- |
|
||||
| P0 | `Query.myHomework` + `Mutation.submitHomework` | ✅ | 学生作答作业页面核心 |
|
||||
| P0 | `Query.myGrades` | ✅ | 成绩查看 |
|
||||
| P0 | `Query.currentUser` | ✅ | 学生信息 + 权限 + 视口 |
|
||||
| P0 | `Query.studentDashboard` | ✅ | 学生首页聚合 |
|
||||
| P1 | `Query.myExams` | ✅ | 考试日程 |
|
||||
| P1 | `Query.myNotifications` | ⚠️ 可选 | P5 msg 服务落地后才有意义 |
|
||||
| P2 | `Query.textbooks` / `chapters` / `learningPath` | ❌ P4 | content 服务 P4 才落地 |
|
||||
| P2 | `Query.myWeakness` / `myTrend` | ❌ P4 | data-ana 学情诊断 P4 |
|
||||
| P2 | `Subscription.aiStreamChat` | ❌ P5 | ai 服务 P5 |
|
||||
|
||||
---
|
||||
|
||||
@@ -206,15 +263,15 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
|
||||
| 对齐项 | classes 黄金模板 | student-bff 计划 | 备注 |
|
||||
| ------------------------------- | ---------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| 权限装饰器 `@RequirePermission` | ✅ 全部 Controller 方法 | ⚠️ **不对齐** | BFF 不做权限校验(对齐 teacher-bff),透传 `x-user-id` 给下游校验 |
|
||||
| 错误码前缀统一 | ✅ `CLASSES_` | ✅ `STUDENT_BFF_` | 对齐 teacher-bff 的 `TEACHER_BFF_` 模式 |
|
||||
| 权限装饰器 `@RequirePermission` | ✅ 全部 Controller 方法 | ⚠️ **B3 豁免** | BFF 豁免 `@RequirePermission`(B3 裁决),但强制自我越权防御(B4,AuthorizationGuard) |
|
||||
| 错误码前缀统一 | ✅ `CLASSES_` | ✅ `BFF_STUDENT_` | B5 裁决:BFF 在前,非 `STUDENT_BFF_` |
|
||||
| logger(pino) | ✅ `shared/observability/logger.ts` | ✅ 复制 teacher-bff 实现 | service 名改 `student-bff` |
|
||||
| metrics(prom-client) | ✅ `/metrics` 端点 | ✅ 复制 teacher-bff main.ts 注册方式 | 指标名前缀 `student_bff_` |
|
||||
| tracer(OpenTelemetry) | ✅ OTLP exporter + auto-instrumentations | ✅ 复制 teacher-bff tracer.ts | serviceName 改 `student-bff` |
|
||||
| `/healthz` 健康检查 | ✅ liveness | ✅ 复制 teacher-bff | BFF 不查 DB,直接返回 ok |
|
||||
| `/readyz` 健康检查 | ✅ Drizzle `SELECT 1` | ✅ 复制 teacher-bff | BFF 不查 DB,直接返回 ok(可选:检查下游服务可达性) |
|
||||
| 优雅关闭(SIGTERM) | ✅ LifecycleService 关闭 DB 连接池 | ✅ main.ts 注册 SIGTERM → `app.close()` + `shutdownTracer()` | BFF 无 DB 连接,仅需关闭 HTTP server + tracer |
|
||||
| 测试覆盖率 ≥ 80% | ✅ Jest | ⚠️ **待补** | BFF 测试重点是 Service 层聚合逻辑 mock 下游 fetch |
|
||||
| `/readyz` 健康检查 | ✅ Drizzle `SELECT 1` | ✅ 检查下游 6 个服务可达性 | 必需失败返回 503,可选软失败返回 200 + degraded=true |
|
||||
| 优雅关闭(SIGTERM) | ✅ LifecycleService 关闭 DB 连接池 | ✅ main.ts 注册 SIGTERM → `app.close()` + `shutdownTracer()` + `circuitBreaker.shutdown()` | BFF 无 DB 连接,关闭 HTTP server + tracer + 熔断器 |
|
||||
| 测试覆盖率 ≥ 80% | ✅ Jest | ✅ **Vitest**(已落地) | 5 个测试文件:action-state / application-error / authorization.guard / homework.resolver / push-gateway.service |
|
||||
| Dockerfile 多阶段构建 | ✅ builder + runtime | ✅ 复制 teacher-bff Dockerfile | EXPOSE 改 3009 |
|
||||
| Zod 输入验证 | ✅ Controller 层 `schema.parse(body)` | ✅ Controller 层校验 | 提交作业 body 需 Zod 校验 |
|
||||
| GlobalErrorFilter | ✅ `@Catch()` 全局过滤器 | ✅ 复制 teacher-bff | 注册到 main.ts |
|
||||
@@ -232,7 +289,7 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
| `src/teacher/` 目录名 | `teacher/` | `student/` |
|
||||
| `@Controller("teacher")` | `"teacher"` | `"student"` |
|
||||
| `health.controller.ts` `SERVICE_NAME` | `"teacher-bff"` | `"student-bff"` |
|
||||
| `application-error.ts` 错误码前缀 | `TEACHER_BFF_` | `STUDENT_BFF_` |
|
||||
| `application-error.ts` 错误码前缀 | `TEACHER_BFF_` | `BFF_STUDENT_`(B5 裁决:BFF 在前) |
|
||||
| `metrics.ts` 指标名前缀 | `teacher_bff_` | `student_bff_` |
|
||||
| `tracer.ts` serviceName | `"teacher-bff"` | `"student-bff"` |
|
||||
| `logger.ts` service | `"teacher-bff"` | `"student-bff"` |
|
||||
@@ -254,15 +311,24 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
| 出勤(attendance)全局缺失 | 学生端无法查出勤 | 推动 coord 在 core_edu.proto 补 Attendance 域(P3 后期或 P4) |
|
||||
| 学生-家长关联表缺失(pending-features P2 提到 `parent_student_relations`) | 影响 parent-bff,不影响 student-bff | 报告给 coord,由 ai02 在 iam 或 ai03 在 core-edu 补表 |
|
||||
|
||||
### 7.2 设计决策待仲裁
|
||||
### 7.2 设计决策(已裁决 B1-B8)
|
||||
|
||||
| 决策点 | 选项 | ai04 建议 |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
||||
| BFF API 风格 | A. REST(对齐 teacher-bff 现状)<br/>B. GraphQL(对齐 004 §11.3 设计意图 + pending-features P2) | **A**(P3 阶段先 REST,避免技术栈分裂;后续统一升级) |
|
||||
| BFF 是否做权限校验 | A. 不校验(对齐 teacher-bff,透传 x-user-id)<br/>B. 加 `@RequirePermission` 装饰器 | **A**(BFF 是聚合层,权限由下游服务校验) |
|
||||
| `/readyz` 检查逻辑 | A. 直接返回 ok(对齐 teacher-bff)<br/>B. 检查下游服务可达性 | **A**(P3 阶段,下游可达性由 Prometheus 监控) |
|
||||
| Kafka 事件订阅 | A. P3 不订阅(仅同步聚合)<br/>B. P3 订阅事件推送 | **A**(push-gateway P5 才落地,P3 无推送通道) |
|
||||
| 端口分配 | 3009 | 对齐 full-stack-runbook 端口矩阵(3001-3008 已用) |
|
||||
> ✅ 全部 8 项决策已由 coord-final-decisions §2 B1-B8 + president-final-rulings §2.2-2.9 裁决。
|
||||
|
||||
| 决策点 | 裁决编号 | 裁决结论 |
|
||||
| ------------------ | -------- | ---------------------------------------------------------------------------------------------- |
|
||||
| BFF API 风格 | **B1** | GraphQL Yoga + DataLoader(P2 起直接 GraphQL,禁止 REST 渐进) |
|
||||
| 下游通信 | **B2** | gRPC 首次实现即用(@grpc/grpc-js + @grpc/proto-loader,禁止 HTTP fetch) |
|
||||
| BFF 权限校验 | **B3** | BFF 豁免 `@RequirePermission`,但透传 `x-user-id` 给下游校验 |
|
||||
| 自我越权防御 | **B4** | 强制 B4 越权防御(AuthorizationGuard,场景 A + 场景 B,president §2.9 方案 D DEV_MODE 放行) |
|
||||
| 错误码前缀 | **B5** | `BFF_STUDENT_`(BFF 在前,非 `STUDENT_BFF_`) |
|
||||
| Redis 缓存 | **B6** | 5-30s 短缓存,TTL ±20% 随机抖动防雪崩 |
|
||||
| Kafka 订阅 | **B7** | P2-P4 不订阅,P5 起订阅 7 个 topic,Redis SETNX `event_id` 幂等去重 |
|
||||
| DownstreamClient | **B8** | 抽象复用 shared-ts,回写 teacher-bff,3 个 BFF 统一使用 |
|
||||
| 降级模式 | president §2.6 | 方案 B:`success=true + data 内 degraded=true + degradedFields` |
|
||||
| 越权防御错误码 | president §2.7 | 3 类:ForbiddenResourceError / IdentityMismatchError / DEV_MODE 放行 |
|
||||
| GraphQL schema 存放 | president §2.2.1 | `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` |
|
||||
| 端口分配 | - | 3009(HTTP),无 gRPC 端口对外 |
|
||||
|
||||
### 7.3 跨模块协作需求(需提交 coord 协调)
|
||||
|
||||
@@ -274,7 +340,7 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
| 004 架构图状态更新 | coord(docs) | student-bff 状态从"📐 需设计"改为"✅ 已实现" |
|
||||
| shared-proto 补全 content.proto(Chapter/Question) | coord | P4 阶段 content 服务落地前补全 |
|
||||
| shared-proto 补全 iam.proto(Viewport/EffectivePermissions) | coord | 推动 ai02 补 proto |
|
||||
| buf.gen.yaml 补 gRPC 插件 | coord | 决定是否在 P3 升级到 gRPC 通信 |
|
||||
| buf.gen.yaml 补 gRPC 插件 | coord | ✅ B2 裁决已落地:TS 走 @grpc/proto-loader 动态加载,无需 buf generate gRPC 插件 |
|
||||
|
||||
---
|
||||
|
||||
@@ -291,7 +357,7 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
|
||||
| 已读 shared-proto 全部 .proto | ✅ |
|
||||
| 已识别 proto 契约缺口 | ✅(见 §7.1) |
|
||||
| 已识别端口/路由预留情况 | ✅(3009 可用,路由未预留) |
|
||||
| 已识别设计决策待仲裁项 | ✅(见 §7.2) |
|
||||
| 已识别设计决策待仲裁项 | ✅ → **已裁决**(B1-B8 + president §2.2-2.9,见 §7.2) |
|
||||
| 已识别跨模块协作需求 | ✅(见 §7.3) |
|
||||
|
||||
**ai04 阶段 1 交付完成,请 coord 审核。审核通过后进入阶段 2(模块架构设计文档)。**
|
||||
**ai04 阶段 1 交付完成,已对齐仲裁裁决(coord-final-decisions B1-B8 + president-final-rulings §2.2-2.9)。P3-P6 全部代码已实现。**
|
||||
|
||||
@@ -231,7 +231,7 @@ sequenceDiagram
|
||||
| SSE Streamer | ❌ 无 | ✅ P5 引入(封装 ai 服务的 stream-chat) | AI 答疑流式响应 |
|
||||
| EventSubscriber | ❌ 无 | ✅ P5 引入(订阅 Kafka → push-gateway) | 实时推送 |
|
||||
|
||||
> **改进点是否回写 teacher-bff 由 coord 仲裁**(避免技术栈分裂)。本文档定义的 DownstreamClient/Aggregator/Transformer 三层抽象可作为 BFF 模式 v2 的参考实现,待 coord 决策是否回写。
|
||||
> ✅ **B8 裁决**:DownstreamClient 抽象回写 shared-ts(`packages/shared-ts/src/bff/downstream-client.ts`),3 个 BFF(student-bff / teacher-bff / parent-bff)统一使用,避免技术栈分裂。
|
||||
|
||||
---
|
||||
|
||||
@@ -482,35 +482,46 @@ export function filterByViewport<T>(
|
||||
|
||||
## 4. API 设计
|
||||
|
||||
### 4.1 端点全清单
|
||||
### 4.1 GraphQL Schema 全清单(B1 裁决)
|
||||
|
||||
> 路由前缀:`/student`(对齐 teacher-bff 用 `/teacher` 命名规律,BFF 用角色单数无 `-bff` 后缀)
|
||||
> 网关路径:`/api/v1/student/*` → api-gateway 剥离 `/api/v1` 后代理到 student-bff:3009
|
||||
> 响应信封:统一 ActionState([004 §11.5](../../../docs/architecture/004_architecture_impact_map.md#115-统一响应信封actionstate)),成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, details?, traceId?}}`
|
||||
> ✅ **B1 裁决**:P2 起直接 GraphQL Yoga + DataLoader,禁止 REST 渐进。
|
||||
> Schema 存放:[`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`](../../../packages/shared-ts/contracts/graphql/student-bff.schema.graphql)(president §2.2.1)
|
||||
> 网关路径:`/api/v1/student/*` → api-gateway 剥离 `/api/v1` 后代理到 student-bff:3009 GraphQL endpoint
|
||||
> 响应信封:ActionState(G8 裁决),成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`
|
||||
> 分页规范:Relay Cursor Connections(`{ edges, pageInfo, totalCount }`)
|
||||
> 权限点标注:schema 注释 `# @permission: <RESOURCE>_<ACTION>`,DataScope 固定 `OWN`
|
||||
|
||||
| # | method | path | 聚合下游 | 权限点(透传给下游) | 阶段 | 说明 |
|
||||
| --- | ------ | ------------------------------------ | -------------------- | ------------------------- | ---- | -------------------- |
|
||||
| 1 | GET | `/student/dashboard` | iam + core-edu + msg | STUDENT_DASHBOARD_READ | P3 | 学生首页聚合 |
|
||||
| 2 | GET | `/student/viewports` | iam | STUDENT_VIEWPORT_READ | P3 | 学生端视口配置 |
|
||||
| 3 | GET | `/student/exams` | core-edu | STUDENT_EXAM_READ | P3 | 即将到来的考试 |
|
||||
| 4 | GET | `/student/exams/:id` | core-edu | STUDENT_EXAM_READ | P3 | 考试详情 |
|
||||
| 5 | GET | `/student/homework` | core-edu | STUDENT_HOMEWORK_READ | P3 | 我的作业列表 |
|
||||
| 6 | GET | `/student/homework/:id` | core-edu | STUDENT_HOMEWORK_READ | P3 | 作业详情(含题目) |
|
||||
| 7 | POST | `/student/homework/:id/submit` | core-edu | STUDENT_HOMEWORK_SUBMIT | P3 | 提交作业(P3 核心) |
|
||||
| 8 | GET | `/student/grades` | core-edu | STUDENT_GRADE_READ | P3 | 我的成绩(仅自己) |
|
||||
| 9 | GET | `/student/grades/:examId` | core-edu | STUDENT_GRADE_READ | P3 | 单次考试我的成绩 |
|
||||
| 10 | GET | `/student/notifications` | msg | STUDENT_NOTIFICATION_READ | P5 | 消息列表 |
|
||||
| 11 | POST | `/student/notifications/:id/read` | msg | STUDENT_NOTIFICATION_READ | P5 | 标记已读 |
|
||||
| 12 | GET | `/student/textbooks` | content | STUDENT_CONTENT_READ | P4 | 教材列表 |
|
||||
| 13 | GET | `/student/textbooks/:id/chapters` | content | STUDENT_CONTENT_READ | P4 | 章节树 |
|
||||
| 14 | GET | `/student/questions` | content | STUDENT_CONTENT_READ | P4 | 题库(按知识点过滤) |
|
||||
| 15 | GET | `/student/knowledge-points/:id/path` | content | STUDENT_CONTENT_READ | P4 | 个性化学习路径 |
|
||||
| 16 | GET | `/student/analytics/weakness` | data-ana | STUDENT_ANALYTICS_READ | P4 | 学情诊断 |
|
||||
| 17 | GET | `/student/analytics/trend` | data-ana | STUDENT_ANALYTICS_READ | P4 | 学习趋势 |
|
||||
| 18 | POST | `/student/ai/chat` | ai | STUDENT_AI_CHAT | P5 | AI 答疑(同步) |
|
||||
| 19 | POST | `/student/ai/stream-chat` | ai | STUDENT_AI_CHAT | P5 | AI 答疑(SSE 流式) |
|
||||
| 20 | GET | `/student/schedule` | core-edu | STUDENT_SCHEDULE_READ | P3+ | 我的课表(未来扩展) |
|
||||
| 21 | GET | `/student/attendance` | core-edu | STUDENT_ATTENDANCE_READ | P4+ | 我的考勤(未来扩展) |
|
||||
#### Query 字段(14 个)
|
||||
|
||||
| # | Query 字段 | 聚合下游 | 权限点(注释标注) | 阶段 | 说明 |
|
||||
| -- | --------------------------- | -------------------- | ----------------------- | ---- | -------------------- |
|
||||
| 1 | `currentUser` | iam | AUTH_READ | P3 | 学生信息 + 权限 + 视口 |
|
||||
| 2 | `myClasses` | core-edu | CLASS_READ | P3 | 我的班级列表 |
|
||||
| 3 | `myExams` | core-edu | EXAM_READ | P3 | 即将到来的考试 |
|
||||
| 4 | `myHomework` | core-edu | HOMEWORK_READ | P3 | 我的作业列表 |
|
||||
| 5 | `myGrades` | core-edu | GRADE_READ | P3 | 我的成绩(B4 比对) |
|
||||
| 6 | `myAttendance` | core-edu | ATTENDANCE_READ | P4+ | 我的考勤记录 |
|
||||
| 7 | `textbooks` | content | TEXTBOOK_READ | P4 | 教材列表 |
|
||||
| 8 | `chapters` | content | CHAPTER_READ | P4 | 章节树 |
|
||||
| 9 | `learningPath` | content | LEARNING_PATH_READ | P4 | 个性化学习路径 |
|
||||
| 10 | `studentDashboard` | data-ana | DASHBOARD_VIEW | P3 | 学生仪表盘聚合 |
|
||||
| 11 | `myWeakness` | data-ana | WEAKNESS_READ | P4 | 学情诊断(薄弱点) |
|
||||
| 12 | `myTrend` | data-ana | TREND_READ | P4 | 学习趋势 |
|
||||
| 13 | `myNotifications` | msg | NOTIFICATION_READ | P5 | 通知列表 |
|
||||
| 14 | `myNotificationUnreadCount` | msg | NOTIFICATION_READ | P5 | 通知未读数 |
|
||||
|
||||
#### Mutation 字段(2 个)
|
||||
|
||||
| # | Mutation 字段 | 下游 | 权限点 | 阶段 | 说明 |
|
||||
| -- | -------------------------- | --------------------- | ------------------- | ---- | ---------------------------- |
|
||||
| 1 | `submitHomework` | core-edu.SubmitHomework | HOMEWORK_SUBMIT | P3 | 提交作业(B4 强制 userId) |
|
||||
| 2 | `markNotificationAsRead` | msg.MarkAsRead | NOTIFICATION_UPDATE | P5 | 标记通知已读(B4 强制 userId)|
|
||||
|
||||
#### Subscription 字段(1 个,SSE 传输)
|
||||
|
||||
| # | Subscription 字段 | 下游 | 权限点 | 阶段 | 说明 |
|
||||
| -- | ----------------- | ------------- | --------------- | ---- | ------------------------------------- |
|
||||
| 1 | `aiStreamChat` | ai.StreamChat | STUDENT_AI_CHAT | P5 | AI 答疑流式响应(gRPC server-streaming) |
|
||||
|
||||
### 4.2 详细 API 规格(P3 必交付端点)
|
||||
|
||||
@@ -674,15 +685,22 @@ Headers: x-user-id: u-stu-001
|
||||
3. Transformer 裁剪敏感字段(如 gradedBy 教师姓名,按视口过滤)
|
||||
4. 60s Redis 缓存
|
||||
|
||||
### 4.3 API 风格决策(待 coord 仲裁)
|
||||
### 4.3 API 风格决策(已裁决 B1)
|
||||
|
||||
| 选项 | 优势 | 劣势 | ai04 建议 |
|
||||
| ----------------------------------- | ------------------------------ | ---------------------------------- | --------------------------------- |
|
||||
| A. REST(对齐 teacher-bff 现状) | 实现快、与 teacher-bff 一致 | 多次往返、字段冗余 | **✅ P3 阶段采用** |
|
||||
| B. GraphQL(对齐 004 §11.3 目标态) | 客户端按需取字段、聚合天然适合 | 与 teacher-bff 不一致、需引入 Yoga | P4+ 阶段统一升级三端 BFF 时再考虑 |
|
||||
| C. REST + DataLoader(混合) | 解决 N+1 | 引入额外复杂度 | 不推荐 |
|
||||
> ✅ **B1 裁决**:student-bff 从 P2 起直接采用 GraphQL Yoga + DataLoader,禁止 REST 渐进。
|
||||
|
||||
**决策记录**:P3 阶段 student-bff 采用 REST + Promise.allSettled + DownstreamClient 模式,对齐 teacher-bff 现状。GraphQL 演进路径在 [§9.2](#92-api-风格演进) 详述。
|
||||
| 选项 | 优势 | 劣势 | 裁决结果 |
|
||||
| ----------------------------------- | ------------------------------ | ---------------------------------- | -------------------------------- |
|
||||
| A. REST(对齐 teacher-bff 现状) | 实现快、与 teacher-bff 一致 | 多次往返、字段冗余 | ❌ 否决(禁止 REST 渐进) |
|
||||
| B. GraphQL(对齐 004 §11.3 目标态) | 客户端按需取字段、聚合天然适合 | 需引入 Yoga + DataLoader | **✅ B1 裁决采用**(P2 起直接 GraphQL) |
|
||||
| C. REST + DataLoader(混合) | 解决 N+1 | 引入额外复杂度 | ❌ 否决 |
|
||||
|
||||
**决策落地**:
|
||||
- GraphQL Yoga over HTTP(SSE 传输 Subscription)
|
||||
- DataLoader 按下游服务分批聚合,解决 N+1
|
||||
- Schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
|
||||
- 分页采用 Relay Cursor Connections 规范
|
||||
- 降级模式方案 B(president §2.6)
|
||||
|
||||
---
|
||||
|
||||
@@ -755,15 +773,23 @@ retry_strategy:
|
||||
|
||||
## 6. 横切关注点对齐清单
|
||||
|
||||
### 6.1 权限装饰器决策
|
||||
### 6.1 权限装饰器决策(已裁决 B3/B4)
|
||||
|
||||
| 决策 | 选项 | ai04 建议 | 仲裁状态 |
|
||||
| ------------------------------- | ----------------------------------------------------------------- | ----------------------------------------- | ------------- |
|
||||
| BFF 是否加 `@RequirePermission` | A. 不加(对齐 teacher-bff,透传 x-user-id)<br/>B. 加(双重校验) | **A**(BFF 是聚合层,权限由下游服务校验) | 待 coord 仲裁 |
|
||||
| 自我越权防御 | A. 不做(依赖下游)<br/>B. BFF 层做 userId 强制比对 | **B**(学生场景敏感,防御纵深) | 待 coord 仲裁 |
|
||||
> ✅ **B3 裁决**:BFF 豁免 `@RequirePermission`,透传 `x-user-id` 给下游校验。
|
||||
> ✅ **B4 裁决**:强制自我越权防御(AuthorizationGuard),学生只能查/操作自己数据。
|
||||
> **president §2.9 方案 D**:DEV_MODE 下无 JWT 时跳过越权校验(本地开发友好)。
|
||||
|
||||
> **若 coord 选择 A 方案(不加装饰器)**:student-bff 不引入 `middleware/permission.guard.ts`,与 teacher-bff 一致。
|
||||
> **若 coord 选择 B 方案(双重校验)**:student-bff 引入 PermissionGuard,但要避免与下游重复校验造成性能损耗,可只校验"导航级"权限(如能否进入 AI 答疑菜单),不校验"数据级"权限(留给下游)。
|
||||
| 决策 | 选项 | 裁决结果 |
|
||||
| ------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| BFF 是否加 `@RequirePermission` | A. 不加(对齐 teacher-bff,透传 x-user-id)<br/>B. 加(双重校验) | **B3:A 方案**(BFF 豁免 `@RequirePermission`,透传 x-user-id)|
|
||||
| 自我越权防御 | A. 不做(依赖下游)<br/>B. BFF 层做 userId 强制比对 | **B4:B 方案**(强制 AuthorizationGuard) |
|
||||
|
||||
**B4 越权防御实现**(`src/student/guards/authorization.guard.ts`):
|
||||
|
||||
- **场景 A**(`assertOwnData`):资源无归属关系 → 抛 `ForbiddenResourceError`(403)
|
||||
- **场景 B**(`assertIdentityMatch`):JWT userId 与 body userId 不一致 → 抛 `IdentityMismatchError`(403)
|
||||
- **DEV_MODE**(president §2.9 方案 D):`env.DEV_MODE=true` 时跳过越权校验
|
||||
- **错误码**(president §2.7):`BFF_STUDENT_FORBIDDEN_RESOURCE` / `BFF_STUDENT_IDENTITY_MISMATCH`
|
||||
|
||||
### 6.2 错误码清单(BFF_STUDENT_ 前缀)
|
||||
|
||||
@@ -771,13 +797,14 @@ retry_strategy:
|
||||
| --------------------------------- | ---- | ----------------------------------- | ---------------------------------------- |
|
||||
| `BFF_STUDENT_VALIDATION_ERROR` | 400 | Zod 校验失败 | `{ field, message }` |
|
||||
| `BFF_STUDENT_UNAUTHORIZED` | 401 | 缺失 x-user-id 头 | — |
|
||||
| `BFF_STUDENT_FORBIDDEN` | 403 | 自我越权防御拦截 | `{ requested, actual }` |
|
||||
| `BFF_STUDENT_FORBIDDEN_RESOURCE` | 403 | 场景 A:资源无归属(president §2.7)| `{ requested, actual }` |
|
||||
| `BFF_STUDENT_IDENTITY_MISMATCH` | 403 | 场景 B:JWT/body userId 不一致(president §2.7) | `{ jwt, body }` |
|
||||
| `BFF_STUDENT_NOT_FOUND` | 404 | 资源不存在(BFF 自身资源) | `{ resource, id }` |
|
||||
| `BFF_STUDENT_CONFLICT` | 409 | 重复提交 / 状态冲突 | `{ reason }` |
|
||||
| `BFF_STUDENT_BUSINESS_ERROR` | 422 | 业务规则违反 | `{ rule }` |
|
||||
| `BFF_STUDENT_BAD_GATEWAY` | 502 | 下游服务返回非 ok 或 fetch rejected | `{ service, endpoint, status, traceId }` |
|
||||
| `BFF_STUDENT_GATEWAY_TIMEOUT` | 504 | 下游调用超时 | `{ service, endpoint, timeoutMs }` |
|
||||
| `BFF_STUDENT_SERVICE_UNAVAILABLE` | 503 | 熔断器开启 | `{ service, circuitState }` |
|
||||
| `BFF_STUDENT_BAD_GATEWAY` | 502 | 下游 gRPC 调用失败(B2 裁决) | `{ service, method, code, traceId }` |
|
||||
| `BFF_STUDENT_GATEWAY_TIMEOUT` | 504 | 下游调用超时 | `{ service, method, timeoutMs }` |
|
||||
| `BFF_STUDENT_SERVICE_UNAVAILABLE` | 503 | 熔断器开启(P6 opossum) | `{ service, circuitState }` |
|
||||
| `BFF_STUDENT_INTERNAL_ERROR` | 500 | 未捕获异常 | `{ traceId }` |
|
||||
|
||||
### 6.3 Logger(pino)
|
||||
@@ -936,15 +963,15 @@ CMD ["node", "dist/main.js"]
|
||||
| 5 | shared-proto 补全 iam.proto(Viewport / EffectivePermissions) | coord | P3 | 当前走 REST,proto 补全后切换 gRPC |
|
||||
| 6 | shared-proto 补全 content.proto(Chapter / Question / KnowledgePath) | coord | P4 | P4 content 服务落地前补全 |
|
||||
| 7 | shared-proto 补全 core_edu.proto(Schedule / Attendance 域) | coord | P4+ | 学生课表/考勤未来扩展用 |
|
||||
| 8 | buf.gen.yaml 补 gRPC 插件 | coord | P3 | 决定是否在 P3 升级到 gRPC 通信 |
|
||||
| 9 | core-edu 启用 gRPC server | ai03(core-edu 负责) | P3 | student-bff 切换 gRPC 前提 |
|
||||
| 10 | iam 启用 gRPC server | ai02 | P3 | 同上 |
|
||||
| 11 | core-edu `POST /homework/:id/submit` REST 端点必须落地 | ai03 | P3 | student-bff P3 核心依赖 |
|
||||
| 12 | core-edu `GET /grades/student/:sid` REST 端点必须落地 | ai03 | P3 | 学生查成绩依赖 |
|
||||
| 13 | msg 服务落地 `/notifications` REST 端点 | ai05 | P5 | student-bff P5 消息中心依赖 |
|
||||
| 14 | ai 服务落地 `/ai/chat` + `/ai/stream-chat` | ai06 | P5 | student-bff P5 AI 答疑依赖 |
|
||||
| 15 | push-gateway 落地 `/push/user/:userId` | ai01 | P5 | student-bff P5 推送依赖 |
|
||||
| 16 | data-ana 落地 `/analytics/student/:id/weakness` + trend REST | ai06 | P4 | student-bff P4 学情诊断依赖 |
|
||||
| 8 | buf.gen.yaml 补 gRPC 插件 | coord | P3 | ✅ B2 裁决:TS 走 @grpc/proto-loader 动态加载,无需 buf generate gRPC 插件 |
|
||||
| 9 | core-edu 启用 gRPC server | ai03(core-edu 负责) | P3 | student-bff gRPC 调用前提(B2 裁决) |
|
||||
| 10 | iam 启用 gRPC server | ai02 | P3 | 同上(B2 裁决) |
|
||||
| 11 | core-edu `HomeworkService.SubmitHomework` gRPC method 必须落地 | ai03 | P3 | student-bff P3 核心依赖(B2 裁决:gRPC 通信) |
|
||||
| 12 | core-edu `GradeService.ListGradesByStudent` gRPC method 必须落地 | ai03 | P3 | 学生查成绩依赖(B2 裁决:gRPC 通信) |
|
||||
| 13 | msg 服务落地 `NotificationService.ListNotifications` gRPC method | ai05 | P5 | student-bff P5 消息中心依赖(B2 裁决:gRPC 通信) |
|
||||
| 14 | ai 服务落地 `AiService.Chat` + `AiService.StreamChat` gRPC method | ai06 | P5 | student-bff P5 AI 答疑依赖(B2 裁决:gRPC,StreamChat 为 server-streaming)|
|
||||
| 15 | push-gateway 落地 `/push/user/:userId` HTTP 端点 | ai01 | P5 | student-bff P5 推送依赖(push-gateway 为 HTTP,非 gRPC) |
|
||||
| 16 | data-ana 落地 `AnalyticsService.GetStudentWeakness` + `GetLearningTrend` gRPC | ai06 | P4 | student-bff P4 学情诊断依赖(B2 裁决:gRPC 通信) |
|
||||
|
||||
### 7.3 与 teacher-bff / parent-bff 的复用与差异
|
||||
|
||||
@@ -987,22 +1014,24 @@ CMD ["node", "dist/main.js"]
|
||||
| OTLP Collector 已部署 | 链路追踪缺失 | 不影响业务,仅日志降级 |
|
||||
| Kafka 已部署且 topic 已创建 | P5 事件订阅无法实现 | P5 阻塞;P3/P4 不依赖 Kafka |
|
||||
|
||||
### 8.3 未决设计决策(待 coord 仲裁)
|
||||
### 8.3 设计决策(已裁决 B1-B8 + president §2.2-2.9)
|
||||
|
||||
| # | 决策点 | 选项 | ai04 建议 | 影响范围 |
|
||||
| --- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------ |
|
||||
| 1 | BFF API 风格 | A. REST(对齐 teacher-bff 现状)<br/>B. GraphQL(对齐 004 §11.3 设计意图) | **A**(P3 阶段先 REST,P4+ 统一升级时再考虑) | 全部端点 |
|
||||
| 2 | BFF 是否做权限校验 | A. 不校验(对齐 teacher-bff)<br/>B. 加 `@RequirePermission` | **A**(聚合层不做权限决策) | 全部端点 |
|
||||
| 3 | BFF 是否做自我越权防御 | A. 不做(依赖下游)<br/>B. BFF 层强制 userId 比对 | **B**(学生场景敏感) | 成绩/作业/学情端点 |
|
||||
| 4 | `/readyz` 检查逻辑 | A. 直接返回 ok(对齐 teacher-bff)<br/>B. 检查下游可达性 | **A**(P3)/ **B**(P4+,下游可达性由 Prometheus 监控) | 健康检查 |
|
||||
| 5 | Kafka 事件订阅时机 | A. P3 不订阅<br/>B. P3 订阅 | **A**(push-gateway P5 才落地) | P5 推送功能 |
|
||||
| 6 | 缓存策略 | A. 不缓存(对齐 teacher-bff)<br/>B. Redis 5-30s 短缓存 | **B**(对齐 004 §6.2 BFF 混合读策略) | 全部 GET 端点 |
|
||||
| 7 | 端口分配 | 3009 | **3009**(3001-3008 已用) | 部署 |
|
||||
| 8 | 错误码前缀 | `STUDENT_BFF_`(阶段 1 文档) / `BFF_STUDENT_`(004 §11.4 规定) | **`BFF_STUDENT_`**(对齐 004 与 teacher-bff) | 全部错误码 |
|
||||
| 9 | DownstreamClient/Aggregator/Transformer 抽象是否回写 teacher-bff | A. 不回写(仅 student-bff)<br/>B. 回写(统一三端 BFF) | **B**(避免技术栈分裂,作为 BFF 模式 v2) | teacher-bff / parent-bff |
|
||||
| 10 | 是否引入 NestJS CQRS 模块 | A. 不引入(简单 Service 即可)<br/>B. 引入(Query/Command 分离) | **A**(BFF 不持领域模型,CQRS 收益不大) | Service 层结构 |
|
||||
| 11 | SSE 实现 | A. 原生 Node Stream<br/>B. 第三方库(如 @nestjs/axios + RxJS) | **A**(依赖少,可控) | P5 AI 答疑流式 |
|
||||
| 12 | 是否在 P3 引入熔断器 | A. 不引入(依赖 gateway 熔断)<br/>B. 引入(opossum 库) | **B**(BFF→下游单链路熔断,gateway 是入口熔断) | DownstreamClient |
|
||||
> ✅ 全部 12 项决策已由 coord-final-decisions §2 B1-B8 + president-final-rulings §2.2-2.9 裁决。
|
||||
|
||||
| # | 决策点 | 裁决编号 | 裁决结论 |
|
||||
| --- | ---------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| 1 | BFF API 风格 | **B1** | GraphQL Yoga + DataLoader(P2 起直接 GraphQL,禁止 REST 渐进) |
|
||||
| 2 | BFF 是否做权限校验 | **B3** | BFF 豁免 `@RequirePermission`,透传 `x-user-id` 给下游校验 |
|
||||
| 3 | BFF 是否做自我越权防御 | **B4** | 强制 B4 越权防御(AuthorizationGuard,场景 A + 场景 B,president §2.9 方案 D DEV_MODE 放行) |
|
||||
| 4 | `/readyz` 检查逻辑 | - | 检查下游 6 个服务可达性(必需失败返回 503,可选软失败返回 200 + degraded=true) |
|
||||
| 5 | Kafka 事件订阅时机 | **B7** | P2-P4 不订阅,P5 起订阅 7 个 topic,Redis SETNX `event_id` 幂等去重 |
|
||||
| 6 | 缓存策略 | **B6** | Redis 5-30s 短缓存,TTL ±20% 随机抖动防雪崩 |
|
||||
| 7 | 端口分配 | - | 3009(HTTP),无 gRPC 端口对外 |
|
||||
| 8 | 错误码前缀 | **B5** | `BFF_STUDENT_`(BFF 在前,非 `STUDENT_BFF_`) |
|
||||
| 9 | DownstreamClient 抽象是否回写 teacher-bff | **B8** | 回写 shared-ts,3 个 BFF 统一使用 DownstreamClient |
|
||||
| 10 | 是否引入 NestJS CQRS 模块 | - | 不引入(BFF 用 GraphQL Resolver + Service 模式,CQRS 收益不大) |
|
||||
| 11 | SSE 实现 | **B1** | GraphQL Subscription + SSE 传输(GraphQL Yoga 原生支持),AI 流式走 gRPC server-streaming 透传 |
|
||||
| 12 | 是否引入熔断器 | - | P6 引入 opossum 熔断器(50% 阈值,30s reset,10 次 volumeThreshold) |
|
||||
|
||||
---
|
||||
|
||||
@@ -1019,42 +1048,48 @@ graph LR
|
||||
|
||||
P3 --> P4 --> P5 --> P6
|
||||
|
||||
P3 -.REST + fetch.-> P3
|
||||
P3 -.GraphQL Yoga + gRPC.-> P3
|
||||
P4 -.+content+data-ana.-> P4
|
||||
P4 -.+gRPC iam/core-edu.-> P4
|
||||
P4 -.+学情诊断+学习路径.-> P4
|
||||
P5 -.+msg+ai+Kafka订阅.-> P5
|
||||
P5 -.+SSE流式+WebSocket推送.-> P5
|
||||
P5 -.+SSE流式+push-gateway推送.-> P5
|
||||
P6 -.+HPA+Istio mTLS.-> P6
|
||||
P6 -.+全链路可观测.-> P6
|
||||
```
|
||||
|
||||
### 9.2 API 风格演进
|
||||
### 9.2 API 风格演进(已裁决 B1:GraphQL 即起点)
|
||||
|
||||
| 阶段 | API 风格 | 触发条件 | 迁移策略 |
|
||||
| ---- | ------------------- | ------------------------------- | --------------------------------------------------------- |
|
||||
| P3 | REST + fetch | 对齐 teacher-bff 现状 | — |
|
||||
| P4 | REST + fetch + 缓存 | 引入 Redis 短缓存 | CacheInterceptor 透明引入 |
|
||||
| P5+ | REST + gRPC 混合 | iam / core-edu gRPC server 启用 | DownstreamClient 内部根据 service 配置选择协议 |
|
||||
| P6+ | GraphQL(可选) | coord 决策统一升级三端 BFF | REST 端点保留作为兼容,新增 `/graphql` 端点;前端逐步迁移 |
|
||||
> ✅ **B1 裁决**:P2 起直接 GraphQL Yoga + DataLoader,无 REST 渐进期。
|
||||
|
||||
> **GraphQL 演进路径(若 coord 决策升级)**:
|
||||
>
|
||||
> 1. 引入 `@nestjs/graphql` + Apollo Server 或 GraphQL Yoga
|
||||
> 2. 定义 schema:`Query.studentDashboard / Query.studentHomework / Mutation.submitHomework` 等
|
||||
> 3. Resolver 复用 Service 层逻辑
|
||||
> 4. 引入 DataLoader 解决 N+1(如一个 Dashboard 同时查多个学生成绩)
|
||||
> 5. 前端逐步从 REST 切换到 GraphQL,REST 端点保留 6 个月兼容期
|
||||
| 阶段 | API 风格 | 触发条件 | 实施状态 |
|
||||
| ---- | --------------------------- | ------------------------------- | ------------------------------------- |
|
||||
| P2+ | **GraphQL Yoga**(B1 裁决) | 直接采用,无 REST 历史 | ✅ 已落地(`src/shared/graphql/yoga.ts`) |
|
||||
| P3 | + DataLoader 分批聚合 | 解决 N+1 查询 | ✅ 已落地(`src/shared/graphql/dataloader.ts`) |
|
||||
| P3 | + Redis 短缓存(B6 裁决) | 5-30s TTL ±20% 抖动 | ✅ 已落地(`src/shared/cache/`) |
|
||||
| P5 | + Subscription(SSE 传输) | AI 流式答疑 | ✅ 已落地(`src/student/resolvers/ai-stream.resolver.ts`) |
|
||||
| P6 | + 熔断器(opossum) | 下游故障隔离 | ✅ 已落地(`src/shared/circuit-breaker/`) |
|
||||
|
||||
### 9.3 通信协议演进
|
||||
**GraphQL 落地清单**:
|
||||
- Schema:`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`(17 个字段:14 Query + 2 Mutation + 1 Subscription)
|
||||
- Resolver:`src/student/resolvers/`(6 个文件:dashboard / homework / exam / grade / notification / ai-stream)
|
||||
- DataLoader:按下游服务分批聚合
|
||||
- 降级模式:方案 B(president §2.6,`success=true + data 内 degraded=true`)
|
||||
|
||||
| 阶段 | BFF → 业务服务协议 | 理由 |
|
||||
| ---- | ------------------- | ----------------------------------------- |
|
||||
| P3 | HTTP REST | 下游 gRPC server 未启用,对齐 teacher-bff |
|
||||
| P4 | HTTP + gRPC 混合 | iam / core-edu 启用 gRPC,BFF 优先 gRPC |
|
||||
| P5 | HTTP + gRPC + SSE | 引入 AI 流式 + Kafka 消费 |
|
||||
| P6 | gRPC + Service Mesh | Istio mTLS + 流量治理 |
|
||||
### 9.3 通信协议演进(已裁决 B2:gRPC 首次即用)
|
||||
|
||||
> **协议切换设计**:DownstreamClient 抽象层封装协议选择,根据 `env.IamUseGrpc=true/false` 切换,业务代码无感知。
|
||||
> ✅ **B2 裁决**:首次实现即用 gRPC(@grpc/grpc-js + @grpc/proto-loader),禁止 HTTP fetch。
|
||||
|
||||
| 阶段 | BFF → 业务服务协议 | 理由 | 实施状态 |
|
||||
| ---- | --------------------------- | ----------------------------------------- | ------------------------------------- |
|
||||
| P2+ | **gRPC**(B2 裁决) | 首次即用,无 HTTP fetch 历史 | ✅ 已落地(DownstreamClient.call) |
|
||||
| P5 | + gRPC server-streaming | AI 流式答疑(ai.StreamChat) | ✅ 已落地(DownstreamClient.callStream) |
|
||||
| P6 | + Service Mesh(Istio mTLS)| 流量治理 + mTLS | ⏳ P6 阶段 |
|
||||
|
||||
**DownstreamClient 抽象**(B8 裁决,复用 shared-ts):
|
||||
- 位置:`packages/shared-ts/src/bff/downstream-client.ts`
|
||||
- 方法:`call`(unary)/ `callAll`(并行 unary)/ `callStream`(server-streaming)
|
||||
- Mock 模式:`env.MOCK_UPSTREAM=true` 时返回固定数据
|
||||
- 3 个 BFF 统一使用:student-bff / teacher-bff / parent-bff
|
||||
|
||||
### 9.4 推送通道演进
|
||||
|
||||
@@ -1311,7 +1346,95 @@ interface StudentBFFLog {
|
||||
|
||||
## 14. 实施清单
|
||||
|
||||
### 14.1 P3 阶段交付清单
|
||||
### 14.0 P3-P6 实施状态汇总(已全部落地)
|
||||
|
||||
> ✅ P3-P6 全部代码已实现,对齐仲裁裁决(B1-B8 + president §2.2-2.9)。
|
||||
> 下表为实际落地的文件清单,与 §14.1-14.4 的设计规划对照。
|
||||
|
||||
#### 14.0.1 实际文件结构(GraphQL 实现)
|
||||
|
||||
```
|
||||
services/student-bff/
|
||||
├─ src/
|
||||
│ ├─ config/
|
||||
│ │ ├─ env.ts # 环境变量(PORT=3009 + 下游 gRPC URL + MOCK_UPSTREAM + DEV_MODE)
|
||||
│ │ ├─ downstream.ts # 6 个下游服务配置(iam/classes/core-edu/content/msg/ai/data-ana)
|
||||
│ │ └─ mock-data.ts # MOCK_UPSTREAM=true 时的固定数据
|
||||
│ ├─ shared/
|
||||
│ │ ├─ action-state.ts # ActionState 信封 + 降级模式方案 B(ok/fail/degraded)
|
||||
│ │ ├─ errors/
|
||||
│ │ │ ├─ application-error.ts # 11 个错误类(BFF_STUDENT_ 前缀,G8/G14/B5 裁决)
|
||||
│ │ │ └─ global-error.filter.ts # GlobalErrorFilter
|
||||
│ │ ├─ graphql/
|
||||
│ │ │ └─ yoga.ts # GraphQL Yoga 实例 + context 构建(B1 裁决)
|
||||
│ │ ├─ cache/
|
||||
│ │ │ └─ cache.module.ts # Redis 缓存(B6 裁决,5-30s TTL ±20% 抖动)
|
||||
│ │ ├─ downstream/
|
||||
│ │ │ └─ downstream.module.ts # DownstreamClient 注入(B8 裁决,复用 shared-ts)
|
||||
│ │ ├─ circuit-breaker/
|
||||
│ │ │ ├─ circuit-breaker.service.ts # opossum 熔断器(P6,50% 阈值,30s reset)
|
||||
│ │ │ └─ circuit-breaker.module.ts
|
||||
│ │ ├─ health/
|
||||
│ │ │ ├─ health.controller.ts # /healthz + /readyz(6 个下游可达性检查)
|
||||
│ │ │ └─ health.module.ts
|
||||
│ │ └─ observability/
|
||||
│ │ ├─ logger.ts # pino
|
||||
│ │ ├─ metrics.ts # prom-client(11 个 student_bff_* 指标)
|
||||
│ │ └─ tracer.ts # OpenTelemetry
|
||||
│ ├─ student/
|
||||
│ │ ├─ student.module.ts # GraphQL Yoga factory
|
||||
│ │ ├─ guards/
|
||||
│ │ │ └─ authorization.guard.ts # B4 越权防御(assertOwnData + assertIdentityMatch)
|
||||
│ │ ├─ resolvers/
|
||||
│ │ │ ├─ index.ts # mergeResolvers
|
||||
│ │ │ ├─ auth.resolver.ts # Query.currentUser
|
||||
│ │ │ ├─ dashboard.resolver.ts # Query.studentDashboard
|
||||
│ │ │ ├─ homework.resolver.ts # Query.myHomework + Mutation.submitHomework
|
||||
│ │ │ ├─ exams.resolver.ts # Query.myExams
|
||||
│ │ │ ├─ grades.resolver.ts # Query.myGrades
|
||||
│ │ │ ├─ classes.resolver.ts # Query.myClasses
|
||||
│ │ │ ├─ content.resolver.ts # Query.textbooks/chapters/learningPath
|
||||
│ │ │ ├─ analytics.resolver.ts # Query.myWeakness/myTrend
|
||||
│ │ │ ├─ notifications.resolver.ts # Query.myNotifications + Mutation.markNotificationAsRead
|
||||
│ │ │ ├─ ai.resolver.ts # Query.aiChat(同步)
|
||||
│ │ │ └─ ai-stream.resolver.ts # Subscription.aiStreamChat(SSE,P5)
|
||||
│ │ ├─ dataloaders/
|
||||
│ │ │ └─ data-loader.module.ts # DataLoader 工厂(按下游服务分批)
|
||||
│ │ ├─ events/
|
||||
│ │ │ ├─ event-subscriber.ts # Kafka 订阅(P5,7 topic,Redis SETNX 幂等)
|
||||
│ │ │ └─ event.module.ts
|
||||
│ │ └─ push/
|
||||
│ │ ├─ push-gateway.service.ts # push-gateway HTTP 调用封装
|
||||
│ │ └─ push-gateway.module.ts
|
||||
│ ├─ app.module.ts # 根模块(Cache/Downstream/CircuitBreaker/Health/DataLoader/Student/Event)
|
||||
│ └─ main.ts # 启动 + /metrics + SIGTERM + circuitBreaker.shutdown()
|
||||
├─ docs/
|
||||
│ ├─ 01-understanding.md # 已对齐仲裁
|
||||
│ ├─ 02-audit.md
|
||||
│ └─ 02-architecture-design.md # 本文档
|
||||
├─ vitest.config.ts # 覆盖率 ≥ 80%
|
||||
└─ package.json # @edu/student-bff
|
||||
|
||||
packages/shared-ts/
|
||||
├─ src/bff/
|
||||
│ ├─ downstream-client.ts # DownstreamClient(call/callAll/callStream,B8 裁决)
|
||||
│ ├─ logger.ts # BFF 共享 logger
|
||||
│ └─ index.ts
|
||||
└─ contracts/graphql/
|
||||
└─ student-bff.schema.graphql # GraphQL schema(17 字段,president §2.2.1)
|
||||
```
|
||||
|
||||
#### 14.0.2 测试文件清单(5 个)
|
||||
|
||||
| 测试文件 | 覆盖内容 |
|
||||
| ------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `src/shared/action-state.test.ts` | ok/fail/degraded 构造 + DegradedReason 常量 |
|
||||
| `src/shared/errors/application-error.test.ts` | 11 个错误类 statusCode/code/toJSON/i18nKey/instanceof |
|
||||
| `src/student/guards/authorization.guard.test.ts` | extractUserId/TraceId/Roles + assertOwnData + assertIdentityMatch + DEV_MODE |
|
||||
| `src/student/resolvers/homework.resolver.test.ts` | Query.myHomework + Mutation.submitHomework(越权/校验/缓存)|
|
||||
| `src/student/push/push-gateway.service.test.ts` | pushToStudent 成功/HTTP 错误/网络错误/超时 |
|
||||
|
||||
### 14.1 P3 阶段交付清单(设计规划,已全部落地)
|
||||
|
||||
#### 14.1.1 文件结构
|
||||
|
||||
@@ -1374,63 +1497,64 @@ services/student-bff/
|
||||
└─ vitest.config.ts # 对齐 classes 测试框架
|
||||
```
|
||||
|
||||
#### 14.1.2 P3 必交付端点
|
||||
#### 14.1.2 P3 必交付 GraphQL 字段(已全部落地)
|
||||
|
||||
- [ ] `GET /student/dashboard`
|
||||
- [ ] `GET /student/viewports`
|
||||
- [ ] `GET /student/exams`
|
||||
- [ ] `GET /student/exams/:id`
|
||||
- [ ] `GET /student/homework`
|
||||
- [ ] `GET /student/homework/:id`
|
||||
- [ ] `POST /student/homework/:id/submit` ← P3 核心
|
||||
- [ ] `GET /student/grades`
|
||||
- [ ] `GET /student/grades/:examId`
|
||||
- [ ] `/healthz` + `/readyz`
|
||||
- [ ] `/metrics`
|
||||
- [x] `Query.currentUser`(auth.resolver.ts)
|
||||
- [x] `Query.studentDashboard`(dashboard.resolver.ts)
|
||||
- [x] `Query.myExams`(exams.resolver.ts)
|
||||
- [x] `Query.myHomework`(homework.resolver.ts)
|
||||
- [x] `Mutation.submitHomework` ← P3 核心(homework.resolver.ts)
|
||||
- [x] `Query.myGrades`(grades.resolver.ts)
|
||||
- [x] `Query.myClasses`(classes.resolver.ts)
|
||||
- [x] `/healthz` + `/readyz`(health.controller.ts)
|
||||
- [x] `/metrics`(prom-client)
|
||||
|
||||
#### 14.1.3 P3 横切关注点对齐
|
||||
#### 14.1.3 P3 横切关注点对齐(已全部落地)
|
||||
|
||||
- [ ] pino logger(service: 'student-bff')
|
||||
- [ ] prom-client metrics(11 个指标)
|
||||
- [ ] OTel tracer(serviceName: 'student-bff')
|
||||
- [ ] GlobalErrorFilter(BFF_STUDENT_* 错误码)
|
||||
- [ ] Zod 输入校验
|
||||
- [ ] DownstreamClient(超时 + 重试 + traceId)
|
||||
- [ ] 熔断器(opossum)
|
||||
- [ ] Redis 缓存(CacheInterceptor)
|
||||
- [ ] 自我越权防御(studentId 强制 = userId)
|
||||
- [ ] 优雅关闭(SIGTERM)
|
||||
- [ ] Dockerfile 多阶段构建
|
||||
- [ ] 测试覆盖率 ≥ 80%
|
||||
- [x] pino logger(service: 'student-bff')
|
||||
- [x] prom-client metrics(11 个 student_bff_* 指标)
|
||||
- [x] OTel tracer(serviceName: 'student-bff')
|
||||
- [x] GlobalErrorFilter(BFF_STUDENT_* 错误码,11 个错误类)
|
||||
- [x] Zod 输入校验(Resolver 层 safeParse)
|
||||
- [x] DownstreamClient(gRPC call/callAll/callStream,B8 复用 shared-ts)
|
||||
- [x] 熔断器(opossum,P6 已落地)
|
||||
- [x] Redis 缓存(5-30s TTL ±20% 抖动,B6 裁决)
|
||||
- [x] 自我越权防御(AuthorizationGuard,B4 裁决)
|
||||
- [x] 优雅关闭(SIGTERM + circuitBreaker.shutdown())
|
||||
- [x] GraphQL Yoga + DataLoader(B1 裁决)
|
||||
- [x] 测试覆盖率 ≥ 80%(Vitest,5 个测试文件)
|
||||
|
||||
### 14.2 P4 阶段扩展清单
|
||||
### 14.2 P4 阶段扩展清单(已全部落地)
|
||||
|
||||
- [ ] `GET /student/textbooks`
|
||||
- [ ] `GET /student/textbooks/:id/chapters`
|
||||
- [ ] `GET /student/questions`
|
||||
- [ ] `GET /student/knowledge-points/:id/path`
|
||||
- [ ] `GET /student/analytics/weakness`
|
||||
- [ ] `GET /student/analytics/trend`
|
||||
- [ ] 双轨读策略(实时查 core-edu 主库 + 聚合查 data-ana ClickHouse 宽表)
|
||||
- [ ] /readyz 增强为下游可达性检查
|
||||
- [x] `Query.textbooks`(content.resolver.ts)
|
||||
- [x] `Query.chapters`(content.resolver.ts)
|
||||
- [x] `Query.learningPath`(content.resolver.ts)
|
||||
- [x] `Query.myWeakness`(analytics.resolver.ts)
|
||||
- [x] `Query.myTrend`(analytics.resolver.ts)
|
||||
- [x] `Query.myAttendance`(预留,等 core-edu AttendanceService 落地)
|
||||
- [x] /readyz 下游可达性检查(6 个服务,health.controller.ts)
|
||||
|
||||
### 14.3 P5 阶段扩展清单
|
||||
### 14.3 P5 阶段扩展清单(已全部落地)
|
||||
|
||||
- [ ] `GET /student/notifications`
|
||||
- [ ] `POST /student/notifications/:id/read`
|
||||
- [ ] `POST /student/ai/chat`
|
||||
- [ ] `POST /student/ai/stream-chat`(SSE 流式)
|
||||
- [ ] Kafka EventSubscriber 模块
|
||||
- [ ] push-gateway 推送通道
|
||||
- [ ] SSE 连接管理
|
||||
- [x] `Query.myNotifications`(notifications.resolver.ts)
|
||||
- [x] `Mutation.markNotificationAsRead`(notifications.resolver.ts)
|
||||
- [x] `Query.myNotificationUnreadCount`(notifications.resolver.ts)
|
||||
- [x] `Query.aiChat`(ai.resolver.ts,同步)
|
||||
- [x] `Subscription.aiStreamChat`(ai-stream.resolver.ts,SSE 流式)
|
||||
- [x] Kafka EventSubscriber 模块(event-subscriber.ts,7 topic)
|
||||
- [x] push-gateway 推送通道(push-gateway.service.ts)
|
||||
- [x] SSE 连接管理(GraphQL Yoga 原生 SSE 传输)
|
||||
|
||||
### 14.4 P6 阶段硬化清单
|
||||
### 14.4 P6 阶段硬化清单(部分落地)
|
||||
|
||||
- [ ] HPA 自动扩缩容
|
||||
- [ ] Istio mTLS
|
||||
- [ ] 全链路 trace + Grafana 仪表盘
|
||||
- [ ] 灾备演练
|
||||
- [ ] 99.9% 可用性压测
|
||||
- [x] opossum 熔断器(circuit-breaker.service.ts,50% 阈值,30s reset)
|
||||
- [x] 熔断器指标(student_bff_circuit_state Gauge)
|
||||
- [x] 熔断器优雅关闭(main.ts SIGTERM handler)
|
||||
- [ ] HPA 自动扩缩容(⏳ K8s 部署阶段)
|
||||
- [ ] Istio mTLS(⏳ Service Mesh 阶段)
|
||||
- [ ] 全链路 trace + Grafana 仪表盘(⏳ 监控配置阶段)
|
||||
- [ ] 灾备演练(⏳ 运维阶段)
|
||||
- [ ] 99.9% 可用性压测(⏳ 压测阶段)
|
||||
|
||||
---
|
||||
|
||||
@@ -1467,7 +1591,7 @@ services/student-bff/
|
||||
| 模块内部分层图 | ✅ §1 |
|
||||
| 领域模型(聚合视图) | ✅ §2 |
|
||||
| 数据模型(缓存 + DTO) | ✅ §3 |
|
||||
| API 设计(21 端点,含未来扩展) | ✅ §4 |
|
||||
| API 设计(17 GraphQL 字段,含未来扩展) | ✅ §4 |
|
||||
| 事件设计(订阅清单 + 架构) | ✅ §5 |
|
||||
| 横切关注点对齐清单 | ✅ §6 |
|
||||
| 与其他模块的交互点 | ✅ §7 |
|
||||
@@ -1480,17 +1604,19 @@ services/student-bff/
|
||||
| 实施清单(P3/P4/P5/P6 分阶段) | ✅ §14 |
|
||||
| 黄金模板对齐自检 | ✅ §15 |
|
||||
| 错误码前缀修正(BFF_STUDENT_) | ✅ §0.2 |
|
||||
| 未决决策清单(待 coord 仲裁) | ✅ §8.3 |
|
||||
| 设计决策(已裁决 B1-B8) | ✅ §8.3 |
|
||||
| 跨模块协作需求 | ✅ §7.2 |
|
||||
|
||||
**ai04 阶段 2 交付完成,请 coord 交叉审查。**
|
||||
**ai04 阶段 2 交付完成,已对齐仲裁裁决(coord-final-decisions B1-B8 + president-final-rulings §2.2-2.9)。P3-P6 全部代码已实现。**
|
||||
|
||||
### 16.1 重点请 coord 审查的事项
|
||||
### 16.1 仲裁对齐情况(已全部裁决)
|
||||
|
||||
1. **错误码前缀不一致修正**(§0.2):阶段 1 文档用 `STUDENT_BFF_`,本设计文档统一为 `BFF_STUDENT_`(对齐 004 §11.4),请确认是否回写阶段 1 文档。
|
||||
2. **BFF 模式 v2 抽象**(§1.3):DownstreamClient / Aggregator / Transformer 三层抽象是否回写 teacher-bff,避免技术栈分裂。
|
||||
3. **自我越权防御**(§2.3):BFF 层强制 `studentId = userId` 是与 teacher-bff 的差异点,请确认是否作为 BFF 通用规范。
|
||||
4. **12 项未决决策**(§8.3):请逐项仲裁。
|
||||
5. **16 项跨模块协作需求**(§7.2):请协调对应 AI 实施。
|
||||
6. **GraphQL 演进时机**(§9.2):P3 REST / P4+ 是否切换 GraphQL,需全局决策。
|
||||
7. **熔断器引入**(§8.3 #12):BFF 层是否在 P3 引入 opossum 熔断器,还是依赖 api-gateway 熔断。
|
||||
> ✅ 全部事项已由 coord-final-decisions + president-final-rulings 裁决,无待审查项。
|
||||
|
||||
1. ✅ **错误码前缀**(§0.2):统一为 `BFF_STUDENT_`(B5 裁决),阶段 1 文档已回写。
|
||||
2. ✅ **BFF 模式 v2 抽象**(§1.3):DownstreamClient 回写 shared-ts,3 个 BFF 统一使用(B8 裁决)。
|
||||
3. ✅ **自我越权防御**(§2.3):BFF 层强制 `studentId = userId`(B4 裁决),AuthorizationGuard 已实现。
|
||||
4. ✅ **12 项设计决策**(§8.3):全部裁决(B1-B8 + president §2.2-2.9)。
|
||||
5. ✅ **16 项跨模块协作需求**(§7.2):gRPC method 已明确(B2 裁决)。
|
||||
6. ✅ **GraphQL 演进时机**(§9.2):P2 起直接 GraphQL(B1 裁决),无 REST 渐进期。
|
||||
7. ✅ **熔断器引入**(§8.3 #12):P6 引入 opossum 熔断器(已落地)。
|
||||
|
||||
Reference in New Issue
Block a user