Files
Edu/apps/teacher-portal/docs/03-long-term-architecture.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md)

2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration)

3.各服务 01/02 文档补全

4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens)

5.Proto 契约补全

6.004 架构影响地图更新

7.端口分配表

8.设计规格文档
2026-07-10 12:58:22 +08:00

50 KiB
Raw Blame History

teacher-portal 长远架构补全

AIai074 端合并设计,承接 ai13 审核 §3 长远遗漏补全) 阶段:阶段 2 补充交付物(长远架构) 日期2026-07-09 关联:

本文档承接 ai13 在 00-ai13-audit-report.md §3 列出的 12 项长远遗漏,逐一补全。原则:架构不堵死未来演进路径,当前不实现的也要预留接入点。


1. 演进与迁移策略(补 ai13 §3.1 / §3.11

1.1 阶段能力累积矩阵

阶段 teacher-portal 必备能力 依赖上游 退出标准
P2 登录 + AppShell + 视口导航 + classes CRUD + GraphQL clienturqlF9 裁决 P2 起) + 设计令牌三层 + ErrorBoundary + /api/health&ready iam、teacher-bff、classes 教师登录看到按角色的侧边栏 + 班级 CRUD
P3 考试/作业/成绩页面 + MF Shell 配置就绪(暂无 Remote+ 乐观更新 + 多 Tab 会话同步 core-eduexams/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 裁决各服务分阶段启用 gRPCP3 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 GraphQLP2 起,已裁决) 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 加 NextFederationPluginShellexposes AppShell暂无 Remote 删除 plugin 配置 现有页面不受影响
4 student-portalP3接入为首个 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 切换策略"(过渡方案描述)。按总裁裁决 F9P2 起 BFF 用 GraphQL+ §4.4(删除过渡方案本身,保留设计决策记录)+ ISSUE-036-ai13 回写义务,已替换为最终方案描述。

裁决依据coord-final-decisions F9 + 总裁裁决 §2.17MF AppShell GraphQL client 单例,采纳方案 AShell 暴露 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 起)

// teacher-portal Shellpackages/hooks 提供 useGraphQLClientShell 注入 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 包裹 <Provider value={client}>,所有 Remote 通过 Shell 暴露的 useGraphQLClient() 取同一 client
  • token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链),见 §2.7

1.4.3 错误处理与 ActionState 契约

场景 处理
GraphQL 错误errors[] 解析 extensions.codeBFF_TEACHER_* 前缀G14 裁决),映射到 i18n key error.teacher_bff.<code_snake>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
用户权限/视口 0always 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 过渡 localStorageZustand 持久化) 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: nosniffX-Frame-Options: DENY(防点击劫持)、Referrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=(), camera=()

2.3 MF Remote 完整性校验

Remote remoteEntry.js 由 Shell 动态加载,存在"被篡改 remote 注入恶意代码到 Shell 上下文"风险MF 共享 singleton 使 remote 可访问 Shell 全局状态)。

缓解措施 说明
Subresource IntegritySRI remoteEntry.js 计算 SHA-384Shell 加载时校验MF 2.0 支持 integrity 字段)
Remote origin 白名单 CSP script-src 限定已知 remote 域名
最小暴露面 Shell exposes 仅暴露必要组件,不暴露整个 store/ApiClient 内部
Remote 隔离 Remote 不直接持有 Shell 的 token统一通过 useApi() Hook 取 ApiClienttoken 注入在 Hook 层)
CDN 静态资源 SRI next/image、next/font 产物带 integrity 属性
  • SameSite=Strict cookie 阻断跨站携带
  • 对 state-changing mutationPOST/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 §4AI 流式内容先用 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 钩子剥离 PIISession Replay 对敏感输入框 mask 所有文本
  • 前端不持久化密码原文,登录后立即从内存清除

