# 模块架构设计文档 — teacher-portal

> AI：ai07（TS/React · 教学场景域前端 shell）
> 阶段：阶段 2 交付物
> 日期：2026-07-09
> 关联：[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)
> 状态：待 coord 交叉审查

---

## 1. 模块内部分层图（4 端统一 MF 架构）

```mermaid
graph TB
    subgraph Browser["浏览器"]
        URL[URL 路由]
    end

    subgraph Shell["teacher-portal（Shell 宿主）"]
        AppShell[AppShell<br/>左栏导航 + 主内容区]
        RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
        Router[Next.js App Router]
        SharedDeps["共享依赖暴露<br/>react/react-dom/@tanstack/react-query/zustand/nuqs"]
    end

    subgraph RemoteTeacher["teacher-portal Remote 模块"]
        TeacherPages[教学场景页面<br/>dashboard/classes/exams/homework/grades/ai-assist]
    end

    subgraph RemoteStudent["student-portal（Remote）"]
        StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
    end

    subgraph RemoteParent["parent-portal（Remote）"]
        ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
    end

    subgraph RemoteAdmin["admin-portal（Remote）"]
        AdminPages[管理场景页面<br/>users/roles/permissions/viewports/monitoring]
    end

    subgraph Shared["共享层（packages/）"]
        UITokens[ui-tokens<br/>三层设计令牌]
        UIComponents[ui-components<br/>shadcn + A11y + ErrorBoundary]
        Contracts[contracts<br/>Permissions 常量 + 类型]
        Hooks[hooks<br/>usePermission/useAuth/useA11y]
        LibTS[shared-ts<br/>通用工具]
    end

    subgraph Gateway["api-gateway"]
        GW[Gin 路由/鉴权/限流]
    end

    Browser --> URL
    URL --> RootLayout
    RootLayout --> AppShell
    AppShell --> Router
    Router -->|动态加载| RemoteTeacher
    Router -->|动态加载| RemoteStudent
    Router -->|动态加载| RemoteParent
    Router -->|动态加载| RemoteAdmin

    RemoteTeacher --> SharedDeps
    RemoteStudent --> SharedDeps
    RemoteParent --> SharedDeps
    RemoteAdmin --> SharedDeps

    Shell --> UITokens
    Shell --> UIComponents
    Shell --> Contracts
    Shell --> Hooks
    RemoteTeacher --> UITokens
    RemoteStudent --> UITokens
    RemoteParent --> UITokens
    RemoteAdmin --> UITokens

    AppShell -->|fetch /api/v1/iam/effective-permissions| Hooks
    Hooks -->|透传 token| GW
    RemoteTeacher -->|fetch /api/v1/teacher/*| GW
    RemoteStudent -->|fetch /api/v1/student/*| GW
    RemoteParent -->|fetch /api/v1/parent/*| GW
    RemoteAdmin -->|fetch /api/v1/iam/* + /api/v1/admin/*| GW
```

### 1.1 MF 拓扑选型

| 方案                                 | 选否 | 理由                                                                                    |
| ------------------------------------ | ---- | --------------------------------------------------------------------------------------- |
| 4 端独立部署 + 独立域名 + 各自 Shell | ❌   | 4 套 Shell 重复，登录态/权限/组件库要重复实现                                           |
| 单 Shell + 4 Remote（**采用**）      | ✅   | teacher-portal 作为 Shell 宿主，提供 AppShell + 共享依赖；其余 3 端作为 Remote 动态加载 |
| 单一 Next.js 应用 + 4 路由组         | ❌   | 违反"微前端独立部署"目标（ADR-012）                                                     |

**Shell 职责**（teacher-portal）：

- RootLayout（字体、设计令牌、i18n Provider、TanStack QueryClientProvider、Zustand StoreProvider）
- AppShell（左侧导航 + 主内容区 + 用户信息 + 登出）
- 共享依赖暴露（react、react-dom、@tanstack/react-query、zustand、nuqs、ui-components、ui-tokens、contracts、hooks）
- 路由表（4 端路由前缀：`/teacher/*`、`/student/*`、`/parent/*`、`/admin/*`）
- 登录页（统一登录入口，按角色重定向到对应 portal）

