chore(teacher-portal): merge teacher-portal full implementation into main
This commit is contained in:
@@ -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