3. 性能架构(补 ai13 §3.3

3.1 性能预算

指标 目标 度量 CI 门禁
LCP < 2.5sP75 Web Vitals RUM lighthouse CI
INP < 200msP75 Web Vitals RUMCWV 2024 lighthouse CI
CLS < 0.1 Web Vitals RUM lighthouse CI
TTFB < 800ms Web Vitals RUM
长任务(>50ms < 5 次/页 PerformanceObserver Long Task
Shell JS bundlegzip < 150KB @next/bundle-analyzer size-limit fail
每个 Remote JSgzip < 80KB bundle analyzer size-limit fail
CSSgzip < 50KB bundle analyzer
shared deps 重复 0MF 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 强交互、筛选、分页
详情页(成绩/学情) ISRrevalidate 60s+ CSR 交互 数据准实时CDN 缓存降负载
AI 出题SSE CSR 长连接流式
知识图谱可视化 CSR 交互重
管理端配置 CSR 纯交互
考试作答student CSR 防刷新丢答 + 离线缓存

3.3 Next.js 缓存层级App Router

缓存层 用途 失效策略
Request Memoization 单次请求内去重 fetch 请求结束即失效
Data Cachefetch 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/googleself-host无第三方请求
显示策略 font-display: swapFOUT优先文本可见性避免 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 <link rel="preload">
下一页 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_idcrypto.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

维度 方案
平台 SentryP6
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 过滤)
  • 级别debugdev only/info/warn/error
  • 采样info 10%warn 100%error 100%

5. a11y 实施细节(补 ai13 §3.5

5.1 WCAG 2.2 AA 对照清单

准则 实施方式
1.1 非文本内容 所有 <img>alt;装饰图 alt=""
1.3 适应性 语义化 HTML<nav>/<main>/<aside>ARIA role 补充
1.4 可辨别性 对比度 ≥ 4.5:1设计令牌审计禁用纯色信息编码
2.1 键盘可达 全键盘导航;可见焦点(focus-visible);跳过链接
2.4 导航 标题层级正确;<title> 动态;面包屑
2.5 输入模态 触摸目标 ≥ 44px取消指针防误触
3.1 可读 lang 属性;缩写标注
3.2 可预测 一致导航;无意外跳转
3.3 输入辅助 错误标识 + aria-describedby + 修正建议
4.1 兼容 ARIA 正确使用;不依赖仅颜色

5.2 键盘导航

  • 跳过链接skip to main content首位
  • Tab 顺序与视觉顺序一致DOM 顺序即 tab 顺序)
  • Modal 打开时焦点陷阱(focus-trap),关闭后焦点返回触发元素
  • 快捷键:/ 聚焦搜索、g 后接字母跳转teacher/student/parent/admin

5.3 屏幕阅读器测试矩阵

读屏器 平台 阶段
NVDA Windows P6 审计
JAWS Windows P6 抽测
VoiceOver macOS/iOS P6 审计
TalkBack Android P6 抽测

5.4 自动化检测

  • eslint-plugin-jsx-a11yerror 级CI 强制)
  • @axe-core/playwright E2E 集成,每版本扫描关键页面
  • 设计令牌对比度审计脚本primitive 色板两两组合计算对比度)

