teacher-portal 长远架构补全
AI:ai07(4 端合并设计,承接 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 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)
| 步骤 |
动作 |
回滚 |
验证 |
| 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 起)
- 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.code(BFF_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) |
| 用户权限/视口 |
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 注入:
- 生产环境 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 |
<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)
- 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)
- 级别: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 非文本内容 |
所有 <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-a11y(error 级,CI 强制)
@axe-core/playwright E2E 集成,每版本扫描关键页面
- 设计令牌对比度审计脚本(primitive 色板两两组合计算对比度)
6. i18n 实施细节(补 ai13 §3.6)
| 维度 |
方案 |
| 库 |
next-intl(App Router 原生支持) |
| 路由 |
/[locale]/teacher/...(前缀式,默认 zh) |
| 检测 |
middleware.ts:cookie NEXT_LOCALE → Accept-Language → 默认 zh |
| 加载 |
按路由分片动态加载 message(getMessages + useTranslations) |
| fallback |
zh 为 fallback locale,缺 key 回退;dev 环境缺 key console.warn |
| 复数/性别 |
ICU MessageFormat(next-intl 内置):{count, plural, =0 {无} one {# 项} other {# 项}} |
| 数字/日期 |
Intl.NumberFormat / Intl.DateTimeFormat(按 locale,不用 dayjs 本地化) |
| 错误码 i18n |
error.{{service}}.{{code_snake_case}}(见 02 §6.2) |
| RTL 预留 |
布局用 logical properties(margin-inline/padding-inline),未来阿拉伯语只需切换 direction |
| 文档术语 |
业务术语统一 glossary(packages/contracts/src/i18n-glossary.ts)防翻译漂移 |
| 翻译协作 |
i18n/locales/*.json 纳入 git;CI 校验 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-portal(Shell) |
独立 |
滚动 |
Shell 变更频率最低 |
| student/parent/admin(Remote) |
各自独立 |
蓝绿/金丝雀 |
remoteEntry.js 不可变文件名 + CDN 长缓存 |
- 部署顺序约束:破坏性 shared dep 升级时,Remote 先升、Shell 后升(Shell singleton 向下兼容旧 Remote)
- CDN:remoteEntry.js 设 immutable 长缓存,版本化文件名
remoteEntry.{version}.js 解决缓存失效
- 预拉取:Shell 在 idle 时预拉取用户角色对应 Remote 的 remoteEntry.js(
requestIdleCallback)
- Edge middleware:locale 检测、A/B 测试、灰度路由放 edge(Vercel/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 schema,CI 校验 Shell 暴露与 Remote 消费一致
- 跨端 E2E:Playwright 启动 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 移动端折叠侧边栏为抽屉(汉堡菜单)
- 触摸目标 ≥ 44px(WCAG 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/ApiClient,UI 层重写
- 不为此提前抽象,保持当前 Next.js 实现简洁(YAGNI)
10. 数据流与乐观更新(补 ai13 §3.12)
10.1 乐观更新模式
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 各自 Semver;remoteEntry.js 路径带版本 remoteEntry.{version}.js |
| 契约版本 |
Shell↔Remote 交互契约(暴露组件 props、共享 store shape)独立版本化 @edu/mf-contracts |
| 兼容范围 |
Shell 声明 peerDeps 兼容的 Remote 主版本范围;不兼容版本走 fallback UI |
| 升级顺序 |
先升 shared deps(react 等)→ 再升 Remote → 最后升 Shell;每步灰度 |
| 回滚 |
remoteEntry.js 保留 N 个历史版本,CI 一键切回上一版本 URL |
11.2 Remote 故障与降级
- 每个 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 暴露组件的类型
- 运行时边界:跨端调用一律走 Hook(
useApi()/usePermission()),不直接访问对方内部 store
- 契约测试:
@edu/mf-contracts 携带类型 + 运行时 zod schema,Remote 接入 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.tsx(Suspense)骨架屏 + 滚动恢复 |
12. 远期演进铺垫
12.1 远期能力预留(不立即实现,但架构不堵死)
| 能力 |
预留点 |
触发时机 |
| PWA / 离线 |
Service Worker 预留 sw.ts;TanStack 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 REST(BFF 何时切 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 §14(ai13 已补) |
✅ 闭合 |
| §3.10 移动端策略缺失 |
§9 移动端策略 |
✅ 闭合 |
| §3.11 MF 演进策略缺失 |
§1.3 + §11 MF 运维架构 |
✅ 闭合 |
| §3.12 数据流与乐观更新缺失 |
§10 数据流与乐观更新 |
✅ 闭合 |
AI Agent: ai07(4 端合并设计 / 长远架构补全)
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 F9(P2 起 BFF 用 GraphQL)。
回写内容:
| 位置 |
原内容 |
新内容 |
| §1.1 P2 行 |
"统一 API 层" |
"GraphQL client(urql,F9 裁决 P2 起)" |
| §1.2 表格 |
"BFF 切 GraphQL(P4 起) + feature flag 灰度" |
"BFF GraphQL(P2 起,已裁决) + 前端 P2 起即 urql" |
| §1.4 整节 |
"REST → GraphQL 切换策略"(过渡方案:P2-P4 REST + P5+ 迁移 + feature flag 灰度) |
"GraphQL 客户端架构(P2 起最终方案)"(client 选型/单例/初始化/错误处理/缓存策略/ISSUE 关联/设计决策记录) |
| §12.3 #3 |
待裁决"P2-P3 REST,P4 起 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 过渡方案",不在本次回写范围
关联 ISSUE:ISSUE-036-ai13(已通过本次回写闭合)
AI Agent: ai13(teacher-portal,回写执行)
Branch: feat/teacher-portal-docs-rewrite-issue036-ai13
Coordinator: coord-ai
依据: president-final-rulings.md §3.4 + §4.4