feat(student-bff): 完整实现 student-bff 聚合层

包含 src 全部实现、Dockerfile、shared-ts/bff 包等
This commit is contained in:
SpecialX
2026-07-10 19:10:51 +08:00
parent e5ca4c6c7b
commit f585080e70
55 changed files with 7141 additions and 252 deletions

View File

@@ -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 + DataLoaderP2 起直接 GraphQL禁止 REST 渐进)
> - B2: 下游通信 = gRPC 首次实现即用(@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetch
> - B5: 错误码前缀 = `BFF_STUDENT_`BFF 在前,非 `STUDENT_BFF_`
> - B8: DownstreamClient 抽象复用 shared-ts回写 teacher-bff3 BFF 统一)
> - 本文档中早期将通信方式写为 HTTP REST/fetch、错误码前缀写为 `STUDENT_BFF_` 的部分已修正,以本对齐说明为准。
---
@@ -15,8 +22,8 @@
| 层级 | **L4 BFF 聚合层**004 §3.1 六层架构) |
| 上游调用方 | api-gatewayGo Gin反向代理 `/api/v1/student/*` → student-bff:3009 |
| 下游被调用方 | iam、core-edu、content、data-ana按 004 §4 服务依赖图) |
| 通信方式(入) | HTTP RESTapi-gateway → student-bff当前阶段设计意图为 gRPC004 §4.1 |
| 通信方式(出) | HTTP fetch当前阶段对齐 teacher-bff 模式);设计意图为 gRPC004 §4.1 |
| 通信方式(入) | **GraphQL Yoga over HTTP**api-gateway → student-bff:3009B1 裁决P2 起直接 GraphQL禁止 REST 渐进 |
| 通信方式(出) | **gRPC**@grpc/grpc-js + @grpc/proto-loaderB2 裁决:首次实现即用 gRPC禁止 HTTP fetch |
| 微前端对接 | student-portalai07 负责P3 阶段)通过 api-gateway 调用 student-bff |
| 推送通道 | push-gatewayP5 阶段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-ts3 个 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` | 获取有效权限列表 |
| classescore-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 methodB2 裁决) | 用途 |
| ------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------ |
| iam | `IamService.GetUserInfo` | `iam.GetUserInfo` | 获取学生个人信息 + roles + dataScope |
| iam | `IamService.GetViewports`proto 缺失) | `iam.GetViewports` | 获取学生端导航视口 |
| iam | `IamService.GetEffectivePermissions`proto 缺失) | `iam.GetEffectivePermissions` | 获取有效权限列表 |
| classescore-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-streamingB2 裁决) | AI 答疑 |
### 3.2 我暴露的 API 端点student-bff 对外)
### 3.2 我暴露的 GraphQL APIstudent-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 流式) |
#### Query14 个字段)
| 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 | 通知未读数 |
#### Mutation2 个字段)
| Mutation 字段 | 下游 | 权限点 | 说明 |
| ---------------------------- | --------------------- | ------------------- | ---------------------------- |
| `submitHomework` | core-edu.SubmitHomework | HOMEWORK_SUBMIT | 提交作业B4 强制 userId |
| `markNotificationAsRead` | msg.MarkAsRead | NOTIFICATION_UPDATE | 标记通知已读B4 强制 userId|
#### Subscription1 个字段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` | 场景 BJWT/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 阶段不订阅 KafkaP5 起 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-loaderDownstreamClient 抽象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/functionsbranches ≥ 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 BFFTS/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 HTTPSSE 传输 Subscription
- **DataLoader**:解决 N+1 查询问题,按下游服务分批聚合
- **schema 存放**`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`president §2.2.1
- **分页规范**Relay Cursor Connections`{ edges, pageInfo, totalCount }`
- **降级模式**:方案 Bpresident §2.6`success=true + data 内 degraded=true + degradedFields`
- **越权防御**B4 强制自我越权防御AuthorizationGuard 拦截president §2.9 方案 DDEV_MODE 放行)
**已落地的 GraphQL 核心文件**
| 文件 | 职责 |
| --------------------------------------------- | ------------------------------------------------- |
| `src/shared/graphql/yoga.ts` | GraphQL Yoga 实例 + context 构建 |
| `src/shared/graphql/dataloader.ts` | DataLoader 工厂(按下游服务分批) |
| `src/student/resolvers/*.resolver.ts` | Query/Mutation/Subscription Resolver6 个文件) |
| `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 裁决但强制自我越权防御B4AuthorizationGuard |
| 错误码前缀统一 | ✅ `CLASSES_` | ✅ `BFF_STUDENT_` | B5 裁决BFF 在前,非 `STUDENT_BFF_` |
| loggerpino | ✅ `shared/observability/logger.ts` | ✅ 复制 teacher-bff 实现 | service 名改 `student-bff` |
| metricsprom-client | ✅ `/metrics` 端点 | ✅ 复制 teacher-bff main.ts 注册方式 | 指标名前缀 `student_bff_` |
| tracerOpenTelemetry | ✅ 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 + DataLoaderP2 起直接 GraphQL禁止 REST 渐进) |
| 下游通信 | **B2** | gRPC 首次实现即用(@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetch |
| BFF 权限校验 | **B3** | BFF 豁免 `@RequirePermission`,但透传 `x-user-id` 给下游校验 |
| 自我越权防御 | **B4** | 强制 B4 越权防御AuthorizationGuard场景 A + 场景 Bpresident §2.9 方案 D DEV_MODE 放行) |
| 错误码前缀 | **B5** | `BFF_STUDENT_`BFF 在前,非 `STUDENT_BFF_` |
| Redis 缓存 | **B6** | 5-30s 短缓存TTL ±20% 随机抖动防雪崩 |
| Kafka 订阅 | **B7** | P2-P4 不订阅P5 起订阅 7 个 topicRedis SETNX `event_id` 幂等去重 |
| DownstreamClient | **B8** | 抽象复用 shared-ts回写 teacher-bff3 个 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` |
| 端口分配 | - | 3009HTTP无 gRPC 端口对外 |
### 7.3 跨模块协作需求(需提交 coord 协调)
@@ -274,7 +340,7 @@ student-bff 在 P3 阶段不一定要实现全部 14 个端点,优先级:
| 004 架构图状态更新 | coorddocs | student-bff 状态从"📐 需设计"改为"✅ 已实现" |
| shared-proto 补全 content.protoChapter/Question | coord | P4 阶段 content 服务落地前补全 |
| shared-proto 补全 iam.protoViewport/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 全部代码已实现**