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.设计规格文档
105 KiB
模块架构设计文档 — parent-bff
AI 标识:ai05(coord 代笔推断 → ai05 复审修订正式接手) 阶段:阶段 2(模块架构设计)· ai05 复审修订 coord 代笔日期:2026-07-09 ai05 复审日期:2026-07-09 深夜 状态:ai05 已复审,补充 ai05 视角的长远架构演进设计,进入 coord 交叉审查 关联文档:
- 阶段 1 理解确认书(ai04 撰写 + ai05 复审修订)
- 004 架构影响地图
- coord 交叉审查报告
- ai-allocation §3.2/§5/§7 模板
- pending-features P4/P5
- project_rules
- 多 AI 协作指南
- 参考实现:teacher-bff 02-architecture-design.md、student-bff 02-architecture-design.md、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)
graph TB
subgraph Client["前端层"]
PP[parent-portal<br/>Next.js MF Remote<br/>urql GraphQL Client]
end
subgraph Gateway["网关层"]
AGW[api-gateway<br/>Go/Gin<br/>JWT RS256 校验 + 限流 + 熔断<br/>注入 x-user-id / x-user-roles / x-request-id]
end
subgraph ParentBFF["parent-bff(本模块)P4 终态"]
direction TB
GQL[GraphQL Yoga Endpoint<br/>POST /graphql<br/>+ GET /graphql]
HEALTH[Health Controller<br/>/healthz /readyz /metrics]
subgraph Middleware["中间件层"]
CTX[Context Middleware<br/>解析 x-user-id / x-user-roles<br/>注入 GraphQL context]
ERR[GlobalErrorFilter<br/>统一错误兜底 → ActionState 信封]
end
subgraph Aggregation["聚合编排层"]
RES[GraphQL Resolvers<br/>按家长场景域组织]
DL[DataLoader Registry<br/>per-request 实例<br/>批量去重 N+1]
ORCH[Orchestrator<br/>Promise.allSettled + 降级]
GUARD[ChildGuard<br/>DataScope=CHILDREN 越权校验]
MAP[Response Mapper<br/>proto → GraphQL type]
end
subgraph Cache["缓存层"]
RCLIENT[RedisClient<br/>ioredis]
CKV[CacheKey Builder<br/>bff:parent:*]
end
subgraph Clients["下游 Client 抽象层"]
IAM_C[IamClient<br/>gRPC adapter]
CE_C[CoreEduClient<br/>gRPC adapter]
DA_C[DataAnaClient<br/>gRPC adapter]
MSG_C[MsgClient<br/>gRPC adapter]
end
subgraph Obs["可观测层"]
LOG[pino Logger]
MET[prom-client Metrics]
TRC[OTel Tracer<br/>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<br/>含 GetChildrenByParent<br/>待 ai02 补]
CE[core-edu:50053<br/>Exam/Homework/Grade Service]
DA[data-ana:50055<br/>AnalyticsService]
MSG[msg:50056<br/>NotificationService]
end
subgraph Infra["基础设施"]
R[(Redis 7<br/>edu-redis:6379)]
J[Jaeger<br/>OTLP]
P[Prometheus<br/>: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 场景)
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<br/>{ query: "dashboard { user children { id name lastGrade } }" }
GW->>GW: RS256 公钥校验 + 提取 userId
GW->>C: POST /parent/graphql<br/>x-user-id: u-parent-001<br/>x-request-id: r-xxx
C->>C: extractUserId(req) → u-parent-001<br/>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 会话。
// 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 头解析
}
设计要点:
- ParentSession 每次请求重建,不跨请求保留状态(符合 004 §12.1 无状态约束)
requestedChildId由前端在 GraphQL query 参数传入(方案 A),BFF 不维护"当前选中孩子"POST /parent/children/:childId/select(selectChild mutation)仅记录审计日志,不持久化会话- ChildGuard 在 Resolver 执行前校验
requestedChildId ∈ iam.GetChildrenByParent(parentId)
2.3 DataScope=CHILDREN 越权校验(BFF 层强制)
BFF 不做权限决策(U4 豁免 @RequirePermission),但做越权防御(与 student-bff 的 SELF 防御同理,CHILDREN 更复杂):
// aggregation/child-guard.ts
async validateChildAccess(session: ParentSession, childId: string): Promise<void> {
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 层先拦截可:
- 早失败,减少无效下游 gRPC 调用
- 提升审计能力(越权尝试在 BFF 层记录)
- 降低跨家庭数据泄露风险(家长 A 不能查家长 B 的孩子)
- 不依赖下游服务实现 DataScope=CHILDREN 语义(core-edu 当前仅支持 SELF/CLASS/GRADE/SCHOOL)
2.4 聚合视图间关系
graph LR
Dashboard[ParentDashboard<br/>聚合视图]
Dashboard --> Children[ChildrenList]
Dashboard --> Grades[ChildGrades]
Dashboard --> Notif[ParentNotifications]
Children -.切换.-> SelectChild[selectChild Mutation<br/>仅审计日志]
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 缓存失效策略
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
# ============ 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_*":
{
"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.<aggregate>.<action>格式。
| 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)
graph LR
K[(Kafka)]
ES[EventSubscriber<br/>NestJS Module]
Idempotency[(Redis SETNX<br/>event_id 去重)]
Filter[偏好过滤器<br/>按 NotificationPreferences 过滤]
DC[DownstreamClient]
Push[push-gateway<br/>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):
- 拉取家长
NotificationPreferences(Redis 缓存 300s) - 按
eventTypeMap映射事件类型 → 偏好开关(GRADE→gradeReleased、HOMEWORK→homeworkGraded、EXAM→examPublished、ATTENDANCE→attendanceAlert、ANNOUNCEMENT→schoolAnnouncement) - 若偏好开关为 false → 不推送(家长关闭了该类通知)
- 取
prefs.channels与event.channels交集,无交集则不推送 - 调用
pushClient.pushViaHttp(parentId, payload, allowedChannels)(U2:HTTP /internal/push)
5.5 事件订阅消费者组设计(P5)
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):
- BFF 是聚合层,不持有业务资源,权限决策无意义
- Gateway 已做 JWT 校验(RS256),非法请求无法到达 BFF
- 下游服务(iam/core-edu/data-ana/msg)各自用
@RequirePermission校验,BFF 透传x-user-id即可- 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 不检查下游可达性。理由:
- BFF 无 DB,无需 SELECT 1
- 下游不可达时 BFF 降级返回 partial 数据,不阻塞 readiness
- 下游健康由各自 /healthz + Prometheus 监控,BFF 不重复探针
- 若 K8s readiness 探针依赖下游,会导致下游故障时 BFF Pod 被摘流,反而失去降级能力
6.7 优雅关闭
SIGTERM 关闭顺序(main.ts 已处理 app.close(),P5+ 补充 Kafka consumer):
- 拒绝新请求(readyz 返回 503)
- 停止消费 Kafka(commit 最后 offset)[P5+]
- 等待 in-flight GraphQL 请求完成(最多 10s)
- 关闭 Redis 连接
- flush 剩余 OTel span
- 进程退出
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()全局 - 行为:
- ApplicationError → 按 statusCode + code 返回 ActionState / GraphQL errors
- ZodError → 400 +
BFF_PARENT_VALIDATION_ERROR+ details - GraphQL 错误 → 包装为 GraphQL errors 数组(携带 extensions.code)
- 未知 Error → 500 +
BFF_PARENT_INTERNAL_ERROR+ traceId - 注入 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)+ teacher-bff/student-bff 模式 + 用户 4 项仲裁决策(U1-U4)+ coord 补充仲裁(C1-C6)推断生成,非 ai05 原创交付。
coord 推断依据
- ai04 阶段 1 交付:01-understanding.md 已识别 parent-bff 位置、限界上下文、契约缺口、P0 阻塞项。本设计文档基于该理解深化。
- teacher-bff/student-bff 模式参考:分层结构、目录结构、横切关注点、错误码清单、可观测性设计参考两份 02-architecture-design.md。
- 用户 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 范围):
- iam 补
GetEffectiveAccess(parentUserId) → {perms, viewports, dataScope, children}聚合 RPC(减少 2 次 RTT) - core-edu 补
GetClassesByStudent(student_id) → Class[]RPC(childId→classId 转换当前缺失) - data-ana 补
GetParentDashboard(parent_id) → ParentDashboard聚合 RPC(多子女场景防 N+1) - iam 补 parent 角色权限映射 或确认采用 coord §16.5 #4 仲裁豁免(parent 角色无权限点)
- ChildGuard 缓存击穿保护:singleflight 模式防止缓存过期瞬间 N 个请求同时调 iam.GetChildrenByParent
- 多子女切换跨标签同步:BroadcastChannel + LWW(前端方案,BFF 仅提供 selectChild 审计 mutation)
- GraphQL Schema 版本化策略:
@deprecated+ Schema Registry + Breaking Change 检测 - msg 服务 P5 落地后 parent-bff 接入方式:gRPC 50056(推荐)还是 HTTP REST(兼容)
10. 长远架构演进设计(ai05 补全,覆盖 P5-P7+)
本节为 ai05 视角的长远架构演进,符合"通用架构设计标准 + 为未来可能发生的做好铺垫"要求。覆盖 10 个长远预留点,确保 parent-bff 在 P4 之后的演进中不需重构核心抽象。
10.1 演进路线图(P4-P7+)
graph LR
P4[P4 MVP<br/>GraphQL + DataLoader<br/>多子女切换 + ChildGuard<br/>iam + core-edu + data-ana]
P5[P5 通知接入<br/>msg gRPC 50056<br/>通知偏好配置<br/>push-gateway HTTP 推送<br/>Kafka consumer 缓存失效]
P6[P6 硬化<br/>熔断器 opossum<br/>HPA + mTLS<br/>SLO 监控 + 告警<br/>灰度发布]
P7[P7+ 长远<br/>多租户隔离<br/>合规审计<br/>i18n 5 语言<br/>移动端 PWA<br/>AI 家长助手<br/>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 阶段硬化项
- 🔜 熔断器
opossumper-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 项原则:
- BFF 本分:只聚合、裁剪、协议转换,不持业务状态、不做权限决策、不直访业务 DB(004 §3.2 + coord §16.5 #4)
- 契约驱动:所有跨模块交互以 proto 契约为唯一源,proto 缺失项显式标注并提请 coord 仲裁
- 平滑演进:每一阶段都能独立交付价值,不依赖未来阶段;每次升级通过抽象层隔离变更
- 为未来铺垫:抽象层预留 gRPC / GraphQL / SSE / Kafka / 熔断 / 多租户扩展点,未来需求接入时无需重构核心
- 降级优先:任一下游故障时 BFF 应优雅降级(返回缓存陈旧数据或部分字段 null),而非整体 500
- 可观测性:三支柱(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 配置项分组
// 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<typeof envSchema>;
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 环境变量优先级
- 进程环境变量(K8s ConfigMap / Secret / docker-compose env_file)
.env文件(仅开发环境,dotenv加载)- Zod schema 默认值
13. 黄金模板对齐 checklist(ai05 补全,16 项)
对照 project_rules §3 + known-issues §2.2 classes 黄金模板 + coord §16.4 黄金模板对齐表
| # | 对齐项 | 状态 | 说明 |
|---|---|---|---|
| 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} |
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(ai04 撰写 + ai05 复审修订) coord 代笔日期: 2026-07-09 ai05 复审日期: 2026-07-09 深夜 文档版本: v2(coord 推断 v1 + ai05 复审 v2) 阶段 3 进入条件: 本文档经 coord 交叉审查通过后进入阶段 3 实施
本文件由 coord 基于 ai04 阶段 1 交付(01-understanding.md)+ teacher-bff/student-bff 模式 + 用户 4 项仲裁决策推断生成,非 ai05 原创交付。
推断依据
- ai04 阶段 1 交付:01-understanding.md 已识别 parent-bff 位置、限界上下文、契约缺口、P0 阻塞项。本设计文档基于该理解深化。
- teacher-bff/student-bff 模式参考:分层结构、目录结构、横切关注点、错误码清单、可观测性设计参考两份 02-architecture-design.md。
- 用户 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 服务侧)。
后续动作
- ai05 review:确认或修改 §8.4 的 10 项决策
- coord 同步 004:parent-bff 依赖图状态改为"✅ 已实现",依赖扩展为 iam + core-edu + data-ana + msg(C6)
- coord 协调 ai02:P0 阻塞项——iam 补 GetChildrenByParent RPC +
iam_student_guardians表 - 阶段 3 进入条件:本文档经 ai05 review + coord 交叉审查通过后进入阶段 3
AI Agent: ai05(coord 代笔) Coordinator: coord-ai Branch: 单仓库并行模式(直接 push main) 关联阶段 1 文档: 01-understanding.md(ai04 撰写) 代笔日期: 2026-07-09