### 1.2 MF 配置（teacher-portal/next.config.js）

```javascript
// teacher-portal/next.config.js（Shell）
const NextFederationPlugin = require("@module-federation/nextjs-mf");

const remotes = (isServer) => ({
  student: `student_app@http://localhost:3001/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
  parent: `parent_app@http://localhost:3002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
  admin: `admin_app@http://localhost:3003/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
});

module.exports = {
  reactStrictMode: true,
  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: "teacher_app",
        filename: "static/chunks/remoteEntry.js",
        remotes: remotes(isServer),
        exposes: {
          "./AppShell": "./src/components/AppShell",
          "./shared-deps": "./src/shared/deps",
        },
        shared: {
          react: { singleton: true, requiredVersion: "^18.3.0" },
          "react-dom": { singleton: true, requiredVersion: "^18.3.0" },
          "@tanstack/react-query": { singleton: true },
          zustand: { singleton: true },
          nuqs: { singleton: true },
        },
        extraOptions: { exposePages: false },
      }),
    );
    return config;
  },
  async rewrites() {
    return [
      {
        source: "/api/v1/:path*",
        destination: `${process.env.API_GATEWAY_URL || "http://localhost:8080"}/api/v1/:path*`,
      },
    ];
  },
};
```

> Remote 端配置对称：`name: 'student_app'`，`exposes: { './pages': './src/pages' }`，`remotes: { teacher: 'teacher_app@...' }`。

## 2. 领域模型（前端视角）

前端不持有业务聚合根，仅持有"视图模型"（ViewModel）和"会话状态"。

### 2.1 会话状态（Session）

```typescript
interface Session {
  user: UserInfo; // { id, email, name, roles, permissions, dataScope }
  tokens: { accessToken: string; refreshToken: string };
  viewports: ViewportItem[]; // L1 导航视口
  expiresAt: number; // access token 过期时间戳
}
```

存储：Zustand sessionSlice（L3）+ localStorage 持久化（刷新恢复）+ TanStack Query 缓存 `['session']`（L2）。

### 2.2 视口模型（Viewport）

```typescript
interface ViewportItem {
  key: string; // 'dashboard' | 'classes' | ...
  label: string; // i18n key 或显式文案
  route: string; // '/teacher/dashboard'
  icon: string | null; // 图标 key（按需）
  sortOrder: number; // 排序
  requiredPermission: string | null; // 'CLASSES_READ' 等
  scope: "teacher" | "student" | "parent" | "admin"; // 标记归属哪个 portal
}
```

来源：`GET /api/v1/{scope}/viewports`（BFF 聚合 iam 视口配置）。AppShell 按 `scope` 过滤渲染对应 portal 的导航。

### 2.3 权限模型（Permission）

```typescript
interface PermissionState {
  permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]
  dataScope: DataScope; // L0-L5
  hasPermission: (perm: string) => boolean;
  hasAnyPermission: (perms: string[]) => boolean;
  hasAllPermissions: (perms: string[]) => boolean;
}
```

来源：`GET /api/v1/iam/effective-permissions` → `{ permissions, viewports, dataScope }`。Redis 缓存 5min（iam 侧），前端 TanStack Query 缓存 5min，角色变更主动 invalidate。

## 3. 数据模型（前端）

前端无数据库，仅有缓存层：

| 数据类型                | 存储                   | TTL                         | 失效策略                               |
| ----------------------- | ---------------------- | --------------------------- | -------------------------------------- |
| Session（token + user） | localStorage + Zustand | access 15min / refresh 7day | 401 自动 refresh，refresh 失败跳登录   |
| 权限列表                | TanStack Query cache   | 5min                        | 角色变更事件 invalidate                |
| 视口列表                | TanStack Query cache   | 5min                        | 同上                                   |
| 班级/年级列表           | TanStack Query cache   | 5min                        | staleTime 5min，mutation 后 invalidate |
| 教学资源详情            | TanStack Query cache   | 30s                         | staleTime 30s                          |
| 学情宽表                | TanStack Query cache   | 30s                         | staleTime 30s（实时性由 BFF 决定）     |
| URL 状态（分页/筛选）   | nuqs                   | —                           | 永久（可分享）                         |
| 表单临时态              | react-hook-form        | —                           | 卸载即销毁                             |

## 4. API 设计（前端 → 后端）

前端不设计后端 API，仅声明消费的端点。详见 [01-understanding.md §3.1](./01-understanding.md)。

### 4.1 统一 API 请求层（lib/api.ts）

```typescript
// packages/shared-ts/src/api-client.ts（共享）
interface ApiClientOptions {
  baseUrl?: string; // 默认 ''（走 Next.js rewrites）
  getToken?: () => string | null;
  onUnauthorized?: () => void; // 401 → refresh → 重试 / 跳登录
  onError?: (error: ApiError) => void; // 全局 toast
}

