Files
Edu/services/student-bff/docs/02-architecture-design.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
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.设计规格文档
2026-07-10 12:58:22 +08:00

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

改进点是否回写 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-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 端点全清单

路由前缀:/student(对齐 teacher-bff 用 /teacher 命名规律BFF 用角色单数无 -bff 后缀) 网关路径:/api/v1/student/* → api-gateway 剥离 /api/v1 后代理到 student-bff:3009 响应信封:统一 ActionState004 §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/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 风格决策(待 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. 加(双重校验)
ABFF 是聚合层,权限由下游服务校验) 待 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 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 决定是否在 P3 升级到 gRPC 通信
9 core-edu 启用 gRPC server ai03core-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-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 未决设计决策(待 coord 仲裁)

# 决策点 选项 ai04 建议 影响范围
1 BFF API 风格 A. REST对齐 teacher-bff 现状)
B. GraphQL对齐 004 §11.3 设计意图)
AP3 阶段先 RESTP4+ 统一升级时再考虑) 全部端点
2 BFF 是否做权限校验 A. 不校验(对齐 teacher-bff
B. 加 @RequirePermission
A(聚合层不做权限决策) 全部端点
3 BFF 是否做自我越权防御 A. 不做(依赖下游)
B. BFF 层强制 userId 比对
B(学生场景敏感) 成绩/作业/学情端点
4 /readyz 检查逻辑 A. 直接返回 ok对齐 teacher-bff
B. 检查下游可达性
AP3/ BP4+,下游可达性由 Prometheus 监控) 健康检查
5 Kafka 事件订阅时机 A. P3 不订阅
B. P3 订阅
Apush-gateway P5 才落地) P5 推送功能
6 缓存策略 A. 不缓存(对齐 teacher-bff
B. Redis 5-30s 短缓存
B(对齐 004 §6.2 BFF 混合读策略) 全部 GET 端点
7 端口分配 3009 30093001-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 分离)
ABFF 不持领域模型CQRS 收益不大) Service 层结构
11 SSE 实现 A. 原生 Node Stream
B. 第三方库(如 @nestjs/axios + RxJS
A(依赖少,可控) P5 AI 答疑流式
12 是否在 P3 引入熔断器 A. 不引入(依赖 gateway 熔断)
B. 引入opossum 库)
BBFF→下游单链路熔断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 决策升级)

  1. 引入 @nestjs/graphql + Apollo Server 或 GraphQL Yoga
  2. 定义 schemaQuery.studentDashboard / Query.studentHomework / Mutation.submitHomework
  3. Resolver 复用 Service 层逻辑
  4. 引入 DataLoader 解决 N+1如一个 Dashboard 同时查多个学生成绩)
  5. 前端逐步从 REST 切换到 GraphQLREST 端点保留 6 个月兼容期

9.3 通信协议演进

阶段 BFF → 业务服务协议 理由
P3 HTTP REST 下游 gRPC server 未启用,对齐 teacher-bff
P4 HTTP + gRPC 混合 iam / core-edu 启用 gRPCBFF 优先 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-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.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 loggerservice: 'student-bff'
  • prom-client metrics11 个指标)
  • OTel tracerserviceName: 'student-bff'
  • GlobalErrorFilterBFF_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-chatSSE 流式)
  • Kafka EventSubscriber 模块
  • push-gateway 推送通道
  • SSE 连接管理

14.4 P6 阶段硬化清单

  • HPA 自动扩缩容
  • Istio mTLS
  • 全链路 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 设计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.3DownstreamClient / Aggregator / Transformer 三层抽象是否回写 teacher-bff避免技术栈分裂。
  3. 自我越权防御§2.3BFF 层强制 studentId = userId 是与 teacher-bff 的差异点,请确认是否作为 BFF 通用规范。
  4. 12 项未决决策§8.3):请逐项仲裁。
  5. 16 项跨模块协作需求§7.2):请协调对应 AI 实施。
  6. GraphQL 演进时机§9.2P3 REST / P4+ 是否切换 GraphQL需全局决策。
  7. 熔断器引入§8.3 #12BFF 层是否在 P3 引入 opossum 熔断器,还是依赖 api-gateway 熔断。