6. i18n 实施细节(补 ai13 §3.6

维度 方案
next-intlApp Router 原生支持)
路由 /[locale]/teacher/...(前缀式,默认 zh
检测 middleware.tscookie NEXT_LOCALEAccept-Language → 默认 zh
加载 按路由分片动态加载 messagegetMessages + useTranslations
fallback zh 为 fallback locale缺 key 回退dev 环境缺 key console.warn
复数/性别 ICU MessageFormatnext-intl 内置):{count, plural, =0 {无} one {# 项} other {# 项}}
数字/日期 Intl.NumberFormat / Intl.DateTimeFormat(按 locale不用 dayjs 本地化)
错误码 i18n error.{{service}}.{{code_snake_case}}(见 02 §6.2
RTL 预留 布局用 logical propertiesmargin-inline/padding-inline),未来阿拉伯语只需切换 direction
文档术语 业务术语统一 glossarypackages/contracts/src/i18n-glossary.ts)防翻译漂移
翻译协作 i18n/locales/*.json 纳入 gitCI 校验 key 完整性zh/en key 集合一致);后续可接 Crowdin

BFF/服务返回 i18n key + 参数(不返回已翻译文案),前端统一翻译,避免多端文案不一致。


7. 部署拓扑(补 ai13 §3.7

7.1 部署演进

阶段 目标 方式
P1-P5 单机 Docker Compose docker compose up -d --build(每服务 Dockerfile 多阶段)
P6+ K8s 每服务 Deployment + HPA + Service + Ingress

7.2 MF 部署与 CDN

部署单元 策略 备注
teacher-portalShell 独立 滚动 Shell 变更频率最低
student/parent/adminRemote 各自独立 蓝绿/金丝雀 remoteEntry.js 不可变文件名 + CDN 长缓存
  • 部署顺序约束:破坏性 shared dep 升级时Remote 先升、Shell 后升Shell singleton 向下兼容旧 Remote
  • CDNremoteEntry.js 设 immutable 长缓存,版本化文件名 remoteEntry.{version}.js 解决缓存失效
  • 预拉取Shell 在 idle 时预拉取用户角色对应 Remote 的 remoteEntry.jsrequestIdleCallback
  • Edge middlewarelocale 检测、A/B 测试、灰度路由放 edgeVercel/Cloudflare Workers

7.3 多环境配置

环境 配置来源 说明
dev .env.local 本地开发
staging K8s ConfigMap + Secret 预发布
prod K8s Secret 生产
  • 前端变量必须 NEXT_PUBLIC_ 前缀project_rules §4服务端变量不加
  • 环境校验用 @t3-oss/env-nextjs + Zod启动时 fail-fast

7.4 健康检查端点

端点 用途 实现
GET /api/health Dockerfile HEALTHCHECK / liveness 返回 { status: 'ok', ts }
GET /api/ready K8s readinessProbe 检查 API_GATEWAY_URL 可达 + MF remoteEntry 可达Shell+ 内存 < 阈值

7.5 灰度发布与回滚

场景 策略
Remote 灰度 remoteEntry.js 多版本并存manifest 按用户分桶切换
回滚 remoteEntry.js 保留 N 版CDN 切上一版本;数据库无变更,无数据回滚风险
会话保持 回滚不强制登出,旧版本 Session 兼容(向前兼容设计)

8. 测试策略细节(补 ai13 §3.8

8.1 测试金字塔

层级 工具 覆盖目标 范围
单元 Vitest + @testing-library/react hooks ≥90% / utils ≥90% / 纯组件 ≥80% / pages ≥60% Hook、工具函数、纯组件
集成 Vitest + MSW 关键链路 ApiClient + TanStack Query + 权限流
契约 Vitest + @edu/mf-contracts zod 100% Shell 暴露 Shell↔Remote 契约
视觉回归 Playwright + Percy/Chromatic 关键页面 设计令牌、布局
E2E Playwright 4 端关键流 登录→操作→结果
性能 Lighthouse CI 每版本 LCP/INP/CLS 回归

8.2 MF 集成测试

  • Shell 单独测试mock Remote 为 stub 组件
  • Remote 单独测试mock Shell 暴露的 Hook
  • 契约测试:@edu/mf-contracts 携带 zod schemaCI 校验 Shell 暴露与 Remote 消费一致
  • 跨端 E2EPlaywright 启动 Shell + 真实 Remote覆盖"教师发作业→学生提交→家长查看"全链路

8.3 E2E 关键路径

路径 步骤
登录流 登录→侧边栏按角色渲染→dashboard
班级 CRUD 新建→列表→详情→编辑→删除
考试全链路 教师建考→发布→学生作答→教师批改→成绩发布→家长查看
AI 出题 选知识点→AI 流式生成→教师审核→入库
权限降级 角色变更→视口收敛→路由重定向

8.4 CI 稳定策略

  • E2E 并行分片Playwright shard
  • 重试 2 次flaky 容忍)
  • 截图归档失败用例
  • 独立测试 DB每次重置

9. 移动端策略(补 ai13 §3.10

9.1 响应式适配

适配 说明
teacher-portal 响应式(桌面优先) 教师主要用桌面,手机用于查看/通知
student-portal 移动优先 学生作答多用手机
parent-portal 移动优先 家长查看看手机
  • Tailwind 断点:sm 640 / md 768 / lg 1024 / xl 1280
  • AppShell 移动端折叠侧边栏为抽屉(汉堡菜单)
  • 触摸目标 ≥ 44pxWCAG 2.5.5

9.2 PWA / 离线(预留,补 ai13 §3.10

能力 预留点 触发时机
Service Worker sw.ts 预留next-pwa 接入 教师弱网批改需求
离线缓存 TanStack Query onlineManager + mutation 离线队列 弱网
可安装 manifest.json + installable 学校要求桌面图标
后台同步 Background Sync API 离线提交作业

当前不实现,但 ApiClient 的 mutation 设计为可排队(失败入队列,上线重放)。

9.3 原生 App远期补 ai13 §3.10

  • hooks/contracts 纯 TS 不耦合 DOM可被 React Native 复用
  • 若未来出 RN App复用 usePermission/useAuth/ApiClientUI 层重写
  • 不为此提前抽象,保持当前 Next.js 实现简洁YAGNI

10. 数据流与乐观更新(补 ai13 §3.12

10.1 乐观更新模式

const gradeMutation = useMutation({
  mutationFn: (input) => api.post("/api/v1/grades", input),
  onMutate: async (input) => {
    await queryClient.cancelQueries({
      queryKey: ["teacher", "grades", input.classId],
    });
    const previous = queryClient.getQueryData([
      "teacher",
      "grades",
      input.classId,
    ]);
    queryClient.setQueryData(["teacher", "grades", input.classId], (old) =>
      old?.map((g) => (g.id === input.id ? { ...g, score: input.score } : g)),
    );
    return { previous };
  },
  onError: (err, input, context) => {
    queryClient.setQueryData(
      ["teacher", "grades", input.classId],
      context.previous,
    ); // 回滚
    toast.error("保存失败,已回滚");
  },
  onSettled: (data, err, input) => {
    queryClient.invalidateQueries({
      queryKey: ["teacher", "grades", input.classId],
    }); // 与服务端同步
  },
});

10.2 缓存失效链路

mutate 一个资源后,需联动失效所有依赖它的缓存:

mutation 失效 queryKey
创建班级 ['teacher','classes']['teacher','dashboard']
发布考试 ['teacher','exams']['teacher','classes',{id},'exams']
批改作业 ['teacher','grades']['teacher','homework',{id}]['student','grades'](跨端,走 WebSocket 推送 invalidate
角色变更 ['session','effective-permissions']['session','viewports']、所有业务 query

10.3 冲突解决

场景 策略
多教师同时批改同一作业 后端乐观锁version 字段);前端 409 时重新拉取 + 提示"数据已被他人修改"
学生重复提交 后端幂等event_id 去重);前端禁用提交按钮 + loading
缓存与服务端不一致 staleTime 到期自动重取;关键操作后强制 invalidate

11. MF 运维架构(补 ai13 §3.11

11.1 Remote 版本与兼容性

维度 策略
版本号 Shell 与 Remote 各自 SemverremoteEntry.js 路径带版本 remoteEntry.{version}.js
契约版本 Shell↔Remote 交互契约(暴露组件 props、共享 store shape独立版本化 @edu/mf-contracts
兼容范围 Shell 声明 peerDeps 兼容的 Remote 主版本范围;不兼容版本走 fallback UI
升级顺序 先升 shared depsreact 等)→ 再升 Remote → 最后升 Shell每步灰度
回滚 remoteEntry.js 保留 N 个历史版本CI 一键切回上一版本 URL

11.2 Remote 故障与降级

graph LR
    A[Shell 加载 Remote] --> B{remoteEntry.js 可达?}
    B -- 否 --> C[重试 3 次指数退避]
    C --> D{仍失败?}
    D -- 是 --> E[渲染 Fallback UI]
    E --> F[提示模块暂不可用 + 重试按钮 + 通知运维]
    B -- 是 --> G[执行 Remote 模块]
    G --> H{运行时抛错?}
    H -- 是 --> I[Remote 级 ErrorBoundary 捕获]
    I --> E
    H -- 否 --> J[正常渲染]
  • 每个 Remote 用独立 <ErrorBoundary fallback={...}> 包裹,单 Remote 崩溃不带垮整个 Shell
  • 动态 import 用 @next/dynamic + ssr: false + loading 占位
  • Remote 不可用不阻断其他 Remote 与 Shell 自身路由

11.3 共享依赖演化

场景 处理
React 18 → 19 大版本升级 全 4 端统一窗口期升级;过渡期 MF shared.singleton 容忍 minor 差异major 不一致报错
新增共享依赖(如 zustand 先在 @edu/mf-contracts 登记 → Shell exposes shared → Remote 声明 shared
移除共享依赖 先 Remote 移除引用 → 确认无 Remote 引用 → Shell 移除 shared
版本漂移检测 CI 校验 4 端 package.json 的 shared deps 版本一致pnpm dedupe + 自定义检查)

11.4 跨端类型安全

  • Shell 通过 @module-federation/typescript 生成 @mf-types/*Remote 消费 Shell 暴露组件的类型
  • 运行时边界:跨端调用一律走 HookuseApi()/usePermission()),不直接访问对方内部 store
  • 契约测试:@edu/mf-contracts 携带类型 + 运行时 zod schemaRemote 接入 Shell 时校验

11.5 路由策略

关注点 方案
路由前缀 /teacher/* /student/* /parent/* /admin/*004 §5.4 场景域划分)
深链直入 URL /student/exams/123 → Shell middleware 识别前缀 → 动态加载 student remote → remote 内部路由匹配
404 Shell 全局 not-found.tsx + 各 Remote 内部 not-found.tsx
路由守卫 middleware.ts 统一鉴权:无 token → /login;权限不足当前前缀 → 重定向到对应 scope dashboard
路由切换 loading next.js loading.tsxSuspense骨架屏 + 滚动恢复

12. 远期演进铺垫

12.1 远期能力预留(不立即实现,但架构不堵死)

能力 预留点 触发时机
PWA / 离线 Service Worker 预留 sw.tsTanStack Query onlineManager 离线队列 教师弱网批改需求
多租户 / SaaS 前端按 tenant_id 注入i18n glossary 支持租户覆写 多校部署
Feature Flags packages/contracts/src/flags.ts 统一枚举middleware 注入 灰度发布
移动端复用 hooks/contracts 纯 TS 可被 React Native 复用;不耦合 DOM RN App
插件化 Shell 预留 Remote 注册表(动态 manifest第三方 Remote 可挂载 开放生态
Web Components 抽取 ui-components 用 lit 封装跨框架版本(远期) 跨技术栈
A/B 测试 edge middleware 分桶 + feature flag 增长需求

12.2 ADR架构决策记录登记

本设计产生的关键决策应沉淀为 ADR 供未来回溯:

ADR 决策 状态
ADR-013 Token 存储localStorage 过渡 → 内存+httpOnly cookie 终态 拟采纳
ADR-014 MF 拓扑:单 Shell + 3 Remote 已采纳(沿用 ADR-012
ADR-015 状态管理 5 层模型 拟采纳
ADR-016 i18n 路由前缀式 + next-intl 拟采纳
ADR-017 性能预算 + bundle CI 门禁 拟采纳
ADR-018 MF Remote SRI + 最小暴露面 拟采纳
ADR-019 多 Tab 会话同步 BroadcastChannel 拟采纳

ADR 正式登记由 coord 在 docs/architecture/adr/ 维护ai07 提请。

12.3 待 coord 协调问题

# 问题 影响 建议裁决
1 full-stack-runbook.md §2.1 端口矩阵未更新到 4 端(仍记 teacher-portal=3000 端口权威源不一致 coord 更新 runbook 为 4000-4003
2 packages 归属ui-tokens/ui-components/hooks 维护边界 ai07 维护前端专属包coord 维护 shared-ts/contracts
3 GraphQL vs RESTBFF 何时切 GraphQL 已裁决F9 + 总裁 §0.3 前端是否引入 urql P2 起 BFF 即 GraphQL前端即 urql无 REST 中间态(见 §1.4
4 admin-bff 是否存在004 §4 未列出,但 admin-portal 消费 /admin/* admin-portal 契约缺失 coord 确认 admin-bff 归属或由 iam 承载管理 API
5 shared deps 版本对齐 CI 校验机制 MF singleton 安全 coord 在 CI 增加 4 端 package.json diff 检查
6 @edu/mf-contracts 包归属 Shell↔Remote 契约载体 建议 coord 维护(跨 AI 共享)
7 Next.js 14→15 升级窗口 App Router 破坏性变更 P2 收尾统一升级4 端同步

13. 覆盖度对照ai13 审核 §3 遗漏逐项闭合)

ai13 §3 遗漏项 本文档章节 状态
§3.1 演进路线缺失 §1 演进与迁移策略 闭合
§3.2 安全架构缺失 §2 安全架构 闭合
§3.3 性能架构缺失 §3 性能架构 闭合
§3.4 可观测性深度缺失 §4 可观测性深度 闭合
§3.5 a11y 实施细节缺失 §5 a11y 实施细节 闭合
§3.6 i18n 实施细节缺失 §6 i18n 实施细节 闭合
§3.7 部署拓扑缺失 §7 部署拓扑 闭合
§3.8 测试策略细节缺失 §8 测试策略细节 闭合
§3.9 文件/目录结构缺失 02 §14ai13 已补) 闭合
§3.10 移动端策略缺失 §9 移动端策略 闭合
§3.11 MF 演进策略缺失 §1.3 + §11 MF 运维架构 闭合
§3.12 数据流与乐观更新缺失 §10 数据流与乐观更新 闭合

AI Agent: ai074 端合并设计 / 长远架构补全) Branch: docs/teacher-portal-long-term-architecture-ai07 Coordinator: coord-ai Closes: ai13 audit §3 all 12 items


14. 回写记录

14.1 ai13 回写2026-07-09批次 0.15 后,总裁裁决 §3.4 ISSUE-036-ai13

回写依据:总裁裁决 §3.4 + §4.4 第 5 条(删除过渡方案本身,保留设计决策记录)+ coord F9P2 起 BFF 用 GraphQL

回写内容

位置 原内容 新内容
§1.1 P2 行 "统一 API 层" "GraphQL clienturqlF9 裁决 P2 起)"
§1.2 表格 "BFF 切 GraphQLP4 起) + feature flag 灰度" "BFF GraphQLP2 起,已裁决) + 前端 P2 起即 urql"
§1.4 整节 "REST → GraphQL 切换策略"过渡方案P2-P4 REST + P5+ 迁移 + feature flag 灰度) "GraphQL 客户端架构P2 起最终方案)"client 选型/单例/初始化/错误处理/缓存策略/ISSUE 关联/设计决策记录)
§12.3 #3 待裁决"P2-P3 RESTP4 起 BFF 切 GraphQL" 已裁决"P2 起 BFF 即 GraphQL前端即 urql无 REST 中间态"

未删除内容(设计决策记录,按 §4.4 保留):

  • §1.4.6 设计决策记录:明确记录原过渡方案已废弃 + 裁决来源 + 影响范围,供未来回溯
  • §2.1 Token 存储演进P2 localStorage → P6 httpOnly cookie属独立决策F12 裁决 P2 用 localStorage非"REST→GraphQL 过渡方案",不在本次回写范围

关联 ISSUEISSUE-036-ai13已通过本次回写闭合

AI Agent: ai13teacher-portal回写执行 Branch: feat/teacher-portal-docs-rewrite-issue036-ai13 Coordinator: coord-ai 依据: president-final-rulings.md §3.4 + §4.4