class ApiClient {
  async get<T>(path: string, query?: Record<string, string>): Promise<T>;
  async post<T>(path: string, body: unknown): Promise<T>;
  async put<T>(path: string, body: unknown): Promise<T>;
  async delete<T>(path: string): Promise<T>;
  async sse<T>(path: string, body: unknown): AsyncIterable<T>; // AI 流式
}

// 错误结构
interface ApiError {
  code: string; // 'IAM_INVALID_CREDENTIALS'
  message: string; // 已 i18n 翻译或后端原文
  details?: unknown;
  httpStatus: number;
}
```

**职责**：

- 自动注入 `Authorization: Bearer ${token}`
- 401 自动 refresh token 一次，失败调 `onUnauthorized`
- 解析 `ActionState`，success=false 抛 `ApiError`
- 按 `error.code` 前缀路由 i18n key
- 全局错误 toast（除 401）
- 请求/响应 trace_id 透传（从响应头 `X-Request-Id` 提取）

### 4.2 TanStack Query 约定

```typescript
// Query Key 命名：[scope, resource, ...args]
queryKey: ["teacher", "classes", { gradeId }];
queryKey: ["teacher", "exams", classId];
queryKey: ["session", "effective-permissions"];
queryKey: ["session", "viewports", "teacher"];

// Mutation 约定
const mutation = useMutation({
  mutationFn: (input) => api.post("/api/v1/classes", input),
  onSuccess: () =>
    queryClient.invalidateQueries({ queryKey: ["teacher", "classes"] }),
  onError: (e: ApiError) => toast.error(e.message),
});
```

## 5. 事件设计

前端不发布 Kafka 事件，仅消费 WebSocket 推送（P5）和 Server-Sent Events（AI 流式）。

### 5.1 WebSocket 推送（P5）

| 事件                    | 触发         | teacher-portal 前端动作         |
| ----------------------- | ------------ | ------------------------------- |
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1  |
| `HomeworkSubmitted`     | 学生提交作业 | toast + 作业批改列表 invalidate |

### 5.2 SSE 流式（P5 AI 辅助出题）

```
GET /api/v1/ai/generate-questions (SSE)
data: {"delta": "题目"}\n\n
data: {"delta": "A. option1"}\n\n
data: {"done": true}\n\n
```

前端用 `AsyncIterable<T>` 消费，Tiptap 逐字插入。

## 6. 横切关注点对齐清单

### 6.1 权限（前端等价）

| 路由                          | requiredPermission       |
| ----------------------------- | ------------------------ |
| `/teacher/dashboard`          | `TEACHER_DASHBOARD_VIEW` |
| `/teacher/classes`            | `CLASSES_READ`           |
| `/teacher/classes/new`        | `CLASSES_CREATE`         |
| `/teacher/exams`              | `EXAMS_READ`             |
| `/teacher/exams/new`          | `EXAMS_CREATE`           |
| `/teacher/homework`           | `HOMEWORK_READ`          |
| `/teacher/homework/:id/grade` | `HOMEWORK_GRADE`         |
| `/teacher/grades`             | `GRADES_READ`            |
| `/teacher/ai-assist`          | `AI_GENERATE`            |
| `/teacher/lesson-prep`        | `LESSON_PREP_VIEW`       |
| `/teacher/knowledge-graph`    | `CONTENT_READ`           |

> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`（待建立，coord 负责 shared-ts，ai07 负责调用）。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。

