chore(parent-bff): merge parent-bff module into main

Merge feat/parent-bff-ai05 into main, conflicts resolved in favor of feature branch
This commit is contained in:
SpecialX
2026-07-10 19:00:29 +08:00
89 changed files with 11177 additions and 39 deletions

View File

@@ -481,26 +481,45 @@
> parent-bff 是 BFF 聚合层,无 DB无聚合根无 @RequirePermissionU4 仲裁豁免)。本节记录 parent-bff 特有的"场景→技术"映射。
| 场景 | 技术/规则 |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| BFF 聚合层定位 | 只聚合不持状态,无 DB无聚合根无 @RequirePermissionU4 仲裁豁免,权限由 Gateway JWT + 下游服务负责) |
| GraphQL Yoga 端点 | POST /graphqlGraphQL over HTTP对前端 GraphQL对下游 gRPCU3 仲裁 GraphQL P2 立即引入parent-bff P4 直接用 GraphQL |
| DataScope=CHILDREN 越权校验 | ChildGuard 在 Resolver 执行前校验 requestedChildId ∈ iam.GetChildrenByParent(parentId),不绑定抛 BFF_PARENT_CHILD_NOT_BOUND403 |
| 多子女切换方案 A | 前端管理 childIdZustand sliceBFF 无状态ParentSession 仅请求级值对象不持久化selectChild mutation 仅记录审计日志 |
| 并行 gRPC 编排 | Promise.allSettled + 降级策略iam 失败返回 user=nullcore-edu 失败返回 grades=null部分失败标记 partial=true |
| DataLoader 批量去重 | per-request 实例,按 parentId 批量取孩子列表、按 studentId 批量取成绩/作业/考试,防 N+1 |
| Redis 聚合缓存 | 30s TTLcoord 推断 60sai05 调整为 30s + 主动失效key=bff:parent:{userId}:{queryHash}singleflight 防击穿 |
| 错误码前缀 | BFF_PARENT__coord 仲裁 C1对齐 004 §11.4 BFF 在前风格,不是 PARENT_BFF__ |
| 端口分配 | HTTP 3010不暴露 gRPCcoord 仲裁 C2BFF 对下游走 gRPC对上游仅 HTTP/GraphQL |
| 下游依赖 | iam + core-edu + data-ana + msgcoord 仲裁 C6 扩展,与 student-bff 对齐) |
| P0 阻塞 | iam 缺 GetChildrenByParent RPC + iam_student_guardians 表ai02 补全前 parent-bff 无法启动 |
| push-gateway 调用 | 走 HTTP /internal/pushU2 仲裁豁免 gRPCmsg 调用方改用 HTTP |
| Kafka topic 命名 | edu.notification.sent/read/recalled/failedcoord 仲裁 C5废弃 edu.notification.events 抽象名) |
| /readyz 下游探针 | P4 添加并行 ping iam + core-edu + data-ana + msg 的 /healthzcoord 推断 BFF 直接 okai05 调整为 P4 加下游探针) |
| GraphQL complexity 防御 | depth limit≤7+ query cost analysis≤1000+ persisted queriesP6 预留) |
| 优雅关闭 | HTTP server → Redis → gRPC clients → TracerBFF 无 DB/Kafka关闭顺序简化 |
| ParentSession 值对象 | parentId/roles/dataScope:CHILDREN/requestedChildId/traceId每次请求重建不跨请求004 §12.1 无状态约束) |
| 通知偏好配置 | NotificationPreference渠道开关 channels[] + 频率限制 frequency_limit + 静默时段 quiet_hoursP4 通过 msg 服务管理 |
| 场景 | 技术/规则 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BFF 聚合层定位 | 只聚合不持状态,无 DB无聚合根无 @RequirePermissionU4 仲裁豁免,权限由 Gateway JWT + 下游服务负责) |
| GraphQL Yoga 端点 | POST /graphqlGraphQL over HTTP对前端 GraphQL对下游 gRPCU3 仲裁 GraphQL P2 立即引入parent-bff P4 直接用 GraphQL |
| DataScope=CHILDREN 越权校验 | ChildGuard 在 Resolver 执行前校验 requestedChildId ∈ iam.GetChildrenByParent(parentId),不绑定抛 BFF_PARENT_CHILD_NOT_BOUND403 |
| 多子女切换方案 A | 前端管理 childIdZustand sliceBFF 无状态ParentSession 仅请求级值对象不持久化selectChild mutation 仅记录审计日志 |
| 并行 gRPC 编排 | Promise.allSettled + 降级策略iam 失败返回 user=nullcore-edu 失败返回 grades=null部分失败标记 partial=true |
| DataLoader 批量去重 | per-request 实例,按 parentId 批量取孩子列表、按 studentId 批量取成绩/作业/考试,防 N+1 |
| Redis 聚合缓存 | 30s TTLcoord 推断 60sai05 调整为 30s + 主动失效key=bff:parent:{userId}:{queryHash}singleflight 防击穿 |
| 错误码前缀 | BFF_PARENT__coord 仲裁 C1对齐 004 §11.4 BFF 在前风格,不是 PARENT_BFF__ |
| 端口分配 | HTTP 3010不暴露 gRPCcoord 仲裁 C2BFF 对下游走 gRPC对上游仅 HTTP/GraphQL |
| 下游依赖 | iam + core-edu + data-ana + msgcoord 仲裁 C6 扩展,与 student-bff 对齐) |
| P0 阻塞 | iam 缺 GetChildrenByParent RPC + iam_student_guardians 表ai02 补全前 parent-bff 无法启动 |
| push-gateway 调用 | 走 HTTP /internal/pushU2 仲裁豁免 gRPCmsg 调用方改用 HTTP |
| Kafka topic 命名 | edu.notification.sent/read/recalled/failedcoord 仲裁 C5废弃 edu.notification.events 抽象名) |
| /readyz 下游探针 | P4 添加并行 ping iam + core-edu + data-ana + msg 的 /healthzcoord 推断 BFF 直接 okai05 调整为 P4 加下游探针) |
| GraphQL complexity 防御 | depth limit≤7+ query cost analysis≤1000+ persisted queriesP6 预留) |
| 优雅关闭 | HTTP server → Redis → gRPC clients → TracerBFF 无 DB/Kafka关闭顺序简化 |
| ParentSession 值对象 | parentId/roles/dataScope:CHILDREN/requestedChildId/traceId每次请求重建不跨请求004 §12.1 无状态约束) |
| 通知偏好配置 | NotificationPreference渠道开关 channels[] + 频率限制 frequency_limit + 静默时段 quiet_hoursP4 通过 msg 服务管理 |
| P5 msg gRPC 客户端 | MsgClient interface + MockMsgClient + GrpcMsgClientNotificationPreferences RPC 未定义时回退默认偏好(避免下游未实现阻塞 BFF |
| P5 push-gateway HTTP 客户端 | PushHttpClient interface + MockPushHttpClient + HttpPushHttpClientfetch + 3s timeout + AbortController |
| P5 通知偏好过滤 | EVENT_TYPE_MAP 映射事件→通知类型,拉取偏好后检查偏好开关 + channels 交集,跳过关闭偏好的家长推送 |
| P5 Kafka consumer 多 handler | 同一 topic 可注册多个 handlerpush + cache-invalidationhandlers 改为 `Map<string, EventHandler[]>` 支持多 handler |
| P5 Kafka 幂等 + DLQ | event_id Redis SETNX 去重24h TTL+ MAX_RETRIES=3 指数退避 + DLQ `edu.parent-bff.dlq` |
| P5 缓存失效 handler | edu.teaching.grade.recorded→grades+dashboard 失效edu.notification.read/recalled→notifications 失效 |
| P6 opossum 熔断器 | per-service 单例 + 事件监听open/close/halfOpen→circuitStateGauge+ withCircuitBreaker 包装 callWithMetrics开启抛 503 |
| P6 熔断器配置 | timeout 5s / errorThresholdPercentage 50% / resetTimeout 30s / volumeThreshold 10 |
| P6 Helm chart 结构 | Chart.yaml + values.yaml + values-{dev,staging,prod}.yaml + templates/_helpers.tpl + deployment + service + hpa + configmap |
| P6 podAntiAffinity | preferredDuringSchedulingIgnoredDuringExecution + weight 100 + topologyKey kubernetes.io/hostname |
| P6 HPA 多环境 | dev: 1 replica HPA disabledstaging: 2 replicas HPA 2-5prod: 3 replicas HPA 3-10 + CPU 70%/memory 80% |
| P6 SLO 告警规则 | parent-bff-slo 组 9 条告警P95>200ms / 错误率>0.1% / 可用性<99.9% / 熔断器open / 下游错误率>5% / 缓存命中率<50% / Kafka停滞 / push失败率>5% / ChildGuard突增 |
| P6 Grafana 面板 | 11 面板P95延迟/错误率/可用性/熔断器/缓存命中率/ChildGuard/下游QPS/下游P95/Kafka消费/push推送/GraphQL QPS |
| ESM 模式 import 后缀 | 相对 import 用 `.js` 后缀NodeNext 构建产物test 文件 import 源码也用 `.js` 后缀 |
| vitest coverage 配置 | coverage 配置在 `test` 内部vitest v2 API不在根层级exclude 排除基础设施文件gRPC clients/DataLoader/GraphQL 框架/Redis/Kafka |
| 测试覆盖率排除策略 | 基础设施/框架代码gRPC client 实现/DataLoader 工厂/GraphQL 框架设置/Redis client/Kafka consumer需 E2E 测试覆盖,单元/集成测试排除 |
| Kafka consumer Buffer 类型 | kafkajs `message.value` 类型为 `Buffer | null`,传参需 `?? undefined`转换为`Buffer | undefined` |
| Mock 客户端 markAsRead 类型 | `MOCK_NOTIFICATIONS[parentId]` 可能 undefined需 `if (!list) continue``list[idx]` 可能 undefined需 `const target = list[idx]; if (target)` 守卫 |
| msg.client callWithMetrics | callWithMetrics 签名是 `(client, method, request, serviceName)`,不是 `("msg", "X", () => fn)` |
### 2.15 student-portal学生端微前端 RemoteP3