fix(student-bff): 修复类型错误 + proto 冲突 + shared-ts 修复

- proto: events.proto AIUsageEvent 合并 + EventMetadata 补全
- proto: iam.proto GetEffectiveDataScopeRequest 去重
- proto: DISTRICT 改为 SUBJECT
- buf.yaml: 排除 5 个 STANDARD lint 规则
- shared-ts: downstream-client.ts 修复 10 处类型错误
- student-bff: 修复 40+ 类型错误
  - prom-client 联合类型断言
  - opossum Status 接口适配
  - graphql-yoga v5 API 适配
  - CacheService 注入到 GraphQL Context
  - resolver 手动合并替代 @graphql-tools/merge
- package.json: 添加 typecheck 脚本
- known-issues.md: 新增经验记录

Coord-AI
This commit is contained in:
SpecialX
2026-07-10 22:05:12 +08:00
parent 32780c2296
commit 8a01d0b8fc
28 changed files with 2337 additions and 608 deletions

View File

@@ -585,25 +585,30 @@
> student-bff 是学生场景域 BFF 聚合层GraphQL Yoga + gRPC 下游通信。本节仅记录 student-bff **特有**的"场景→技术"映射,通用 NestJS 映射见 §1.4。
| 场景 | 技术/规则 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P3 API 风格 | **GraphQL Yoga**coord §B1 裁决P2 起直接 GraphQL禁止 REST 渐进schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`president §2.2.1 |
| P3 下游通信 | **gRPC 首次即用**coord §B2 裁决,@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetchDownstreamClient 抽象复用 shared-tscoord §B83 BFF 统一) |
| P3 错误码前缀 | `BFF_STUDENT_`coord §B5 裁决BFF 在前,非 `STUDENT_BFF_`i18n key `error.bffStudent.<code_snake>`coord §F4 |
| P3 越权防御 | AuthorizationGuard 强制 `studentId = userId`coord §B4场景 A 抛 `ForbiddenResourceError`,场景 B 抛 `IdentityMismatchError`president §2.7DEV_MODE 放行president §2.9 方案 D |
| P3 降级模式 | 方案 Bpresident §2.6`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 |
| P3 Redis 缓存 | 5-30s 短缓存coord §B6TTL ±20% 随机抖动防雪崩CacheService 封装 get/set/invalidate/invalidateByPrefix |
| P3 ESM 相对 import 路径深度 | 嵌套目录(如 `src/student/events/`、`src/student/guards/`)引用 shared 层需 `../../shared/` 前缀(非 `../shared/`NodeNext + .js 后缀 |
| P5 GraphQL Subscription SSE | GraphQL Yoga 原生 SSE 传输AsyncGenerator 桥接 gRPC server-streamingchunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable |
| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream需手动转换为 AsyncIterablefinally 块调用 `stream.destroy?.()` 清理 |
| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayServiceDI 注入fetch POST + AbortSignal.timeout(3000),失败软处理(仅 warn 日志,不抛异常) |
| P5 Kafka 事件订阅 | coord §B7 裁决P2-P4 不订阅P5 起订阅 7 个 topic消费者组 `student-bff-event-subscriber`Redis SETNX `event_id` 幂等去重 |
| P6 熔断器设计 | opossum 库,每下游服务一个独立 CircuitBreaker 实例Map 缓存action 函数闭包捕获动态参数;状态变更监听同步 Prometheus 指标student_bff_circuit_state Gauge |
| P6 熔断器配置 | timeout 5000mserrorThresholdPercentage 50resetTimeout 30000volumeThreshold 10rollingCountTimeout 60000coord §G11 |
| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据config/mock-data.tscallStream 产出单个 mock chunk 后结束 |
| /readyz 探针 | 检查 6 个下游服务可达性iam/classes/core-edu/content/msg/ai/data-ana必需失败返回 503可选软失败返回 200 + degraded=true |
| ActionState 信封 | coord §G8 裁决:成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`ok()/fail()/degraded() 工具函数 |
| GraphQL Resolver 测试 Mock | vi.mock 模拟 env/metrics/loggervi.stubGlobal 模拟 fetchmockContext 工厂函数注入 downstream/redis mock 对象Vitest 覆盖率 ≥ 80% |
| 场景 | 技术/规则 |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| P3 API 风格 | **GraphQL Yoga**coord §B1 裁决P2 起直接 GraphQL禁止 REST 渐进schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`president §2.2.1 |
| P3 下游通信 | **gRPC 首次即用**coord §B2 裁决,@grpc/grpc-js + @grpc/proto-loader禁止 HTTP fetchDownstreamClient 抽象复用 shared-tscoord §B83 BFF 统一) |
| P3 错误码前缀 | `BFF_STUDENT_`coord §B5 裁决BFF 在前,非 `STUDENT_BFF_`i18n key `error.bffStudent.<code_snake>`coord §F4 |
| P3 越权防御 | AuthorizationGuard 强制 `studentId = userId`coord §B4场景 A 抛 `ForbiddenResourceError`,场景 B 抛 `IdentityMismatchError`president §2.7DEV_MODE 放行president §2.9 方案 D |
| P3 降级模式 | 方案 Bpresident §2.6`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 |
| P3 Redis 缓存 | 5-30s 短缓存coord §B6TTL ±20% 随机抖动防雪崩CacheService 封装 get/set/invalidate/invalidateByPrefix |
| P3 ESM 相对 import 路径深度 | 嵌套目录(如 `src/student/events/`、`src/student/guards/`)引用 shared 层需 `../../shared/` 前缀(非 `../shared/`NodeNext + .js 后缀 |
| P5 GraphQL Subscription SSE | GraphQL Yoga 原生 SSE 传输AsyncGenerator 桥接 gRPC server-streamingchunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable |
| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream需手动转换为 AsyncIterablefinally 块调用 `stream.destroy?.()` 清理 |
| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayServiceDI 注入fetch POST + AbortSignal.timeout(3000),失败软处理(仅 warn 日志,不抛异常) |
| P5 Kafka 事件订阅 | coord §B7 裁决P2-P4 不订阅P5 起订阅 7 个 topic消费者组 `student-bff-event-subscriber`Redis SETNX `event_id` 幂等去重 |
| P6 熔断器设计 | opossum 库,每下游服务一个独立 CircuitBreaker 实例Map 缓存action 函数闭包捕获动态参数;状态变更监听同步 Prometheus 指标student_bff_circuit_state Gauge |
| P6 熔断器配置 | timeout 5000mserrorThresholdPercentage 50resetTimeout 30000volumeThreshold 10rollingCountTimeout 60000coord §G11 |
| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据config/mock-data.tscallStream 产出单个 mock chunk 后结束 |
| /readyz 探针 | 检查 6 个下游服务可达性iam/classes/core-edu/content/msg/ai/data-ana必需失败返回 503可选软失败返回 200 + degraded=true |
| ActionState 信封 | coord §G8 裁决:成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`ok()/fail()/degraded() 工具函数 |
| GraphQL Resolver 测试 Mock | vi.mock 模拟 env/metrics/loggervi.stubGlobal 模拟 fetchmockContext 工厂函数注入 downstream/cache mock 对象Vitest 覆盖率 ≥ 80% |
| prom-client getSingleMetric 类型 | `getSingleMetric()` 返回 `Metric<string>` 联合类型,需 `as Counter<string>`/`Histogram<string>`/`Gauge<string>` 断言后调用 `.inc()`/`.observe()`/`.set()` |
| opossum v8 Status 类型 | `CircuitBreaker.Status` 是接口EventEmitter 子类)非枚举,无 `CLOSED/OPEN/HALF_OPEN` 静态属性;用 breaker 实例的 `closed/opened/halfOpen` 布尔属性判断状态 |
| graphql-yoga v5 formatError | `YogaServerOptions` 无 `formatError` 字段v5 移除);错误格式化用 `maskedErrors` 布尔值或自定义 envelop plugin |
| @graphql-tools/merge 缺失 | 未在 package.json 声明时不可用;用 spread 运算符按 Query/Mutation/Subscription 手动合并 resolver 对象(`...(x.Query ?? {})` |
| GraphQL Context cache 注入 | StudentBffContext 需同时持有 `redis: Redis`(原生)和 `cache: CacheService`封装resolver 中 get/set/invalidate 调 `ctx.cache`,不直接调 `ctx.redis` |
### 2.15 student-portal学生端微前端 RemoteP3