### 6.2 错误码清单（前端 i18n 路由）

| 前缀           | 来源服务    | i18n key 模式            |
| -------------- | ----------- | ------------------------ |
| `IAM_`         | iam         | `iam.error.{{code}}`     |
| `CORE_EDU_`    | core-edu    | `coreEdu.error.{{code}}` |
| `CLASSES_`     | core-edu    | `classes.error.{{code}}` |
| `CONTENT_`     | content     | `content.error.{{code}}` |
| `MSG_`         | msg         | `msg.error.{{code}}`     |
| `AI_`          | ai          | `ai.error.{{code}}`      |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}`     |
| `GW_`          | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_`     | 前端网络层  | `network.error.{{code}}` |

### 6.3 Logger

```typescript
// packages/shared-ts/src/logger.ts
interface Logger {
  info(msg: string, meta?: Record<string, unknown>): void;
  warn(msg: string, meta?: Record<string, unknown>): void;
  error(msg: string, meta?: Record<string, unknown>): void;
}

// 实现：开发环境 console + 结构化；生产环境 → Sentry（P6）
// 必含字段：trace_id（从响应头提取）、user_id、scope、path
```

### 6.4 Metrics（Web Vitals）

| 指标                          | 类型 | 上报                                                |
| ----------------------------- | ---- | --------------------------------------------------- |
| `teacher_portal_lcp_seconds`  | LCP  | `next/web-vitals` → `POST /api/v1/admin/web-vitals` |
| `teacher_portal_cls`          | CLS  | 同上                                                |
| `teacher_portal_fid_seconds`  | FID  | 同上                                                |
| `teacher_portal_ttfb_seconds` | TTFB | 同上                                                |

P6 接入，P2-P5 暂缓。

### 6.5 Tracer（OTel browser SDK，P6）

```typescript
// packages/shared-ts/src/tracer.ts
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
// BatchSpanProcessor → OTLP exporter → collector → Tempo
// 自动埋点：fetch、XMLHttpRequest、document load、user interaction
```

### 6.6 健康检查

| 端点              | 用途                   | 实现                                                           |
| ----------------- | ---------------------- | -------------------------------------------------------------- |
| `GET /api/health` | Dockerfile HEALTHCHECK | Next.js Route Handler，返回 `{ status: 'ok', ts: Date.now() }` |
| `GET /api/ready`  | K8s readinessProbe     | 检查 `process.env.API_GATEWAY_URL` 可达 + 内存 < 阈值          |

### 6.7 优雅关闭

Next.js 无长连接（除 SSE/WS），无需特殊处理。SSE/WS 在 P5 由 push-gateway 管理，前端断线自动重连。

## 7. 共享组件库（packages/ui-components/，待建立，ai07 维护）

| 组件                                       | 用途                                                                                                | 来源                           |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------ |
| `AppShell`                                 | 左侧栏 + 主内容区布局                                                                               | teacher-portal 现有 → 抽取共享 |
| `RequirePermission`                        | L3 组件级视口控制（无权限不渲染 children）                                                          | 新建                           |
| `ErrorBoundary`                            | React 渲染异常兜底（fallback UI）                                                                   | 新建                           |
| `Loading`                                  | 骨架屏（Skeleton）                                                                                  | 新建                           |
| `Empty`                                    | 空态（插画 + 文案 + CTA）                                                                           | 新建                           |
| `Modal` / `Dialog`                         | 全局 Modal（ModalRoot + Zustand ui-store）                                                          | shadcn/ui                      |
| `Toast`                                    | 全局 toast（错误/成功/警告）                                                                        | shadcn/ui sonner               |
| `Button` / `Input` / `Select` / `Textarea` | 基础表单                                                                                            | shadcn/ui                      |
| `DataTable`                                | 表格（排序/分页/筛选）                                                                              | shadcn/ui + TanStack Table     |
| `Chart`                                    | 图表封装（recharts）                                                                                | 新建                           |
| `A11y` 工具集                              | useA11yId / mergeA11yProps / describeInput / focus-trap / skip-link / visually-hidden / aria-status | 迁移指南 §7.7                  |
| `Form`                                     | react-hook-form + zodResolver 封装                                                                  | 新建                           |
| `RichTextEditor`                           | Tiptap 封装（备课/出题/反馈）                                                                       | 新建                           |

