# 模块架构设计文档 — 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) | 实时推送 | > **改进点是否回写 teacher-bff 由 coord 仲裁**(避免技术栈分裂)。本文档定义的 DownstreamClient/Aggregator/Transformer 三层抽象可作为 BFF 模式 v2 的参考实现,待 coord 决策是否回写。 --- ## 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 端点全清单 > 路由前缀:`/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?}}` | # | 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+ | 我的考勤(未来扩展) | ### 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 风格决策(待 coord 仲裁) | 选项 | 优势 | 劣势 | ai04 建议 | | ----------------------------------- | ------------------------------ | ---------------------------------- | --------------------------------- | | A. REST(对齐 teacher-bff 现状) | 实现快、与 teacher-bff 一致 | 多次往返、字段冗余 | **✅ P3 阶段采用** | | B. GraphQL(对齐 004 §11.3 目标态) | 客户端按需取字段、聚合天然适合 | 与 teacher-bff 不一致、需引入 Yoga | P4+ 阶段统一升级三端 BFF 时再考虑 | | C. REST + DataLoader(混合) | 解决 N+1 | 引入额外复杂度 | 不推荐 | **决策记录**:P3 阶段 student-bff 采用 REST + Promise.allSettled + DownstreamClient 模式,对齐 teacher-bff 现状。GraphQL 演进路径在 [§9.2](#92-api-风格演进) 详述。 --- ## 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 权限装饰器决策 | 决策 | 选项 | ai04 建议 | 仲裁状态 | | ------------------------------- | ----------------------------------------------------------------- | ----------------------------------------- | ------------- | | BFF 是否加 `@RequirePermission` | A. 不加(对齐 teacher-bff,透传 x-user-id)
B. 加(双重校验) | **A**(BFF 是聚合层,权限由下游服务校验) | 待 coord 仲裁 | | 自我越权防御 | A. 不做(依赖下游)
B. BFF 层做 userId 强制比对 | **B**(学生场景敏感,防御纵深) | 待 coord 仲裁 | > **若 coord 选择 A 方案(不加装饰器)**:student-bff 不引入 `middleware/permission.guard.ts`,与 teacher-bff 一致。 > **若 coord 选择 B 方案(双重校验)**:student-bff 引入 PermissionGuard,但要避免与下游重复校验造成性能损耗,可只校验"导航级"权限(如能否进入 AI 答疑菜单),不校验"数据级"权限(留给下游)。 ### 6.2 错误码清单(BFF_STUDENT_ 前缀) | 错误码 | HTTP | 触发条件 | 详情字段 | | --------------------------------- | ---- | ----------------------------------- | ---------------------------------------- | | `BFF_STUDENT_VALIDATION_ERROR` | 400 | Zod 校验失败 | `{ field, message }` | | `BFF_STUDENT_UNAUTHORIZED` | 401 | 缺失 x-user-id 头 | — | | `BFF_STUDENT_FORBIDDEN` | 403 | 自我越权防御拦截 | `{ requested, actual }` | | `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_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 | 决定是否在 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 学情诊断依赖 | ### 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 未决设计决策(待 coord 仲裁) | # | 决策点 | 选项 | ai04 建议 | 影响范围 | | --- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------ | | 1 | BFF API 风格 | A. REST(对齐 teacher-bff 现状)
B. GraphQL(对齐 004 §11.3 设计意图) | **A**(P3 阶段先 REST,P4+ 统一升级时再考虑) | 全部端点 | | 2 | BFF 是否做权限校验 | A. 不校验(对齐 teacher-bff)
B. 加 `@RequirePermission` | **A**(聚合层不做权限决策) | 全部端点 | | 3 | BFF 是否做自我越权防御 | A. 不做(依赖下游)
B. BFF 层强制 userId 比对 | **B**(学生场景敏感) | 成绩/作业/学情端点 | | 4 | `/readyz` 检查逻辑 | A. 直接返回 ok(对齐 teacher-bff)
B. 检查下游可达性 | **A**(P3)/ **B**(P4+,下游可达性由 Prometheus 监控) | 健康检查 | | 5 | Kafka 事件订阅时机 | A. P3 不订阅
B. P3 订阅 | **A**(push-gateway P5 才落地) | P5 推送功能 | | 6 | 缓存策略 | A. 不缓存(对齐 teacher-bff)
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)
B. 回写(统一三端 BFF) | **B**(避免技术栈分裂,作为 BFF 模式 v2) | teacher-bff / parent-bff | | 10 | 是否引入 NestJS CQRS 模块 | A. 不引入(简单 Service 即可)
B. 引入(Query/Command 分离) | **A**(BFF 不持领域模型,CQRS 收益不大) | Service 层结构 | | 11 | SSE 实现 | A. 原生 Node Stream
B. 第三方库(如 @nestjs/axios + RxJS) | **A**(依赖少,可控) | P5 AI 答疑流式 | | 12 | 是否在 P3 引入熔断器 | A. 不引入(依赖 gateway 熔断)
B. 引入(opossum 库) | **B**(BFF→下游单链路熔断,gateway 是入口熔断) | DownstreamClient | --- ## 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 -.REST + fetch.-> P3 P4 -.+content+data-ana.-> P4 P4 -.+gRPC iam/core-edu.-> P4 P5 -.+msg+ai+Kafka订阅.-> P5 P5 -.+SSE流式+WebSocket推送.-> P5 P6 -.+HPA+Istio mTLS.-> P6 P6 -.+全链路可观测.-> P6 ``` ### 9.2 API 风格演进 | 阶段 | 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` 端点;前端逐步迁移 | > **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 个月兼容期 ### 9.3 通信协议演进 | 阶段 | 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 + 流量治理 | > **协议切换设计**:DownstreamClient 抽象层封装协议选择,根据 `env.IamUseGrpc=true/false` 切换,业务代码无感知。 ### 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.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 必交付端点 - [ ] `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` #### 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% ### 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 增强为下游可达性检查 ### 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 连接管理 ### 14.4 P6 阶段硬化清单 - [ ] HPA 自动扩缩容 - [ ] Istio mTLS - [ ] 全链路 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 设计(21 端点,含未来扩展) | ✅ §4 | | 事件设计(订阅清单 + 架构) | ✅ §5 | | 横切关注点对齐清单 | ✅ §6 | | 与其他模块的交互点 | ✅ §7 | | 风险与假设 | ✅ §8 | | 演进路线图(长远规划) | ✅ §9 | | 扩展点设计(为未来铺垫) | ✅ §10 | | 性能与容量规划 | ✅ §11 | | 安全与合规 | ✅ §12 | | 可观测性详细设计 | ✅ §13 | | 实施清单(P3/P4/P5/P6 分阶段) | ✅ §14 | | 黄金模板对齐自检 | ✅ §15 | | 错误码前缀修正(BFF_STUDENT_) | ✅ §0.2 | | 未决决策清单(待 coord 仲裁) | ✅ §8.3 | | 跨模块协作需求 | ✅ §7.2 | **ai04 阶段 2 交付完成,请 coord 交叉审查。** ### 16.1 重点请 coord 审查的事项 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 熔断。