# 模块架构设计文档 — student-bff > AI 标识:ai04 > 阶段:阶段 2(模块架构设计) > 日期:2026-07-09 > 状态:待 coord 交叉审查 > 关联文档: > > - [阶段 1 理解确认书](./01-understanding.md) > - [阶段 1 服务审计表](./02-audit.md) > - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) > - [ai-allocation §7 阶段 2 模板](../../../docs/architecture/ai-allocation.md) > - [pending-features P3](../../../docs/architecture/roadmap/pending-features.md) > - [project_rules](../../../.trae/rules/project_rules.md) > - [多 AI 协作指南](../../../docs/standards/multi-ai-collaboration.md) > - 参考实现:[teacher-bff](../../teacher-bff/)、[classes 黄金模板](../../classes/) --- ## 0. 设计原则与文档导读 ### 0.1 设计原则 | # | 原则 | 在 student-bff 的具体体现 | | --- | ------------------ | ----------------------------------------------------------------------------------------------------------------- | | P1 | 单一职责 | student-bff 仅做"学习场景域聚合 + 裁剪 + 协议转换",不持有业务状态、不直接访问 DB | | P2 | 契约先行 | 所有跨服务调用先对齐 proto / REST 契约;契约变更通过 coord 统一管理 | | P3 | DataScope 严格隔离 | 学生视角强制 `DataScope=SELF`,BFF 层透传 `x-user-id`,下游按 userId 过滤;BFF 不做权限决策但做"自我越权防御" | | P4 | 渐进演进 | 通信协议(REST → gRPC)、API 风格(REST → GraphQL)、推送通道(同步 → SSE/WebSocket)按阶段演进,预留切换点 | | P5 | 故障隔离 | 下游调用容错(超时 + 重试 + 熔断 + 降级),单服务故障不影响聚合整体,Dashboard 部分失败返回 partial 数据 + 警告位 | | P6 | 可观测性三支柱 | pino 日志 + prom-client 指标 + OpenTelemetry 链路,全链路 traceId 透传 | | P7 | 黄金模板对齐 | 1:1 克隆 teacher-bff 模式,与 classes/teacher-bff 共享 shared/ 实现,差异点收敛到 11 处命名改造 | | P8 | 可逆决策优于不可逆 | API 风格、缓存策略、熔断阈值、推送通道均为可逆决策;端口分配、错误码前缀、路由前缀为不可逆决策需提前固化 | | P9 | 为未来场景预留 | 多角色复用(学习委员/课代表)、多端适配(H5/小程序)、AI 答疑流式、家长查询子女数据等场景需在设计上预留扩展点 | ### 0.2 与阶段 1 文档的差异修正 > 阶段 1 文档(01-understanding.md / 02-audit.md)中将错误码前缀写为 `STUDENT_BFF_`,与 [004 §11.4 错误码前缀矩阵](../../../docs/architecture/004_architecture_impact_map.md#114-错误码前缀矩阵) 规定的 `BFF_STUDENT_`(BFF 在前)不一致。本设计文档统一修正为 **`BFF_STUDENT_`**,对齐 teacher-bff 的 `BFF_TEACHER_`、parent-bff 的 `BFF_PARENT_` 模式。阶段 1 文档保留历史记录,以本文档为准。 ### 0.3 文档结构 1. [模块内部分层图](#1-模块内部分层图) — 物理分层与调用链 2. [领域模型(聚合视图)](#2-领域模型聚合视图) — BFF 无领域模型,定义"场景聚合"概念 3. [数据模型(缓存与 DTO)](#3-数据模型缓存与-dto) — BFF 不持 DB,定义 Redis Schema 与传输 DTO 4. [API 设计](#4-api-设计) — 14 个端点 + 阶段化交付 5. [事件设计](#5-事件设计) — BFF 订阅事件用于实时推送(P5) 6. [横切关注点对齐清单](#6-横切关注点对齐清单) — 完整对齐 classes/teacher-bff 7. [与其他模块的交互点](#7-与其他模块的交互点契约清单) — 完整契约矩阵 8. [风险与假设](#8-风险与假设) — 技术风险、外部依赖、未决决策 9. [演进路线图](#9-演进路线图) — P3/P4/P5/P6 各阶段演进路径(长远规划) 10. [扩展点设计](#10-扩展点设计) — 为未来场景预留 11. [性能与容量规划](#11-性能与容量规划) 12. [安全与合规](#12-安全与合规) 13. [可观测性详细设计](#13-可观测性详细设计) 14. [实施清单](#14-实施清单) --- ## 1. 模块内部分层图 ### 1.1 物理分层 ```mermaid graph TB subgraph Client["客户端"] Portal[student-portal
Next.js MF Remote] Mobile[H5/小程序
未来扩展] end subgraph Gateway["网关层"] APIGW[api-gateway
路由 + JWT 鉴权 + 限流] end subgraph StudentBFF["student-bff(本模块)"] direction TB Controller["@Controller('student')
HTTP 入口 + Zod 校验 + ActionState 信封"] Service["StudentService
聚合编排 + DataScope 透传"] Aggregator["Aggregator 层
场景化聚合策略"] Transformer["Transformer 层
响应裁剪 + 视口过滤"] Cache["Cache 层
Redis 5-30s 短缓存"] Client2["DownstreamClient
统一封装 fetch + 超时 + 重试 + traceId 透传"] SSE["SSE Streamer
P5 AI 答疑流式响应"] EventSub["Event Subscriber
P5 订阅 Kafka 推送给 push-gateway"] CrossCutting["横切关注点
GlobalErrorFilter + Logger + Metrics + Tracer + Health"] Controller --> Service Service --> Aggregator Aggregator --> Cache Cache --> Client2 Aggregator --> Transformer Controller -.可选.-> SSE EventSub -.P5.-> Client2 CrossCutting -.拦截.-> Controller end subgraph Downstream["下游业务服务"] IAM[iam
:3002] CoreEdu[core-edu
:3004] Content[content
:3005 P4] Msg[msg
:3007 P5] DataAna[data-ana
:3006 P4] AI[ai
:3008 P5] end subgraph Push["推送层"] PushGW[push-gateway
:8081 P5] end subgraph Infra["基础设施"] Redis[(Redis 7)] Kafka[(Kafka)] OTel[OTLP Collector] Prom[Prometheus] end Portal --> APIGW Mobile --> APIGW APIGW -->|HTTP /api/v1/student/*| Controller Client2 -->|HTTP / gRPC P3+| IAM Client2 -->|HTTP / gRPC P3+| CoreEdu Client2 -->|HTTP / gRPC P4+| Content Client2 -->|HTTP / gRPC P5+| Msg Client2 -->|HTTP / gRPC P4+| DataAna Client2 -->|HTTP / gRPC P5+| AI Cache --> Redis EventSub --> Kafka EventSub -.推送.-> PushGW CrossCutting --> OTel CrossCutting --> Prom ``` ### 1.2 调用链详解 #### 1.2.1 同步聚合链(P3 主要链路) ```mermaid sequenceDiagram participant P as student-portal participant GW as api-gateway participant C as StudentController participant S as StudentService participant A as Aggregator participant Cache as Redis participant DC as DownstreamClient participant IAM as iam participant Core as core-edu P->>GW: GET /api/v1/student/dashboard
Authorization: Bearer GW->>GW: RS256 公钥校验 + 提取 userId GW->>C: GET /student/dashboard
x-user-id: u-xxx
x-request-id: r-xxx C->>C: extractUserId(req) → u-xxx
Zod 校验 query C->>S: getDashboard(u-xxx) S->>A: aggregateDashboard(u-xxx) A->>Cache: GET student:dashboard:u-xxx alt 缓存命中 Cache-->>A: { data, cachedAt } A-->>S: data else 缓存未命中 par 并行下游 A->>DC: callIAM('/iam/me', u-xxx) DC->>IAM: GET /iam/me x-user-id: u-xxx
x-request-id: r-xxx IAM-->>DC: 200 { user, roles, dataScope } and A->>DC: callCoreEdu('/homework/class/:cid', u-xxx) DC->>Core: GET /homework/class/:cid x-user-id: u-xxx Core-->>DC: 200 { homework: [...] } end DC-->>A: 合并结果 + 部分失败标记 A->>A: Transformer 裁剪 + 视口过滤 A->>Cache: SET student:dashboard:u-xxx EX 15 A-->>S: data end S-->>C: DashboardData C-->>GW: 200 { success: true, data, meta: { cachedAt, partial } } GW-->>P: 200 响应 ``` #### 1.2.2 SSE 流式链(P5 AI 答疑) ```mermaid sequenceDiagram participant P as student-portal participant GW as api-gateway participant C as StudentController participant SSE as SSE Streamer participant AI as ai 服务 P->>GW: POST /api/v1/student/ai/stream-chat
Accept: text/event-stream GW->>C: POST /student/ai/stream-chat C->>SSE: streamChat(messages, model) SSE->>AI: POST /ai/stream-chat
Authorization: Bearer loop 流式分块 AI-->>SSE: chunk: {content: "...", done: false} SSE-->>P: data: {content: "...", done: false} end AI-->>SSE: chunk: {done: true} SSE-->>P: data: {done: true} SSE-->>C: stream closed ``` #### 1.2.3 事件订阅推送链(P5) ```mermaid sequenceDiagram participant K as Kafka participant ES as EventSubscriber participant DS as DownstreamClient participant Push as push-gateway participant P as student-portal K->>ES: homework.graded (studentId=u-xxx, grade=90) ES->>ES: 解析 studentId → 查 Redis 在线 session ES->>DS: callPushGW('/push/user/u-xxx', payload) DS->>Push: POST /push/user/u-xxx Push-->>P: WebSocket 推送
{type: 'homework.graded', payload} ``` ### 1.3 与 teacher-bff 分层对比 | 层 | teacher-bff(现状) | student-bff(设计目标) | 改进理由 | | ---------------- | ------------------- | --------------------------------------------------------------- | ---------------------------- | | Controller | ✅ 有 | ✅ 复制 | 对齐 | | Service | ✅ 有 | ✅ 复制 + 拆分 Aggregator/Transformer | 单一职责 | | Aggregator | ❌ 散落在 Service | ✅ 独立层,封装并行调用策略 | 解耦编排逻辑,便于测试 | | Transformer | ❌ 无 | ✅ 独立层,裁剪响应字段 + 视口过滤 | 减少冗余字段,适配多端 | | Cache | ❌ 无 | ✅ NestJS CacheInterceptor + Redis | 对齐 004 §6.2 BFF 混合读策略 | | DownstreamClient | ❌ 散落 fetch() | ✅ 统一封装:超时 + 重试 + 熔断 + traceId 透传 + 错误信封归一化 | 解决 teacher-bff 痛点 | | SSE Streamer | ❌ 无 | ✅ P5 引入(封装 ai 服务的 stream-chat) | AI 答疑流式响应 | | EventSubscriber | ❌ 无 | ✅ P5 引入(订阅 Kafka → push-gateway) | 实时推送 | > ✅ **B8 裁决**:DownstreamClient 抽象回写 shared-ts(`packages/shared-ts/src/bff/downstream-client.ts`),3 个 BFF(student-bff / teacher-bff / parent-bff)统一使用,避免技术栈分裂。 --- ## 2. 领域模型(聚合视图) > BFF 不持有 DDD 领域模型(不写 DB、不定义聚合根)。本节定义 student-bff 的"**场景聚合视图**"概念,用于组织 Service 层逻辑。 ### 2.1 场景聚合视图清单 | 聚合视图 | 业务含义 | 主要下游服务 | 读写特性 | DataScope | | -------------------- | ---------------------------- | ------------------------- | --------------- | --------- | | StudentDashboard | 学生首页:个人信息+待办+未读 | iam + core-edu + msg | 读 / 聚合 | SELF | | StudentExams | 我的考试列表(即将到来) | core-edu | 读 | SELF | | StudentHomework | 我的作业列表 + 提交 | core-edu | 读 + 写(提交) | SELF | | StudentGrades | 我的成绩历史 | core-edu | 读 | SELF | | StudentNotifications | 消息中心:列表 + 已读 | msg | 读 + 写(已读) | SELF | | StudentContent | 教材 + 章节 + 题库练习 | content | 读 | SELF | | StudentAnalytics | 学情诊断 + 学习趋势 | data-ana | 读 | SELF | | StudentAI | AI 答疑(同步 + 流式) | ai | 写(chat) | SELF | | StudentSchedule | 我的课表(未来扩展) | core-edu(schedule 域) | 读 | SELF | | StudentAttendance | 我的考勤(未来扩展) | core-edu(attendance 域) | 读 | SELF | ### 2.2 聚合视图间关系 ```mermaid graph LR Dashboard[StudentDashboard
聚合视图] Dashboard --> Exams[StudentExams] Dashboard --> Homework[StudentHomework] Dashboard --> Notif[StudentNotifications] Dashboard --> Grades[StudentGrades] Homework -.提交后触发.-> Grades Grades -.数据流.-> Analytics[StudentAnalytics] Analytics -.推荐.-> Content[StudentContent] Content -.知识点弱项.-> AI[StudentAI] Schedule[StudentSchedule
未来扩展] Attendance[StudentAttendance
未来扩展] Dashboard -.未来.-> Schedule Dashboard -.未来.-> Attendance ``` ### 2.3 DataScope=SELF 的强制实现 BFF 不做权限决策(对齐 teacher-bff),但做**自我越权防御**: ```typescript // StudentService 伪代码示意 async listGrades(userId: string, query: ListGradesQuery): Promise { // 防御:学生只能查自己的成绩 if (query.studentId && query.studentId !== userId) { throw new ForbiddenError('STUDENT_CANNOT_VIEW_OTHERS_GRADES', { requested: query.studentId, actual: userId, }); } // 强制 studentId = userId const safeQuery = { ...query, studentId: userId }; return this.client.callCoreEdu('/grades/student/' + userId, safeQuery); } ``` > **设计权衡**:BFF 层做"自我越权防御"是 P3 安全硬化项。即使下游 core-edu 也按 DataScope 过滤,BFF 层先拦截可避免无效下游调用、提升审计能力、降低跨用户数据泄露风险。这与 teacher-bff 完全透传 x-user-id 不同,因为学生场景的越权风险面更敏感(成绩/作业/学情)。 --- ## 3. 数据模型(缓存与 DTO) > BFF 不持有 DB Schema。本节定义 Redis 缓存 key 规范、TTL 策略、传输 DTO。 ### 3.1 Redis 缓存 Schema #### 3.1.1 Key 命名规范 | 用途 | Key 模式 | TTL | 失效策略 | | -------------- | --------------------------------------------- | ----- | --------------------------------- | | 学生 Dashboard | `student:dashboard:{userId}` | 15s | 短 TTL + 主动失效(成绩发布事件) | | 学生考试列表 | `student:exams:{userId}:{classId}` | 30s | 短 TTL | | 学生作业列表 | `student:homework:{userId}:{classId}` | 30s | 短 TTL + 提交后失效 | | 学生成绩列表 | `student:grades:{userId}:{page}` | 60s | 短 TTL + 成绩发布事件 | | 学生通知列表 | `student:notifications:{userId}:{page}` | 15s | 短 TTL + 已读后失效 | | 教材列表 | `student:textbooks:{gradeId}:{subjectId}` | 300s | 中 TTL | | 章节树 | `student:chapters:{textbookId}` | 300s | 中 TTL | | 题库列表 | `student:questions:{knowledgePointId}:{page}` | 60s | 短 TTL | | 学情诊断 | `student:analytics:weakness:{userId}` | 300s | 中 TTL(每日刷新) | | 学习趋势 | `student:analytics:trend:{userId}:{range}` | 600s | 长 TTL(趋势变化慢) | | 学生视口缓存 | `student:viewports:{userId}` | 300s | 中 TTL + 角色变更事件 | | 在线 session | `student:session:{userId}` | 30min | 滑动过期(P5 推送用) | | SSE 连接映射 | `student:sse:{userId}` | 30min | 连接关闭即删 | > **Key 设计原则**: > > - 全部以 `student:` 前缀,避免与 teacher-bff (`teacher:`)、parent-bff (`parent:`) 冲突 > - 按 userId 维度隔离,便于按用户失效 > - TTL 分档:15s(高频变更)/ 30-60s(中频)/ 300s(低频)/ 600s(趋势) #### 3.1.2 缓存失效策略 ```mermaid flowchart TD W[写操作:提交作业/已读通知] --> INV[主动失效相关 key] E[Kafka 事件:成绩发布/作业批改] --> SUB[EventSubscriber 消费] SUB --> INV2[失效 student:grades:userId:*] T[TTL 到期] --> NATURAL[自然过期] INV --> REDIS[DEL key] INV2 --> REDIS ``` ### 3.2 传输 DTO 设计 > DTO 用 Zod schema 定义,自动推导 TS 类型,避免 `unknown` 滥用(解决 teacher-bff 痛点)。 #### 3.2.1 下游响应包装 ```typescript // shared/dto/downstream-envelope.ts import { z } from "zod"; export const DownstreamEnvelopeSchema = ( dataSchema: T, ) => z.object({ success: z.boolean(), data: dataSchema.optional(), error: z .object({ code: z.string(), message: z.string(), details: z.unknown().optional(), traceId: z.string().optional(), }) .optional(), }); export type DownstreamEnvelope = z.infer< ReturnType>> >; ``` #### 3.2.2 学生端核心 DTO ```typescript // student/dto/student-dashboard.dto.ts export const StudentDashboardSchema = z.object({ user: z.object({ id: z.string(), name: z.string(), avatar: z.string().nullable(), grade: z.string(), class: z.object({ id: z.string(), name: z.string() }), }), pendingHomework: z.array( z.object({ id: z.string(), title: z.string(), dueDate: z.string(), subject: z.string().optional(), }), ), upcomingExams: z.array( z.object({ id: z.string(), title: z.string(), examDate: z.string(), daysLeft: z.number(), }), ), unreadNotifications: z.number(), lastGrade: z .object({ examTitle: z.string(), score: z.number(), date: z.string() }) .nullable(), }); export type StudentDashboard = z.infer; ``` #### 3.2.3 输入 Schema(Zod) ```typescript // student/dto/student-inputs.dto.ts export const SubmitHomeworkSchema = z.object({ answers: z .array( z.object({ questionId: z.string(), content: z.string().max(5000), attachments: z.array(z.string().url()).max(5).optional(), }), ) .min(1) .max(100), }); export const ListGradesQuerySchema = z.object({ page: z.coerce.number().int().min(1).default(1), pageSize: z.coerce.number().int().min(1).max(50).default(20), subject: z.string().optional(), startDate: z.string().datetime().optional(), endDate: z.string().datetime().optional(), }); export const AIChatSchema = z.object({ messages: z .array( z.object({ role: z.enum(["user", "assistant"]), content: z.string().max(8000), }), ) .min(1) .max(20), model: z .enum(["gpt-4o-mini", "baichuan-53b", "local-qwen-7b"]) .default("gpt-4o-mini"), context: z .object({ subject: z.string(), knowledgePointId: z.string().optional() }) .optional(), }); ``` ### 3.3 视口裁剪模型 > 学生端视口来自 iam 的 `getEffectivePermissions`,BFF 用视口过滤可见字段。 ```typescript // student/transformers/viewport-filter.ts export interface StudentViewport { // L1 导航:可见菜单项 navigation: string[]; // ['dashboard', 'homework', 'grades', 'content', 'analytics', 'ai'] // L4 数据范围:学生固定 SELF,但可细分(如禁看历史成绩) dataScope: { showHistoryGrades: boolean; // 是否显示历史成绩 showClassRanking: boolean; // 是否显示班级排名 enableAIChat: boolean; // 是否启用 AI 答疑 }; } export function filterByViewport( data: T, viewport: StudentViewport, ): Partial { // 按视口裁剪字段,如 enableAIChat=false 则隐藏 AI 入口数据 } ``` --- ## 4. API 设计 ### 4.1 GraphQL Schema 全清单(B1 裁决) > ✅ **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: _`,DataScope 固定 `OWN` #### 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 必交付端点) #### 4.2.1 GET `/student/dashboard` **请求**: ``` GET /student/dashboard Headers: x-user-id: u-stu-001 x-request-id: r-abc123 cookie: access_token= Query: ?classId=c-001 (optional, 默认从 iam 推导) ``` **响应 200**: ```json { "success": true, "data": { "user": { "id": "u-stu-001", "name": "张三", "avatar": null, "grade": "高一", "class": { "id": "c-001", "name": "高一(1)班" } }, "pendingHomework": [ { "id": "h-001", "title": "数学作业第三章", "dueDate": "2026-07-12", "subject": "数学" } ], "upcomingExams": [ { "id": "e-001", "title": "期中考试", "examDate": "2026-07-15", "daysLeft": 6 } ], "unreadNotifications": 3, "lastGrade": { "examTitle": "月考", "score": 92, "date": "2026-07-01" } }, "meta": { "cachedAt": "2026-07-09T10:00:00Z", "partial": false, "degradedServices": [] } } ``` **部分降级响应**(下游 msg 不可达): ```json { "success": true, "data": { "...": "...", "unreadNotifications": 0 }, "meta": { "partial": true, "degradedServices": ["msg"], "traceId": "r-abc123" } } ``` **错误响应**: | HTTP | code | 触发条件 | | ---- | ---------------------------- | -------------------------------- | | 401 | `BFF_STUDENT_UNAUTHORIZED` | 缺失 x-user-id 头 | | 502 | `BFF_STUDENT_BAD_GATEWAY` | 必需下游全部失败(iam+core-edu) | | 500 | `BFF_STUDENT_INTERNAL_ERROR` | 未捕获异常 | #### 4.2.2 POST `/student/homework/:id/submit`(P3 核心 mutation) **请求**: ``` POST /student/homework/h-001/submit Headers: x-user-id: u-stu-001 Content-Type: application/json Body: { "answers": [ { "questionId": "q-001", "content": "答案是..." }, { "questionId": "q-002", "content": "解答过程...", "attachments": ["https://oss/edu/hw.pdf"] } ] } ``` **响应 200**: ```json { "success": true, "data": { "submissionId": "sub-001", "homeworkId": "h-001", "submittedAt": "2026-07-09T10:30:00Z", "status": "submitted" } } ``` **错误响应**: | HTTP | code | 触发条件 | | ---- | ------------------------------ | ------------------------------ | | 400 | `BFF_STUDENT_VALIDATION_ERROR` | Zod 校验失败(answers 为空等) | | 409 | `BFF_STUDENT_CONFLICT` | 重复提交 / 已过截止时间 | | 502 | `BFF_STUDENT_BAD_GATEWAY` | core-edu 不可达 | **BFF 行为**: 1. Zod 校验 body 2. 透传 `x-user-id` 给 core-edu 3. core-edu 写入 homework_submissions + Outbox 事件 `edu.teaching.homework.submitted` 4. BFF 失效 `student:homework:u-stu-001:*` 缓存 5. 返回提交回执 #### 4.2.3 GET `/student/grades`(自我越权防御示例) **请求**: ``` GET /student/grades?page=1&pageSize=20&subject=数学 Headers: x-user-id: u-stu-001 ``` **响应 200**: ```json { "success": true, "data": { "grades": [ { "id": "g-001", "examTitle": "月考", "subject": "数学", "score": 92, "gradedAt": "2026-07-01T15:00:00Z", "feedback": "解题思路清晰" } ], "pagination": { "page": 1, "pageSize": 20, "total": 5 } } } ``` **BFF 行为**: 1. 强制 `studentId = userId`(自我越权防御) 2. 调用 core-edu `GET /grades/student/u-stu-001` 3. Transformer 裁剪敏感字段(如 gradedBy 教师姓名,按视口过滤) 4. 60s Redis 缓存 ### 4.3 API 风格决策(已裁决 B1) > ✅ **B1 裁决**:student-bff 从 P2 起直接采用 GraphQL Yoga + DataLoader,禁止 REST 渐进。 | 选项 | 优势 | 劣势 | 裁决结果 | | ----------------------------------- | ------------------------------ | ---------------------------------- | -------------------------------- | | 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) --- ## 5. 事件设计 > BFF **不发布**领域事件(无业务事务)。BFF 可**订阅**事件用于实时推送(P5 阶段)。 ### 5.1 订阅事件清单(P5 阶段) | Topic | 事件 | 消费动作 | 幂等性 | | -------------------------------- | ------------------------- | ----------------------------------------- | ------------------- | | `edu.teaching.homework.assigned` | 教师布置作业 | 查 Redis 学生 session → 推送 push-gateway | event_id SETNX 去重 | | `edu.teaching.homework.graded` | 作业批改完成 | 失效 `student:grades:*` + 推送 | event_id SETNX 去重 | | `edu.teaching.exam.published` | 考试发布 | 失效 `student:exams:*` + 推送考试提醒 | event_id SETNX 去重 | | `edu.teaching.exam.updated` | 考试更新(时间/地点变更) | 失效 `student:exams:*` + 推送变更通知 | event_id SETNX 去重 | | `edu.teaching.grade.recorded` | 成绩录入 | 失效 `student:grades:*` + 推送成绩通知 | event_id SETNX 去重 | | `edu.identity.user.role_changed` | 学生角色变更 | 失效 `student:viewports:userId` | event_id SETNX 去重 | | `edu.notification.events` | 通知事件 | 推送给学生 | event_id SETNX 去重 | ### 5.2 事件订阅架构(P5) ```mermaid graph LR K[(Kafka)] ES[EventSubscriber
NestJS Module] Idempotency[(Redis SETNX
event_id 去重)] Session[(Redis
学生在线 session)] DC[DownstreamClient] Push[push-gateway] Cache[Redis Cache Invalidation] K -->|homework.graded| ES ES --> Idempotency Idempotency -->|首次| Session Session -->|在线| DC DC --> Push ES --> Cache ``` ### 5.3 事件订阅消费者组设计 ```yaml # P5 阶段 Kafka 消费者组配置 consumer_group: student-bff-event-subscriber topics: - edu.teaching.homework.assigned - edu.teaching.homework.graded - edu.teaching.exam.published - edu.teaching.exam.updated - edu.teaching.grade.recorded - edu.identity.user.role_changed - edu.notification.events commit_strategy: manual # 处理成功后手动 commit retry_strategy: max_retries: 3 backoff: exponential dlq_topic: edu.student-bff.dlq ``` ### 5.4 不订阅事件的设计决策 | 候选事件 | 是否订阅 | 理由 | | -------------------------------- | --------- | ------------------------------------------------ | | `edu.teaching.exam.deleted` | ❌ 不订阅 | 学生已查看的考试删除走 next render 自然刷新 | | `edu.content.question.published` | ❌ 不订阅 | 题库变更不影响学生首页,按需查询即可 | | `edu.insight.mastery.updated` | ❌ 不订阅 | 掌握度更新通过 data-ana 查询时获取,无需主动推送 | | `edu.insight.ai.usage` | ❌ 不订阅 | AI 用量统计不推送给学生 | --- ## 6. 横切关注点对齐清单 ### 6.1 权限装饰器决策(已裁决 B3/B4) > ✅ **B3 裁决**:BFF 豁免 `@RequirePermission`,透传 `x-user-id` 给下游校验。 > ✅ **B4 裁决**:强制自我越权防御(AuthorizationGuard),学生只能查/操作自己数据。 > **president §2.9 方案 D**:DEV_MODE 下无 JWT 时跳过越权校验(本地开发友好)。 | 决策 | 选项 | 裁决结果 | | ------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------- | | BFF 是否加 `@RequirePermission` | A. 不加(对齐 teacher-bff,透传 x-user-id)
B. 加(双重校验) | **B3:A 方案**(BFF 豁免 `@RequirePermission`,透传 x-user-id)| | 自我越权防御 | A. 不做(依赖下游)
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_ 前缀) | 错误码 | HTTP | 触发条件 | 详情字段 | | --------------------------------- | ---- | ----------------------------------- | ---------------------------------------- | | `BFF_STUDENT_VALIDATION_ERROR` | 400 | Zod 校验失败 | `{ field, message }` | | `BFF_STUDENT_UNAUTHORIZED` | 401 | 缺失 x-user-id 头 | — | | `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 | 下游 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) | 项 | 值 | | ------------ | --------------------------------------------------------------------- | | 文件位置 | `src/shared/observability/logger.ts` | | service 字段 | `'student-bff'` | | level | env.LOG_LEVEL(默认 `info`) | | 输出 | JSON stdout | | 字段 | `time, level, service, msg, traceId, userId, endpoint, duration, err` | | 采样 | 生产环境 warn+ 100% 采样,info 10% 采样 | ### 6.4 Metrics(prom-client) | 指标名 | 类型 | 标签 | 描述 | | ----------------------------------------- | --------- | ------------------------------- | ------------------------------------------- | | `student_bff_requests_total` | Counter | `method, path, status` | 请求总数 | | `student_bff_request_duration_seconds` | Histogram | `method, path` | 请求延迟 | | `student_bff_downstream_calls_total` | Counter | `service, endpoint, status` | 下游调用次数 | | `student_bff_downstream_duration_seconds` | Histogram | `service, endpoint` | 下游调用延迟 | | `student_bff_downstream_errors_total` | Counter | `service, endpoint, error_type` | 下游调用错误数 | | `student_bff_cache_hits_total` | Counter | `cache_key_pattern` | 缓存命中 | | `student_bff_cache_misses_total` | Counter | `cache_key_pattern` | 缓存未命中 | | `student_bff_circuit_state` | Gauge | `service, state` | 熔断器状态(0=closed, 1=open, 2=half-open) | | `student_bff_sse_connections` | Gauge | — | SSE 连接数(P5) | | `student_bff_event_consumed_total` | Counter | `topic, event_type` | 事件消费数(P5) | | `student_bff_event_pushed_total` | Counter | `topic, push_status` | 推送数(P5) | ### 6.5 Tracer(OpenTelemetry) | 项 | 值 | | --------------------- | ------------------------------------------------- | | 文件位置 | `src/shared/observability/tracer.ts` | | serviceName | `'student-bff'` | | exporter | OTLP HTTP → collector | | auto-instrumentations | http, nestjs-core, express, fetch, redis, kafka | | span 属性 | `userId, endpoint, downstream.service, cache.hit` | | 采样率 | 生产 10%,开发 100% | | 上下文传播 | W3C Trace Context(traceparent 头) | ### 6.6 健康检查 | 端点 | 检查逻辑 | 响应 | | ---------- | ---------------------------------------------------- | ------------------------------------------------- | | `/healthz` | 进程存活(直接返回 ok) | `200 { status: 'ok', service: 'student-bff' }` | | `/readyz` | P3:直接返回 ok
P4+:检查下游可达性(HEAD 请求) | `200 { status: 'ready', checks: {...} }` 或 `503` | > **/readyz 增强方案(P4+)**: > > - 并行 HEAD 请求 iam / core-edu 的 /healthz > - 任一关键下游不可达 → 返回 503(让 K8s 不分发流量) > - 非关键下游(如 data-ana)不可达 → 返回 200 + `degraded: true` > - 检查结果 5s Redis 缓存,避免高频探测 ### 6.7 优雅关闭 ```mermaid sequenceDiagram participant K8s as K8s/SIGTERM participant App as NestApp participant HTTP as HTTP Server participant SSE as SSE Connections participant Sub as EventSubscriber participant Cache as Redis participant Tracer as OTel K8s->>App: SIGTERM App->>App: 1. 拒绝新请求(readyz 返回 503) App->>Sub: 2. 停止消费 Kafka(commit 最后 offset) App->>SSE: 3. 通知所有 SSE 连接关闭(发送 done 事件) App->>HTTP: 4. 等待 in-flight 请求完成(最多 10s) App->>Cache: 5. 关闭 Redis 连接 App->>Tracer: 6. flush 剩余 span App-->>K8s: 7. 进程退出 ``` ### 6.8 输入验证(Zod) | 端点 | Zod Schema | 校验项 | | -------------------------------------- | ----------------------- | -------------------------------------- | | POST `/student/homework/:id/submit` | `SubmitHomeworkSchema` | answers 非空、每题 content ≤ 5000 字符 | | POST `/student/notifications/:id/read` | 无 body | path param `id` 非空 | | POST `/student/ai/chat` | `AIChatSchema` | messages 1-20 条、content ≤ 8000 字符 | | GET `/student/grades` | `ListGradesQuerySchema` | page ≥ 1、pageSize 1-50 | | 所有 GET 端点 | query schema | 分页参数、过滤参数 | ### 6.9 全局错误过滤器 - 文件:`src/shared/errors/global-error.filter.ts` - 装饰:`@Catch()` 全局 - 行为: 1. ApplicationError → 按 statusCode + code 返回 ActionState 2. ZodError → 400 + `BFF_STUDENT_VALIDATION_ERROR` + details 3. 未知 Error → 500 + `BFF_STUDENT_INTERNAL_ERROR` + traceId 4. 注入 traceId(从 `x-request-id` 头或新生成) ### 6.10 Dockerfile ```dockerfile # 多阶段构建,对齐 teacher-bff FROM node:22-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install --frozen-lockfile COPY tsconfig.json nest-cli.json ./ COPY src/ ./src/ RUN pnpm run build FROM node:22-alpine AS runtime WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install --prod --frozen-lockfile COPY --from=builder /app/dist ./dist EXPOSE 3009 CMD ["node", "dist/main.js"] ``` --- ## 7. 与其他模块的交互点(契约清单) ### 7.1 完整交互矩阵 | 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 | | ------ | -------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------- | ------ | | 被调用 | api-gateway | HTTP | `/student/*`(全部端点) | 网关路由 | P3 | | 被调用 | student-portal | HTTP | 同上(经网关) | 前端调用 | P3 | | 调用 | iam | HTTP→gRPC P3+ | `GET /iam/me` / `GET /iam/viewports` / `GET /iam/permissions/effective` | 学生信息 + 视口 + 权限 | P3 | | 调用 | core-edu | HTTP→gRPC P3+ | `GET /exams/class/:cid` / `GET /homework/class/:cid` / `POST /homework/:id/submit` / `GET /grades/student/:sid` | 考试/作业/成绩 | P3 | | 调用 | content | HTTP→gRPC P4+ | `GET /textbooks` / `GET /chapters` / `GET /questions` / `GET /knowledge-points/:id/learning-path` | 教材/章节/题库/学习路径 | P4 | | 调用 | data-ana | HTTP→gRPC P4+ | `GET /analytics/student/:id/weakness` / `GET /analytics/student/:id/trend` | 学情诊断 | P4 | | 调用 | msg | HTTP→gRPC P5+ | `GET /notifications` / `POST /notifications/:id/read` | 消息中心 | P5 | | 调用 | ai | HTTP→gRPC P5+ | `POST /ai/chat` / `POST /ai/stream-chat` | AI 答疑 | P5 | | 调用 | push-gateway | HTTP | `POST /push/user/:userId` | 推送给在线学生 | P5 | | 消费 | Kafka | Kafka | `edu.teaching.homework.assigned` | 推送作业通知 | P5 | | 消费 | Kafka | Kafka | `edu.teaching.homework.graded` | 推送批改完成通知 + 失效缓存 | P5 | | 消费 | Kafka | Kafka | `edu.teaching.exam.published` | 推送考试提醒 | P5 | | 消费 | Kafka | Kafka | `edu.teaching.exam.updated` | 推送考试变更 | P5 | | 消费 | Kafka | Kafka | `edu.teaching.grade.recorded` | 推送成绩 + 失效缓存 | P5 | | 消费 | Kafka | Kafka | `edu.identity.user.role_changed` | 失效视口缓存 | P5 | | 消费 | Kafka | Kafka | `edu.notification.events` | 推送通知 | P5 | | 依赖 | shared-proto | 静态导入 | iam.proto / core_edu.proto / content.proto / analytics.proto / ai.proto / msg.proto / events.proto | 契约定义 | 跨阶段 | | 依赖 | shared-ts | 静态导入(待建立) | BFF 通用工具(DownstreamClient、CacheKey 生成器) | 共享工具 | P3+ | | 依赖 | Redis | TCP | 缓存 | 短缓存 | P3 | | 依赖 | OTLP Collector | HTTP | trace 上报 | 可观测性 | P3 | ### 7.2 跨模块协作需求(需提交 coord 协调) | # | 需求 | 涉及 AI | 阻塞阶段 | 协调内容 | | --- | --------------------------------------------------------------------- | --------------------- | -------- | --------------------------------------------------------- | | 1 | api-gateway 新增 `/student` 路由 | ai01 | P3 | 在 main.go + config.go 新增 `StudentBffURL` 字段 + 路由块 | | 2 | docker-compose.deploy.yml 新增 student-bff 服务定义 | coord(infra) | P3 | 端口 3009,加入 edu-net + edu-shared 网络 | | 3 | full-stack-runbook 端口矩阵更新 | coord(docs) | P3 | 追加 3009 行 | | 4 | 004 §15 文档位置矩阵更新 | coord(docs) | P3 | student-bff 状态从"📐 需设计"改为"✅ 已实现" | | 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 | ✅ 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 的复用与差异 | 维度 | teacher-bff(教师场景) | student-bff(学生场景) | parent-bff(家长场景) | | ---------- | ---------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------- | | DataScope | CLASS(教师看本班) | SELF(学生只看自己) | CHILDREN(家长看绑定子女) | | 越权防御 | 不做(透传 x-user-id) | **做**(强制 userId 比对) | **做**(强制 childId 必须在绑定列表) | | 端口 | 3003 | 3009 | 3010 | | 路由前缀 | `/teacher` | `/student` | `/parent` | | 错误码前缀 | `BFF_TEACHER_` | `BFF_STUDENT_` | `BFF_PARENT_` | | 指标前缀 | `teacher_bff_` | `student_bff_` | `parent_bff_` | | 聚合复杂度 | 高(教师跨班跨年级) | 低(学生单维度) | 中(多子女切换) | | 主要下游 | iam + core-edu + content + data-ana + ai | iam + core-edu(P3)→ + content + data-ana(P4)→ + msg + ai(P5) | iam + core-edu(P4)→ + msg + data-ana(P4+) | --- ## 8. 风险与假设 ### 8.1 技术风险 | 风险 | 概率 | 影响 | 缓解措施 | | ------------------------------- | ---- | ---- | ----------------------------------------------------------------------------- | | 下游服务故障导致 Dashboard 全白 | 中 | 高 | Promise.allSettled 容错 + partial 标记 + 关键下游(iam)熔断降级返回基础信息 | | AI 流式响应中断(SSE 断连) | 中 | 中 | 客户端断线重连机制 + 服务端清理孤儿连接 + last-event-id 续传 | | Redis 缓存雪崩 | 低 | 高 | TTL 加随机抖动(±20%)+ 熔断 + 单飞模式(同 key 并发只放一个去下游) | | Kafka 消费堆积 | 低 | 中 | 消费者组并行度配置 + DLQ + 告警阈值(lag > 1000) | | 高并发作业提交(截止前扎堆) | 中 | 中 | 透传 core-edu 处理(Redis 分布式锁),BFF 层加 IP+userId 限流(透传 gateway) | | 缓存与 DB 不一致 | 中 | 中 | 短 TTL(15-60s)+ 事件驱动主动失效 + 提交后立即 DEL | | BFF 单点故障 | 低 | 高 | 无状态设计,K8s 多副本部署 + HPA | ### 8.2 外部依赖假设 | 假设 | 若假设不成立的影响 | Fallback 方案 | | ------------------------------------------- | -------------------------- | --------------------------------------------------- | | iam `/iam/me` 返回包含 classId | Dashboard 无法聚合待办作业 | 调用 core-edu 反查学生所在班级 | | core-edu `POST /homework/:id/submit` 已实现 | P3 核心端点无法交付 | 阻塞 P3 退出标准,提请 coord 协调 ai03 | | core-edu `GET /grades/student/:sid` 已实现 | 学生查成绩端点无法交付 | 阻塞 P3,提请 coord 协调 ai03 | | api-gateway 已注册 `/student` 路由 | 前端请求 404 | P3 阻塞,提请 coord 协调 ai01 | | Redis 已部署且网络可达 | 缓存层失效,性能下降 | Cache 层降级为内存 LRU(如 cache-manager 内存模式) | | OTLP Collector 已部署 | 链路追踪缺失 | 不影响业务,仅日志降级 | | Kafka 已部署且 topic 已创建 | P5 事件订阅无法实现 | P5 阻塞;P3/P4 不依赖 Kafka | ### 8.3 设计决策(已裁决 B1-B8 + president §2.2-2.9) > ✅ 全部 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) | --- ## 9. 演进路线图 ### 9.1 各阶段演进总览 ```mermaid graph LR P3[P3 核心教学
M7-M10] P4[P4 内容分析
M11-M13] P5[P5 沟通AI
M14-M16] P6[P6 硬化
M17-M18] P3 --> P4 --> P5 --> P6 P3 -.GraphQL Yoga + gRPC.-> P3 P4 -.+content+data-ana.-> P4 P4 -.+学情诊断+学习路径.-> P4 P5 -.+msg+ai+Kafka订阅.-> P5 P5 -.+SSE流式+push-gateway推送.-> P5 P6 -.+HPA+Istio mTLS.-> P6 P6 -.+全链路可观测.-> P6 ``` ### 9.2 API 风格演进(已裁决 B1:GraphQL 即起点) > ✅ **B1 裁决**:P2 起直接 GraphQL Yoga + DataLoader,无 REST 渐进期。 | 阶段 | 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/`) | **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`) ### 9.3 通信协议演进(已裁决 B2:gRPC 首次即用) > ✅ **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 推送通道演进 | 阶段 | 推送方式 | 触发场景 | | ---- | ---------------------- | -------------------------- | | P3 | 无推送 | 学生主动查询 | | P5 | SSE 单向推送 | AI 答疑流式 + 成绩发布通知 | | P5+ | WebSocket 双向推送 | 实时通知 + 在线状态 | | P6+ | 移动端推送(FCM/APNs) | 离线推送(未来扩展) | ### 9.5 多角色复用演进 | 阶段 | student-bff 复用角色 | 视口差异化策略 | | ---- | ---------------------------------- | --------------------- | | P3 | 学生 | 单一视口 | | P4+ | 学习委员(学生 + 班级汇总视口) | L1 增加"班级学情"菜单 | | P5+ | 课代表(学生 + 学科作业收集视口) | L1 增加"作业收集"菜单 | | P6+ | 走读生/住宿生差异化(作息/课程表) | L4 数据范围细化 | > **设计预留**:视口模型(§3.3)已支持 `dataScope.showHistoryGrades` 等细粒度开关,新角色只需在 iam 配置视口,BFF 自动适配,无需改代码。 --- ## 10. 扩展点设计 ### 10.1 多端适配扩展(H5/小程序) ```mermaid graph LR A[student-bff API] --> B[Transformer 层] B --> C[Web 版响应] B --> D[H5 版响应(字段精简)] B --> E[小程序版响应(字段精简 + 数据预取)] ``` **实现**:在 Transformer 层根据 `x-client-type` 头(web/h5/miniapp)选择不同的字段裁剪策略。 ### 10.2 国际化扩展 ```typescript // 预留 i18n 接入点 export interface I18nContext { locale: 'zh-CN' | 'en-US'; timezone: 'Asia/Shanghai' | 'America/Los_Angeles'; } // Service 层接收 i18n context,透传给下游 async getDashboard(userId: string, i18n: I18nContext): Promise { // 透传 Accept-Language 头给下游 } ``` ### 10.3 多子女切换扩展(家长场景借鉴) 虽然 parent-bff 是独立服务,但 student-bff 的设计可为 parent-bff 提供借鉴: | 共享模式 | student-bff 实现 | parent-bff 复用方式 | | -------------------- | ---------------- | --------------------------- | | DownstreamClient | 通用封装 | 直接复用,仅改 service 名 | | Cache key 命名 | `student:*` | 改为 `parent:*` | | 自我越权防御 | userId 强制比对 | 改为 childId 必须在绑定列表 | | Transformer 视口过滤 | StudentViewport | 改为 ParentViewport | | EventSubscriber | 订阅学生相关事件 | 订阅子女相关事件 | ### 10.4 离线模式扩展 未来支持学生端离线查看已加载的作业/教材: | 端点 | 离线策略 | | ------------------------------------- | --------------------------------------- | | `GET /student/homework/:id` | 返回 ETag + Last-Modified,支持条件请求 | | `GET /student/textbooks/:id/chapters` | 长缓存(300s)+ ETag | ### 10.5 AI 答疑增强扩展 ```mermaid graph LR A[学生提问] --> B{是否需查知识点?} B -- 是 --> C[调用 content 查知识点] B -- 否 --> D[直接调 ai/chat] C --> D D --> E{是否需查学情?} E -- 是 --> F[调用 data-ana 查弱项] E -- 否 --> G[LLM 生成] F --> G G --> H[返回答案 + 引用知识点] ``` > 设计上 StudentService.AIChat 方法预留 context 参数(subject + knowledgePointId),未来可扩展为多步编排。 ### 10.6 学习路径推荐扩展 ```typescript // 预留接口 async getRecommendedLearningPath(userId: string): Promise { // 1. 查 data-ana 学情诊断 const weakness = await this.client.callDataAna(`/analytics/student/${userId}/weakness`); // 2. 调 content 知识图谱 const path = await this.client.callContent(`/knowledge-points/${weakness.weakPoints[0].knowledgePointId}/learning-path`); // 3. 返回个性化路径 return path; } ``` --- ## 11. 性能与容量规划 ### 11.1 性能目标(SLO) | 端点 | P50 延迟 | P95 延迟 | P99 延迟 | 错误率 | | -------------------------------------- | ---------- | -------- | -------- | ------ | | `GET /student/dashboard`(缓存命中) | 50ms | 100ms | 200ms | <0.1% | | `GET /student/dashboard`(缓存未命中) | 300ms | 800ms | 1500ms | <0.5% | | `GET /student/homework` | 100ms | 300ms | 500ms | <0.1% | | `POST /student/homework/:id/submit` | 200ms | 500ms | 1000ms | <0.5% | | `GET /student/grades` | 100ms | 300ms | 500ms | <0.1% | | `POST /student/ai/stream-chat` | 200ms TTFT | 1s TTFT | 2s TTFT | <1% | ### 11.2 容量规划 | 维度 | P3 估算 | P5 估算 | P6 估算 | | -------------- | -------- | ------- | ----------- | | 日活学生 | 1,000 | 10,000 | 50,000 | | QPS 峰值 | 50 | 500 | 2,500 | | SSE 连接数 | 0 | 1,000 | 10,000 | | Kafka 消费 TPS | 0 | 100 | 500 | | Redis 内存 | 50MB | 500MB | 2GB | | 实例数 | 1 | 2-3 | 5-10(HPA) | | 单实例 CPU | 0.5 core | 1 core | 2 core | | 单实例内存 | 256MB | 512MB | 1GB | ### 11.3 限流策略(透传 api-gateway) | 端点 | 限流维度 | 阈值 | | ----------------------------------- | ----------- | -------------- | | `POST /student/homework/:id/submit` | userId | 10/min | | `POST /student/ai/chat` | userId + IP | 30/min | | `POST /student/ai/stream-chat` | userId | 5/min + 1 并发 | | `GET /student/*`(读) | userId | 600/min | --- ## 12. 安全与合规 ### 12.1 身份与认证 | 维度 | 实现 | | ---------- | ---------------------------------------------------------------- | | 认证 | JWT RS256 在 api-gateway 校验,BFF 仅读 `x-user-id` 头(不验签) | | 会话 | 无状态(BFF 不持 session),Redis 仅缓存数据 | | Token 刷新 | 透传 401 给前端,由前端走 refresh 流程 | ### 12.2 授权与数据隔离 | 维度 | 实现 | | -------------- | --------------------------------------------------------------------- | | 权限校验 | 透传 `x-user-id` 给下游,下游按 `@RequirePermission` + DataScope 校验 | | 自我越权防御 | BFF 层强制 `studentId = userId`(成绩/作业/学情端点) | | 跨班级数据隔离 | 学生只能查自己所在班级(classId 由 iam 推导,不接受前端传入) | | AI 内容安全 | AI 答疑请求透传 content moderation(ai 服务侧实现) | ### 12.3 输入安全 | 维度 | 实现 | | -------- | -------------------------------------------------------------------------------- | | 输入验证 | 全部 Zod 校验,拒绝 unknown 字段 | | SQL 注入 | BFF 无 DB,不涉及;下游用 Drizzle 参数化查询 | | XSS | 响应 Content-Type: application/json,禁止 HTML 渲染;富文本由前端 DOMPurify 清洗 | | CSRF | Cookie 必须 SameSite=Strict + 后端校验 Origin 头 | | 文件上传 | 作业附件走 OSS 直传(预签名 URL),BFF 不接收文件流 | ### 12.4 数据合规 | 维度 | 实现 | | ----------- | ----------------------------------------------------------------------------- | | 学生隐私 | 成绩/学情仅本人可见,不返回同班其他学生数据 | | 日志脱敏 | 日志中不记录 answers 内容、score 数值;仅记录 metadata(homeworkId, traceId) | | 数据保留 | BFF 不持久化数据,Redis 缓存 TTL ≤ 600s | | GDPR/个保法 | 学生数据导出/删除请求透传给 iam / core-edu 处理 | ### 12.5 审计 | 维度 | 实现 | | ------------ | ---------------------------------------------------------------------- | | 请求审计 | 全部请求记录 access log(userId, endpoint, status, duration, traceId) | | 越权尝试审计 | 自我越权防御触发时记录 warn 日志 + 告警 | | 异常行为 | 短时高频请求(如 1s 内 10 次 `/student/grades`)触发告警 | --- ## 13. 可观测性详细设计 ### 13.1 日志规范 ```typescript // 标准日志字段 interface StudentBFFLog { time: string; level: "info" | "warn" | "error" | "debug"; service: "student-bff"; msg: string; traceId?: string; userId?: string; endpoint?: string; method?: string; status?: number; duration?: number; downstream?: { service: string; endpoint: string; status: number; duration: number; }; cache?: { key: string; hit: boolean }; err?: { message: string; stack: string; code: string }; } ``` ### 13.2 关键业务 span | Span 名 | 触发点 | 关键属性 | | ----------------------------- | --------------------------------- | ----------------------------------------- | | `student_bff.dashboard` | GET /student/dashboard | userId, cached, partial, degradedServices | | `student_bff.submit_homework` | POST /student/homework/:id/submit | homeworkId, questionCount | | `student_bff.list_grades` | GET /student/grades | userId, page, total | | `student_bff.ai_chat` | POST /student/ai/chat | userId, model, tokens | | `student_bff.ai_stream_chat` | POST /student/ai/stream-chat | userId, model, chunks, duration | | `student_bff.downstream_call` | 任一下游调用 | service, endpoint, status, duration | ### 13.3 告警规则 | 告警名 | 触发条件 | 严重度 | | ------------------------------ | ------------------------------- | ------ | | `StudentBFFHighErrorRate` | 5xx 错误率 > 1% 持续 5 分钟 | 严重 | | `StudentBFFHighLatency` | P95 延迟 > 1s 持续 5 分钟 | 警告 | | `StudentBFFDownstreamFailures` | 下游调用失败率 > 5% 持续 5 分钟 | 警告 | | `StudentBFFCacheHitRateLow` | 缓存命中率 < 50% 持续 10 分钟 | 提示 | | `StudentBFFCircuitOpen` | 熔断器开启持续 1 分钟 | 严重 | | `StudentBFFSSEConnectionsHigh` | SSE 连接数 > 5000 | 警告 | | `StudentBFFKafkaLag` | Kafka 消费 lag > 1000 | 警告 | ### 13.4 Grafana Dashboard 面板 | 面板 | 内容 | | ------------ | -------------------------------------- | | 总览 | QPS / 错误率 / P95 延迟 / 缓存命中率 | | 下游服务健康 | 各下游服务调用成功率 / 延迟 / 熔断状态 | | 端点细分 | 各端点 QPS / 延迟 / 错误率 | | SSE 推送 | 连接数 / 推送成功率 / 推送延迟 | | Kafka 消费 | 消费 TPS / lag / DLQ 数量 | --- ## 14. 实施清单 ### 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 文件结构 ``` services/student-bff/ ├─ src/ │ ├─ config/ │ │ └─ env.ts # 环境变量(PORT=3009 + 下游 URL) │ ├─ shared/ │ │ ├─ errors/ │ │ │ ├─ application-error.ts # 错误类(BFF_STUDENT_ 前缀) │ │ │ └─ global-error.filter.ts # 全局错误过滤器 │ │ ├─ health/ │ │ │ ├─ health.controller.ts # /healthz + /readyz │ │ │ └─ health.module.ts │ │ ├─ observability/ │ │ │ ├─ logger.ts # pino │ │ │ ├─ metrics.ts # prom-client + student_bff_* 指标 │ │ │ └─ tracer.ts # OTel │ │ ├─ cache/ │ │ │ └─ cache.module.ts # Redis CacheInterceptor │ │ ├─ downstream/ │ │ │ ├─ downstream-client.ts # 统一封装 fetch + 超时 + 重试 + traceId │ │ │ ├─ circuit-breaker.ts # opossum 熔断器 │ │ │ └─ downstream.module.ts │ │ └─ dto/ │ │ └─ downstream-envelope.ts # 下游响应包装 │ ├─ student/ │ │ ├─ student.controller.ts # @Controller('student') │ │ ├─ student.service.ts # 聚合编排 │ │ ├─ student.module.ts │ │ ├─ aggregators/ │ │ │ ├─ dashboard.aggregator.ts # Dashboard 并行聚合策略 │ │ │ └─ homework.aggregator.ts │ │ ├─ transformers/ │ │ │ ├─ dashboard.transformer.ts # 字段裁剪 + 视口过滤 │ │ │ └─ grades.transformer.ts │ │ ├─ dto/ │ │ │ ├─ student-dashboard.dto.ts │ │ │ ├─ student-homework.dto.ts │ │ │ └─ student-grades.dto.ts │ │ └─ schemas/ │ │ └─ submit-homework.schema.ts # Zod 输入校验 │ ├─ app.module.ts │ └─ main.ts # 启动 + /metrics + SIGTERM ├─ test/ │ └─ unit/ │ ├─ student.service.test.ts │ ├─ aggregators/*.test.ts │ └─ transformers/*.test.ts ├─ docs/ │ ├─ 01-understanding.md │ ├─ 02-audit.md │ ├─ 02-architecture-design.md # 本文档 │ └─ experience-log.md ├─ Dockerfile # 多阶段构建,EXPOSE 3009 ├─ nest-cli.json ├─ package.json # @edu/student-bff ├─ tsconfig.json # NodeNext + incremental: false └─ vitest.config.ts # 对齐 classes 测试框架 ``` #### 14.1.2 P3 必交付 GraphQL 字段(已全部落地) - [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 横切关注点对齐(已全部落地) - [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 阶段扩展清单(已全部落地) - [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 阶段扩展清单(已全部落地) - [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 阶段硬化清单(部分落地) - [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% 可用性压测(⏳ 压测阶段) --- ## 15. 与黄金模板对齐自检 > 对照 [ai-allocation §6 模板第 6 节](../../../docs/architecture/ai-allocation.md) + [004 §15](../../../docs/architecture/004_architecture_impact_map.md#15-ai-架构设计文档索引) | 对齐项 | classes 黄金模板 | student-bff 设计 | 状态 | | ------------------------------- | --------------------------------- | ---------------------------------------------------- | ------ | | 权限装饰器 `@RequirePermission` | ✅ 全部 Controller 方法 | ⚠️ 不对齐(BFF 不做权限校验,透传 x-user-id) | 已识别 | | 错误码前缀统一 | ✅ `CLASSES_` | ✅ `BFF_STUDENT_`(对齐 004 §11.4) | ✅ | | logger(pino) | ✅ shared/observability/logger.ts | ✅ 复制 teacher-bff,service: 'student-bff' | ✅ | | metrics(prom-client) | ✅ /metrics 端点 | ✅ 复制 teacher-bff,11 个 student_bff_* 指标 | ✅ | | tracer(OpenTelemetry) | ✅ OTLP exporter | ✅ 复制 teacher-bff,serviceName: 'student-bff' | ✅ | | `/healthz` 健康检查 | ✅ liveness | ✅ 复制 teacher-bff | ✅ | | `/readyz` 健康检查 | ✅ Drizzle SELECT 1 | ✅ P3 直接返回 ok;P4+ 检查下游可达性 | ✅ | | 优雅关闭(SIGTERM) | ✅ LifecycleService | ✅ main.ts 注册 SIGTERM → app.close + shutdownTracer | ✅ | | 测试覆盖率 ≥ 80% | ✅ Jest | ✅ Vitest(对齐 classes),重点测 Service/Aggregator | ✅ | | Dockerfile 多阶段构建 | ✅ builder + runtime | ✅ 复制 teacher-bff,EXPOSE 3009 | ✅ | | Zod 输入验证 | ✅ Controller 层 | ✅ SubmitHomeworkSchema / AIChatSchema 等 | ✅ | | GlobalErrorFilter | ✅ @Catch() | ✅ 复制 teacher-bff | ✅ | | ESM `.js` 后缀 import | ✅ tsconfig NodeNext | ✅ 复制 teacher-bff tsconfig | ✅ | | `import type` 纯类型导入 | ✅ | ✅ | ✅ | | 环境变量 Zod 校验 | ✅ config/env.ts | ✅ 复制 teacher-bff,扩展 7 个下游 URL | ✅ | | 端口分配 | — | ✅ 3009(对齐 004 §1.2) | ✅ | | 路由前缀 | — | ✅ `/student`(对齐 teacher-bff `/teacher` 模式) | ✅ | --- ## 16. 阶段 2 自检结论 | 检查项 | 状态 | | ------------------------------- | ------- | | 模块内部分层图 | ✅ §1 | | 领域模型(聚合视图) | ✅ §2 | | 数据模型(缓存 + DTO) | ✅ §3 | | API 设计(17 GraphQL 字段,含未来扩展) | ✅ §4 | | 事件设计(订阅清单 + 架构) | ✅ §5 | | 横切关注点对齐清单 | ✅ §6 | | 与其他模块的交互点 | ✅ §7 | | 风险与假设 | ✅ §8 | | 演进路线图(长远规划) | ✅ §9 | | 扩展点设计(为未来铺垫) | ✅ §10 | | 性能与容量规划 | ✅ §11 | | 安全与合规 | ✅ §12 | | 可观测性详细设计 | ✅ §13 | | 实施清单(P3/P4/P5/P6 分阶段) | ✅ §14 | | 黄金模板对齐自检 | ✅ §15 | | 错误码前缀修正(BFF_STUDENT_) | ✅ §0.2 | | 设计决策(已裁决 B1-B8) | ✅ §8.3 | | 跨模块协作需求 | ✅ §7.2 | **ai04 阶段 2 交付完成,已对齐仲裁裁决(coord-final-decisions B1-B8 + president-final-rulings §2.2-2.9)。P3-P6 全部代码已实现。** ### 16.1 仲裁对齐情况(已全部裁决) > ✅ 全部事项已由 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 熔断器(已落地)。