## 8. 共享 Hooks（packages/hooks/，待建立，ai07 维护）

| Hook                  | 职责                                                |
| --------------------- | --------------------------------------------------- |
| `useAuth()`           | 会话状态（user/token/refresh/login/logout）         |
| `usePermission()`     | 权限查询（hasPermission/hasAny/hasAll + dataScope） |
| `useViewports(scope)` | 视口列表（按 scope 过滤）                           |
| `useApi()`            | ApiClient 实例（注入 token + 401 处理）             |
| `useA11yId()`         | 唯一 ARIA ID 生成                                   |
| `useAriaLive()`       | aria-live 区域管理                                  |
| `useToast()`          | 全局 toast（Zustand ui-store）                      |

## 9. 设计令牌三层（packages/ui-tokens/，待建立，ai07 维护）

```
packages/ui-tokens/
├─ primitive.css              # Layer 1 原始色板/字号/间距/阴影
├─ semantic-light.css         # Layer 2 语义令牌（亮色）
├─ semantic-dark.css          # Layer 2 语义令牌（暗色）
├─ tailwind-theme.css         # Layer 3 @theme inline 暴露 bg-*/text-*/font-*
└─ package.json
```

**强制规则**（project_rules §3.10）：

- 禁止 `#hex` 字面量（ESLint `no-restricted-syntax`）
- 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量（ESLint `design-tokens/no-hardcoded-fonts`）
- 禁止 `font-size: Npx`（用 `var(--font-size-1~9)`）
- 禁止 Tailwind 任意值 `w-[Npx]`（用 `--space-*` 或默认阶梯）

**令牌命名**（迁移指南 §7.1）：

| Layer 1 Primitive  | Layer 2 Semantic   | Layer 3 Tailwind |
| ------------------ | ------------------ | ---------------- |
| `--color-blue-500` | `--color-accent`   | `bg-accent`      |
| `--font-size-3`    | `--font-size-body` | `text-body`      |
| `--space-4`        | `--space-md`       | `p-md`           |

## 10. 与其他模块的交互点（契约清单）

| 方向   | 对方服务     | 协议               | 接口/事件                                           | 用途                         | 阶段 |
| ------ | ------------ | ------------------ | --------------------------------------------------- | ---------------------------- | ---- |
| 调用   | api-gateway  | HTTP/REST          | `/api/v1/*` 代理                                    | 全部业务请求                 | P1+  |
| 调用   | push-gateway | WebSocket          | `ws://push-gateway/ws`                              | 实时推送                     | P5   |
| 调用   | ai           | SSE                | `GET /api/v1/ai/generate-questions`                 | AI 流式出题                  | P5   |
| 被调用 | —            | —                  | —                                                   | 前端不暴露接口给其他服务     | —    |
| 消费   | teacher-bff  | HTTP（经 Gateway） | `GET /teacher/viewports` 等                         | 教师场景聚合                 | P2+  |
| 消费   | iam          | HTTP（经 Gateway） | `/iam/*`                                            | 登录/权限/视口/用户管理      | P2+  |
| 消费   | core-edu     | HTTP（经 Gateway） | `/classes/*` `/exams/*` `/homework/*` `/grades/*`   | 教学核心                     | P2+  |
| 消费   | content      | HTTP（经 Gateway） | `/textbooks/*` `/knowledge-points/*` `/questions/*` | 内容资源                     | P4+  |
| 消费   | data-ana     | HTTP（经 Gateway） | `/analytics/*`                                      | 学情分析                     | P4+  |
| 消费   | msg          | HTTP（经 Gateway） | `/notifications/*`                                  | 通知中心                     | P5+  |
| 依赖   | coord 维护   | —                  | `packages/shared-proto`                             | TS 类型（仅 contracts 部分） | P1+  |
| 依赖   | coord 维护   | —                  | `packages/shared-ts`（待建）                        | ApiClient/Logger/通用工具    | P2+  |
| 依赖   | ai07 维护    | —                  | `packages/ui-tokens`（待建）                        | 三层设计令牌                 | P2+  |
| 依赖   | ai07 维护    | —                  | `packages/ui-components`（待建）                    | shadcn + 共享组件            | P2+  |
| 依赖   | ai07 维护    | —                  | `packages/hooks`（待建）                            | usePermission/useAuth 等     | P2+  |
| 依赖   | coord 维护   | —                  | `packages/contracts`（待建）                        | Permissions 常量 + 类型      | P2+  |

