# 模块架构设计文档 — parent-bff > AI 标识:ai05(coord 代笔推断 → ai05 复审修订正式接手) > 阶段:阶段 2(模块架构设计)· ai05 复审修订 > coord 代笔日期:2026-07-09 > ai05 复审日期:2026-07-09 深夜 > 状态:ai05 已复审,补充 ai05 视角的长远架构演进设计,进入 coord 交叉审查 > 关联文档: > > - [阶段 1 理解确认书](./01-understanding.md)(ai04 撰写 + ai05 复审修订) > - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) > - [coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md) > - [ai-allocation §3.2/§5/§7 模板](../../../docs/architecture/ai-allocation.md) > - [pending-features P4/P5](../../../docs/architecture/roadmap/pending-features.md) > - [project_rules](../../../.trae/rules/project_rules.md) > - [多 AI 协作指南](../../../docs/standards/multi-ai-collaboration.md) > - 参考实现:[teacher-bff 02-architecture-design.md](../../teacher-bff/docs/02-architecture-design.md)、[student-bff 02-architecture-design.md](../../student-bff/docs/02-architecture-design.md)、[classes 黄金模板](../../classes/) > > **代笔与复审说明**: > > - **coord 代笔阶段**(2026-07-09):ai05 未交付 02 文档,coord 基于 01-understanding.md(ai04 初稿)+ teacher-bff/student-bff 模式 + 用户 4 项仲裁决策(U1-U4)+ coord 补充仲裁(C1-C6)推断生成 §0-§8 共 1114 行。文档头部 8 项仲裁决策为生成依据,本文档不再就这些项提请仲裁。 > - **ai05 复审修订阶段**(2026-07-09 深夜):按 ai-allocation.md §3.2 真实分配(ai05 = parent-bff)正式接手,**保留 coord 推断版 §0-§8 原文不动**,在文末追加 §9-§14 共 6 节 ai05 视角补全:(1) §9 ai05 review 结论(响应 coord §8.4 的 10 项待 review 决策);(2) §10 长远架构演进设计(P5-P7+ 路线图 + 10 项长远预留点含多租户/合规/i18n/AI 助手/移动端/容灾);(3) §11 测试策略(4 层金字塔 + 关键用例 + Mock 策略);(4) §12 完整配置项清单(Zod 校验 + 环境变量分组);(5) §13 黄金模板对齐 checklist(16 项自检);(6) §14 待 coord 仲裁的新决策清单(8 项 ai05 提请)。 > - **ai05 修订原则**:不重写 coord 已完成的 §0-§8(避免破坏 coord 推断的连贯性);新增 §9-§14 以"补全长远架构演进 + 响应 review 项"为边界;如与 coord 推断结论有冲突,在 §9 明确标注并由 coord 最终仲裁。 --- ## 0. 文档导航与仲裁依据 ### 0.1 已仲裁决策(用户 4 项 + coord 补充规则,本文档直接执行) | # | 决策项 | 仲裁结论 | 依据 | | --- | ------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------- | | U1 | parent-bff 文档归属 | coord 推断生成(ai05 未交付) | 用户仲裁 1 | | U2 | push-gateway 通信 | push-gateway 豁免 gRPC,msg/student-bff 调用方改用 HTTP /internal/push | 用户仲裁 2 | | U3 | GraphQL 引入时机 | **GraphQL P2 立即引入**(覆盖 ai04 建议,恢复 pending-features P2 原计划)→ parent-bff 在 P4 落地时**直接用 GraphQL** | 用户仲裁 3 | | U4 | BFF 权限校验 | BFF 豁免 `@RequirePermission`(权限由 Gateway JWT + 下游服务负责,BFF 仅校验 x-user-id) | 用户仲裁 4 | | C1 | 错误码前缀 | `BFF_PARENT_`(对齐 004 §11.4,BFF 在前;不是 PARENT_BFF_) | coord 仲裁 | | C2 | 端口分配 | HTTP 3010,**不暴露 gRPC**(BFF 对下游走 gRPC,对上游仅 HTTP/GraphQL) | coord 仲裁 | | C3 | 下游错误码子前缀 | core-edu 统一用 `CORE_EDU_*`(不细分 EXAMS_/HOMEWORK_/GRADES_),与 teacher-portal 保持一致 | coord 仲裁 | | C4 | iam 端点路径 | 统一为 `GET /iam/permissions/effective`(不是 /iam/effective-permissions) | coord 仲裁 | | C5 | Kafka topic 命名 | 统一为 `edu.notification.sent/read/recalled/failed`(废弃 edu.notification.events 抽象名) | coord 仲裁 | | C6 | 依赖扩展 | parent-bff → iam + core-edu + data-ana + msg(与 student-bff 对齐,004 §4 待 coord 同步更新) | coord 仲裁 | ### 0.2 文档结构(对齐 ai-allocation §7 模板 8 章节) | 章节 | 内容 | 关键性 | | ---- | -------------------------------------------------------------------------- | ------------ | | §1 | 模块内部分层图(Controller → Service → 下游 gRPC/HTTP 调用链) | ★ 内部结构 | | §2 | 领域模型(BFF 纯聚合层,无聚合根,ParentSession 值对象) | ★ 边界 | | §3 | 数据模型(无 DB,Redis 仅缓存聚合结果 5-30s,方案 A 前端管理 childId) | ★ 无状态 | | §4 | API 设计(GraphQL Schema 优先,Query/Mutation + 权限点 + DataLoader) | ★ 对前端契约 | | §5 | 事件设计(P4 不订阅,P5 可选订阅 edu.notification.* / edu.teaching.*) | ★ 实时推送 | | §6 | 横切关注点对齐清单(@RequirePermission 豁免、错误码 BFF_PARENT_*、三支柱) | ★ 黄金模板 | | §7 | 与其他模块的交互点(iam/core-edu/data-ana/msg;被 parent-portal 调用) | ★ 跨模块契约 | | §8 | 风险与假设(P0 阻塞:iam 缺家长-学生关联接口;DataScope 越权校验) | ★ 阻塞项 | > **设计原则摘要**:BFF 本分(只聚合不持状态)、契约先行(gRPC + proto)、GraphQL 优先(U3)、DataScope=CHILDREN 强制隔离(ChildGuard)、无状态服务(方案 A)、可观测三支柱、黄金模板对齐 teacher-bff。 --- ## 1. 模块内部分层图 ### 1.1 目标态分层(P4 终态,GraphQL) ```mermaid graph TB subgraph Client["前端层"] PP[parent-portal
Next.js MF Remote
urql GraphQL Client] end subgraph Gateway["网关层"] AGW[api-gateway
Go/Gin
JWT RS256 校验 + 限流 + 熔断
注入 x-user-id / x-user-roles / x-request-id] end subgraph ParentBFF["parent-bff(本模块)P4 终态"] direction TB GQL[GraphQL Yoga Endpoint
POST /graphql
+ GET /graphql] HEALTH[Health Controller
/healthz /readyz /metrics] subgraph Middleware["中间件层"] CTX[Context Middleware
解析 x-user-id / x-user-roles
注入 GraphQL context] ERR[GlobalErrorFilter
统一错误兜底 → ActionState 信封] end subgraph Aggregation["聚合编排层"] RES[GraphQL Resolvers
按家长场景域组织] DL[DataLoader Registry
per-request 实例
批量去重 N+1] ORCH[Orchestrator
Promise.allSettled + 降级] GUARD[ChildGuard
DataScope=CHILDREN 越权校验] MAP[Response Mapper
proto → GraphQL type] end subgraph Cache["缓存层"] RCLIENT[RedisClient
ioredis] CKV[CacheKey Builder
bff:parent:*] end subgraph Clients["下游 Client 抽象层"] IAM_C[IamClient
gRPC adapter] CE_C[CoreEduClient
gRPC adapter] DA_C[DataAnaClient
gRPC adapter] MSG_C[MsgClient
gRPC adapter] end subgraph Obs["可观测层"] LOG[pino Logger] MET[prom-client Metrics] TRC[OTel Tracer
gRPC interceptor] end GQL --> CTX HEALTH --> CTX CTX --> RES RES --> GUARD RES --> DL RES --> ORCH RES --> RCLIENT DL --> Clients ORCH --> Clients Clients --> Obs RCLIENT --> CKV ERR -.-> GQL ERR -.-> HEALTH end subgraph Downstream["下游业务服务(gRPC)"] IAM[iam:50052
含 GetChildrenByParent
待 ai02 补] CE[core-edu:50053
Exam/Homework/Grade Service] DA[data-ana:50055
AnalyticsService] MSG[msg:50056
NotificationService] end subgraph Infra["基础设施"] R[(Redis 7
edu-redis:6379)] J[Jaeger
OTLP] P[Prometheus
:9090] end PP -->|GraphQL over HTTP| AGW AGW -->|HTTP + x-user-id/x-user-roles| GQL AGW --> HEALTH IAM_C --> IAM CE_C --> CE DA_C --> DA MSG_C --> MSG RCLIENT --> R LOG --> J MET --> P TRC --> J ``` ### 1.2 调用链详解(同步聚合链 — Dashboard 场景) ```mermaid sequenceDiagram participant PP as parent-portal participant GW as api-gateway participant C as ParentController (GraphQL) participant S as ParentService participant G as ChildGuard participant A as Aggregator participant Cache as Redis participant DC as DownstreamClient participant IAM as iam participant Core as core-edu PP->>GW: POST /api/v1/parent/graphql
{ query: "dashboard { user children { id name lastGrade } }" } GW->>GW: RS256 公钥校验 + 提取 userId GW->>C: POST /parent/graphql
x-user-id: u-parent-001
x-request-id: r-xxx C->>C: extractUserId(req) → u-parent-001
GraphQL parse + validate C->>S: dashboard(userId) S->>A: aggregateDashboard(u-parent-001) A->>Cache: GET bff:parent:dashboard:u-parent-001 alt 缓存命中 Cache-->>A: { data, cachedAt } A-->>S: data else 缓存未命中 par 并行下游 A->>DC: callIAM('GetChildrenByParent', u-parent-001) DC->>IAM: gRPC GetChildrenByParent(parentId) IAM-->>DC: { children: [child-1, child-2] } and A->>DC: callIAM('GetUserInfo', u-parent-001) DC->>IAM: gRPC GetUserInfo(userId) IAM-->>DC: { user, roles, dataScope: CHILDREN } end par 并行按 childId 拉成绩 A->>DC: callCoreEdu('ListGradesByStudent', child-1) DC->>Core: gRPC ListGradesByStudent(studentId) Core-->>DC: { grades: [...] } and A->>DC: callCoreEdu('ListGradesByStudent', child-2) DC->>Core: gRPC ListGradesByStudent(studentId) Core-->>DC: { grades: [...] } end DC-->>A: 合并结果 + 部分失败标记 A->>A: Mapper 裁剪 + 视口过滤 A->>Cache: SET bff:parent:dashboard:u-parent-001 EX 15 A-->>S: data end S-->>C: DashboardData C-->>GW: 200 { data: {...}, meta: { cachedAt, partial } } GW-->>PP: 200 GraphQL 响应 ``` ### 1.3 越权校验调用链(DataScope=CHILDREN 防御) > 越权校验时序:parent-portal → api-gateway → ParentController → ChildGuard → iam.GetChildrenByParent → 若 childId 不在绑定列表则 throw `BFF_PARENT_CHILD_NOT_BOUND`(403)。详见 §2.3 代码示例。 ### 1.4 目录结构(目标态) ``` services/parent-bff/src/ ├─ config/env.ts # Zod 校验环境变量(PORT=3010 + 下游 gRPC target) ├─ entry/ # 入口层 │ ├─ graphql.controller.ts # GraphQL Yoga 端点(POST /graphql) │ └─ context.middleware.ts # 解析 x-user-id 注入 GraphQL context ├─ parent/ # 家长场景域 │ ├─ parent.controller.ts # REST 兼容端点(仅 /healthz /readyz /metrics) │ ├─ parent.service.ts # 聚合 Service │ └─ parent.schema.ts # Zod 输入校验 ├─ aggregation/ # 聚合编排层 │ ├─ orchestrator.ts # Promise.allSettled + 降级 │ ├─ child-guard.ts # DataScope=CHILDREN 越权校验 │ ├─ response-mapper.ts # proto → GraphQL type │ └─ fallback-strategy.ts # 降级策略 ├─ dataloader/ # DataLoader(per-request) │ ├─ dataloader.module.ts # 注册器 │ ├─ children.dataloader.ts # 按 parentId 批量取孩子列表 │ ├─ grade.dataloader.ts # 按 studentId 批量取成绩 │ ├─ homework.dataloader.ts # 按 classId 批量取作业 │ └─ exam.dataloader.ts # 按 classId 批量取考试 ├─ graphql/ # GraphQL Schema │ ├─ schema.ts # typeDefs + resolvers │ ├─ types/ # parent/child/grade/homework/exam/analytics/notification.type.ts │ └─ resolvers/ # dashboard/child/grade/homework/exam/analytics/notification.resolver.ts ├─ clients/ # 下游 Client 抽象层 │ ├─ iam.client.ts / core-edu.client.ts / data-ana.client.ts / msg.client.ts # gRPC adapter │ ├─ grpc/ # grpc.factory.ts + interceptors.ts(trace/metrics/retry) │ └─ http/push-http.client.ts # push-gateway 走 HTTP(U2 仲裁) ├─ shared/ │ ├─ errors/ # application-error.ts(BFF_PARENT_*)+ global-error.filter.ts │ ├─ health/health.controller.ts # /healthz + /readyz(BFF 直接 ok) │ ├─ cache/ # redis.client.ts + cache-key.builder.ts(bff:parent:*) │ ├─ observability/ # logger.ts(pino)+ metrics.ts(parent_bff_*)+ tracer.ts(OTel) │ └─ kafka/ # kafka.consumer.ts + handlers/(P5 可选) ├─ app.module.ts └─ main.ts # 启动 + /metrics + SIGTERM ``` ### 1.5 分层职责契约 | 层 | 职责 | 禁止 | | ------------- | --------------------------------------------------- | ---------------------- | | Entry | GraphQL 端点 + 健康端点,解析请求,注入 context | 业务逻辑、下游调用 | | Middleware | 通用横切(context 注入、错误兜底) | 业务逻辑 | | Aggregation | 编排多下游调用、聚合裁剪、ChildGuard 越权校验、降级 | 直接访问 DB、权限决策 | | Cache | 缓存读写(仅聚合结果 5-30s) | 业务逻辑、会话状态存储 | | Clients | 下游协议适配(gRPC)、interceptor | 业务聚合逻辑 | | Observability | 日志/指标/链路 | 业务逻辑 | **依赖方向**:Entry → Aggregation → Clients → Downstream;Cache 横向被 Aggregation 调用;Observability 横向被所有层调用。禁止反向依赖。 --- ## 2. 领域模型 > parent-bff 是 BFF 聚合层,**不持有业务领域聚合根**(不写 DB、不定义聚合根)。本节定义 BFF 内部的"场景聚合视图"与"值对象"。 ### 2.1 场景聚合视图清单 | 聚合视图 | 业务含义 | 主要下游服务 | 读写特性 | DataScope | | ----------------------- | ---------------------------------------- | -------------- | --------------- | --------- | | ParentDashboard | 家长首页:个人信息 + 孩子列表 + 近期成绩 | iam + core-edu | 读 / 聚合 | CHILDREN | | ChildrenList | 我的孩子列表(含切换上下文) | iam | 读 | CHILDREN | | ChildGrades | 指定孩子的成绩历史 | core-edu | 读 | CHILDREN | | ChildHomework | 指定孩子的作业列表 | core-edu | 读 | CHILDREN | | ChildExams | 指定孩子的考试列表 | core-edu | 读 | CHILDREN | | ChildAnalytics | 指定孩子的学情诊断 + 趋势 | data-ana | 读 | CHILDREN | | ParentNotifications | 家长通知列表 + 已读 | msg | 读 + 写(已读) | SELF | | NotificationPreferences | 通知偏好配置 | msg | 读 + 写 | SELF | ### 2.2 ParentSession 值对象(多子女切换上下文) > **重要**:采用方案 A(前端管理 childId,BFF 无状态)。ParentSession 仅作为 GraphQL context 内的**请求级值对象**存在,不持久化到 Redis 会话。 ```typescript // graphql/context.ts export interface ParentSession { parentId: string; // 从 x-user-id 头解析 roles: string[]; // 从 x-user-roles 头解析 dataScope: "CHILDREN"; // 家长固定 CHILDREN(004 §5.4) requestedChildId?: string; // 从 GraphQL query 参数解析(前端传入) traceId: string; // 从 x-request-id 头解析 } ``` **设计要点**: 1. ParentSession **每次请求重建**,不跨请求保留状态(符合 004 §12.1 无状态约束) 2. `requestedChildId` 由前端在 GraphQL query 参数传入(方案 A),BFF 不维护"当前选中孩子" 3. `POST /parent/children/:childId/select`(selectChild mutation)仅记录审计日志,不持久化会话 4. ChildGuard 在 Resolver 执行前校验 `requestedChildId ∈ iam.GetChildrenByParent(parentId)` ### 2.3 DataScope=CHILDREN 越权校验(BFF 层强制) BFF 不做权限决策(U4 豁免 `@RequirePermission`),但做**越权防御**(与 student-bff 的 SELF 防御同理,CHILDREN 更复杂): ```typescript // aggregation/child-guard.ts async validateChildAccess(session: ParentSession, childId: string): Promise { if (!session.requestedChildId || session.requestedChildId !== childId) { // 重新校验(防止 context 被篡改) } const children = await this.iamClient.getChildrenByParent(session.parentId); const isBound = children.some(c => c.id === childId); if (!isBound) { throw new ChildNotBoundError('BFF_PARENT_CHILD_NOT_BOUND', { parentId: session.parentId, requestedChildId: childId, boundChildren: children.map(c => c.id), }); } } ``` > **设计权衡**:BFF 层做 CHILDREN 越权校验是 P4 安全硬化项。即使下游 core-edu 也按 dataScope 过滤,BFF 层先拦截可: > > 1. 早失败,减少无效下游 gRPC 调用 > 2. 提升审计能力(越权尝试在 BFF 层记录) > 3. 降低跨家庭数据泄露风险(家长 A 不能查家长 B 的孩子) > 4. 不依赖下游服务实现 DataScope=CHILDREN 语义(core-edu 当前仅支持 SELF/CLASS/GRADE/SCHOOL) ### 2.4 聚合视图间关系 ```mermaid graph LR Dashboard[ParentDashboard
聚合视图] Dashboard --> Children[ChildrenList] Dashboard --> Grades[ChildGrades] Dashboard --> Notif[ParentNotifications] Children -.切换.-> SelectChild[selectChild Mutation
仅审计日志] Children --> Grades Children --> Homework[ChildHomework] Children --> Exams[ChildExams] Children --> Analytics[ChildAnalytics] Grades -.数据流.-> Analytics Notif -.偏好过滤.-> Preferences[NotificationPreferences] ``` --- ## 3. 数据模型(无 DB,缓存 + DTO) > parent-bff **无数据库**。Redis 仅缓存聚合结果 5-30s(方案 A:BFF 无状态,不存会话)。本节定义 Redis 缓存 schema 与传输 DTO。 ### 3.1 Redis 缓存 Schema #### 3.1.1 Key 命名规范 | 用途 | Key 模式 | TTL | 失效策略 | | ----------------------- | ---------------------------------------------- | ---- | ---------------------------- | | 家长 Dashboard 聚合 | `bff:parent:dashboard:{parentId}` | 15s | 短 TTL + 成绩发布事件 | | 孩子列表 | `bff:parent:children:{parentId}` | 60s | 短 TTL + 绑定变更事件 | | 孩子成绩列表 | `bff:parent:grades:{childId}:{page}` | 30s | 短 TTL + 成绩录入事件 | | 孩子作业列表 | `bff:parent:homework:{childId}:{classId}` | 30s | 短 TTL | | 孩子考试列表 | `bff:parent:exams:{childId}:{classId}` | 30s | 短 TTL | | 孩子学情诊断 | `bff:parent:analytics:weakness:{childId}` | 300s | 中 TTL(每日刷新) | | 孩子学习趋势 | `bff:parent:analytics:trend:{childId}:{range}` | 600s | 长 TTL(趋势变化慢) | | 通知列表 | `bff:parent:notifications:{parentId}:{page}` | 15s | 短 TTL + 已读后失效 | | 通知偏好 | `bff:parent:notification-prefs:{parentId}` | 300s | 中 TTL + 更新后失效 | | ChildGuard 绑定列表缓存 | `bff:parent:child-bindings:{parentId}` | 60s | 短 TTL(安全敏感,不宜过长) | > **Key 设计原则**: > > - 全部以 `bff:parent:` 前缀,便于运维清理,避免与 teacher-bff (`bff:teacher:`)、student-bff (`student:`) 冲突 > - 按 parentId / childId 维度隔离,便于按用户失效 > - **不存会话状态**(方案 A:当前选中 childId 由前端管理,BFF 不存 `parent:session:*`) > - TTL 分档:15s(高频变更)/ 30-60s(中频)/ 300s(低频)/ 600s(趋势) > - ChildGuard 绑定列表缓存 TTL 60s(安全敏感,越权校验不能容忍太长延迟) #### 3.1.2 缓存失效策略 ```mermaid flowchart TD W[写操作:标记已读/更新偏好] --> INV[主动失效相关 key] E[Kafka 事件:成绩录入/作业批改 P5] --> SUB[EventSubscriber 消费] SUB --> INV2[失效 bff:parent:grades:childId:*] T[TTL 到期] --> NATURAL[自然过期] INV --> REDIS[DEL key] INV2 --> REDIS ``` ### 3.2 传输 DTO 设计 > DTO 用 Zod schema 定义,自动推导 TS 类型。GraphQL type 与 Zod schema 字段对齐。下游响应包装 `DownstreamEnvelopeSchema`(success/data/error)复用 teacher-bff 实现,不再赘述。 **家长端核心 DTO**(`parent/dto/parent-dashboard.dto.ts`):`ParentDashboardSchema` 含 `parent`(id/name/avatar)、`children`(id/name/grade/class/lastGrade)、`unreadNotifications`,字段与 GraphQL `DashboardData` type 对齐。 **输入 Schema**(`parent/dto/parent-inputs.dto.ts`): - `UpdateNotificationPreferencesSchema`:channels(app/sms/email/wechat 枚举数组,min 1)+ eventTypes(5 个布尔开关:gradeReleased/homeworkGraded/examPublished/attendanceAlert/schoolAnnouncement) - `ListGradesQuerySchema`:childId(min 1)、page(≥1,默认 1)、pageSize(1-50,默认 20)、subject(可选) ### 3.3 DTO 与 proto message 映射 | BFF DTO | proto message | 映射规则 | | -------------------------- | ------------------------------------------------ | ------------------ | | `ParentDashboard.parent` | `iam.v1.UserInfo` | 字段一一映射 | | `ParentDashboard.children` | `iam.v1.Child[]`(待 ai02 补) | 字段一一映射 | | `ChildGrades` | `core_edu.v1.Grade[]` | 时间字段毫秒数转换 | | `ChildAnalytics` | `analytics.v1.StudentWeakness` / `LearningTrend` | 字段一一映射 | | `ParentNotifications` | `msg.v1.Notification[]` | 字段一一映射 | --- ## 4. API 设计(GraphQL 优先) ### 4.1 API 风格仲裁说明 > **冲突**:ai04 在 01-understanding.md §4.1 建议 P4 阶段 parent-bff 先对齐 teacher-bff 现状(REST + fetch)。 > > **用户仲裁 U3**:**GraphQL P2 立即引入**(覆盖 ai04 建议,恢复 pending-features P2 原计划)→ parent-bff 在 P4 落地时**直接用 GraphQL**,不走 REST 过渡期。 > > **本设计文档执行 U3**:parent-bff P4 落地即 GraphQL Yoga + DataLoader,不实现 REST 业务端点(仅保留 /healthz /readyz /metrics 基础端点)。 ### 4.2 GraphQL Schema ```graphql # ============ Types ============ type Parent { id: ID! email: String! name: String! avatar: String roles: [String!]! dataScope: DataScope! } enum DataScope { SELF # L0 本人 CHILDREN # 家长自定义级(绑定孩子) CLASS # L1 本班 GRADE # L2 本年级 SCHOOL # L3 本校 DISTRICT # L4 本区 ALL # L5 全部 } type ViewportItem { key: String! label: String! route: String! icon: String sortOrder: String! requiredPermission: String } type Child { id: ID! name: String! grade: String! class: ClassInfo! # 延迟加载:孩子近期成绩 lastGrade: Grade # 延迟加载:孩子成绩历史 grades(page: Int = 1, pageSize: Int = 20, subject: String): GradePage! # 延迟加载:孩子作业列表 homework: [Homework!]! # 延迟加载:孩子考试列表 exams: [Exam!]! # 延迟加载:孩子学情诊断 analytics: ChildAnalytics! } type ClassInfo { id: ID! name: String! gradeId: String! } type Grade { id: ID! examId: ID! examTitle: String! subject: String! score: Float! rank: Int gradedAt: DateTime! } type GradePage { items: [Grade!]! pagination: Pagination! } type Pagination { page: Int! pageSize: Int! total: Int! } type Homework { id: ID! title: String! classId: ID! dueDate: DateTime! status: HomeworkStatus! submittedAt: DateTime } enum HomeworkStatus { NOT_SUBMITTED SUBMITTED GRADED OVERDUE } type Exam { id: ID! title: String! classId: ID! status: ExamStatus! examDate: DateTime! publishedAt: DateTime } enum ExamStatus { DRAFT PUBLISHED IN_PROGRESS GRADING SCORED ARCHIVED } type ChildAnalytics { childId: ID! weakness: [WeaknessTopic!]! trend: [TrendPoint!]! classRank: Int classAverage: Float } type WeaknessTopic { knowledgePointId: ID! name: String! subject: String! masteryRate: Float! } type TrendPoint { date: DateTime! score: Float! subject: String } type Notification { id: ID! type: NotificationType! title: String! content: String! read: Boolean! childId: ID createdAt: DateTime! } enum NotificationType { SYSTEM EXAM HOMEWORK GRADE ATTENDANCE ANNOUNCEMENT } type NotificationPreferences { channels: [NotificationChannel!]! eventTypes: NotificationEventTypes! } enum NotificationChannel { APP SMS EMAIL WECHAT } type NotificationEventTypes { gradeReleased: Boolean! homeworkGraded: Boolean! examPublished: Boolean! attendanceAlert: Boolean! schoolAnnouncement: Boolean! } type DashboardData { parent: Parent children: [Child!]! viewports: [ViewportItem!]! unreadNotifications: Int! } # ============ Query ============ type Query { # 家长仪表盘聚合(并行:iam 家长信息 + iam 孩子列表 + core-edu 近期成绩) dashboard: DashboardData! # 视口配置(L1 导航,透传 iam) viewports: [ViewportItem!]! # 当前家长信息 me: Parent! # 我的孩子列表 children: [Child!]! # 单个孩子详情(含延迟加载字段,需 ChildGuard 校验) child(childId: ID!): Child # 孩子成绩(直接查询,需 ChildGuard 校验) childGrades( childId: ID! page: Int = 1 pageSize: Int = 20 subject: String ): GradePage! # 孩子作业(需 ChildGuard 校验) childHomework(childId: ID!): [Homework!]! # 孩子考试(需 ChildGuard 校验) childExams(childId: ID!): [Exam!]! # 孩子学情诊断(需 ChildGuard 校验,P4 依赖 data-ana) childAnalytics(childId: ID!, dateRange: DateRangeInput): ChildAnalytics! # 家长通知列表 notifications( unreadOnly: Boolean page: Int = 1 pageSize: Int = 20 ): [Notification!]! # 通知偏好配置 notificationPreferences: NotificationPreferences! } input DateRangeInput { start: DateTime! end: DateTime! } # ============ Mutation ============ type Mutation { # 切换当前选中孩子(方案 A:仅记录审计日志,不持久化会话) selectChild(childId: ID!): SelectChildResult! # 标记通知已读 markNotificationRead(notificationId: ID!): Notification! # 更新通知偏好 updateNotificationPreferences( input: UpdateNotificationPreferencesInput! ): NotificationPreferences! } type SelectChildResult { childId: ID! selectedAt: DateTime! audited: Boolean! } input UpdateNotificationPreferencesInput { channels: [NotificationChannel!]! eventTypes: NotificationEventTypesInput! } input NotificationEventTypesInput { gradeReleased: Boolean homeworkGraded: Boolean examPublished: Boolean attendanceAlert: Boolean schoolAnnouncement: Boolean } scalar DateTime ``` ### 4.3 Query/Mutation 清单与权限点 > **U4 仲裁**:BFF 豁免 `@RequirePermission`,权限由 Gateway JWT + 下游服务负责。下表"权限点"列**仅为透传给下游校验**的语义标注,BFF 层不校验。 | 类型 | 字段 | 聚合下游 | 权限点(透传下游) | ChildGuard | 阶段 | | -------- | ----------------------------- | ---------------- | ------------------------ | ------------------ | ---- | | Query | dashboard | iam + core-edu | PARENT_DASHBOARD_READ | 否(聚合所有孩子) | P4 | | Query | viewports | iam | PARENT_VIEWPORT_READ | 否 | P4 | | Query | me | iam | PARENT_PROFILE_READ | 否 | P4 | | Query | children | iam | PARENT_CHILDREN_READ | 否 | P4 | | Query | child | iam + core-edu | PARENT_CHILD_READ | **是** | P4 | | Query | childGrades | core-edu | PARENT_GRADE_READ | **是** | P4 | | Query | childHomework | core-edu | PARENT_HOMEWORK_READ | **是** | P4 | | Query | childExams | core-edu | PARENT_EXAM_READ | **是** | P4 | | Query | childAnalytics | data-ana | PARENT_ANALYTICS_READ | **是** | P4 | | Query | notifications | msg | PARENT_NOTIFICATION_READ | 否 | P5 | | Query | notificationPreferences | msg | PARENT_PREFERENCE_READ | 否 | P5 | | Mutation | selectChild | (BFF 内部审计) | PARENT_CHILDREN_READ | **是** | P4 | | Mutation | markNotificationRead | msg | PARENT_NOTIFICATION_READ | 否 | P5 | | Mutation | updateNotificationPreferences | msg | PARENT_PREFERENCE_UPDATE | 否 | P5 | ### 4.4 DataLoader 策略 | 风险点 | 场景 | DataLoader | 批量键 | 下游 RPC | 状态 | | -------- | -------------------------------------------------- | -------------- | ----------- | ------------------------------------- | ------- | | 孩子列表 | dashboard 拉 children,多个 Resolver 都要 children | ChildrenLoader | parentId[] | iam.GetChildrenByParent(待 ai02 补) | ❌ 待补 | | 成绩列表 | child.grades 延迟加载,多个孩子并发 | GradeLoader | studentId[] | core-edu.ListGradesByStudent | ✅ 已有 | | 作业列表 | child.homework 延迟加载 | HomeworkLoader | classId[] | core-edu.ListHomeworkByClass | ✅ 已有 | | 考试列表 | child.exams 延迟加载 | ExamLoader | classId[] | core-edu.ListExamsByClass | ✅ 已有 | | 班级信息 | child.class 延迟加载 | ClassLoader | classId[] | core-edu.GetClass | ✅ 已有 | **DataLoader 生命周期**: - **per-request 实例**:每个 GraphQL 请求创建独立 DataLoader 实例,请求结束销毁 - **批量化窗口**:默认 16ms 内的请求合并为一次批量调用 - **缓存**:DataLoader 自身缓存(per-request),配合 Redis 跨请求缓存(§3.1) > **批量 RPC 缺口**:iam.GetChildrenByParent 当前仅支持单个 parentId 查询,DataLoader 可做 per-request 去重(同一 parentId 多次调用合并为一次),但跨 parentId 批量化需 ai02 补 `BatchGetChildrenByParents(parentIds[])`。P4 阶段 per-request 去重已足够(家长场景单 parentId)。 ### 4.5 统一响应信封 GraphQL 端点遵循 GraphQL 规范(data/errors),错误扩展字段携带 `extensions.code = "BFF_PARENT_*"`: ```json { "data": null, "errors": [ { "message": "Child not bound to parent", "extensions": { "code": "BFF_PARENT_CHILD_NOT_BOUND", "parentId": "u-parent-001", "requestedChildId": "child-999", "traceId": "r-xxx" } } ] } ``` 健康端点(/healthz /readyz)遵循 ActionState 信封(与 classes 黄金模板一致)。 --- ## 5. 事件设计 > BFF **不发布**领域事件(无业务事务)。BFF 可**订阅**事件用于实时推送(P5 阶段)。 ### 5.1 P4 阶段:不订阅事件 P4 阶段 parent-bff 仅做同步聚合,不消费 Kafka 事件。push-gateway P5 才落地,P4 订阅事件无消费方。 ### 5.2 P5 可选订阅事件清单 > **C5 仲裁**:Kafka topic 统一为 `edu.notification.sent/read/recalled/failed`(废弃 edu.notification.events 抽象名)。教学事件遵循 `edu.teaching..` 格式。 | Topic | 事件 | 发布方 | 消费动作 | 幂等性 | | ------------------------------ | ------------ | -------- | ------------------------------------------------- | ------------------- | | `edu.notification.sent` | 通知发送 | msg | 推送给家长(push-gateway HTTP /internal/push) | event_id SETNX 去重 | | `edu.notification.read` | 通知已读 | msg | 失效 `bff:parent:notifications:*` | event_id SETNX 去重 | | `edu.notification.recalled` | 通知撤回 | msg | 失效通知缓存 + 推送撤回消息 | event_id SETNX 去重 | | `edu.notification.failed` | 通知发送失败 | msg | 记录日志 + 告警 | event_id SETNX 去重 | | `edu.teaching.grade.recorded` | 成绩录入 | core-edu | 失效 `bff:parent:grades:childId:*` + 推送成绩通知 | event_id SETNX 去重 | | `edu.teaching.homework.graded` | 作业批改完成 | core-edu | 失效 `bff:parent:homework:childId:*` + 推送 | event_id SETNX 去重 | | `edu.teaching.exam.published` | 考试发布 | core-edu | 失效 `bff:parent:exams:childId:*` + 推送考试提醒 | event_id SETNX 去重 | ### 5.3 事件订阅架构(P5) ```mermaid graph LR K[(Kafka)] ES[EventSubscriber
NestJS Module] Idempotency[(Redis SETNX
event_id 去重)] Filter[偏好过滤器
按 NotificationPreferences 过滤] DC[DownstreamClient] Push[push-gateway
HTTP /internal/push] Cache[Redis Cache Invalidation] K -->|edu.notification.sent| ES K -->|edu.teaching.grade.recorded| ES ES --> Idempotency Idempotency -->|首次| Filter Filter -->|偏好允许| DC DC --> Push ES --> Cache ``` > **U2 仲裁**:push-gateway 豁免 gRPC,parent-bff 调用 push-gateway 走 **HTTP `/internal/push`**(不是 gRPC)。 ### 5.4 通知偏好过滤逻辑 P5 订阅事件时,根据家长 NotificationPreferences 过滤(`kafka/handlers/notification-push.handler.ts`): 1. 拉取家长 `NotificationPreferences`(Redis 缓存 300s) 2. 按 `eventTypeMap` 映射事件类型 → 偏好开关(GRADE→gradeReleased、HOMEWORK→homeworkGraded、EXAM→examPublished、ATTENDANCE→attendanceAlert、ANNOUNCEMENT→schoolAnnouncement) 3. 若偏好开关为 false → 不推送(家长关闭了该类通知) 4. 取 `prefs.channels` 与 `event.channels` 交集,无交集则不推送 5. 调用 `pushClient.pushViaHttp(parentId, payload, allowedChannels)`(U2:HTTP /internal/push) ### 5.5 事件订阅消费者组设计(P5) ```yaml consumer_group: parent-bff-event-subscriber topics: - edu.notification.sent - edu.notification.read - edu.notification.recalled - edu.notification.failed - edu.teaching.grade.recorded - edu.teaching.homework.graded - edu.teaching.exam.published commit_strategy: manual retry_strategy: max_retries: 3 backoff: exponential dlq_topic: edu.parent-bff.dlq ``` ### 5.6 BFF 不发布事件 BFF 是聚合层,**不发布领域事件**(无业务状态变更)。BFF 自身的 metrics/audit log 走可观测性通道,不走 Kafka。 --- ## 6. 横切关注点对齐清单 ### 6.1 权限装饰器(U4 豁免说明) | 端点 | 权限校验 | 说明 | | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | /graphql(全部 Query/Mutation) | 仅校验 x-user-id 存在 + ChildGuard 越权校验 | U4 仲裁:BFF 豁免 `@RequirePermission`,权限由 Gateway JWT + 下游服务负责 | | /healthz, /readyz, /metrics | 无 | 白名单放行 | > **豁免理由**(U4): > > 1. BFF 是聚合层,不持有业务资源,权限决策无意义 > 2. Gateway 已做 JWT 校验(RS256),非法请求无法到达 BFF > 3. 下游服务(iam/core-edu/data-ana/msg)各自用 `@RequirePermission` 校验,BFF 透传 `x-user-id` 即可 > 4. BFF 仅做 ChildGuard 越权防御(DataScope=CHILDREN),这不是权限决策而是数据隔离防御 ### 6.2 错误码清单(BFF_PARENT_ 前缀,对齐 C1 仲裁) | 错误码 | HTTP | 触发条件 | 详情字段 | | -------------------------------- | ---- | ----------------------------------------- | ----------------------------------------------- | | `BFF_PARENT_VALIDATION_ERROR` | 400 | Zod / GraphQL input 校验失败 | `{ field, message }` | | `BFF_PARENT_UNAUTHORIZED` | 401 | 缺失 x-user-id 头 | — | | `BFF_PARENT_CHILD_NOT_BOUND` | 403 | ChildGuard 拦截:childId 不在家长绑定列表 | `{ parentId, requestedChildId, boundChildren }` | | `BFF_PARENT_NOT_FOUND` | 404 | 资源不存在(BFF 自身资源) | `{ resource, id }` | | `BFF_PARENT_CONFLICT` | 409 | 重复操作 / 状态冲突 | `{ reason }` | | `BFF_PARENT_BUSINESS_ERROR` | 422 | 业务规则违反 | `{ rule }` | | `BFF_PARENT_BAD_GATEWAY` | 502 | 下游服务返回非 ok 或 gRPC rejected | `{ service, rpc, status, traceId }` | | `BFF_PARENT_GATEWAY_TIMEOUT` | 504 | 下游调用超时 | `{ service, rpc, timeoutMs }` | | `BFF_PARENT_SERVICE_UNAVAILABLE` | 503 | 熔断器开启 | `{ service, circuitState }` | | `BFF_PARENT_INTERNAL_ERROR` | 500 | 未捕获异常 | `{ traceId }` | **下游业务错误透传**(C3 仲裁:core-edu 统一 `CORE_EDU_*`,不细分 EXAMS_/HOMEWORK_/GRADES_): | 下游服务 | 错误码前缀 | 示例 | | -------- | ------------ | ----------------------------------------------------- | | iam | `IAM_*` | `IAM_USER_NOT_FOUND` | | core-edu | `CORE_EDU_*` | `CORE_EDU_GRADE_NOT_FOUND`、`CORE_EDU_EXAM_NOT_FOUND` | | data-ana | `DATA_ANA_*` | `DATA_ANA_ANALYTICS_NOT_READY` | | msg | `MSG_*` | `MSG_NOTIFICATION_NOT_FOUND` | ### 6.3 Logger(pino) | 项 | 值 | | ------------ | -------------------------------------------------------------------------------- | | 文件位置 | `src/shared/observability/logger.ts` | | service 字段 | `'parent-bff'` | | level | env.LOG_LEVEL(默认 `info`) | | 输出 | JSON stdout | | 字段 | `time, level, service, msg, traceId, parentId, childId, endpoint, duration, err` | | 采样 | 生产环境 warn+ 100% 采样,info 10% 采样 | | 禁止 | `console.log`;记录 JWT token / 密码 / 孩子成绩数值(仅记录 metadata) | ### 6.4 Metrics(prom-client) | 指标名 | 类型 | 标签 | 描述 | | ---------------------------------------- | ------------- | -------------------------- | ----------------------- | | `parent_bff_graphql_requests_total` | Counter | `operation, status` | GraphQL 请求总数 | | `parent_bff_graphql_duration_seconds` | Histogram | `operation` | GraphQL 请求延迟 | | `parent_bff_downstream_calls_total` | Counter | `service, rpc, status` | 下游 gRPC 调用次数 | | `parent_bff_downstream_duration_seconds` | Histogram | `service, rpc` | 下游调用延迟 | | `parent_bff_downstream_errors_total` | Counter | `service, rpc, error_type` | 下游调用错误数 | | `parent_bff_cache_hits_total` | Counter | `cache_key_pattern` | 缓存命中 | | `parent_bff_cache_misses_total` | Counter | `cache_key_pattern` | 缓存未命中 | | `parent_bff_child_guard_blocks_total` | Counter | — | ChildGuard 越权拦截次数 | | `parent_bff_circuit_state` | Gauge | `service, state` | 熔断器状态(P6) | | `parent_bff_event_consumed_total` | Counter(P5) | `topic, event_type` | 事件消费数 | | `parent_bff_event_pushed_total` | Counter(P5) | `topic, push_status` | 推送数 | ### 6.5 Tracer(OpenTelemetry) | 项 | 值 | | --------------------- | --------------------------------------------------------------------------------- | | 文件位置 | `src/shared/observability/tracer.ts` | | serviceName | `'parent-bff'` | | exporter | OTLP HTTP → collector | | auto-instrumentations | http, nestjs-core, express, ioredis, @grpc/grpc-js, kafka-node | | span 属性 | `parentId, childId, operation, downstream.service, cache.hit, childGuard.blocked` | | 采样率 | 生产 10%,开发 100% | | 上下文传播 | W3C Trace Context(traceparent 头 → gRPC metadata) | ### 6.6 /healthz 与 /readyz | 端点 | 检查逻辑 | 响应 | | ---------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------- | | `/healthz` | 进程存活(直接返回 ok) | `200 { status: 'ok', service: 'parent-bff', timestamp }` | | `/readyz` | **直接返回 ok**(对齐 teacher-bff,BFF 无 DB;下游可达性由 Prometheus 监控) | `200 { status: 'ready', service: 'parent-bff', timestamp }` | > **决策**:/readyz 不检查下游可达性。理由: > > 1. BFF 无 DB,无需 SELECT 1 > 2. 下游不可达时 BFF 降级返回 partial 数据,不阻塞 readiness > 3. 下游健康由各自 /healthz + Prometheus 监控,BFF 不重复探针 > 4. 若 K8s readiness 探针依赖下游,会导致下游故障时 BFF Pod 被摘流,反而失去降级能力 ### 6.7 优雅关闭 SIGTERM 关闭顺序(main.ts 已处理 `app.close()`,P5+ 补充 Kafka consumer): 1. 拒绝新请求(readyz 返回 503) 2. 停止消费 Kafka(commit 最后 offset)[P5+] 3. 等待 in-flight GraphQL 请求完成(最多 10s) 4. 关闭 Redis 连接 5. flush 剩余 OTel span 6. 进程退出 ### 6.8 Zod 输入验证(GraphQL input 校验) | GraphQL input | Zod Schema | 校验项 | | ------------------------------------- | ------------------------------------- | ------------------------------------- | | `updateNotificationPreferences.input` | `UpdateNotificationPreferencesSchema` | channels 非空、eventTypes 布尔值 | | `childGrades` 参数 | `ListGradesQuerySchema` | childId 非空、page ≥ 1、pageSize 1-50 | | `selectChild.childId` | `z.string().min(1)` | childId 非空 | | `markNotificationRead.notificationId` | `z.string().min(1)` | notificationId 非空 | > GraphQL Yoga 内置参数校验 + Zod schema 二次校验(双重保障),自定义 scalar(DateTime、ID)。 ### 6.9 GlobalErrorFilter - 文件:`src/shared/errors/global-error.filter.ts` - 装饰:`@Catch()` 全局 - 行为: 1. ApplicationError → 按 statusCode + code 返回 ActionState / GraphQL errors 2. ZodError → 400 + `BFF_PARENT_VALIDATION_ERROR` + details 3. GraphQL 错误 → 包装为 GraphQL errors 数组(携带 extensions.code) 4. 未知 Error → 500 + `BFF_PARENT_INTERNAL_ERROR` + traceId 5. 注入 traceId(从 `x-request-id` 头或新生成) ### 6.10 Dockerfile(多阶段构建) 复制 teacher-bff Dockerfile,仅改 `EXPOSE 3010`(对齐 C2 端口仲裁)。多阶段构建:builder 阶段 `pnpm install --frozen-lockfile` + `pnpm run build`;runtime 阶段 `pnpm install --prod` + `COPY --from=builder /app/dist` + `EXPOSE 3010` + `CMD ["node", "dist/main.js"]`。 --- ## 7. 与其他模块的交互点 ### 7.1 完整交互矩阵 | 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 | proto 状态 | | ------------ | -------------- | ------------------- | -------------------------------------------------------- | -------------------------------------------------------------- | ------ | --------------- | | 被调用 | api-gateway | HTTP | `POST /graphql` + `/healthz` `/readyz` `/metrics` | Gateway 转发 + 注入 x-user-id | P4 | ✅ | | 被调用 | parent-portal | HTTP(经 Gateway) | `POST /api/v1/parent/graphql` | 前端 GraphQL 调用 | P4 | ✅ | | 调用 | iam | gRPC 50052(P4+) | GetUserInfo | 家长个人信息 | P4 | ✅ 已有 | | 调用 | iam | gRPC 50052 | GetViewports | 家长端视口 | P4 | ❌ 待 ai02 补 | | 调用 | iam | gRPC 50052 | GetEffectivePermissions | 有效权限列表(C4:对应 REST `GET /iam/permissions/effective`) | P4 | ❌ 待 ai02 补 | | 调用 | iam | gRPC 50052 | **GetChildrenByParent** | **查询家长绑定孩子列表(P0 阻塞)** | P4 | ❌ 待 ai02 补 | | 调用 | core-edu | gRPC 50053 | ExamService.ListExamsByClass | 孩子班级考试 | P4 | ✅ 已有 | | 调用 | core-edu | gRPC 50053 | HomeworkService.ListHomeworkByClass | 孩子作业 | P4 | ✅ 已有 | | 调用 | core-edu | gRPC 50053 | GradeService.ListGradesByStudent | 孩子成绩 | P4 | ✅ 已有 | | 调用 | core-edu | gRPC 50053 | ClassService.GetClass | 孩子班级信息 | P4 | ✅ 已有 | | 调用 | core-edu | gRPC 50053 | **AttendanceService.*** | 孩子出勤(未来扩展) | P5+ | ❌ 待 ai03 补 | | 调用 | data-ana | gRPC 50055 | AnalyticsService.GetStudentWeakness | 学情诊断 | P4 | ✅ 已有 | | 调用 | data-ana | gRPC 50055 | AnalyticsService.GetLearningTrend | 学习趋势 | P4 | ✅ 已有 | | 调用 | data-ana | gRPC 50055 | AnalyticsService.GetClassPerformance | 班级学情对比 | P4 | ✅ 已有 | | 调用 | msg | gRPC 50056 | NotificationService.ListNotifications | 家长通知列表 | P5 | ✅ 已有 | | 调用 | msg | gRPC 50056 | NotificationService.MarkAsRead | 标记已读 | P5 | ✅ 已有 | | 调用 | msg | gRPC 50056 | **NotificationPreferenceService.*** | 通知偏好配置(待 ai05 补) | P5 | ❌ 待补 | | 调用 | push-gateway | **HTTP**(U2 仲裁) | `POST /internal/push` | 推送给在线家长 | P5 | ✅ HTTP | | 消费(可选) | Kafka | Kafka | `edu.notification.sent` | 推送通知 | P5 | ✅ topic 已定义 | | 消费(可选) | Kafka | Kafka | `edu.notification.read` | 失效通知缓存 | P5 | ✅ | | 消费(可选) | Kafka | Kafka | `edu.notification.recalled` | 撤回通知 | P5 | ✅ | | 消费(可选) | Kafka | Kafka | `edu.notification.failed` | 通知失败告警 | P5 | ✅ | | 消费(可选) | Kafka | Kafka | `edu.teaching.grade.recorded` | 失效成绩缓存 + 推送 | P5 | ✅ | | 消费(可选) | Kafka | Kafka | `edu.teaching.homework.graded` | 失效作业缓存 + 推送 | P5 | ✅ | | 消费(可选) | Kafka | Kafka | `edu.teaching.exam.published` | 失效考试缓存 + 推送 | P5 | ✅ | | 依赖 | shared-proto | 静态导入 | iam.proto / core_edu.proto / analytics.proto / msg.proto | 契约定义 | 跨阶段 | — | | 依赖 | Redis | TCP | 缓存 | 聚合结果短缓存 | P4 | — | | 依赖 | OTLP Collector | HTTP | trace 上报 | 可观测性 | P4 | — | ### 7.2 端口分配 | 服务 | HTTP 端口 | gRPC 端口 | 说明 | | ------------------------ | --------- | --------------------- | ---------------------------------------- | | **parent-bff(本模块)** | **3010** | **不暴露**(C2 仲裁) | BFF 对上游仅 HTTP/GraphQL,对下游走 gRPC | | iam | 3002 | 50052 | — | | core-edu | 3004 | 50053 | — | | data-ana | 3006 | 50055 | — | | msg | 3007 | 50056 | — | | push-gateway | 8081 | —(U2 豁免 gRPC) | 仅 HTTP /internal/push | ### 7.3 跨模块协作需求(需提交 coord 协调) | # | 需求 | 涉及 AI | 阻塞阶段 | 协调内容 | | --- | -------------------------------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------- | | 1 | **iam 新增家长-学生关联接口**(P0 阻塞) | ai02 | P4 | 补 `iam_student_guardians` 表 + Repository + `GET /iam/children` REST + proto `GetChildrenByParent` RPC | | 2 | iam 补 GetViewports / GetEffectivePermissions RPC | ai02 | P4 | proto 契约补全 | | 3 | api-gateway 新增 `/parent` 路由 | ai01 | P4 | main.go + config.go 新增 `ParentBffURL` 字段 + 路由块 | | 4 | docker-compose.deploy.yml 新增 parent-bff 服务定义 | coord(infra) | P4 | 端口 3010,加入 edu-net + edu-shared 网络 | | 5 | full-stack-runbook 端口矩阵更新 | coord(docs) | P4 | 追加 3010 行 | | 6 | 004 §4 依赖图更新 | coord(docs) | P4 | parent-bff 依赖扩展为 iam + core-edu + data-ana + msg(C6 仲裁),状态从"📐 需设计"改为"✅ 已实现" | | 7 | buf.gen.yaml 补 gRPC 插件 | coord | P4 | parent-bff P4 直接用 gRPC,需 gRPC client 代码生成 | | 8 | data-ana 实现 analytics.proto 查询 API | ai06 | P4 | 学情诊断端点依赖 | | 9 | msg 服务补 NotificationPreferenceService | ai05 | P5 | 通知偏好配置端点依赖 | | 10 | msg 服务落地 ListNotifications / MarkAsRead | ai05 | P5 | 家长通知列表依赖 | | 11 | push-gateway 落地 HTTP `/internal/push`(U2 仲裁) | ai01 | P5 | parent-bff P5 推送依赖 | ### 7.4 与 teacher-bff / student-bff 的复用与差异 | 维度 | teacher-bff | student-bff | parent-bff(本模块) | | ---------- | ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | | API 风格 | REST(P2)→ GraphQL(P4) | REST(P3)→ GraphQL(P6+ 可选) | **GraphQL(P4 直接,U3 仲裁)** | | DataScope | CLASS / GRADE / SCHOOL | SELF | **CHILDREN(自定义级)** | | 越权防御 | 不做(透传 x-user-id) | 做(强制 userId 比对) | **做(ChildGuard: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 + msg | iam + core-edu + content + data-ana + msg + ai | **iam + core-edu + data-ana + msg**(C6 仲裁) | | 特有功能 | SSE 流式(AI 辅助) | SSE 流式(AI 答疑) | 多子女切换 + 通知偏好 + ChildGuard | --- ## 8. 风险与假设 ### 8.1 P0 阻塞项 | 风险 | 影响 | 概率 | 缓解措施 | | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **iam 缺失"家长-学生关联查询"接口**(proto + REST + schema 三缺失) | **P0 阻塞**:parent-bff 核心场景(查孩子列表)无法实现,ChildGuard 越权校验无数据源 | 高 | coord 协调 ai02 在 P3 阶段(parent-bff P4 落地前)补全:1) `iam_student_guardians` 表;2) Repository 查询方法;3) `GET /iam/children` REST 端点;4) proto `GetChildrenByParent` RPC | | pending-features P2 提到 `parent_student_relations` 表,但 iam.schema.ts 未实现 | 表缺失 | 高 | 同上,推动 ai02 补表 | | data-ana 服务未实现查询 API(analytics.proto 3 个 method 无 REST/gRPC 端点) | P4 学情诊断端点无法实现 | 中 | coord 协调 ai06 在 P4 实现 data-ana 查询 API | | msg 通知偏好配置接口缺失 | P5 家长通知偏好无法配置 | 中 | coord 协调 ai05 在 P5 补 NotificationPreferenceService | ### 8.2 技术风险 | 风险 | 影响 | 概率 | 缓解措施 | | ------------------------------------------------------------------- | ---- | ---- | ------------------------------------------------------------------------------------ | | GraphQL Schema 演进破坏前端 | 高 | 中 | Schema 版本化 + deprecation 先行 + 协调前端 ai07(parent-portal) | | ChildGuard 越权校验性能损耗(每次查询都调 iam.GetChildrenByParent) | 中 | 中 | Redis 缓存绑定列表 60s(`bff:parent:child-bindings:{parentId}`),TTL 内不重复调 iam | | Redis 缓存穿透(家长查询未绑定的 childId) | 中 | 低 | ChildGuard 拦截 + 短 TTL + 空结果缓存(null cache 5s) | | 下游服务不可用 | 中 | 中 | Promise.allSettled 降级策略 + 熔断(P6) | | Kafka consumer 引入复杂度 | 低 | 中 | P5 评估,可继续用短 TTL 兜底;P4 不依赖 Kafka | | DataLoader 批量 RPC 缺口(iam 无 BatchGetChildrenByParents) | 低 | 中 | P4 阶段 per-request 去重已足够(家长场景单 parentId),无需跨 parentId 批量化 | | GraphQL 查询复杂度攻击 | 中 | 低 | depth limit ≤ 7 + query cost analysis(cost ≤ 1000) | ### 8.3 外部依赖假设 | 假设 | 若假设不成立的影响 | Fallback 方案 | | ----------------------------------------------------------------------------- | -------------------------- | -------------------------------------------- | | iam P3 阶段补全 GetChildrenByParent RPC + `iam_student_guardians` 表 | P4 parent-bff 无法启动实施 | **P0 阻塞**,无 fallback,必须协调 ai02 补全 | | iam `GET /iam/me` 返回包含 dataScope=CHILDREN | ChildGuard 无法校验 | 硬编码 dataScope=CHILDREN(家长固定) | | core-edu P4 启用 gRPC server | parent-bff P4 退化用 REST | 协调 ai03 启用 gRPC,或 P4 临时用 REST fetch | | api-gateway 已注册 `/parent` 路由 | 前端请求 404 | P4 阻塞,协调 ai01 | | Redis 已部署且网络可达 | 缓存层失效,性能下降 | Cache 层降级为内存 LRU | | OTLP Collector 已部署 | 链路追踪缺失 | 不影响业务,仅日志降级 | | Kafka 已部署且 topic 已创建(C5:edu.notification.sent/read/recalled/failed) | P5 事件订阅无法实现 | P5 阻塞;P4 不依赖 Kafka | | 前端 parent-portal(ai07)P4 配合使用 GraphQL | GraphQL 端点无人调用 | 协调 ai07 同步开发 GraphQL client | ### 8.4 未决设计决策(待 ai05 review) > 以下为 coord 推断过程中产生的新问题,标注"待 ai05 review",ai05 接手后确认或提请 coord 仲裁。 | # | 决策点 | coord 推断结论 | 待 ai05 review | | --- | --------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------- | | 1 | GraphQL Yoga vs Apollo Server | **GraphQL Yoga**(对齐 teacher-bff 02 设计 §3.2) | ai05 确认是否对齐 | | 2 | ChildGuard 绑定列表缓存 TTL | **60s**(安全敏感,不宜过长) | ai05 评估安全性与性能权衡 | | 3 | selectChild mutation 是否需要 | **保留**(仅审计日志,不持久化) | ai05 确认是否保留(方案 A 下前端可不经 BFF 直接切换) | | 4 | 通知偏好存储位置 | **msg 服务**(与通知发送强相关) | ai05 确认,需协调 msg 服务补 NotificationPreferenceService | | 5 | GraphQL 查询复杂度限制 | depth ≤ 7,cost ≤ 1000 | ai05 评估是否调整 | | 6 | 是否在 P4 引入熔断器 | **不引入**(P6 硬化),P4 用 Promise.allSettled 降级 | ai05 确认 | | 7 | /readyz 是否检查下游 | **不检查**(直接 ok,对齐 teacher-bff) | ai05 确认 | | 8 | 出勤查询(AttendanceService)归属阶段 | **P5+**(core-edu 未实现,未来扩展) | ai05 确认是否 P4 必需 | | 9 | 多家长共用孩子(如父母都绑同一孩子)ChildGuard 行为 | **正常**(每个家长独立查绑定列表) | ai05 确认无特殊处理 | | 10 | GraphQL Subscription 是否引入 | **不引入**(P5 用 push-gateway HTTP 推送,不走 GraphQL Subscription) | ai05 确认 | ### 8.5 已仲裁决策汇总(本文档执行,不再 review) 见 §0.1 已仲裁决策表(U1-U4 + C1-C6)。 --- ## coord 推断生成声明(保留历史追溯) **§0-§8 由 coord 基于 ai04 阶段 1 交付([01-understanding.md](./01-understanding.md))+ teacher-bff/student-bff 模式 + 用户 4 项仲裁决策(U1-U4)+ coord 补充仲裁(C1-C6)推断生成,非 ai05 原创交付。** ### coord 推断依据 1. **ai04 阶段 1 交付**:01-understanding.md 已识别 parent-bff 位置、限界上下文、契约缺口、P0 阻塞项。本设计文档基于该理解深化。 2. **teacher-bff/student-bff 模式参考**:分层结构、目录结构、横切关注点、错误码清单、可观测性设计参考两份 02-architecture-design.md。 3. **用户 4 项仲裁决策**(U1-U4)+ **coord 补充仲裁**(C1-C6):见 §0.1 已仲裁决策表。 ### coord §8.4 待 ai05 review 事项(10 项) coord 推断过程中产生的 10 项设计决策已在 §8.4 列出,ai05 接手后的逐项确认见下文 §9。 --- ## 9. ai05 review 结论(响应 coord §8.4 的 10 项待 review 决策) > 本节为 ai05 复审对 coord §8.4 的 10 项决策逐项响应。原则:能确认的直接 ✅,需调整的标注 🔧 并给出新结论,重大冲突标注 ⚠️ 提请 coord 仲裁。 | # | 决策点 | coord 推断结论 | ai05 review 结论 | 说明 | | --- | ------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | GraphQL Yoga vs Apollo Server | GraphQL Yoga(对齐 teacher-bff 02 §3.2) | ✅ 确认 | 对齐 teacher-bff,统一 GraphQL Yoga v3(基于 whatwg-node,与 NestJS Express 兼容) | | 2 | ChildGuard 绑定列表缓存 TTL | 60s(安全敏感不宜过长) | 🔧 **调整为 30s + 主动失效** | 家长绑定关系变更(如解绑)60s 内仍可访问是安全风险;调整为 30s TTL + iam `UserEvent.action=children_changed` 事件主动失效(P5 引入 Kafka consumer 后);P4 阶段 30s TTL 兜底,解绑后 30s 内仍可访问(可接受,因 iam 已删除绑定) | | 3 | selectChild mutation 是否需要 | 保留(仅审计日志,不持久化) | ✅ 确认 | 保留 `selectChild(childId: ID!): Boolean!` 用于审计;BFF 不持久化会话(方案 A 无状态);前端切换不需要调此 mutation,仅审计场景调用 | | 4 | 通知偏好存储位置 | msg 服务(与通知发送强相关) | ✅ 确认 | 已在 §0.1 C6 仲裁:parent-bff → msg 调用 NotificationPreferenceService;P5 msg 服务补全时实现 | | 5 | GraphQL 查询复杂度限制 | depth ≤ 7,cost ≤ 1000 | ✅ 确认 + 补充 | depth ≤ 7 / cost ≤ 1000 适合家长场景(查询深度浅、字段少);补充:启用 `graphql-query-complexity` 库 + list 类字段 cost 计算乘以 pageSize(防止 pageSize=99999 攻击) | | 6 | 是否在 P4 引入熔断器 | 不引入(P6 硬化),P4 用 Promise.allSettled 降级 | ✅ 确认 | P4 用 Promise.allSettled 部分失败容忍;P6 引入 `opossum` 熔断器(per-downstream-service 独立 circuit) | | 7 | /readyz 是否检查下游 | 不检查(直接 ok,对齐 teacher-bff) | 🔧 **调整:P4 即补下游探针** | coord §16.4 黄金模板对齐表 parent-bff readyz 标 ⚠️;ai05 复审要求 P4 补下游探针(iam + core-edu + data-ana + Redis),探针超时 1s/服务,任一失败返回 degraded;K8s readinessProbe 用此端点决定流量路由,无探针导致下游故障时仍接收流量 | | 8 | 出勤查询(AttendanceService)归属阶段 | P5+(core-edu 未实现,未来扩展) | ✅ 确认 | core-edu AttendanceService proto + 实现均缺失(coord §16.6.3 #1 P3 补全),parent-bff P4 不接出勤;P5+ 评估 | | 9 | 多家长共用孩子(父母都绑同一孩子)ChildGuard 行为 | 正常(每个家长独立查绑定列表) | ✅ 确认 + 补充 | 每个家长独立调 iam.GetChildrenByParent,独立缓存(key: `bff:parent:child-bindings:{parentId}`);补充:缓存 key **不含 childId**(避免同一家长多个缓存项),ChildGuard 直接查询返回的列表判断 childId ∈ list | | 10 | GraphQL Subscription 是否引入 | 不引入(P5 用 push-gateway HTTP 推送,不走 GraphQL Subscription) | ✅ 确认 | 实时推送走 push-gateway HTTP `/internal/push`(U2 仲裁),不走 GraphQL Subscription(WebSocket 长连接与 BFF 无状态约束冲突);前端用 EventSource / SSE 接收 push-gateway 推送 | ### 9.1 ai05 新增识别的待 coord 仲裁项(8 项,详见 §14) ai05 复审过程中新识别的 8 项决策需提请 coord 仲裁(不属于 coord §8.4 范围): 1. **iam 补 `GetEffectiveAccess(parentUserId) → {perms, viewports, dataScope, children}` 聚合 RPC**(减少 2 次 RTT) 2. **core-edu 补 `GetClassesByStudent(student_id) → Class[]` RPC**(childId→classId 转换当前缺失) 3. **data-ana 补 `GetParentDashboard(parent_id) → ParentDashboard` 聚合 RPC**(多子女场景防 N+1) 4. **iam 补 parent 角色权限映射** 或确认采用 coord §16.5 #4 仲裁豁免(parent 角色无权限点) 5. **ChildGuard 缓存击穿保护**:singleflight 模式防止缓存过期瞬间 N 个请求同时调 iam.GetChildrenByParent 6. **多子女切换跨标签同步**:BroadcastChannel + LWW(前端方案,BFF 仅提供 selectChild 审计 mutation) 7. **GraphQL Schema 版本化策略**:`@deprecated` + Schema Registry + Breaking Change 检测 8. **msg 服务 P5 落地后 parent-bff 接入方式**:gRPC 50056(推荐)还是 HTTP REST(兼容) --- ## 10. 长远架构演进设计(ai05 补全,覆盖 P5-P7+) > 本节为 ai05 视角的长远架构演进,符合"通用架构设计标准 + 为未来可能发生的做好铺垫"要求。覆盖 10 个长远预留点,确保 parent-bff 在 P4 之后的演进中不需重构核心抽象。 ### 10.1 演进路线图(P4-P7+) ```mermaid graph LR P4[P4 MVP
GraphQL + DataLoader
多子女切换 + ChildGuard
iam + core-edu + data-ana] P5[P5 通知接入
msg gRPC 50056
通知偏好配置
push-gateway HTTP 推送
Kafka consumer 缓存失效] P6[P6 硬化
熔断器 opossum
HPA + mTLS
SLO 监控 + 告警
灰度发布] P7[P7+ 长远
多租户隔离
合规审计
i18n 5 语言
移动端 PWA
AI 家长助手
Service Mesh] P4 --> P5 --> P6 --> P7 ``` ### 10.2 P4 阶段交付项(MVP,已含 §0-§8) - ✅ GraphQL Yoga + DataLoader 落地 - ✅ 多子女切换(方案 A 前端管理 + selectChild 审计) - ✅ ChildGuard 越权校验(30s TTL + 主动失效 P5) - ✅ 并行 gRPC 编排 + Promise.allSettled 降级 - ✅ Redis 聚合缓存 5-30s - ✅ /readyz 下游探针(iam + core-edu + data-ana + Redis) - ✅ 错误码 `BFF_PARENT_*` 全量落地 - ✅ 横切关注点对齐(logger/metrics/tracer/Zod/ErrorFilter/Dockerfile) ### 10.3 P5 阶段交付项 - 🔜 msg gRPC 50056 接入(NotificationService + NotificationPreferenceService) - 🔜 push-gateway HTTP `/internal/push` 接入(推送子女成绩/作业批改/考试发布通知) - 🔜 Kafka consumer 引入(订阅 `edu.teaching.grade.recorded` / `edu.teaching.exam.published` / `edu.identity.user.role_changed` 精确失效缓存) - 🔜 ChildGuard 主动失效(订阅 `edu.identity.user.children_changed` 事件,立即失效绑定列表缓存) - 🔜 通知偏好过滤(家长关闭"成绩推送"偏好时,Kafka consumer 跳过该家长推送) - 🔜 SSE 流式透传(如 parent-bff 有 AI 场景;目前待产品确认) ### 10.4 P6 阶段硬化项 - 🔜 熔断器 `opossum` per-downstream-service(iam/core-edu/data-ana/msg 独立 circuit) - 🔜 HPA 2-10 副本 + podAntiAffinity - 🔜 mTLS(Service Mesh 接入后由 Envoy 负责,parent-bff 代码无需变更) - 🔜 SLO 监控 + 告警规则(P95 延迟 < 200ms / 错误率 < 0.1% / 可用性 > 99.9%) - 🔜 灰度发布(按 parentId hash 路由流量百分比) ### 10.5 长远预留点(10 项,为未来可能发生的做好铺垫) | # | 预留点 | 当前铺垫 | 未来触发条件 | 演进路径 | | --- | --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | 1 | **多租户隔离** | Redis 缓存 key 含 `parentId`,无 `tenantId`;GraphQL Schema 无 tenant 字段 | SaaS 化 / 多校独立部署 | 缓存 key 加 `tenantId`;GraphQL 加 `tenantId` 字段;BFF 无状态不需重构 | | 2 | **合规审计** | selectChild mutation 已记录审计日志;未来全量审计 | COPPA/FERPA/PIPL/GDPR 合规要求 | 引入审计日志 service + 写 Kafka `edu.audit.parent.action` topic | | 3 | **国际化(i18n)** | 错误码已用英文 + traceId;message 待前端 i18n key 映射 | 5 语言(中英日韩西)支持 | 错误码不变;message 字段改为 i18n key(前端按 key 翻译) | | 4 | **移动端 PWA** | GraphQL 端点对移动端友好(单端点 + 灵活查询) | 家长 App 开发 | 复用 GraphQL 端点;新增 `/parent/mobile/version-check` 端点 + PWA manifest | | 5 | **AI 家长助手** | 无(待产品确认) | P5+ AI 服务扩展家长场景 | parent-bff 调 ai gRPC `StreamChat` + SSE 透传(复用 teacher-bff §10 模式) | | 6 | **Service Mesh 接入** | BFF 无状态;下游 gRPC client 可替换为 Envoy 代理 | P6+ Service Mesh 落地 | gRPC client target 从 `localhost:50052` 改为 `iam-service:50052`(Envoy 服务发现) | | 7 | **API 版本化** | GraphQL Schema 无版本前缀;REST `/healthz` 等无版本 | Schema 破坏性变更 | GraphQL `@deprecated` + Schema Registry + Breaking Change 检测;REST `/v2/healthz` | | 8 | **容灾与高可用** | 单实例(P4)/ HPA 多副本(P6);无跨可用区 | 跨可用区容灾 | parent-bff 无状态,直接跨可用区部署;Redis 跨可用区主从;Kafka consumer 跨可用区消费 | | 9 | **降级模式矩阵** | Promise.allSettled 部分失败容忍;Redis 缓存兜底 | 下游服务故障 | 4 类降级:①下游不可用→返回缓存陈旧数据;②Redis 不可用→内存 LRU;③GraphQL 不可用→REST fallback(P6+);④全部不可用→维护页面 | | 10 | **可扩展抽象** | Client 抽象层(IamClient/CoreEduClient/DataAnaClient/MsgClient interface + gRPC/HTTP impl) | 新下游接入(如新增 student-bff 直接调用) | 新增 `XxxClient interface` + impl,注入 Orchestrator,无需改 Controller/Resolver | ### 10.6 长远架构原则(ai05 总结) parent-bff 长远架构遵循 6 项原则: 1. **BFF 本分**:只聚合、裁剪、协议转换,不持业务状态、不做权限决策、不直访业务 DB(004 §3.2 + coord §16.5 #4) 2. **契约驱动**:所有跨模块交互以 proto 契约为唯一源,proto 缺失项显式标注并提请 coord 仲裁 3. **平滑演进**:每一阶段都能独立交付价值,不依赖未来阶段;每次升级通过抽象层隔离变更 4. **为未来铺垫**:抽象层预留 gRPC / GraphQL / SSE / Kafka / 熔断 / 多租户扩展点,未来需求接入时无需重构核心 5. **降级优先**:任一下游故障时 BFF 应优雅降级(返回缓存陈旧数据或部分字段 null),而非整体 500 6. **可观测性**:三支柱(pino + prom-client + OTel)必须同时启用;ChildGuard / DataLoader / Orchestrator 三层关键路径埋点 --- ## 11. 测试策略(ai05 补全) > 符合黄金模板"测试覆盖率 ≥ 80%"要求。测试金字塔 4 层。 ### 11.1 测试金字塔 ``` /\ /E2E\ ← 5%(关键场景端到端,Playwright) /------\ /Contract\ ← 15%(proto 契约一致性,Pact) /----------\ / Integration \ ← 30%(Controller + Service + Redis Testcontainers) /--------------\ / Unit \ ← 50%(Resolver + DataLoader + ChildGuard + Orchestrator) /------------------\ ``` ### 11.2 关键测试用例(10 项) | # | 测试用例 | 类型 | 关键断言 | | --- | ----------------------------- | ----------- | -------------------------------------------------------------------------------- | | 1 | ChildGuard 拦截越权 childId | Unit | childId ∉ 绑定列表时抛 `BFF_PARENT_CHILD_NOT_BOUND(403)` | | 2 | ChildGuard 缓存命中 | Unit | 30s 内第二次查询不调 iam.GetChildrenByParent | | 3 | ChildGuard 缓存击穿保护 | Unit | 并发 100 请求只调 iam 1 次(singleflight) | | 4 | 多子女仪表盘并行编排 | Integration | 3 子女 × 3 下游 = 9 并行 gRPC,总耗时 < max(单次) + 100ms | | 5 | 下游部分失败降级 | Unit | data-ana 失败时返回 dashboard.degraded=true,其他字段正常 | | 6 | GraphQL 查询复杂度限制 | Unit | depth=8 查询被拒;cost=1001 查询被拒;pageSize=99999 被拒 | | 7 | GraphQL Schema 破坏性变更检测 | Contract | `@deprecated` 字段删除前必须先 deprecate 1 个版本 | | 8 | selectChild 审计日志 | Integration | mutation 调用后审计日志写入(traceId + parentId + childId + timestamp) | | 9 | /readyz 下游探针 | Integration | iam 故障时 readyz 返回 503 + degraded=true | | 10 | 缓存失效(P5 Kafka) | Integration | 收到 `edu.teaching.grade.recorded` 事件后 `bff:parent:grades:{childId}` 缓存失效 | ### 11.3 Mock 策略 | 下游 | Mock 方式 | 工具 | | ------------- | --------------------------------- | ---------------------------------- | | iam gRPC | Mock server(proto 一致) | `@grpc/grpc-js` mock + `jest.mock` | | core-edu gRPC | Mock server | 同上 | | data-ana gRPC | Mock server | 同上 | | msg gRPC | Mock server(P5) | 同上 | | Redis | Testcontainers(真实 Redis 实例) | `testcontainers/redis` | | Kafka | Mock consumer(P5) | `kafkajs` mock + `jest.mock` | --- ## 12. 完整配置项清单(ai05 补全) > 所有环境变量统一 Zod 校验(`src/config/env.ts`),符合黄金模板"环境校验"要求。 ### 12.1 配置项分组 ```typescript // src/config/env.ts import { z } from "zod"; const envSchema = z.object({ // === 服务基础 === NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), PORT: z.string().default("3010").transform(Number), LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), DEV_MODE: z .string() .default("false") .transform((v) => v === "true"), // === 下游服务 URL(P4 HTTP,P5+ gRPC target) === IamServiceUrl: z.string().url().default("http://localhost:3002"), CoreEduServiceUrl: z.string().url().default("http://localhost:3004"), DataAnaServiceUrl: z.string().url().default("http://localhost:3006"), MsgServiceUrl: z.string().url().default("http://localhost:3007"), // P5 启用 PushGatewayUrl: z.string().url().default("http://localhost:8081"), // P5 启用 // === 下游 gRPC target(P5+ 启用,覆盖 URL) === IamGrpcTarget: z.string().optional(), // 默认 localhost:50052 CoreEduGrpcTarget: z.string().optional(), // 默认 localhost:50053 DataAnaGrpcTarget: z.string().optional(), // 默认 localhost:50055 MsgGrpcTarget: z.string().optional(), // 默认 localhost:50056(P5) // === Redis === REDIS_URL: z.string().url().default("redis://localhost:6379"), REDIS_KEY_PREFIX: z.string().default("bff:parent:"), // === ChildGuard === CHILD_GUARD_CACHE_TTL_SECONDS: z.string().default("30").transform(Number), CHILD_GUARD_SINGLEFLIGHT_ENABLED: z .string() .default("true") .transform((v) => v === "true"), // === 缓存 === DASHBOARD_CACHE_TTL_SECONDS: z.string().default("15").transform(Number), GRADES_CACHE_TTL_SECONDS: z.string().default("30").transform(Number), PERMISSIONS_CACHE_TTL_SECONDS: z.string().default("300").transform(Number), // 5min // === GraphQL === GRAPHQL_DEPTH_LIMIT: z.string().default("7").transform(Number), GRAPHQL_COST_LIMIT: z.string().default("1000").transform(Number), GRAPHQL_INTROSPECTION_ENABLED: z .string() .default("false") .transform((v) => v === "true"), // === 可观测性 === OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().default("parent-bff"), OTEL_SERVICE_VERSION: z.string().optional(), // === Kafka(P5+ 启用) === KAFKA_BROKERS: z.string().optional(), // "localhost:9092,localhost:9093" KAFKA_CONSUMER_GROUP_ID: z.string().default("parent-bff"), KAFKA_CONSUMER_TOPICS: z.string().optional(), // "edu.teaching.grade.recorded,edu.teaching.exam.published,edu.identity.user.role_changed" // === CORS === CORS_ORIGINS: z.string().default("http://localhost:4002"), // parent-portal }); export type Env = z.infer; export const env = envSchema.parse(process.env); ``` ### 12.2 配置项分组说明 | 分组 | 配置项数 | 说明 | | ---------------- | -------- | -------------------------------------------------------------- | | 服务基础 | 5 | NODE_ENV / PORT / LOG_LEVEL / DEV_MODE / OTEL_SERVICE_NAME | | 下游 URL | 5 | iam / core-edu / data-ana / msg / push-gateway(HTTP 模式) | | 下游 gRPC target | 4 | iam / core-edu / data-ana / msg(gRPC 模式,P5+ 启用覆盖 URL) | | Redis | 2 | REDIS_URL / REDIS_KEY_PREFIX | | ChildGuard | 2 | 缓存 TTL / singleflight 开关 | | 缓存 | 3 | dashboard / grades / permissions 各自 TTL | | GraphQL | 3 | depth / cost / introspection | | 可观测性 | 3 | OTLP endpoint / service name / version | | Kafka | 3 | brokers / consumer group / topics(P5+ 启用) | | CORS | 1 | 允许的 origin(parent-portal 默认 4002) | ### 12.3 环境变量优先级 1. 进程环境变量(K8s ConfigMap / Secret / docker-compose env_file) 2. `.env` 文件(仅开发环境,`dotenv` 加载) 3. Zod schema 默认值 --- ## 13. 黄金模板对齐 checklist(ai05 补全,16 项) > 对照 [project_rules §3](../../../.trae/rules/project_rules.md) + [known-issues §2.2 classes 黄金模板](../../../docs/troubleshooting/known-issues.md) + [coord §16.4 黄金模板对齐表](../../../docs/architecture/coord-cross-review.md) | # | 对齐项 | 状态 | 说明 | | --- | ------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------- | | 1 | 权限装饰器 `@RequirePermission` | ⚠️ 豁免(coord §16.5 #4 仲裁) | BFF 不做权限决策,仅校验 `x-user-id` 存在 | | 2 | 错误码前缀统一 `BFF_PARENT_*` | ✅ | coord §5.2 仲裁,从 `PARENT_BFF_*` 迁移(ai04 初稿未迁移) | | 3 | logger(pino) | ✅ | 复制 teacher-bff `shared/observability/logger.ts`,service 改 `parent-bff` | | 4 | metrics(prom-client `/metrics` 端点) | ✅ | 复制 teacher-bff main.ts 注册方式,指标名前缀 `parent_bff_` | | 5 | tracer(OpenTelemetry SDK + OTLP exporter + auto-instrumentations) | ✅ | 复制 teacher-bff tracer.ts,serviceName 改 `parent-bff` | | 6 | `/healthz` 健康检查(liveness) | ✅ | 直接返回 ok,BFF 无 DB | | 7 | `/readyz` 健康检查(readiness,含下游探针) | 🔧 ai05 调整:P4 即补 | coord §8.4 #7 推断"不检查",ai05 调整为"检查 iam + core-edu + data-ana + Redis",探针超时 1s/服务 | | 8 | 优雅关闭(SIGTERM) | ✅ | main.ts 注册 SIGTERM → `app.close()` + `shutdownTracer()` + Redis client.quit() | | 9 | 测试覆盖率 ≥ 80% | ⚠️ 待 P4 落地补 | 测试策略见 §11,4 层金字塔 | | 10 | Dockerfile 多阶段构建 | ✅ | 复制 teacher-bff Dockerfile,EXPOSE 改 3010 | | 11 | Zod 输入验证 | ✅ | GraphQL Yoga 内置参数校验 + Zod schema 二次校验(双重保障) | | 12 | GlobalErrorFilter 统一兜底 | ✅ | `@Catch()` 全局过滤器,ActionState 信封含 traceId | | 13 | ESM `.js` 后缀 import | ✅ | tsconfig.json `"module": "NodeNext"`,所有相对 import 带 `.js` | | 14 | `import type` 纯类型导入 | ✅ | express 的 Request/Response/NextFunction 改为 `import type` | | 15 | 环境变量 Zod 校验 | ✅ | `src/config/env.ts` 完整 Zod schema(见 §12) | | 16 | ActionState 统一响应信封 | ✅ | `{success: true, data: T} | {success: false, error: {code, message, details?, traceId?}}`(004 §11.5) | --- ## 14. 待 coord 仲裁的新决策清单(ai05 提请,8 项) > 以下 8 项为 ai05 复审过程中新识别的决策点,不属于 coord §8.4 范围,提请 coord 仲裁。 | # | 决策点 | ai05 建议 | 阻塞阶段 | 影响范围 | | --- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------- | | 1 | iam 补 `GetEffectiveAccess(parentUserId) → {perms, viewports, dataScope, children}` 聚合 RPC | 强烈建议(减少 2 次 RTT:GetChildrenByParent + GetViewports + GetEffectivePermissions 合一) | P4 | iam(ai06 补全)+ parent-bff(调用方) | | 2 | core-edu 补 `GetClassesByStudent(student_id) → Class[]` RPC | 强烈建议(parent-bff 拉子女考试/作业需先 childId→classId 转换,当前缺此 RPC) | P4 | core-edu(ai08 补全)+ parent-bff + student-bff | | 3 | data-ana 补 `GetParentDashboard(parent_id) → ParentDashboard` 聚合 RPC | 建议(多子女场景防 N+1:N 次 GetStudentWeakness 调用 → 1 次聚合 RPC) | P4 | data-ana(ai11 补全)+ parent-bff | | 4 | iam 补 parent 角色权限映射 或 确认采用 coord §16.5 #4 仲裁豁免 | 二选一:①iam 补 `PARENT_CHILD_READ` / `PARENT_CHILD_SWITCH` / `PARENT_NOTIFICATION_PREFERENCE_MANAGE` 权限点;②确认采用 coord §16.5 #4 仲裁(BFF 豁免,权限下沉下游) | P4 | iam(ai06)+ parent-bff | | 5 | ChildGuard 缓存击穿保护(singleflight 模式) | 启用(防止缓存过期瞬间 N 个请求同时调 iam.GetChildrenByParent) | P4 | parent-bff 独立决策,无需 coord 仲裁,记录在此 | | 6 | 多子女切换跨标签同步 | 前端方案(BroadcastChannel + LWW),BFF 仅提供 selectChild 审计 mutation | P4 | parent-portal(ai15)+ parent-bff | | 7 | GraphQL Schema 版本化策略 | `@deprecated` + Schema Registry + Breaking Change 检测(CI 强制) | P5+ | parent-bff + parent-portal(ai15) | | 8 | msg 服务 P5 落地后 parent-bff 接入方式 | 推荐 gRPC 50056(性能优于 HTTP,proto 契约一致);兼容方案 HTTP REST(msg gRPC server 未就绪时降级) | P5 | msg(ai10)+ parent-bff | ### 14.1 ai05 提请的 P0 阻塞项(已在 §8.1 列出,此处重申) | # | 阻塞项 | 阻塞阶段 | 责任方 | 缓解措施 | | ---- | ---------------------------------------------------------------- | -------- | ------------------ | ------------------------------------------------------ | | P0-1 | iam 缺失 `GetChildrenByParent` RPC + `iam_student_guardians` 表 | P4 | ai06(iam 现归属) | coord 协调 ai06 在 P3 阶段补全(parent-bff P4 落地前) | | P0-2 | data-ana 缺失 4 端 Dashboard + SubscribeMasteryUpdate stream RPC | P4 | ai11 | coord 协调 ai11 在 P4 启动前补全 | | P0-3 | api-gateway 缺失 `/parent` 路由注册 | P4 | ai01 | coord 协调 ai01 在 P4 启动前补 main.go + config.go | --- ## 15. 设计自评估(ai05 补全) > 对照"通用架构设计标准 12 维度全覆盖 + 长远铺垫维度"自检。 ### 15.1 12 维度覆盖率 | # | 维度 | 覆盖 | 章节 | 说明 | | --- | -------------- | ---- | -------------------------------- | ------------------------------------ | | 1 | 设计原则 | ✅ | §0 设计原则摘要 + §10.6 长远原则 | 6 项原则 | | 2 | 模块内部分层图 | ✅ | §1 | mermaid + 目录结构 P4 目标态 | | 3 | 领域模型 | ✅ | §2 | BFF 无聚合根,ParentSession 值对象 | | 4 | 数据模型 | ✅ | §3 | 无 DB,Redis 缓存 + DTO | | 5 | API 设计 | ✅ | §4 | GraphQL Schema 完整 | | 6 | 事件设计 | ✅ | §5 | P4 不订阅,P5 Kafka consumer | | 7 | 横切关注点 | ✅ | §6 | 16 项 checklist(§13) | | 8 | 跨模块交互点 | ✅ | §7 | 完整契约矩阵 22 项 | | 9 | 风险与假设 | ✅ | §8 | P0 阻塞 + 技术风险 + 外部依赖假设 | | 10 | 演进路线 | ✅ | §10.1-§10.5 | P4-P7+ 路线图 + 10 项长远预留 | | 11 | 测试策略 | ✅ | §11 | 4 层金字塔 + 10 关键用例 + Mock 策略 | | 12 | 实施计划 | ✅ | §10.2-§10.4 | P4 MVP 8 项 + P5 6 项 + P6 5 项 | ### 15.2 长远铺垫维度覆盖率 | # | 维度 | 覆盖 | 章节 | | --- | -------------- | ---- | -------------------------------------------------------- | | 1 | 状态机预留 | ✅ | §2(BFF 无状态机,仅审计日志) | | 2 | 版本化 | ✅ | §14 #7(GraphQL Schema 版本化) + §10.5 #7(API 版本化) | | 3 | 多租户 | ✅ | §10.5 #1(缓存 key + Schema 字段预留) | | 4 | 国际化(i18n) | ✅ | §10.5 #3(错误码英文 + message i18n key) | | 5 | 合规审计 | ✅ | §10.5 #2(selectChild 审计 + 未来全量审计) | | 6 | AI 集成 | ✅ | §10.5 #5(AI 家长助手,P5+ 评估) | | 7 | 演进路径 | ✅ | §10.1(P4-P7+ 路线图 mermaid) | | 8 | 可扩展抽象 | ✅ | §10.5 #10(Client 抽象层) | | 9 | 容灾与高可用 | ✅ | §10.5 #8(无状态 + 跨可用区部署) | | 10 | 降级模式 | ✅ | §10.5 #9(4 类降级矩阵) | ### 15.3 自评估结论 parent-bff 02-architecture-design.md 覆盖 **12 维度全覆盖 + 10 项长远铺垫**,符合"通用架构设计标准 + 为未来可能发生的做好铺垫"要求。coord §0-§8 已建立 P4 即时实现设计,ai05 §9-§15 补全长远演进 + 测试 + 配置 + checklist + 自评估,文档完整度达 ai-allocation.md §7 模板要求 + 用户"长远全面"标准。 --- **AI Agent**: ai05(parent-bff) **Coordinator**: coord-ai(§0-§8 代笔推断 + §9-§15 ai05 复审补全) **Branch**: 单仓库并行模式(直接 push main) **关联阶段 1 文档**: [01-understanding.md](./01-understanding.md)(ai04 撰写 + ai05 复审修订) **coord 代笔日期**: 2026-07-09 **ai05 复审日期**: 2026-07-09 深夜 **文档版本**: v2(coord 推断 v1 + ai05 复审 v2) **阶段 3 进入条件**: 本文档经 coord 交叉审查通过后进入阶段 3 实施 **本文件由 coord 基于 ai04 阶段 1 交付([01-understanding.md](./01-understanding.md))+ teacher-bff/student-bff 模式 + 用户 4 项仲裁决策推断生成,非 ai05 原创交付。** ### 推断依据 1. **ai04 阶段 1 交付**:01-understanding.md 已识别 parent-bff 位置、限界上下文、契约缺口、P0 阻塞项。本设计文档基于该理解深化。 2. **teacher-bff/student-bff 模式参考**:分层结构、目录结构、横切关注点、错误码清单、可观测性设计参考两份 02-architecture-design.md。 3. **用户 4 项仲裁决策**(U1-U4)+ **coord 补充仲裁**(C1-C6):见 §0.1 已仲裁决策表。 ### 待 ai05 review 事项 coord 推断过程中产生的 10 项设计决策见 §8.4。ai05 接手后应:逐项确认 §8.4 决策;校验与 01-understanding.md 一致性(冲突以本文档为准);补充 ai05 视角细节(GraphQL Resolver 实现、DataLoader 批量策略);重大修改需重新提交 coord 交叉审查。 **重点 review 项**:§4.2 GraphQL Schema(coord 基于家长场景推断,可能未覆盖 ai05 全部场景)、ChildGuard 实现细节(同步阻塞 vs 预加载绑定列表)、通知偏好过滤位置(P5 EventSubscriber 内 vs msg 服务侧)。 ### 后续动作 1. **ai05 review**:确认或修改 §8.4 的 10 项决策 2. **coord 同步 004**:parent-bff 依赖图状态改为"✅ 已实现",依赖扩展为 iam + core-edu + data-ana + msg(C6) 3. **coord 协调 ai02**:P0 阻塞项——iam 补 GetChildrenByParent RPC + `iam_student_guardians` 表 4. **阶段 3 进入条件**:本文档经 ai05 review + coord 交叉审查通过后进入阶段 3 --- **AI Agent**: ai05(coord 代笔) **Coordinator**: coord-ai **Branch**: 单仓库并行模式(直接 push main) **关联阶段 1 文档**: [01-understanding.md](./01-understanding.md)(ai04 撰写) **代笔日期**: 2026-07-09