# 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` 包裹 `<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 注入：

```
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      | `<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_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 非文本内容 | 所有 `<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 乐观更新模式

```typescript
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 各自 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 故障与降级

```mermaid
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 暴露组件的类型
- 运行时边界：跨端调用一律走 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