> **proto 不直接消费**：前端不调用 gRPC，BFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量（TS 文件，非 proto 生成）。

## 11. 风险与假设

### 11.1 假设

1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**：包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立，ai07 自行在 `apps/teacher-portal/src/shared/` 内实现，后续提取到 packages。
2. **假设 iam 提供 `GET /iam/effective-permissions`**：返回 `{ permissions, viewports, dataScope }`。当前已实现（known-issues §2.3 iam）。
3. **假设 teacher-bff 提供 `GET /teacher/viewports`**：返回 L1 导航视口。当前已实现。
4. **假设 core-edu classes 模块维持 `ActionState` 响应结构**：前端 API 请求层依赖此契约。
5. **假设 Next.js 14+ Module Federation 2.0 稳定**：`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定，降级为 4 端独立部署 + 各自 Shell（重复实现 AppShell）。

### 11.2 技术风险

| 风险                     | 影响                                                        | 缓解                                                                               |
| ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| MF SSR 对齐复杂          | Remote 在 SSR 时需 Shell 提供上下文                         | 优先 CSR，SSR 仅用于首屏 dashboard；MF 2.0 支持 SSR                                |
| 共享依赖版本漂移         | Remote 与 Shell 的 react/react-dom 版本不一致导致运行时错误 | MF `shared.singleton: true` + CI 检查版本对齐                                      |
| Token 刷新竞态           | 多请求同时 401 触发多次 refresh                             | ApiClient 全局单例 + refresh promise 复用                                          |
| 权限缓存陈旧             | 角色变更后前端 5min 内仍用旧权限                            | iam 角色变更发 Kafka 事件 → msg 推送 WebSocket → 前端 invalidate                   |
| 设计令牌迁移破坏现有样式 | teacher-portal 现有硬编码令牌迁移到三层模型后样式漂移       | 灰度迁移：先建 ui-tokens 包，teacher-portal 引入但不删除旧 globals.css，验证后切换 |
| TanStack Query 缓存膨胀  | 长时间使用后缓存项过多                                      | `gcTime` 5min + `staleTime` 按数据类型分级                                         |

### 11.3 未决设计决策（需 coord 仲裁）

1. **packages 归属**：`ui-tokens` / `ui-components` / `hooks` 是 ai07 维护还是 coord 维护？建议：ai07 维护（前端专属），coord 仅维护 `shared-ts` / `contracts`（跨语言/跨服务）。
2. **GraphQL vs REST**：004 §11.3 提到 BFF GraphQL Yoga + DataLoader，但当前 teacher-bff 实现为 REST。前端 API 请求层是否需要 GraphQL client（urql/apollo）？建议：P2-P3 用 REST，P4 起若 BFF 切 GraphQL 再引入 urql。
3. **i18n key 命名**：`iam.error.IAM_INVALID_CREDENTIALS` 还是 `error.iam.invalid_credentials`？建议：`error.{{service}}.{{code_snake_case}}`，与错误码前缀对齐。
4. **MF 暴露粒度**：Shell 暴露整个 AppShell 还是暴露更细粒度的组件（Sidebar、Header、Content）？建议：暴露 AppShell 整体 + 各 Remote 自行决定内部布局。

## 12. coord 交叉审查所需信息

### 12.1 端口矩阵

| 端             | dev 端口 | 生产端口 | 备注       |
| -------------- | -------- | -------- | ---------- |
| teacher-portal | 3000     | 3000     | Shell 宿主 |

> 与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐。

### 12.2 依赖的共享包（需 coord 建立）

| 包              | 路径                      | 维护方       | 内容                                              |
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
| `shared-ts`     | `packages/shared-ts/`     | coord        | ApiClient、Logger、通用工具                       |
| `contracts`     | `packages/contracts/`     | coord        | Permissions 常量、ActionState 类型、UserInfo 类型 |
| `ui-tokens`     | `packages/ui-tokens/`     | ai07（建议） | 三层设计令牌                                      |
| `ui-components` | `packages/ui-components/` | ai07（建议） | shadcn + ErrorBoundary + RequirePermission        |
| `hooks`         | `packages/hooks/`         | ai07（建议） | usePermission、useAuth、useViewports              |

### 12.3 依赖的后端契约（需对应 AI 确认）

| 契约                                                               | 提供方             | 当前状态            |
| ------------------------------------------------------------------ | ------------------ | ------------------- |
| `POST /iam/login`、`GET /iam/effective-permissions`、`GET /iam/me` | iam                | ✅ 已实现           |
| `GET /teacher/viewports`、`GET /teacher/dashboard`                 | teacher-bff        | ✅ 已实现           |
| `/classes/*` CRUD                                                  | core-edu           | ✅ 已实现           |
| `/exams/*` `/homework/*` `/grades/*`                               | core-edu           | ✅ 已实现（P3）     |
| `/textbooks/*` `/knowledge-points/*` `/questions/*`                | content            | ✅ 已实现（P4）     |
| `/analytics/*`                                                     | data-ana           | ✅ 已实现（P4 CDC） |
| `/notifications/*` + WebSocket 推送                                | msg + push-gateway | 📐 待 P5            |
| `GET /ai/generate-questions`（SSE）                                | ai                 | 📐 待 P5            |

### 12.4 错误码前缀（前端 i18n 路由依赖）

前端不产生错误码，仅消费。需各服务确认错误码前缀不重叠：

| 前缀                           | 服务        | 状态      |
| ------------------------------ | ----------- | --------- |
| `IAM_`                         | iam         | ✅ 已用   |
| `CLASSES_`                     | core-edu    | ✅ 已用   |
| `EXAMS_`/`HOMEWORK_`/`GRADES_` | core-edu    | ⚠️ 待确认 |
| `CONTENT_`                     | content     | ⚠️ 待确认 |
| `MSG_`                         | msg         | ⚠️ 待确认 |
| `AI_`                          | ai          | ⚠️ 待确认 |
| `BFF_TEACHER_`                 | teacher-bff | ⚠️ 待确认 |
| `GW_`                          | api-gateway | ✅ 已用   |
| `NETWORK_`                     | 前端        | ai07 自有 |

### 12.5 不产生 Kafka 事件

前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。

## 13. 实施路线（ai07 自用）

### P2 收尾（teacher-portal 审计对齐）

1. 建 `packages/ui-tokens/`（三层设计令牌）+ `packages/ui-components/`（ErrorBoundary/RequirePermission/Loading/Empty）+ `packages/hooks/`（usePermission/useAuth）
2. teacher-portal 引入 TanStack Query + Zustand + nuqs + react-hook-form
3. 抽取 `lib/api.ts` 统一 API 请求层
4. AppShell 改用 `usePermission()`，删除 `user.roles.join(", ")` 硬编码
5. globals.css / tailwind.config.js 迁移到 ui-tokens 三层令牌
6. 引入 next-intl + i18n key 路由
7. 引入 ESLint flat config 自定义规则（no-hardcoded-fonts / design-tokens）
8. 补 ErrorBoundary + /api/health route
9. 配置 next.config.js Module Federation（Shell 角色）
10. 补 Vitest 单测 + Playwright E2E（覆盖率 ≥ 80%）

### P5（推送 + AI 接入）

1. teacher-portal 接入 WebSocket（push-gateway）
2. teacher-portal AI 辅助出题（SSE + Tiptap）

### P6（硬化）

1. Web Vitals + OTel browser SDK 接入
2. A11y WCAG 2.2 AA 审计
3. 性能优化（MF shared 单例验证、bundle 分析）

---

**AI Agent**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
**Coordinator**: coord-ai
