Files
Edu/services/student-bff/docs/02-architecture-design.md
SpecialX f585080e70 feat(student-bff): 完整实现 student-bff 聚合层
包含 src 全部实现、Dockerfile、shared-ts/bff 包等
2026-07-10 19:10:51 +08:00

88 KiB
Raw Permalink Blame History

模块架构设计文档 — 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=SELFBFF 层透传 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 文档结构

  1. 模块内部分层图 — 物理分层与调用链
  2. 领域模型(聚合视图) — BFF 无领域模型,定义"场景聚合"概念
  3. 数据模型(缓存与 DTO — BFF 不持 DB定义 Redis Schema 与传输 DTO
  4. API 设计 — 14 个端点 + 阶段化交付
  5. 事件设计 — BFF 订阅事件用于实时推送P5
  6. 横切关注点对齐清单 — 完整对齐 classes/teacher-bff
  7. 与其他模块的交互点 — 完整契约矩阵
  8. 风险与假设 — 技术风险、外部依赖、未决决策
  9. 演进路线图 — P3/P4/P5/P6 各阶段演进路径(长远规划)
  10. 扩展点设计 — 为未来场景预留
  11. 性能与容量规划
  12. 安全与合规
  13. 可观测性详细设计
  14. 实施清单

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 实时推送

B8 裁决DownstreamClient 抽象回写 shared-tspackages/shared-ts/src/bff/downstream-client.ts3 个 BFFstudent-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-eduschedule 域) SELF
StudentAttendance 我的考勤(未来扩展) core-eduattendance 域) 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 输入 SchemaZod

// 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 的 getEffectivePermissionsBFF 用视口过滤可见字段。

// 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 GraphQL Schema 全清单B1 裁决)

