chore(teacher-portal): merge teacher-portal full implementation into main
This commit is contained in:
@@ -131,26 +131,48 @@ gantt
|
||||
|
||||
### §4.1 我依赖的上游就绪标志
|
||||
|
||||
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
|
||||
| --------------------------------------------------------- | ------------------------------------------------- | -------- | ---------------------- |
|
||||
| packages 骨架(ai13 自建) | ui-tokens/ui-components/hooks 可 import | P2 启动 | ✅ 已就绪(批次 0.15) |
|
||||
| teacher-bff GraphQL schema(ai03 + coord 仲裁 ISSUE-037) | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
|
||||
| api-gateway HTTP :8080(ai01) | /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/ws(ai02 P5) | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
|
||||
| ai 服务 gRPC(ai12 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 schema(ai03 + coord 仲裁 ISSUE-037) | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
|
||||
| api-gateway HTTP :8080(ai01) | /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/ws(ai02 P5) | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
|
||||
| ai 服务 gRPC(ai12 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 Remote(P3+) |
|
||||
| 登录流程可用 | POST /api/auth/login → JWT → Dashboard | 无(最前端) |
|
||||
| GraphQL 查询可执行 | currentUser/myClasses 返回数据 | 无(最前端) |
|
||||
| parent-portal Remote 接入点 | MF remotes 配置就绪 | parent-portal(ai15 P4) |
|
||||
| 信号 | 就绪标志 | 消费方 | P2 状态 |
|
||||
| ------------------------------- | ----------------------------------------------------------------------- | ---------------------------------- | --------------------- |
|
||||
| teacher-portal dev server :4000 | next dev -p 4000 可访问 | 无(最前端) | ✅ 已就绪 |
|
||||
| MF Shell 可加载 | 首页渲染 AppShell + 导航 | student/parent/admin Remote(P3+) | ✅ 已就绪 |
|
||||
| 登录流程可用 | 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-portal(ai15 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)
|
||||
- ✅ AppShell(Layout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层 + GraphQL ViewportsQuery + MeQuery + usePermission)
|
||||
- ✅ 登录页(POST /api/auth/login → JWT 存 localStorage,F12)
|
||||
- ✅ 权限上下文(usePermission + buildPermissionContext,权限点 `<RESOURCE>_<ACTION>` F7)
|
||||
- ✅ Dashboard 框架(GraphQL DashboardQuery + 统计卡片)
|
||||
- ✅ 班级列表页(GraphQL ClassesQuery)
|
||||
- ✅ 学生列表页(GraphQL ClassStudentsQuery,P3 扩展 MSW mock)
|
||||
- ✅ 个人设置页(GraphQL MeQuery 只读展示)
|
||||
- ✅ 考试/作业/成绩页面(GraphQL ClassExams/ClassHomework/ExamGrades,P3 扩展 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:scan:TS 14 模块 / 343 符号 / Proto 138 契约)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 token,P2 起由 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** 为 Shell,student:**4001**/parent:**4002**/admin:**4003** 为 Remote(004 §1.2 + coord final §F1);MF 暴露 **AppShell 整体**(coord final §F10),各 Remote 自行决定内部布局 |
|
||||
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error:{code,message,details?,traceId}}` 解析 + 错误码前缀路由 i18n + trace_id 透传;**P2 起对接 GraphQL**(urql/apollo,coord 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_error,coord final §F4);翻译文件按域分 namespace |
|
||||
| P2 权限点命名风格 | **统一 `<RESOURCE>_<ACTION>`**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ),数据范围用后缀 `_OWN`/`_CHILD`(如 GRADE_READ_CHILD,coord final §F7) |
|
||||
| P2 Token 存储 | **P2 用 localStorage**(accessToken + refreshToken,coord final §F12);httpOnly Cookie + CSRF Token **P6 单独评估**(非分阶段迁移,P2 直接采用最终方案的当前阶段形态) |
|
||||
| P2 packages 归属 | **ai13 维护** packages/ui-tokens、ui-components、hooks(coord final §F8);coord 仅维护 shared-ts、contracts;ai13 需建立这 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-permissions,coord final §F6/I8)+ GET /iam/viewports + GET /iam/roles;parent 子女切换 POST /parent/children/:childId/select |
|
||||
| 场景 | 技术/规则 |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
|
||||
| API 调用 | `fetch(${API_BASE}/api/v1/classes)` + `Authorization: Bearer ${TEST_JWT}` |
|
||||
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 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** 为 Shell,student:**4001**/parent:**4002**/admin:**4003** 为 Remote(004 §1.2 + coord final §F1);MF 暴露 **AppShell 整体**(coord final §F10),各 Remote 自行决定内部布局 |
|
||||
| P2 统一 API 请求层 | ApiClient 封装 401 自动刷新 token 轮转 + ActionState `{success,data,error:{code,message,details?,traceId}}` 解析 + 错误码前缀路由 i18n + trace_id 透传;**P2 起对接 GraphQL**(urql/apollo,coord 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_error,coord final §F4);翻译文件按域分 namespace |
|
||||
| P2 权限点命名风格 | **统一 `<RESOURCE>_<ACTION>`**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ),数据范围用后缀 `_OWN`/`_CHILD`(如 GRADE_READ_CHILD,coord final §F7) |
|
||||
| P2 Token 存储 | **P2 用 localStorage**(accessToken + refreshToken,coord final §F12);httpOnly Cookie + CSRF Token **P6 单独评估**(非分阶段迁移,P2 直接采用最终方案的当前阶段形态) |
|
||||
| P2 packages 归属 | **ai13 维护** packages/ui-tokens、ui-components、hooks(coord final §F8);coord 仅维护 shared-ts、contracts;ai13 需建立这 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-permissions,coord final §F6/I8)+ GET /iam/viewports + GET /iam/roles;parent 子女切换 POST /parent/children/:childId/select |
|
||||
| P2 MSW mock 层 | `src/mocks/` 含 handlers.ts(拦截 /api/auth/login + /api/v1/teacher/graphql)+ fixtures/*.ts + browser.ts(setupWorker)+ server.ts(setupServer)+ index.ts(initMocks 按 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.json;Dockerfile HEALTHCHECK 指向 :4000/api/health |
|
||||
| P2 Dockerfile 端口对齐 | teacher-portal 端口 3000→4000(004 §1.2);Dockerfile `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(家长端微前端 Remote,P4)
|
||||
|
||||
@@ -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 fetch);DownstreamClient 抽象复用 shared-ts(coord §B8,3 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.7);DEV_MODE 放行(president §2.9 方案 D)|
|
||||
| P3 降级模式 | 方案 B(president §2.6):`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 |
|
||||
| P3 Redis 缓存 | 5-30s 短缓存(coord §B6),TTL ±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-streaming(chunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable) |
|
||||
| P5 DownstreamClient.callStream| gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream,需手动转换为 AsyncIterable;finally 块调用 `stream.destroy?.()` 清理 |
|
||||
| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayService(DI 注入),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 5000ms,errorThresholdPercentage 50,resetTimeout 30000,volumeThreshold 10,rollingCountTimeout 60000(coord §G11) |
|
||||
| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据(config/mock-data.ts),callStream 产出单个 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/logger,vi.stubGlobal 模拟 fetch,mockContext 工厂函数注入 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 fetch);DownstreamClient 抽象复用 shared-ts(coord §B8,3 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.7);DEV_MODE 放行(president §2.9 方案 D) |
|
||||
| P3 降级模式 | 方案 B(president §2.6):`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 |
|
||||
| P3 Redis 缓存 | 5-30s 短缓存(coord §B6),TTL ±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-streaming(chunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable) |
|
||||
| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream,需手动转换为 AsyncIterable;finally 块调用 `stream.destroy?.()` 清理 |
|
||||
| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayService(DI 注入),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 5000ms,errorThresholdPercentage 50,resetTimeout 30000,volumeThreshold 10,rollingCountTimeout 60000(coord §G11) |
|
||||
| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据(config/mock-data.ts),callStream 产出单个 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/logger,vi.stubGlobal 模拟 fetch,mockContext 工厂函数注入 downstream/redis mock 对象;Vitest 覆盖率 ≥ 80% |
|
||||
|
||||
### 2.15 student-portal(学生端微前端 Remote,P3)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user