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