Files
Edu/services/parent-bff/docs/02-architecture-design.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md)

2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration)

3.各服务 01/02 文档补全

4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens)

5.Proto 契约补全

6.004 架构影响地图更新

7.端口分配表

8.设计规格文档
2026-07-10 12:58:22 +08:00

1507 lines
105 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — parent-bff
> AI 标识ai05coord 代笔推断 → 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-09ai05 未交付 02 文档coord 基于 01-understanding.mdai04 初稿)+ 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 黄金模板对齐 checklist16 项自检);(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 豁免 gRPCmsg/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.4BFF 在前;不是 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 | 数据模型(无 DBRedis 仅缓存聚合结果 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<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 场景)
```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<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/ # DataLoaderper-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.tstrace/metrics/retry
│ └─ http/push-http.client.ts # push-gateway 走 HTTPU2 仲裁)
├─ shared/
│ ├─ errors/ # application-error.tsBFF_PARENT_*+ global-error.filter.ts
│ ├─ health/health.controller.ts # /healthz + /readyzBFF 直接 ok
│ ├─ cache/ # redis.client.ts + cache-key.builder.tsbff:parent:*
│ ├─ observability/ # logger.tspino+ metrics.tsparent_bff_*+ tracer.tsOTel
│ └─ 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 → DownstreamCache 横向被 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前端管理 childIdBFF 无状态。ParentSession 仅作为 GraphQL context 内的**请求级值对象**存在,不持久化到 Redis 会话。
```typescript
// graphql/context.ts
export interface ParentSession {
parentId: string; // 从 x-user-id 头解析
roles: string[]; // 从 x-user-roles 头解析
dataScope: "CHILDREN"; // 家长固定 CHILDREN004 §5.4
requestedChildId?: string; // 从 GraphQL query 参数解析(前端传入)
traceId: string; // 从 x-request-id 头解析
}
```
**设计要点**
1. ParentSession **每次请求重建**,不跨请求保留状态(符合 004 §12.1 无状态约束)
2. `requestedChildId` 由前端在 GraphQL query 参数传入(方案 ABFF 不维护"当前选中孩子"
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<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 层先拦截可:
>
> 1. 早失败,减少无效下游 gRPC 调用
> 2. 提升审计能力(越权尝试在 BFF 层记录)
> 3. 降低跨家庭数据泄露风险(家长 A 不能查家长 B 的孩子)
> 4. 不依赖下游服务实现 DataScope=CHILDREN 语义core-edu 当前仅支持 SELF/CLASS/GRADE/SCHOOL
### 2.4 聚合视图间关系
```mermaid
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方案 ABFF 无状态,不存会话)。本节定义 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`channelsapp/sms/email/wechat 枚举数组min 1+ eventTypes5 个布尔开关gradeReleased/homeworkGraded/examPublished/attendanceAlert/schoolAnnouncement
- `ListGradesQuerySchema`childIdmin 1、page≥1默认 1、pageSize1-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.<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
```mermaid
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 豁免 gRPCparent-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)`U2HTTP /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 Loggerpino
| 项 | 值 |
| ------------ | -------------------------------------------------------------------------------- |
| 文件位置 | `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 Metricsprom-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` | CounterP5 | `topic, event_type` | 事件消费数 |
| `parent_bff_event_pushed_total` | CounterP5 | `topic, push_status` | 推送数 |
### 6.5 TracerOpenTelemetry
| 项 | 值 |
| --------------------- | --------------------------------------------------------------------------------- |
| 文件位置 | `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 Contexttraceparent 头 → gRPC metadata |
### 6.6 /healthz 与 /readyz
| 端点 | 检查逻辑 | 响应 |
| ---------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `/healthz` | 进程存活(直接返回 ok | `200 { status: 'ok', service: 'parent-bff', timestamp }` |
| `/readyz` | **直接返回 ok**(对齐 teacher-bffBFF 无 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. 停止消费 Kafkacommit 最后 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 二次校验(双重保障),自定义 scalarDateTime、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 50052P4+ | 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 服务定义 | coordinfra | P4 | 端口 3010加入 edu-net + edu-shared 网络 |
| 5 | full-stack-runbook 端口矩阵更新 | coorddocs | P4 | 追加 3010 行 |
| 6 | 004 §4 依赖图更新 | coorddocs | P4 | parent-bff 依赖扩展为 iam + core-edu + data-ana + msgC6 仲裁),状态从"📐 需设计"改为"✅ 已实现" |
| 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 风格 | RESTP2→ GraphQLP4 | RESTP3→ GraphQLP6+ 可选) | **GraphQLP4 直接U3 仲裁)** |
| DataScope | CLASS / GRADE / SCHOOL | SELF | **CHILDREN自定义级** |
| 越权防御 | 不做(透传 x-user-id | 做(强制 userId 比对) | **做ChildGuardchildId ∈ 绑定列表)** |
| 端口 | 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 服务未实现查询 APIanalytics.proto 3 个 method 无 REST/gRPC 端点) | P4 学情诊断端点无法实现 | 中 | coord 协调 ai06 在 P4 实现 data-ana 查询 API |
| msg 通知偏好配置接口缺失 | P5 家长通知偏好无法配置 | 中 | coord 协调 ai05 在 P5 补 NotificationPreferenceService |
### 8.2 技术风险
| 风险 | 影响 | 概率 | 缓解措施 |
| ------------------------------------------------------------------- | ---- | ---- | ------------------------------------------------------------------------------------ |
| GraphQL Schema 演进破坏前端 | 高 | 中 | Schema 版本化 + deprecation 先行 + 协调前端 ai07parent-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 analysiscost ≤ 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 已创建C5edu.notification.sent/read/recalled/failed | P5 事件订阅无法实现 | P5 阻塞P4 不依赖 Kafka |
| 前端 parent-portalai07P4 配合使用 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 ≤ 7cost ≤ 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 调用 NotificationPreferenceServiceP5 msg 服务补全时实现 |
| 5 | GraphQL 查询复杂度限制 | depth ≤ 7cost ≤ 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/服务,任一失败返回 degradedK8s 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 SubscriptionWebSocket 长连接与 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<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 阶段硬化项
- 🔜 熔断器 `opossum` per-downstream-serviceiam/core-edu/data-ana/msg 独立 circuit
- 🔜 HPA 2-10 副本 + podAntiAffinity
- 🔜 mTLSService 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** | 错误码已用英文 + traceIdmessage 待前端 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 fallbackP6+);④全部不可用→维护页面 |
| 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 本分**:只聚合、裁剪、协议转换,不持业务状态、不做权限决策、不直访业务 DB004 §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 serverproto 一致) | `@grpc/grpc-js` mock + `jest.mock` |
| core-edu gRPC | Mock server | 同上 |
| data-ana gRPC | Mock server | 同上 |
| msg gRPC | Mock serverP5 | 同上 |
| Redis | Testcontainers真实 Redis 实例) | `testcontainers/redis` |
| Kafka | Mock consumerP5 | `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"),
// === 下游服务 URLP4 HTTPP5+ 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 targetP5+ 启用,覆盖 URL ===
IamGrpcTarget: z.string().optional(), // 默认 localhost:50052
CoreEduGrpcTarget: z.string().optional(), // 默认 localhost:50053
DataAnaGrpcTarget: z.string().optional(), // 默认 localhost:50055
MsgGrpcTarget: z.string().optional(), // 默认 localhost:50056P5
// === 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(),
// === KafkaP5+ 启用) ===
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-gatewayHTTP 模式) |
| 下游 gRPC target | 4 | iam / core-edu / data-ana / msggRPC 模式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 / topicsP5+ 启用) |
| CORS | 1 | 允许的 originparent-portal 默认 4002 |
### 12.3 环境变量优先级
1. 进程环境变量K8s ConfigMap / Secret / docker-compose env_file
2. `.env` 文件(仅开发环境,`dotenv` 加载)
3. Zod schema 默认值
---
## 13. 黄金模板对齐 checklistai05 补全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 | loggerpino | ✅ | 复制 teacher-bff `shared/observability/logger.ts`service 改 `parent-bff` |
| 4 | metricsprom-client `/metrics` 端点) | ✅ | 复制 teacher-bff main.ts 注册方式,指标名前缀 `parent_bff_` |
| 5 | tracerOpenTelemetry SDK + OTLP exporter + auto-instrumentations | ✅ | 复制 teacher-bff tracer.tsserviceName 改 `parent-bff` |
| 6 | `/healthz` 健康检查liveness | ✅ | 直接返回 okBFF 无 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 落地补 | 测试策略见 §114 层金字塔 |
| 10 | Dockerfile 多阶段构建 | ✅ | 复制 teacher-bff DockerfileEXPOSE 改 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 次 RTTGetChildrenByParent + GetViewports + GetEffectivePermissions 合一) | P4 | iamai06 补全)+ parent-bff调用方 |
| 2 | core-edu 补 `GetClassesByStudent(student_id) → Class[]` RPC | 强烈建议parent-bff 拉子女考试/作业需先 childId→classId 转换,当前缺此 RPC | P4 | core-eduai08 补全)+ parent-bff + student-bff |
| 3 | data-ana 补 `GetParentDashboard(parent_id) → ParentDashboard` 聚合 RPC | 建议(多子女场景防 N+1N 次 GetStudentWeakness 调用 → 1 次聚合 RPC | P4 | data-anaai11 补全)+ 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 | iamai06+ parent-bff |
| 5 | ChildGuard 缓存击穿保护singleflight 模式) | 启用(防止缓存过期瞬间 N 个请求同时调 iam.GetChildrenByParent | P4 | parent-bff 独立决策,无需 coord 仲裁,记录在此 |
| 6 | 多子女切换跨标签同步 | 前端方案BroadcastChannel + LWWBFF 仅提供 selectChild 审计 mutation | P4 | parent-portalai15+ parent-bff |
| 7 | GraphQL Schema 版本化策略 | `@deprecated` + Schema Registry + Breaking Change 检测CI 强制) | P5+ | parent-bff + parent-portalai15 |
| 8 | msg 服务 P5 落地后 parent-bff 接入方式 | 推荐 gRPC 50056性能优于 HTTPproto 契约一致);兼容方案 HTTP RESTmsg gRPC server 未就绪时降级) | P5 | msgai10+ parent-bff |
### 14.1 ai05 提请的 P0 阻塞项(已在 §8.1 列出,此处重申)
| # | 阻塞项 | 阻塞阶段 | 责任方 | 缓解措施 |
| ---- | ---------------------------------------------------------------- | -------- | ------------------ | ------------------------------------------------------ |
| P0-1 | iam 缺失 `GetChildrenByParent` RPC + `iam_student_guardians` 表 | P4 | ai06iam 现归属) | 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 | 无 DBRedis 缓存 + 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 | 状态机预留 | ✅ | §2BFF 无状态机,仅审计日志) |
| 2 | 版本化 | ✅ | §14 #7GraphQL Schema 版本化) + §10.5 #7API 版本化) |
| 3 | 多租户 | ✅ | §10.5 #1(缓存 key + Schema 字段预留) |
| 4 | 国际化i18n | ✅ | §10.5 #3(错误码英文 + message i18n key |
| 5 | 合规审计 | ✅ | §10.5 #2selectChild 审计 + 未来全量审计) |
| 6 | AI 集成 | ✅ | §10.5 #5AI 家长助手P5+ 评估) |
| 7 | 演进路径 | ✅ | §10.1P4-P7+ 路线图 mermaid |
| 8 | 可扩展抽象 | ✅ | §10.5 #10Client 抽象层) |
| 9 | 容灾与高可用 | ✅ | §10.5 #8(无状态 + 跨可用区部署) |
| 10 | 降级模式 | ✅ | §10.5 #94 类降级矩阵) |
### 15.3 自评估结论
parent-bff 02-architecture-design.md 覆盖 **12 维度全覆盖 + 10 项长远铺垫**,符合"通用架构设计标准 + 为未来可能发生的做好铺垫"要求。coord §0-§8 已建立 P4 即时实现设计ai05 §9-§15 补全长远演进 + 测试 + 配置 + checklist + 自评估,文档完整度达 ai-allocation.md §7 模板要求 + 用户"长远全面"标准。
---
**AI Agent**: ai05parent-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 深夜
**文档版本**: v2coord 推断 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 Schemacoord 基于家长场景推断,可能未覆盖 ai05 全部场景、ChildGuard 实现细节(同步阻塞 vs 预加载绑定列表、通知偏好过滤位置P5 EventSubscriber 内 vs msg 服务侧)。
### 后续动作
1. **ai05 review**:确认或修改 §8.4 的 10 项决策
2. **coord 同步 004**parent-bff 依赖图状态改为"✅ 已实现",依赖扩展为 iam + core-edu + data-ana + msgC6
3. **coord 协调 ai02**P0 阻塞项——iam 补 GetChildrenByParent RPC + `iam_student_guardians`
4. **阶段 3 进入条件**:本文档经 ai05 review + coord 交叉审查通过后进入阶段 3
---
**AI Agent**: ai05coord 代笔)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main
**关联阶段 1 文档**: [01-understanding.md](./01-understanding.md)ai04 撰写)
**代笔日期**: 2026-07-09