# teacher-portal 长远架构补全 > AI:ai07(4 端合并设计,承接 ai13 审核 §3 长远遗漏补全) > 阶段:阶段 2 补充交付物(长远架构) > 日期:2026-07-09 > 关联: > > - [02-architecture-design.md](./02-architecture-design.md)(ai13 修正版,阶段 2 主文档) > - [00-ai13-audit-report.md](./00-ai13-audit-report.md) §3 长远遗漏分析 > - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/§3.1/§5.4 > - [project_rules](../../../.trae/rules/project_rules.md) §3.10/§4 > > 本文档承接 ai13 在 [00-ai13-audit-report.md](./00-ai13-audit-report.md) §3 列出的 12 项长远遗漏,逐一补全。原则:**架构不堵死未来演进路径**,当前不实现的也要预留接入点。 --- ## 1. 演进与迁移策略(补 ai13 §3.1 / §3.11) ### 1.1 阶段能力累积矩阵 | 阶段 | teacher-portal 必备能力 | 依赖上游 | 退出标准 | | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------------------------------------------------------------- | | P2 | 登录 + AppShell + 视口导航 + classes CRUD + GraphQL client(urql,F9 裁决 P2 起) + 设计令牌三层 + ErrorBoundary + /api/health&ready | iam、teacher-bff、classes | 教师登录看到按角色的侧边栏 + 班级 CRUD | | P3 | 考试/作业/成绩页面 + MF Shell 配置就绪(暂无 Remote)+ 乐观更新 + 多 Tab 会话同步 | core-edu(exams/homework/grades) | 教师创建考试→学生(student-portal Remote)作答→教师批改全链路 | | P4 | 知识图谱页 + 学情分析页 + parent-portal Remote 接入 | content、data-ana、parent-bff | 教师查看知识图谱 + 学情宽表 5s 返回 | | P5 | WebSocket 推送 + AI 辅助出题 SSE + 通知中心 | msg、push-gateway、ai | 全校广播实时到达 + AI 流式出题 | | P6 | Sentry + RUM + OTel browser + A11y 审计 + 性能优化 + localStorage→httpOnly Cookie 迁移 | 全链路可观测 | 99.9% 可用 + WCAG 2.2 AA | ### 1.2 后端 gRPC 启用对前端的影响(补 ai13 §3.1) 004 §4.2 裁决各服务分阶段启用 gRPC(P3 iam+core-edu、P4 content+data-ana、P5 ai)。**前端不直接消费 gRPC**,影响只在 BFF 层。F9 裁决 BFF P2 起即用 GraphQL(非"P4 起 REST→GraphQL 迁移"),前端 P2 起即引入 urql client。 | 后端切换 | BFF 动作 | 前端动作 | | ---------------------------- | --------------------------------------------- | -------------------------------------------- | | 服务 HTTP→gRPC | BFF 改为 gRPC client 调用服务 | 无感(BFF 仍对前端暴露 GraphQL) | | BFF GraphQL(P2 起,已裁决) | teacher-bff P2 起即 GraphQL Yoga + DataLoader | 前端 P2 起即引入 urql client,统一走 GraphQL | **前端契约**:始终消费 `ActionState` 信封(004 §11.5),与传输协议无关。BFF GraphQL schema 由 ai03 维护,coord 在批次 1 启动前仲裁第一版(ISSUE-037)。 ### 1.3 单体 → MF 迁移路径(绞杀者模式,补 ai13 §3.11) ``` 当前态 过渡态 目标态 ┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ teacher-portal │ │ teacher-portal │ │ teacher-portal │ │ 单体 Next.js │ ──> │ Shell + MF 配置 │ ──> │ Shell │ │ 5 个页面 │ │ (暂无 Remote) │ │ + student Remote │ │ │ │ + student-portal │ │ + parent Remote │ │ │ │ 作为首个 Remote │ │ + admin Remote │ └─────────────┘ └──────────────────┘ └──────────────────┘ ``` | 步骤 | 动作 | 回滚 | 验证 | | ---- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | ------------------------- | | 1 | 升级 Next.js 14→15 + react 18→19 | git revert | classes CRUD 端到端 | | 2 | 抽取 ui-tokens/ui-components/hooks 到 packages | 引用回退到本地 | 视觉无变化 | | 3 | teacher-portal 加 NextFederationPlugin(Shell,exposes AppShell,暂无 Remote) | 删除 plugin 配置 | 现有页面不受影响 | | 4 | student-portal(P3)接入为首个 Remote | feature flag `NEXT_PUBLIC_MF_ENABLED=false` 回退单体路由 | 学生作答页在 Shell 内渲染 | | 5 | parent/admin 逐步接入 | 同上 | 各端独立可访问 | **关键**:每步用 feature flag 控制,失败可一键回退到单体路由;保留旧路由文件直到 MF 稳定(2 个版本周期)。 ### 1.4 GraphQL 客户端架构(P2 起最终方案,补 ai13 §3.1) > 本节原为"REST → GraphQL 切换策略"(过渡方案描述)。按总裁裁决 F9(P2 起 BFF 用 GraphQL)+ §4.4(删除过渡方案本身,保留设计决策记录)+ ISSUE-036-ai13 回写义务,已替换为最终方案描述。 **裁决依据**:coord-final-decisions F9 + 总裁裁决 §2.17(MF AppShell GraphQL client 单例,采纳方案 A:Shell 暴露 GraphQLProvider)。 #### 1.4.1 client 选型与单例 | 维度 | 方案 | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | client 库 | urql(轻量、可插拔 exchange、TS 友好;备选 apollo,但 urql bundle 更小,符合 §3.1 性能预算) | | 单例归属 | teacher-portal Shell 暴露 `GraphQLProvider`(@edu/mf-contracts 登记),student/parent/admin Remote 复用 Shell 单例(总裁 §2.17 方案 A) | | MF shared 配置 | `react` / `urql` / `graphql` 设为 MF singleton,避免 Remote 各自初始化导致多实例 + 缓存不一致 | | 缓存交换 | urql `cacheExchange`(文档缓存 +normalized 可选);MF 下 Shell 单例 cache 跨 Remote 共享 | #### 1.4.2 client 初始化(P2 起) ```typescript // teacher-portal Shell:packages/hooks 提供 useGraphQLClient,Shell 注入 GraphQLProvider import { createClient, Client } from "urql"; import { cacheExchange, fetchExchange } from "urql"; const client: Client = createClient({ url: process.env.NEXT_PUBLIC_TEACHER_BFF_GRAPHQL_URL!, // teacher-bff /graphql endpoint fetchOptions: () => { const token = readAuthToken(); // useAuth 注入 const traceId = readTraceId(); return { headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), "X-Trace-Id": traceId, }, }; }, exchanges: [cacheExchange, fetchExchange], }); ``` - Shell 在 `app/providers.tsx` 包裹 ``,所有 Remote 通过 Shell 暴露的 `useGraphQLClient()` 取同一 client - token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链),见 §2.7 #### 1.4.3 错误处理与 ActionState 契约 | 场景 | 处理 | | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | | GraphQL 错误(errors[]) | 解析 `extensions.code`(BFF_TEACHER_* 前缀,G14 裁决),映射到 i18n key `error.teacher_bff.`(F11) | | 网络错误 | 重试 1 次 → 提示网络异常 → 记录 trace_id | | 401 | useAuth 静默 refresh 一次 → 重试;失败跳登录(见 §2.7) | | 部分失败(data + errors 共存) | 渲染 data 可用部分 + errors 区域提示,不整体失败 | - BFF 始终返回 `ActionState` 信封(004 §11.5),前端统一走 `normalizeActionState()` 工具(packages/hooks) #### 1.4.4 缓存策略 | 数据类型 | staleTime | 失效策略 | | -------------- | ----------------- | --------------------------------------------- | | 班级列表/详情 | 30s | mutation 后 invalidate(见 §10.2) | | 考试/作业/成绩 | 10s | mutation 后 invalidate + 乐观更新(见 §10.1) | | 用户权限/视口 | 0(always fetch) | 角色变更推送后强制 invalidate | | Dashboard 概览 | 60s | ISR + 客户端 stale-while-revalidate | - urql 默认 document cache 适合简单场景;复杂关联(如班级↔考试↔成绩)P3 起评估切 `@urql/exchange-graphcache` normalized cache - 不引入 TanStack Query 做 GraphQL 缓存(urql 自带缓存足够,避免双缓存层);TanStack Query 仅用于非 GraphQL 场景(文件上传/SSE,见 useApi) #### 1.4.5 与 ISSUE-037/038/039 的关联 | ISSUE | 关联点 | 状态 | | -------------- | ----------------------------------------------------------------------- | ------------- | | ISSUE-037-ai13 | teacher-bff GraphQL schema 第一版由 ai03 维护,coord 批次 1 启动前仲裁 | 待 coord 仲裁 | | ISSUE-038-ai13 | MF AppShell 暴露 GraphQL client 单例(总裁 §2.17 方案 A 已采纳) | 已裁决 | | ISSUE-039-ai13 | packages(含 hooks 中的 GraphQL client 工具)建立时机:批次 0.15 已建立 | 已执行 | #### 1.4.6 设计决策记录(原过渡方案追溯) > 本节为决策追溯,非过渡方案描述。 - **决策**:P2 起 BFF 即 GraphQL,前端即 urql,不经过 REST 中间态 - **裁决来源**:coord F9 + 总裁 §0.3"不分阶段"原则 + §2.17 MF GraphQL client 单例 - **原过渡方案**(已废弃):曾考虑 P2-P4 REST + P5+ 评估迁移 GraphQL + feature flag 灰度。该方案违反"不分阶段"原则,已删除 - **影响范围**:02-architecture-design.md(已回写)+ 03-long-term-architecture.md §1.4(本次回写) --- ## 2. 安全架构(补 ai13 §3.2) ### 2.1 Token 存储演进 | 阶段 | access token | refresh token | 风险等级 | | ------- | -------------------------------------- | ------------------------------------------ | -------------- | | P2 过渡 | localStorage(Zustand 持久化) | localStorage | 高(XSS 可读) | | P6 终态 | 内存(Zustand sessionSlice,刷新丢失) | httpOnly + Secure + SameSite=Strict cookie | 极低 | **切换条件**:iam 支持 refresh cookie 端点 + CSRF 防护就绪后迁移。 ### 2.2 CSP(内容安全策略) MF 下 CSP 必须显式允许 remote origin。Next.js 通过 `next.config.js` headers 或 middleware.ts 注入: ``` Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' http://localhost:4001 http://localhost:4002 http://localhost:4003; connect-src 'self' http://localhost:8080 ws://localhost:8081; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; object-src 'none'; ``` - 生产环境 remote origin 改对应域名,逐步移除 `'unsafe-inline'`(用 nonce/hash 替代) - 配套头:`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`(防点击劫持)、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy: geolocation=(), microphone=(), camera=()` ### 2.3 MF Remote 完整性校验 Remote `remoteEntry.js` 由 Shell 动态加载,存在"被篡改 remote 注入恶意代码到 Shell 上下文"风险(MF 共享 singleton 使 remote 可访问 Shell 全局状态)。 | 缓解措施 | 说明 | | ---------------------------- | ----------------------------------------------------------------------------------------------- | | Subresource Integrity(SRI) | remoteEntry.js 计算 SHA-384,Shell 加载时校验(MF 2.0 支持 `integrity` 字段) | | Remote origin 白名单 | CSP `script-src` 限定已知 remote 域名 | | 最小暴露面 | Shell `exposes` 仅暴露必要组件,不暴露整个 store/ApiClient 内部 | | Remote 隔离 | Remote 不直接持有 Shell 的 token,统一通过 `useApi()` Hook 取 ApiClient(token 注入在 Hook 层) | | CDN 静态资源 SRI | next/image、next/font 产物带 integrity 属性 | ### 2.4 CSRF 防护(切换 cookie 后) - SameSite=Strict cookie 阻断跨站携带 - 对 state-changing mutation(POST/PUT/DELETE)额外校验 `X-CSRF-Token` header(双提交 cookie 模式) - Next.js middleware.ts 校验 origin header 与允许域名匹配 ### 2.5 多 Tab 会话同步(关键遗漏补齐) 教师多 Tab 操作时,登出/角色变更须跨 Tab 同步,否则出现"Tab A 登出但 Tab B 仍可用"的越权窗口。 | 事件 | 同步机制 | 动作 | | ---------- | ------------------------------------------------------- | ----------------------------------------- | | 登出 | `BroadcastChannel('edu-session')` postMessage('logout') | 所有 Tab 清 session + 跳登录 | | Token 刷新 | BroadcastChannel('token-refreshed') | 其他 Tab 更新内存 token | | 角色变更 | WebSocket 推送(P5)+ BroadcastChannel | 所有 Tab invalidate 权限缓存 + 重渲染视口 | | 会话过期 | BroadcastChannel('session-expired') | 跳登录 + 提示 | 降级:不支持 BroadcastChannel 的浏览器走 `localStorage` `storage` 事件。 ### 2.6 XSS / 注入防护 - `dangerouslySetInnerHTML` 全局禁用(project_rules §4),AI 流式内容先用 DOMPurify 清洗再渲染 - Tiptap 内容序列化为 JSON 存储,渲染走 ProseMirror schema(白名单节点),禁止 raw HTML - 用户输入展示统一走 React 转义 ### 2.7 鉴权边界与权限失效 | 场景 | 前端动作 | | ------------------ | ----------------------------------------------------------------------------------------------------------------- | | access token 过期 | 401 → 静默 refresh 一次 → 重试;失败跳登录 | | refresh token 过期 | 跳登录 + 提示"会话已过期" | | 角色被撤销 | WebSocket 推送 `RoleRevoked` → invalidate 权限 → 重新拉取 → 视口收敛;权限不足以访问当前路由 → 重定向到 dashboard | | 强制下线 | iam 调 push-gateway 推 `ForceLogout` → 前端清 session | ### 2.8 敏感数据脱敏(补 ai13 §3.2) - 日志/错误上报前过滤:token、password、refreshToken、PII(身份证/手机号)字段置 `[REDACTED]` - Sentry 配置 `beforeSend` 钩子剥离 PII;Session Replay 对敏感输入框 `mask` 所有文本 - 前端不持久化密码原文,登录后立即从内存清除 --- ## 3. 性能架构(补 ai13 §3.3) ### 3.1 性能预算 | 指标 | 目标 | 度量 | CI 门禁 | | ----------------------- | ----------------- | ----------------------------- | --------------- | | LCP | < 2.5s(P75) | Web Vitals RUM | lighthouse CI | | INP | < 200ms(P75) | Web Vitals RUM(CWV 2024) | lighthouse CI | | CLS | < 0.1 | Web Vitals RUM | lighthouse CI | | TTFB | < 800ms | Web Vitals RUM | — | | 长任务(>50ms) | < 5 次/页 | PerformanceObserver Long Task | — | | Shell JS bundle(gzip) | < 150KB | @next/bundle-analyzer | size-limit fail | | 每个 Remote JS(gzip) | < 80KB | bundle analyzer | size-limit fail | | CSS(gzip) | < 50KB | bundle analyzer | — | | shared deps 重复 | 0(MF singleton) | MF manifest 校验 | CI 检查 | | 首屏请求数 | < 20 | lighthouse CI | — | - CI 集成 `@next/bundle-analyzer` + size-limit,超预算 fail build - 每版本 lighthouse CI 报告归档,回归监控 ### 3.2 渲染模式决策矩阵 | 页面类型 | 模式 | 理由 | | -------------------------- | ------------------------------- | ---------------------------- | | 登录页 | SSR | SEO + 首屏快 | | Dashboard 概览 | SSR + Streaming | 首屏骨架 + Suspense 流式填充 | | 列表/表格(classes/exams) | CSR + TanStack Query | 强交互、筛选、分页 | | 详情页(成绩/学情) | ISR(revalidate 60s)+ CSR 交互 | 数据准实时,CDN 缓存降负载 | | AI 出题(SSE) | CSR | 长连接流式 | | 知识图谱可视化 | CSR | 交互重 | | 管理端配置 | CSR | 纯交互 | | 考试作答(student) | CSR | 防刷新丢答 + 离线缓存 | ### 3.3 Next.js 缓存层级(App Router) | 缓存层 | 用途 | 失效策略 | | ------------------------- | -------------------- | --------------------- | | Request Memoization | 单次请求内去重 fetch | 请求结束即失效 | | Data Cache(fetch cache) | 跨请求 fetch 结果 | revalidate / tag 失效 | | Full Route Cache | 静态路由产物 | 按路由 revalidate | | Router Cache(客户端) | RSC payload + 预取 | staleTime 30s | **约定**:BFF 数据 `fetch(..., { next: { revalidate: 30, tags: ['classes'] } })`;mutation 后 `revalidateTag('classes')`。 ### 3.4 Server / Client Component 边界 - **Server Component**:数据获取、静态展示、设计令牌注入、i18n Provider - **Client Component**:交互(onClick/state)、TanStack Query、Zustand、Tiptap、图表 - 边界标记:`'use client'` 仅在交互根节点声明,子树尽量保持 Server Component ### 3.5 字体加载策略(补 ai13 §3.3) | 策略 | 实现 | | -------- | ------------------------------------------------------------------------ | | 加载方式 | `next/font/google`(self-host,无第三方请求) | | 显示策略 | `font-display: swap`(FOUT,优先文本可见性,避免 FOIT 白屏) | | preload | 首屏字体 `preload`,次屏字体 lazy | | 子集化 | Inter/Fraunces 按中文字符子集(next/font 自动 latin,中文需手动 subset) | | 变量暴露 | `var(--font-family-sans/serif/mono)`,禁止字面量(project_rules §3.10) | ### 3.6 图片优化(补 ai13 §3.3) - 统一 `next/image`:自动 AVIF/WebP 转换 + 响应式 `sizes` + lazy load + blur placeholder - 知识图谱节点图、用户头像走 `next/image`;远程图配 `remotePatterns` 白名单 - 图表截图导出走 `canvas.toBlob` + CDN,不走 base64 内联 ### 3.7 路由级代码分割与长任务优化(补 ai13 §3.3) - App Router 默认按路由分割;重型组件(Tiptap/recharts/知识图谱)用 `@next/dynamic` 懒加载 - React 18 concurrent:长列表用 `useDeferredValue`;路由切换用 `useTransition` 标记非紧急更新 - MF Remote 按 scope 动态加载,非当前 scope 不下载 remoteEntry.js ### 3.8 Prefetch / Preload 策略 | 资源 | 策略 | | ------------------ | ------------------------------------------- | | 当前路由数据 | TanStack Query 预取(`prefetchQuery`) | | 可能访问的 Remote | `requestIdleCallback` 预拉取 remoteEntry.js | | 关键字体/Logo | `` | | 下一页 RSC payload | Next.js Link `prefetch={true}`(默认) | --- ## 4. 可观测性深度(补 ai13 §3.4) ### 4.1 三支柱前端落地 | 支柱 | 工具 | 实现 | 阶段 | | ---- | ------------------------------------------ | ------------------------------------------- | ---- | | 日志 | pino-browser → Gateway → Loki | 结构化 JSON,含 trace_id/user_id/scope/path | P6 | | 指标 | Web Vitals → `/api/v1/admin/web-vitals` | LCP/INP/CLS/TTFB + Long Task | P6 | | 链路 | OTel browser SDK → OTLP collector → Jaeger | 自动埋点 fetch/XHR/document load | P6 | ### 4.2 trace_id 全链路透传(补 ai13 §3.4) ``` 浏览器入口生成 trace_id(crypto.randomUUID) → 写入 fetch header 'X-Request-Id' → Gateway 透传 → 服务间 gRPC metadata → 日志/metrics/trace 统一 trace_id → 响应头返回 'X-Request-Id' → 前端日志记录(便于用户报障定位) ``` - MF 透传:Shell 生成 trace_id 后通过共享 `useApi()` 注入所有 Remote 的请求 - Session 模型补 `traceId` 字段(ai13 已在 02 补全) ### 4.3 错误追踪与 Session Replay(补 ai13 §3.4) | 维度 | 方案 | | ---------------- | ------------------------------------------------------------------------- | | 平台 | Sentry(P6) | | DSN | 环境变量 `NEXT_PUBLIC_SENTRY_DSN` | | release tracking | 每次构建注入 commit SHA | | source maps | CI 上传到 Sentry(生产不暴露) | | 采样率 | errors 100%,traces 10%,replay 1% | | PII 过滤 | `beforeSend` 剥离 token/password/身份证;Session Replay 对敏感输入 `mask` | | Session Replay | Sentry Replay,敏感区域 `data-sentry-mask` | ### 4.4 用户行为埋点规范(补 ai13 §3.4) | 事件类别 | 示例 | 上报 | | -------- | -------------------------------------------- | -------------------------- | | 页面访问 | `page_view` {path, scope, role} | Gateway 聚合 | | 关键操作 | `exam_create`/`homework_grade`/`ai_generate` | Gateway 聚合 | | 错误 | `action_failed` {code, path} | Sentry + Gateway | | 性能 | `web_vitals` {lcp, inp, cls} | `/api/v1/admin/web-vitals` | - 埋点统一走 `packages/hooks/src/useAnalytics()`,禁止散落 `gtag`/`mixpanel` 直调 - 隐私合规:不采集身份证/成绩具体值,仅采集聚合指标 ### 4.5 前端日志聚合(补 ai13 §3.4) ``` pino-browser (batch 10 条/5s flush) → POST /api/v1/admin/logs → Gateway → Loki → Grafana 查询(按 trace_id/user_id/path 过滤) ``` - 级别:debug(dev only)/info/warn/error - 采样:info 10%,warn 100%,error 100% --- ## 5. a11y 实施细节(补 ai13 §3.5) ### 5.1 WCAG 2.2 AA 对照清单 | 准则 | 实施方式 | | -------------- | ----------------------------------------------------- | | 1.1 非文本内容 | 所有 `` 有 `alt`;装饰图 `alt=""` | | 1.3 适应性 | 语义化 HTML(`