B1 裁决P2 起直接 GraphQL Yoga + DataLoader禁止 REST 渐进。 Schema 存放:packages/shared-ts/contracts/graphql/student-bff.schema.graphqlpresident §2.2.1 网关路径:/api/v1/student/* → api-gateway 剥离 /api/v1 后代理到 student-bff:3009 GraphQL endpoint 响应信封ActionStateG8 裁决),成功 {success: true, data, meta?},失败 {success: false, error: {code, message, i18nKey, details?, traceId?}} 分页规范Relay Cursor Connections{ edges, pageInfo, totalCount } 权限点标注schema 注释 # @permission: <RESOURCE>_<ACTION>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=<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/submitP3 核心 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 行为

  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

{
  "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 HTTPSSE 传输 Subscription
  • DataLoader 按下游服务分批聚合,解决 N+1
  • Schema 存放 packages/shared-ts/contracts/graphql/student-bff.schema.graphql
  • 分页采用 Relay Cursor Connections 规范
  • 降级模式方案 Bpresident §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

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 权限装饰器决策(已裁决 B3/B4

B3 裁决BFF 豁免 @RequirePermission,透传 x-user-id 给下游校验。 B4 裁决强制自我越权防御AuthorizationGuard学生只能查/操作自己数据。 president §2.9 方案 DDEV_MODE 下无 JWT 时跳过越权校验(本地开发友好)。

决策 选项 裁决结果
BFF 是否加 @RequirePermission A. 不加(对齐 teacher-bff透传 x-user-id
B. 加(双重校验)
B3A 方案BFF 豁免 @RequirePermission,透传 x-user-id
自我越权防御 A. 不做(依赖下游)
B. BFF 层做 userId 强制比对
B4B 方案(强制 AuthorizationGuard

B4 越权防御实现src/student/guards/authorization.guard.ts

  • 场景 AassertOwnData):资源无归属关系 → 抛 ForbiddenResourceError403
  • 场景 BassertIdentityMatchJWT userId 与 body userId 不一致 → 抛 IdentityMismatchError403
  • DEV_MODEpresident §2.9 方案 Denv.DEV_MODE=true 时跳过越权校验
  • 错误码president §2.7BFF_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 场景 BJWT/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 Loggerpino

文件位置 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 Metricsprom-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 TracerOpenTelemetry

文件位置 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 Contexttraceparent 头)

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. 停止消费 Kafkacommit 最后 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. 注入 traceIdx-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 服务定义 coordinfra P3 端口 3009加入 edu-net + edu-shared 网络
3 full-stack-runbook 端口矩阵更新 coorddocs P3 追加 3009 行
4 004 §15 文档位置矩阵更新 coorddocs P3 student-bff 状态从"📐 需设计"改为" 已实现"
5 shared-proto 补全 iam.protoViewport / EffectivePermissions coord P3 当前走 RESTproto 补全后切换 gRPC
6 shared-proto 补全 content.protoChapter / Question / KnowledgePath coord P4 P4 content 服务落地前补全
7 shared-proto 补全 core_edu.protoSchedule / Attendance 域) coord P4+ 学生课表/考勤未来扩展用
8 buf.gen.yaml 补 gRPC 插件 coord P3 B2 裁决TS 走 @grpc/proto-loader 动态加载,无需 buf generate gRPC 插件
9 core-edu 启用 gRPC server ai03core-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 裁决gRPCStreamChat 为 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-eduP3→ + content + data-anaP4→ + msg + aiP5 iam + core-eduP4→ + msg + data-anaP4+

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 不一致 短 TTL15-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 + DataLoaderP2 起直接 GraphQL禁止 REST 渐进)
2 BFF 是否做权限校验 B3 BFF 豁免 @RequirePermission,透传 x-user-id 给下游校验
3 BFF 是否做自我越权防御 B4 强制 B4 越权防御AuthorizationGuard场景 A + 场景 Bpresident §2.9 方案 D DEV_MODE 放行)
4 /readyz 检查逻辑 - 检查下游 6 个服务可达性(必需失败返回 503可选软失败返回 200 + degraded=true
5 Kafka 事件订阅时机 B7 P2-P4 不订阅P5 起订阅 7 个 topicRedis SETNX event_id 幂等去重
6 缓存策略 B6 Redis 5-30s 短缓存TTL ±20% 随机抖动防雪崩
7 端口分配 - 3009HTTP无 gRPC 端口对外
8 错误码前缀 B5 BFF_STUDENT_BFF 在前,非 STUDENT_BFF_
9 DownstreamClient 抽象是否回写 teacher-bff B8 回写 shared-ts3 个 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 reset10 次 volumeThreshold

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 -.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 风格演进(已裁决 B1GraphQL 即起点)

B1 裁决P2 起直接 GraphQL Yoga + DataLoader无 REST 渐进期。

阶段 API 风格 触发条件 实施状态
P2+ GraphQL YogaB1 裁决) 直接采用,无 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 + SubscriptionSSE 传输) AI 流式答疑 已落地(src/student/resolvers/ai-stream.resolver.ts
P6 + 熔断器opossum 下游故障隔离 已落地(src/shared/circuit-breaker/

GraphQL 落地清单

  • Schemapackages/shared-ts/contracts/graphql/student-bff.schema.graphql17 个字段14 Query + 2 Mutation + 1 Subscription
  • Resolversrc/student/resolvers/6 个文件dashboard / homework / exam / grade / notification / ai-stream
  • DataLoader按下游服务分批聚合
  • 降级模式:方案 Bpresident §2.6success=true + data 内 degraded=true

9.3 通信协议演进(已裁决 B2gRPC 首次即用)

B2 裁决:首次实现即用 gRPC@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetch。

阶段 BFF → 业务服务协议 理由 实施状态
P2+ gRPCB2 裁决) 首次即用,无 HTTP fetch 历史 已落地DownstreamClient.call
P5 + gRPC server-streaming AI 流式答疑ai.StreamChat 已落地DownstreamClient.callStream
P6 + Service MeshIstio mTLS 流量治理 + mTLS P6 阶段

DownstreamClient 抽象B8 裁决,复用 shared-ts

  • 位置:packages/shared-ts/src/bff/downstream-client.ts
  • 方法:callunary/ callAll(并行 unary/ callStreamserver-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/小程序)

graph LR
    A[student-bff API] --> B[Transformer 层]
    B --> C[Web 版响应]
    B --> D[H5 版响应(字段精简)]
    B --> E[小程序版响应(字段精简 + 数据预取)]

实现:在 Transformer 层根据 x-client-typeweb/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-10HPA
单实例 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 不持 sessionRedis 仅缓存数据
Token 刷新 透传 401 给前端,由前端走 refresh 流程

12.2 授权与数据隔离

维度 实现
权限校验 透传 x-user-id 给下游,下游按 @RequirePermission + DataScope 校验
自我越权防御 BFF 层强制 studentId = userId(成绩/作业/学情端点)
跨班级数据隔离 学生只能查自己所在班级classId 由 iam 推导,不接受前端传入)
AI 内容安全 AI 答疑请求透传 content moderationai 服务侧实现)

12.3 输入安全

维度 实现
输入验证 全部 Zod 校验,拒绝 unknown 字段
SQL 注入 BFF 无 DB不涉及下游用 Drizzle 参数化查询
XSS 响应 Content-Type: application/json禁止 HTML 渲染;富文本由前端 DOMPurify 清洗
CSRF Cookie 必须 SameSite=Strict + 后端校验 Origin 头
文件上传 作业附件走 OSS 直传(预签名 URLBFF 不接收文件流

12.4 数据合规

维度 实现
学生隐私 成绩/学情仅本人可见,不返回同班其他学生数据
日志脱敏 日志中不记录 answers 内容、score 数值;仅记录 metadatahomeworkId, traceId
数据保留 BFF 不持久化数据Redis 缓存 TTL ≤ 600s
GDPR/个保法 学生数据导出/删除请求透传给 iam / core-edu 处理

12.5 审计

维度 实现
请求审计 全部请求记录 access loguserId, 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.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 信封 + 降级模式方案 Bok/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 熔断器P650% 阈值30s reset
│  │  │  └─ circuit-breaker.module.ts
│  │  ├─ health/
│  │  │  ├─ health.controller.ts             # /healthz + /readyz6 个下游可达性检查)
│  │  │  └─ health.module.ts
│  │  └─ observability/
│  │     ├─ logger.ts                        # pino
│  │     ├─ metrics.ts                       # prom-client11 个 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.aiStreamChatSSEP5
│  │  ├─ dataloaders/
│  │  │  └─ data-loader.module.ts            # DataLoader 工厂(按下游服务分批)
│  │  ├─ events/
│  │  │  ├─ event-subscriber.ts              # Kafka 订阅P57 topicRedis 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                   # DownstreamClientcall/callAll/callStreamB8 裁决)
│  ├─ logger.ts                              # BFF 共享 logger
│  └─ index.ts
└─ contracts/graphql/
   └─ student-bff.schema.graphql             # GraphQL schema17 字段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 字段(已全部落地)

  • Query.currentUserauth.resolver.ts
  • Query.studentDashboarddashboard.resolver.ts
  • Query.myExamsexams.resolver.ts
  • Query.myHomeworkhomework.resolver.ts
  • Mutation.submitHomework ← P3 核心homework.resolver.ts
  • Query.myGradesgrades.resolver.ts
  • Query.myClassesclasses.resolver.ts
  • /healthz + /readyzhealth.controller.ts
  • /metricsprom-client

14.1.3 P3 横切关注点对齐(已全部落地)

  • pino loggerservice: 'student-bff'
  • prom-client metrics11 个 student_bff_* 指标)
  • OTel tracerserviceName: 'student-bff'
  • GlobalErrorFilterBFF_STUDENT_* 错误码11 个错误类)
  • Zod 输入校验Resolver 层 safeParse
  • DownstreamClientgRPC call/callAll/callStreamB8 复用 shared-ts
  • 熔断器opossumP6 已落地)
  • Redis 缓存5-30s TTL ±20% 抖动B6 裁决)
  • 自我越权防御AuthorizationGuardB4 裁决)
  • 优雅关闭SIGTERM + circuitBreaker.shutdown()
  • GraphQL Yoga + DataLoaderB1 裁决)
  • 测试覆盖率 ≥ 80%Vitest5 个测试文件)

14.2 P4 阶段扩展清单(已全部落地)

  • Query.textbookscontent.resolver.ts
  • Query.chapterscontent.resolver.ts
  • Query.learningPathcontent.resolver.ts
  • Query.myWeaknessanalytics.resolver.ts
  • Query.myTrendanalytics.resolver.ts
  • Query.myAttendance(预留,等 core-edu AttendanceService 落地)
  • /readyz 下游可达性检查6 个服务health.controller.ts

14.3 P5 阶段扩展清单(已全部落地)

  • Query.myNotificationsnotifications.resolver.ts
  • Mutation.markNotificationAsReadnotifications.resolver.ts
  • Query.myNotificationUnreadCountnotifications.resolver.ts
  • Query.aiChatai.resolver.ts同步
  • Subscription.aiStreamChatai-stream.resolver.tsSSE 流式)
  • Kafka EventSubscriber 模块event-subscriber.ts7 topic
  • push-gateway 推送通道push-gateway.service.ts
  • SSE 连接管理GraphQL Yoga 原生 SSE 传输)

14.4 P6 阶段硬化清单(部分落地)

  • opossum 熔断器circuit-breaker.service.ts50% 阈值30s reset
  • 熔断器指标student_bff_circuit_state Gauge
  • 熔断器优雅关闭main.ts SIGTERM handler
  • HPA 自动扩缩容( K8s 部署阶段)
  • Istio mTLS Service Mesh 阶段)
  • 全链路 trace + Grafana 仪表盘( 监控配置阶段)
  • 灾备演练( 运维阶段)
  • 99.9% 可用性压测( 压测阶段)

15. 与黄金模板对齐自检

对照 ai-allocation §6 模板第 6 节 + 004 §15

对齐项 classes 黄金模板 student-bff 设计 状态
权限装饰器 @RequirePermission 全部 Controller 方法 ⚠️ 不对齐BFF 不做权限校验,透传 x-user-id 已识别
错误码前缀统一 CLASSES_ BFF_STUDENT_(对齐 004 §11.4
loggerpino shared/observability/logger.ts 复制 teacher-bffservice: 'student-bff'
metricsprom-client /metrics 端点 复制 teacher-bff11 个 student_bff_* 指标
tracerOpenTelemetry OTLP exporter 复制 teacher-bffserviceName: 'student-bff'
/healthz 健康检查 liveness 复制 teacher-bff
/readyz 健康检查 Drizzle SELECT 1 P3 直接返回 okP4+ 检查下游可达性
优雅关闭SIGTERM LifecycleService main.ts 注册 SIGTERM → app.close + shutdownTracer
测试覆盖率 ≥ 80% Jest Vitest对齐 classes重点测 Service/Aggregator
Dockerfile 多阶段构建 builder + runtime 复制 teacher-bffEXPOSE 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.3DownstreamClient 回写 shared-ts3 个 BFF 统一使用B8 裁决)。
  3. 自我越权防御§2.3BFF 层强制 studentId = userIdB4 裁决AuthorizationGuard 已实现。
  4. 12 项设计决策§8.3全部裁决B1-B8 + president §2.2-2.9)。
  5. 16 项跨模块协作需求§7.2gRPC method 已明确B2 裁决)。
  6. GraphQL 演进时机§9.2P2 起直接 GraphQLB1 裁决),无 REST 渐进期。
  7. 熔断器引入§8.3 #12P6 引入 opossum 熔断器(已落地)。