chore(teacher-portal): merge teacher-portal full implementation into main

This commit is contained in:
SpecialX
2026-07-10 19:19:19 +08:00
48 changed files with 2888 additions and 1333 deletions

View File

@@ -131,26 +131,48 @@ gantt
### §4.1 我依赖的上游就绪标志
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
| --------------------------------------------------------- | ------------------------------------------------- | -------- | ---------------------- |
| packages 骨架ai13 自建) | ui-tokens/ui-components/hooks 可 import | P2 启动 | ✅ 已就绪(批次 0.15 |
| teacher-bff GraphQL schemaai03 + coord 仲裁 ISSUE-037 | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
| api-gateway HTTP :8080ai01 | /api/v1/teacher/graphql + /api/auth/login 路由可用 | P2 启动 | ⏳ 待 ai01 |
| teacher-bff core-edu 扩展ai03 P3 | classExams/classHomework/studentGrades query 可用 | P3 启动 | ⏳ 待 ai03 P3 |
| teacher-bff content/data-ana 扩展ai03 P4 | knowledgeGraph/studentAnalytics query 可用 | P4 启动 | ⏳ 待 ai03 P4 |
| push-gateway WebSocket :8081/wsai02 P5 | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
| ai 服务 gRPCai12 P5 | generateQuestion/generateLessonPlan 可调 | P5 启动 | ⏳ 待 ai12 P5 |
| iam refresh cookie 端点ai06 P6 | POST /iam/auth/refresh 返回 httpOnly cookie | P6 启动 | ⏳ 待 ai06 P6 |
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
| --------------------------------------------------------- | -------------------------------------------------- | -------- | ---------------------- |
| packages 骨架ai13 自建) | ui-tokens/ui-components/hooks 可 import | P2 启动 | ✅ 已就绪(批次 0.15 |
| teacher-bff GraphQL schemaai03 + coord 仲裁 ISSUE-037 | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
| api-gateway HTTP :8080ai01 | /api/v1/teacher/graphql + /api/auth/login 路由可用 | P2 启动 | ⏳ 待 ai01 |
| teacher-bff core-edu 扩展ai03 P3 | classExams/classHomework/studentGrades query 可用 | P3 启动 | ⏳ 待 ai03 P3 |
| teacher-bff content/data-ana 扩展ai03 P4 | knowledgeGraph/studentAnalytics query 可用 | P4 启动 | ⏳ 待 ai03 P4 |
| push-gateway WebSocket :8081/wsai02 P5 | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
| ai 服务 gRPCai12 P5 | generateQuestion/generateLessonPlan 可调 | P5 启动 | ⏳ 待 ai12 P5 |
| iam refresh cookie 端点ai06 P6 | POST /iam/auth/refresh 返回 httpOnly cookie | P6 启动 | ⏳ 待 ai06 P6 |
### §4.2 我的就绪信号(供下游消费)
| 信号 | 就绪标志 | 消费方 |
| ------------------------------- | -------------------------------------- | ---------------------------------- |
| teacher-portal dev server :4000 | next dev -p 4000 可访问 | 无(最前端) |
| MF Shell 可加载 | 首页渲染 AppShell + 导航 | student/parent/admin RemoteP3+ |
| 登录流程可用 | POST /api/auth/login → JWT → Dashboard | 无(最前端) |
| GraphQL 查询可执行 | currentUser/myClasses 返回数据 | 无(最前端) |
| parent-portal Remote 接入点 | MF remotes 配置就绪 | parent-portalai15 P4 |
| 信号 | 就绪标志 | 消费方 | P2 状态 |
| ------------------------------- | ----------------------------------------------------------------------- | ---------------------------------- | --------------------- |
| teacher-portal dev server :4000 | next dev -p 4000 可访问 | 无(最前端) | ✅ 已就绪 |
| MF Shell 可加载 | 首页渲染 AppShell + 导航 | student/parent/admin RemoteP3+ | ✅ 已就绪 |
| 登录流程可用 | POST /api/auth/login → JWT → Dashboard | 无(最前端) | ✅ 已就绪MSW mock |
| GraphQL 查询可执行 | DashboardQuery/ClassesQuery/MeQuery/ViewportsQuery/ClassQuery 返回数据 | 无(最前端) | ✅ 已就绪MSW mock |
| P3 扩展 Query 可执行 | ClassStudents/ClassExams/ClassHomework/ExamGrades + UpdateUser Mutation | 无(最前端) | ✅ 已就绪MSW mock |
| parent-portal Remote 接入点 | MF remotes 配置就绪 | parent-portalai15 P4 | ⏳ P4 启用 |
| /api/health + /api/ready | k8s livenessProbe + readinessProbe 端点 | infra部署 | ✅ 已就绪 |
### §4.3 P2 交付物完成清单
- ✅ packages 骨架ui-tokens/ui-components/hooks批次 0.15
- ✅ NextFederationPlugin 配置exposes AppShell + GraphQLProvider + hooks + UI 组件shared react/urql/graphql/@edu/* singleton
- ✅ GraphQLProvider + urql client 单例Shell 暴露Remote 复用§2.17 方案 A
- ✅ AppShellLayout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层 + GraphQL ViewportsQuery + MeQuery + usePermission
- ✅ 登录页POST /api/auth/login → JWT 存 localStorageF12
- ✅ 权限上下文usePermission + buildPermissionContext权限点 `<RESOURCE>_<ACTION>` F7
- ✅ Dashboard 框架GraphQL DashboardQuery + 统计卡片)
- ✅ 班级列表页GraphQL ClassesQuery
- ✅ 学生列表页GraphQL ClassStudentsQueryP3 扩展 MSW mock
- ✅ 个人设置页GraphQL MeQuery 只读展示)
- ✅ 考试/作业/成绩页面GraphQL ClassExams/ClassHomework/ExamGradesP3 扩展 MSW mock
- ✅ MSW mock 层handlers + fixtures + browser/server + initMocks + providers 集成 + mockServiceWorker.js
- ✅ /api/health + /api/ready 路由liveness + readiness
- ✅ Dockerfile 端口 3000→4000 + 健康检查路径更新
- ✅ .env.example 添加 teacher-portal 变量NEXT_PUBLIC_TEACHER_BFF_GRAPHQL_URL / NEXT_PUBLIC_API_MOCKING / NEXT_PUBLIC_MF_ENABLED / API_GATEWAY_URL
- ✅ 质量校验通过lint + typecheck 零错误)
- ✅ arch.db 已更新arch:scanTS 14 模块 / 343 符号 / Proto 138 契约)
---

View File

@@ -466,29 +466,41 @@
### 2.12 teacher-portal微前端宿主P1 测试页)
| 场景 | 技术/规则 |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
| P1 测试 JWT | 开发工具生成 HS256 tokenP2 起由 IAM 签发 RS256 |
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
| P2 路由组 + AppShell | `app/(app)/layout.tsx` 用 AppShell 包裹受保护页;`/login` 与 `/` 不套壳 |
| P2 真实 JWT | 登录后 token 存 localStorage`authHeaders()` 读 `Bearer ${getToken()}` |
| P2 视口驱动侧边栏 | AppShell fetch `/teacher/viewports` 渲染左侧导航active 路由高亮 |
| P2 根路径重定向 | `app/page.tsx` 客户端组件 `router.replace(isAuthenticated() ? '/dashboard' : '/login')` |
| fetch headers 类型 | `authHeaders(): Record<string, string>` 显式标注,避免 `{}` 与 `HeadersInit` 不兼容 |
| P2 MF Shell+Remote 架构 | teacher-portal:**4000** 为 Shellstudent:**4001**/parent:**4002**/admin:**4003** 为 Remote004 §1.2 + coord final §F1MF 暴露 **AppShell 整体**coord final §F10各 Remote 自行决定内部布局 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error:{code,message,details?,traceId}}` 解析 + 错误码前缀路由 i18n + trace_id 透传;**P2 起对接 GraphQL**urql/apollocoord final §F9/B1 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error}` 解析 + 错误码前缀路由 i18n替代页面级 `authHeaders()+fetch` 重复 |
| P2 设计令牌三层模型 | primitive原始色板→ semantic-light/dark语义→ tailwind-theme`@theme inline` 暴露 `bg-*`ESLint `no-restricted-syntax` 禁 `#hex` + `design-tokens/no-hardcoded-fonts` 禁字面量字体§3.10 |
| P2 4 端错误码前缀对齐 | **BFF_TEACHER\_=teacher-bff / BFF_STUDENT\_=student-bff / BFF_PARENT\_=parent-bff**coord final §B5统一 `BFF_XXX_` 风格admin-bff 待 P6 定;前端按前缀路由 i18n key |
| P2 4 端错误码前缀对齐 | TP_=teacher-bff / SP_=student-bff / PP_=parent-bff / AP_=admin-bff前端按前缀路由 i18n key |
| P2 i18n key 命名 | **统一 `error.<service>.<code_snake>`**(如 error.iam.invalid_credentials、error.bffTeacher.validation_errorcoord final §F4翻译文件按域分 namespace |
| P2 权限点命名风格 | **统一 `<RESOURCE>_<ACTION>`**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ数据范围用后缀 `_OWN`/`_CHILD`(如 GRADE_READ_CHILDcoord final §F7 |
| P2 Token 存储 | **P2 用 localStorage**accessToken + refreshTokencoord final §F12httpOnly Cookie + CSRF Token **P6 单独评估**非分阶段迁移P2 直接采用最终方案的当前阶段形态) |
| P2 packages 归属 | **ai13 维护** packages/ui-tokens、ui-components、hookscoord final §F8coord 仅维护 shared-ts、contractsai13 需建立这 3 个共享包 |
| P2 GraphQL 客户端 | **P2 起前端用 urql 或 apollo**coord final §F9对接 teacher-bff GraphQL Yoga + DataLoader废弃 REST fetch 模式ApiClient 适配 GraphQL 操作类型query/mutation/subscription |
| P2 视口端点对齐 | **GET /iam/permissions/effective**(非 /iam/effective-permissionscoord final §F6/I8+ GET /iam/viewports + GET /iam/rolesparent 子女切换 POST /parent/children/:childId/select |
| 场景 | 技术/规则 |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
| P1 测试 JWT | 开发工具生成 HS256 tokenP2 起由 IAM 签发 RS256 |
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
| P2 路由组 + AppShell | `app/(app)/layout.tsx` 用 AppShell 包裹受保护页;`/login` 与 `/` 不套壳 |
| P2 真实 JWT | 登录后 token 存 localStorage`authHeaders()` 读 `Bearer ${getToken()}` |
| P2 视口驱动侧边栏 | AppShell fetch `/teacher/viewports` 渲染左侧导航active 路由高亮 |
| P2 根路径重定向 | `app/page.tsx` 客户端组件 `router.replace(isAuthenticated() ? '/dashboard' : '/login')` |
| fetch headers 类型 | `authHeaders(): Record<string, string>` 显式标注,避免 `{}` 与 `HeadersInit` 不兼容 |
| P2 MF Shell+Remote 架构 | teacher-portal:**4000** 为 Shellstudent:**4001**/parent:**4002**/admin:**4003** 为 Remote004 §1.2 + coord final §F1MF 暴露 **AppShell 整体**coord final §F10各 Remote 自行决定内部布局 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error:{code,message,details?,traceId}}` 解析 + 错误码前缀路由 i18n + trace_id 透传;**P2 起对接 GraphQL**urql/apollocoord final §F9/B1 |
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error}` 解析 + 错误码前缀路由 i18n替代页面级 `authHeaders()+fetch` 重复 |
| P2 设计令牌三层模型 | primitive原始色板→ semantic-light/dark语义→ tailwind-theme`@theme inline` 暴露 `bg-*`ESLint `no-restricted-syntax` 禁 `#hex` + `design-tokens/no-hardcoded-fonts` 禁字面量字体§3.10 |
| P2 4 端错误码前缀对齐 | **BFF_TEACHER\_=teacher-bff / BFF_STUDENT\_=student-bff / BFF_PARENT\_=parent-bff**coord final §B5统一 `BFF_XXX_` 风格admin-bff 待 P6 定;前端按前缀路由 i18n key |
| P2 4 端错误码前缀对齐 | TP_=teacher-bff / SP_=student-bff / PP_=parent-bff / AP_=admin-bff前端按前缀路由 i18n key |
| P2 i18n key 命名 | **统一 `error.<service>.<code_snake>`**(如 error.iam.invalid_credentials、error.bffTeacher.validation_errorcoord final §F4翻译文件按域分 namespace |
| P2 权限点命名风格 | **统一 `<RESOURCE>_<ACTION>`**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ数据范围用后缀 `_OWN`/`_CHILD`(如 GRADE_READ_CHILDcoord final §F7 |
| P2 Token 存储 | **P2 用 localStorage**accessToken + refreshTokencoord final §F12httpOnly Cookie + CSRF Token **P6 单独评估**非分阶段迁移P2 直接采用最终方案的当前阶段形态) |
| P2 packages 归属 | **ai13 维护** packages/ui-tokens、ui-components、hookscoord final §F8coord 仅维护 shared-ts、contractsai13 需建立这 3 个共享包 |
| P2 GraphQL 客户端 | **P2 起前端用 urql 或 apollo**coord final §F9对接 teacher-bff GraphQL Yoga + DataLoader废弃 REST fetch 模式ApiClient 适配 GraphQL 操作类型query/mutation/subscription |
| P2 视口端点对齐 | **GET /iam/permissions/effective**(非 /iam/effective-permissionscoord final §F6/I8+ GET /iam/viewports + GET /iam/rolesparent 子女切换 POST /parent/children/:childId/select |
| P2 MSW mock 层 | `src/mocks/` 含 handlers.ts拦截 /api/auth/login + /api/v1/teacher/graphql+ fixtures/*.ts + browser.tssetupWorker+ server.tssetupServer+ index.tsinitMocks 按 NEXT_PUBLIC_API_MOCKING=enabled 启用) |
| P2 MSW worker 脚本生成 | `npx msw init public/` 生成 mockServiceWorker.js 静态文件package.json 自动添加 `msw.workerDirectory` 配置;必须在 teacher-portal 目录下执行msw 在 devDependencies |
| P2 MSW providers 集成 | GraphQLProvider 内 `initMocks()` ready 前返回 null 避免 GraphQL 请求未拦截server.ts `onUnhandledRequest: "bypass"` 放行非 mock 请求 |
| P2 urql useQuery 类型 | urql useQuery 返回 data 类型推断不足map 回调参数隐式 any需 `import type { XxxItem } from "@/lib/graphql"` + `(data.xxx as XxxItem[]).map(...)` 显式断言 |
| P2 Suspense + useSearchParams | Next.js App Router 中 useSearchParams 必须包裹 `<Suspense>`否则构建报错students/exams/homework/grades 页面均用 `Suspense fallback={<Loading/>}` 包裹 StudentsContent |
| P2 DataScope 类型对齐 | graphql.ts 的 DataScope 必须与 hooks/types.ts 的 DataScope 完全一致(`"SELF" \| "CLASS" \| "GRADE" \| "SCHOOL" \| "DISTRICT" \| "ALL"`ARB-001 §1.2 schema否则 buildPermissionContext 返回类型不匹配 |
| P2 TS4114 override | tsconfig.base.json `noImplicitOverride: true` 下React.Component 子类的 state/componentDidCatch/render 必须加 `override` 修饰符 |
| P2 cn.ts null vs undefined | `match ? match[1] : null` 在 noUncheckedIndexedAccess 下返回 `string \| undefined`,与 `string \| null` 不兼容;改用 `match?.[1] ?? null` |
| P2 AppShell GraphQL 化 | AppShell 从 REST `/teacher/viewports` 改为 GraphQL `ViewportsQuery` + `MeQuery`;权限上下文从 localStorage 读取 + usePermission 注入;视口按 requiredPermission 过滤 |
| P2 健康检查路由 | `app/api/health/route.ts`liveness+ `app/api/ready/route.ts`readiness均 `export const dynamic = "force-dynamic"`,返回 NextResponse.jsonDockerfile HEALTHCHECK 指向 :4000/api/health |
| P2 Dockerfile 端口对齐 | teacher-portal 端口 3000→4000004 §1.2Dockerfile `ENV PORT=4000` + `EXPOSE 4000` + `CMD next start -p 4000` + HEALTHCHECK `http://localhost:4000/api/health` |
| P2 MSW GraphQL 路由 | handlers.ts `http.post("*/api/v1/teacher/graphql")` 按 operationName switch 路由;未传 operationName 时从 query 文本正则提取 `/(?:query\|mutation)\s+(\w+)/` 兜底 |
### 2.13 parent-portal家长端微前端 RemoteP4
@@ -573,25 +585,25 @@
> 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/redis mock 对象Vitest 覆盖率 ≥ 80% |
### 2.15 student-portal学生端微前端 RemoteP3