1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
81 KiB
模块架构设计文档 — student-bff
AI 标识:ai04 阶段:阶段 2(模块架构设计) 日期:2026-07-09 状态:待 coord 交叉审查 关联文档:
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 错误码前缀矩阵 规定的BFF_STUDENT_(BFF 在前)不一致。本设计文档统一修正为BFF_STUDENT_,对齐 teacher-bff 的BFF_TEACHER_、parent-bff 的BFF_PARENT_模式。阶段 1 文档保留历史记录,以本文档为准。
0.3 文档结构
- 模块内部分层图 — 物理分层与调用链
- 领域模型(聚合视图) — BFF 无领域模型,定义"场景聚合"概念
- 数据模型(缓存与 DTO) — BFF 不持 DB,定义 Redis Schema 与传输 DTO
- API 设计 — 14 个端点 + 阶段化交付
- 事件设计 — BFF 订阅事件用于实时推送(P5)
- 横切关注点对齐清单 — 完整对齐 classes/teacher-bff
- 与其他模块的交互点 — 完整契约矩阵
- 风险与假设 — 技术风险、外部依赖、未决决策
- 演进路线图 — P3/P4/P5/P6 各阶段演进路径(长远规划)
- 扩展点设计 — 为未来场景预留
- 性能与容量规划
- 安全与合规
- 可观测性详细设计
- 实施清单
1. 模块内部分层图
1.1 物理分层
graph TB
subgraph Client["客户端"]
Portal[student-portal<br/>Next.js MF Remote]
Mobile[H5/小程序<br/>未来扩展]
end
subgraph Gateway["网关层"]
APIGW[api-gateway<br/>路由 + JWT 鉴权 + 限流]
end
subgraph StudentBFF["student-bff(本模块)"]
direction TB
Controller["@Controller('student')<br/>HTTP 入口 + Zod 校验 + ActionState 信封"]
Service["StudentService<br/>聚合编排 + DataScope 透传"]
Aggregator["Aggregator 层<br/>场景化聚合策略"]
Transformer["Transformer 层<br/>响应裁剪 + 视口过滤"]
Cache["Cache 层<br/>Redis 5-30s 短缓存"]
Client2["DownstreamClient<br/>统一封装 fetch + 超时 + 重试 + traceId 透传"]
SSE["SSE Streamer<br/>P5 AI 答疑流式响应"]
EventSub["Event Subscriber<br/>P5 订阅 Kafka 推送给 push-gateway"]
CrossCutting["横切关注点<br/>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<br/>:3002]
CoreEdu[core-edu<br/>:3004]
Content[content<br/>:3005 P4]
Msg[msg<br/>:3007 P5]
DataAna[data-ana<br/>:3006 P4]
AI[ai<br/>:3008 P5]
end
subgraph Push["推送层"]
PushGW[push-gateway<br/>: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 主要链路)
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<br/>Authorization: Bearer <jwt>
GW->>GW: RS256 公钥校验 + 提取 userId
GW->>C: GET /student/dashboard<br/>x-user-id: u-xxx<br/>x-request-id: r-xxx
C->>C: extractUserId(req) → u-xxx<br/>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<br/>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 答疑)
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<br/>Accept: text/event-stream
GW->>C: POST /student/ai/stream-chat
C->>SSE: streamChat(messages, model)
SSE->>AI: POST /ai/stream-chat<br/>Authorization: Bearer <internal>
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)
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 推送<br/>{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 聚合视图间关系
graph LR
Dashboard[StudentDashboard<br/>聚合视图]
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<br/>未来扩展]
Attendance[StudentAttendance<br/>未来扩展]
Dashboard -.未来.-> Schedule
Dashboard -.未来.-> Attendance
2.3 DataScope=SELF 的强制实现
BFF 不做权限决策(对齐 teacher-bff),但做自我越权防御:
// StudentService 伪代码示意
async listGrades(userId: string, query: ListGradesQuery): Promise<Grade[]> {
// 防御:学生只能查自己的成绩
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 缓存失效策略
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 下游响应包装
// shared/dto/downstream-envelope.ts
import { z } from "zod";
export const DownstreamEnvelopeSchema = <T extends z.ZodTypeAny>(
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<T> = z.infer<
ReturnType<typeof DownstreamEnvelopeSchema<z.ZodType<T>>>
>;
3.2.2 学生端核心 DTO
// 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<typeof StudentDashboardSchema>;
3.2.3 输入 Schema(Zod)
// 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 用视口过滤可见字段。
// 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<T>(
data: T,
viewport: StudentViewport,
): Partial<T> {
// 按视口裁剪字段,如 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),成功{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=<jwt>
Query: ?classId=c-001 (optional, 默认从 iam 推导)
响应 200:
{
"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 不可达):
{
"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:
{
"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 行为:
- Zod 校验 body
- 透传
x-user-id给 core-edu - core-edu 写入 homework_submissions + Outbox 事件
edu.teaching.homework.submitted - BFF 失效
student:homework:u-stu-001:*缓存 - 返回提交回执
4.2.3 GET /student/grades(自我越权防御示例)
请求:
GET /student/grades?page=1&pageSize=20&subject=数学
Headers: x-user-id: u-stu-001
响应 200:
{
"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 行为:
- 强制
studentId = userId(自我越权防御) - 调用 core-edu
GET /grades/student/u-stu-001 - Transformer 裁剪敏感字段(如 gradedBy 教师姓名,按视口过滤)
- 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 详述。
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)
graph LR
K[(Kafka)]
ES[EventSubscriber<br/>NestJS Module]
Idempotency[(Redis SETNX<br/>event_id 去重)]
Session[(Redis<br/>学生在线 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 事件订阅消费者组设计
# 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 优雅关闭
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()全局 - 行为:
- ApplicationError → 按 statusCode + code 返回 ActionState
- ZodError → 400 +
BFF_STUDENT_VALIDATION_ERROR+ details - 未知 Error → 500 +
BFF_STUDENT_INTERNAL_ERROR+ traceId - 注入 traceId(从
x-request-id头或新生成)
6.10 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 各阶段演进总览
graph LR
P3[P3 核心教学<br/>M7-M10]
P4[P4 内容分析<br/>M11-M13]
P5[P5 沟通AI<br/>M14-M16]
P6[P6 硬化<br/>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 决策升级):
- 引入
@nestjs/graphql+ Apollo Server 或 GraphQL Yoga- 定义 schema:
Query.studentDashboard / Query.studentHomework / Mutation.submitHomework等- Resolver 复用 Service 层逻辑
- 引入 DataLoader 解决 N+1(如一个 Dashboard 同时查多个学生成绩)
- 前端逐步从 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/小程序)
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 国际化扩展
// 预留 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<StudentDashboard> {
// 透传 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 答疑增强扩展
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 学习路径推荐扩展
// 预留接口
async getRecommendedLearningPath(userId: string): Promise<LearningPath> {
// 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 日志规范
// 标准日志字段
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/dashboardGET /student/viewportsGET /student/examsGET /student/exams/:idGET /student/homeworkGET /student/homework/:idPOST /student/homework/:id/submit← P3 核心GET /student/gradesGET /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/textbooksGET /student/textbooks/:id/chaptersGET /student/questionsGET /student/knowledge-points/:id/pathGET /student/analytics/weaknessGET /student/analytics/trend- 双轨读策略(实时查 core-edu 主库 + 聚合查 data-ana ClickHouse 宽表)
- /readyz 增强为下游可达性检查
14.3 P5 阶段扩展清单
GET /student/notificationsPOST /student/notifications/:id/readPOST /student/ai/chatPOST /student/ai/stream-chat(SSE 流式)- Kafka EventSubscriber 模块
- push-gateway 推送通道
- SSE 连接管理
14.4 P6 阶段硬化清单
- HPA 自动扩缩容
- Istio mTLS
- 全链路 trace + Grafana 仪表盘
- 灾备演练
- 99.9% 可用性压测
15. 与黄金模板对齐自检
| 对齐项 | 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 审查的事项
- 错误码前缀不一致修正(§0.2):阶段 1 文档用
STUDENT_BFF_,本设计文档统一为BFF_STUDENT_(对齐 004 §11.4),请确认是否回写阶段 1 文档。 - BFF 模式 v2 抽象(§1.3):DownstreamClient / Aggregator / Transformer 三层抽象是否回写 teacher-bff,避免技术栈分裂。
- 自我越权防御(§2.3):BFF 层强制
studentId = userId是与 teacher-bff 的差异点,请确认是否作为 BFF 通用规范。 - 12 项未决决策(§8.3):请逐项仲裁。
- 16 项跨模块协作需求(§7.2):请协调对应 AI 实施。
- GraphQL 演进时机(§9.2):P3 REST / P4+ 是否切换 GraphQL,需全局决策。
- 熔断器引入(§8.3 #12):BFF 层是否在 P3 引入 opossum 熔断器,还是依赖 api-gateway 熔断。