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

> AI：ai13（TS/React · 教学场景域前端 shell）
> 阶段：阶段 2 交付物
> 日期：2026-07-09（2026-07-10 审查回写：对齐 F9 GraphQL P2 起 + ARB-001/002 仲裁，REST→GraphQL 全量重写）
> 关联：[阶段 1 理解确认书](./01-understanding.md)、[长远架构补全](./03-long-term-architecture.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[coord 仲裁 ARB-001/002](../../../docs/architecture/issues/coord.md)、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)
> 状态：✅ 已对齐 F9（GraphQL P2 起）+ ARB-001（schema 第一版）+ ARB-002（MF Shell 暴露清单）

---

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

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

    subgraph Shell["teacher-portal（Shell 宿主 :4000）"]
        RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
        GraphQLProvider[GraphQLProvider<br/>urql client 单例（ARB-002 §2.17 方案 A）]
        AppShell[AppShell<br/>左栏导航 + 主内容区 + 路由守卫]
        Router[Next.js App Router]
        SharedDeps["共享依赖暴露<br/>react/react-dom/urql/graphql/@edu/*（ARB-002 shared singleton）"]
    end

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

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

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

    subgraph RemoteAdmin["admin-portal（Remote，P6+）"]
        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/useGraphQLClient]
        LibTS[shared-ts<br/>GraphQL schema + 通用工具]
    end

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

    subgraph TeacherBFF["teacher-bff :3003"]
        BFF[GraphQL Yoga + DataLoader<br/>POST /graphql（ARB-001 schema）]
    end

    Browser --> URL
    URL --> RootLayout
    RootLayout --> GraphQLProvider
    GraphQLProvider --> AppShell
    AppShell --> Router
    Router -->|动态加载| RemoteTeacher
    Router -->|动态加载 P3+| RemoteStudent
    Router -->|动态加载 P4+| RemoteParent
    Router -->|动态加载 P6+| 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

    GraphQLProvider -->|POST /graphql over HTTP| GW
    GW -->|反向代理 /api/v1/teacher/*| BFF
    BFF -->|gRPC 聚合 iam+classes+core-edu+content+data-ana+msg+ai| Services
```

### 1.1 MF 拓扑选型

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

**Shell 职责**（teacher-portal，ARB-002 §2.2）：

- RootLayout（字体、设计令牌、i18n Provider、GraphQLProvider、Zustand StoreProvider）
- AppShell（左侧导航 + 主内容区 + 用户信息 + 登出 + 路由守卫）
- GraphQLProvider（urql client 单例，总裁 §2.17 方案 A，Remote 复用）
- 共享依赖暴露（react、react-dom、urql、graphql、@edu/ui-tokens、@edu/ui-components、@edu/hooks）
- 路由表（4 端路由前缀：`/teacher/*`、`/student/*`、`/parent/*`、`/admin/*`）
- 登录页（统一登录入口 `POST /api/auth/login`，非 MF，登录是认证前提）

### 1.2 MF 配置（teacher-portal/next.config.js，ARB-002 §2.2 裁决）

```javascript
// teacher-portal/next.config.js（Shell，ARB-002 第一版暴露清单）
const NextFederationPlugin = require("@module-federation/nextjs-mf");

// P2：remotes 为空（NEXT_PUBLIC_MF_ENABLED=false，ARB-002 §2.3），P3+ 逐步接入
const remotes = (isServer) => {
  if (process.env.NEXT_PUBLIC_MF_ENABLED !== "true") return {};
  return {
    student: `student_app@http://localhost:4001/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
    parent: `parent_app@http://localhost:4002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
    admin: `admin_app@http://localhost:4003/_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), // P2 为 {}，ARB-002 §2.3
        exposes: {
          // ARB-002 §2.2 暴露清单
          "./AppShell": "./src/components/AppShell.tsx",
          "./GraphQLProvider": "./src/app/providers.tsx",
          "./useAuth": "./packages/hooks/src/use-auth.ts",
          "./usePermission": "./packages/hooks/src/use-permission.ts",
          "./useGraphQLClient": "./packages/hooks/src/use-graphql-client.ts",
          "./ErrorBoundary": "./packages/ui-components/src/error-boundary.tsx",
          "./Loading": "./packages/ui-components/src/loading.tsx",
          "./Empty": "./packages/ui-components/src/empty.tsx",
          "./RequirePermission": "./packages/ui-components/src/require-permission.tsx",
        },
        shared: {
          // ARB-002 §2.2 singleton 配置
          react: { singleton: true, requiredVersion: "^18.3.0" },
          "react-dom": { singleton: true, requiredVersion: "^18.3.0" },
          urql: { singleton: true, requiredVersion: "^2.2.0" },
          graphql: { singleton: true, requiredVersion: "^16.8.0" },
          "@edu/ui-tokens": { singleton: true },
          "@edu/ui-components": { singleton: true },
          "@edu/hooks": { 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*`,
      },
    ];
  },
};
```

> **P2 Remote 数量 = 0**（ARB-002 §2.3，`NEXT_PUBLIC_MF_ENABLED=false` 默认关）。P3 student-portal 作为首个 Remote 接入（总裁 §1.3 step 4）。feature flag 控制回退单体路由。

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

前端不持有业务聚合根，仅持有"视图模型"（ViewModel）和"会话状态"。数据来源统一为 teacher-bff GraphQL（F9 裁决）。

### 2.1 会话状态（Session）

```typescript
interface Session {
  user: UserInfo; // { id, email, name, roles, dataScope }（GraphQL me Query）
  tokens: { accessToken: string; refreshToken: string }; // F12: localStorage（P6 迁移 cookie）
  viewports: ViewportItem[]; // L1 导航视口（GraphQL viewports Query）
  permissions: string[]; // 权限点（GraphQL me Query 内嵌或 effective-permissions）
  expiresAt: number; // access token 过期时间戳
}
```

存储：Zustand sessionSlice（L3）+ localStorage 持久化（刷新恢复，F12）+ urql cache（L2，GraphQL document cache）。

### 2.2 视口模型（Viewport）

```typescript
// 对齐 ARB-001 §1.2 schema：ViewportItem
interface ViewportItem {
  key: string; // 'dashboard' | 'classes' | ...
  label: string; // i18n key 或显式文案
  route: String; // '/teacher/dashboard'（schema 为 String）
  icon: String | null; // 图标 key（schema 可空）
  sortOrder: String; // 排序（schema 为 String）
  requiredPermission: String | null; // 'CLASSES_READ' 等
}
```

来源：GraphQL `viewports` Query（ARB-001 §1.2）。AppShell 按 `requiredPermission` + `usePermission().hasPermission()` 过滤渲染导航。

### 2.3 权限模型（Permission）

```typescript
interface PermissionState {
  permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]（GraphQL me Query 返回）
  dataScope: DataScope; // SELF | CLASS | GRADE | SCHOOL | DISTRICT | ALL（ARB-001 enum）
  hasPermission: (perm: string) => boolean;
  hasAnyPermission: (perms: string[]) => boolean;
  hasAllPermissions: (perms: string[]) => boolean;
}
```

来源：GraphQL `me` Query 返回 `{ user, roles, dataScope }`（ARB-001 §1.2）。角色变更由 iam 发 Kafka → msg → push-gateway WebSocket 推送 → 前端 invalidate urql cache（见 [03 §2.5](./03-long-term-architecture.md)）。

## 3. 数据模型（前端缓存层）

前端无数据库，仅有 urql 缓存层（03 §1.4.4 缓存策略）：

| 数据类型                | 存储                          | staleTime                         | 失效策略                                      |
| ----------------------- | ----------------------------- | ---------------------------------- | --------------------------------------------- |
| Session（token + user） | localStorage + Zustand（F12） | access 15min / refresh 7day        | 401 自动 refresh，refresh 失败跳登录          |
| 权限列表 + 视口         | urql cacheExchange            | 0（always fetch）                  | 角色变更 WebSocket 推送后强制 invalidate      |
| 班级/年级列表           | urql cacheExchange            | 30s                                | mutation 后 invalidate                        |
| 考试/作业/成绩          | urql cacheExchange            | 10s                                | mutation 后 invalidate + 乐观更新（03 §10.1） |
| 教学资源详情            | urql cacheExchange            | 30s                                | staleTime 30s                                 |
| 学情宽表                | urql cacheExchange            | 60s                                | ISR revalidate 60s + CSR 交互                 |
| URL 状态（分页/筛选）   | nuqs                          | —                                  | 永久（可分享）                                |
| 表单临时态              | react-hook-form               | —                                  | 卸载即销毁                                    |

> urql 默认 document cache 适合简单场景；复杂关联（班级↔考试↔成绩）P3 起评估切 `@urql/exchange-graphcache` normalized cache（03 §1.4.4）。不引入 TanStack Query 做 GraphQL 缓存（避免双缓存层）；TanStack Query 仅用于非 GraphQL 场景（文件上传/SSE）。

## 4. API 设计（前端 → teacher-bff GraphQL）

F9 裁决：P2 起 all-in GraphQL。前端不再设计 REST 请求层，统一通过 urql client 消费 teacher-bff GraphQL（ARB-001 schema 第一版）。

### 4.1 urql client 单例（packages/hooks + Shell GraphQLProvider）

```typescript
// packages/hooks/src/use-graphql-client.ts（Shell 暴露，Remote 复用，ARB-002 §2.17 方案 A）
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 POST /graphql（经 api-gateway 代理）
  fetchOptions: () => {
    const token = readAuthToken(); // useAuth 注入（F12: localStorage）
    const traceId = readTraceId();
    return {
      headers: {
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        "X-Trace-Id": traceId,
      },
    };
  },
  exchanges: [cacheExchange, fetchExchange],
});

// Shell: src/app/providers.tsx 包裹 <Provider value={client}>，所有 Remote 复用同一 client
```

**职责**：

- Shell 初始化 urql client 单例，通过 React Context 注入给所有 Remote（ARB-002 方案 A）
- token 刷新/401 处理由 useAuth 统一拦截（不污染 urql exchange 链，见 [03 §2.7](./03-long-term-architecture.md)）
- MF shared singleton（react/urql/graphql）避免 Remote 多实例 + 缓存不一致
- 请求/响应 trace_id 透传（`X-Trace-Id` header）

### 4.2 GraphQL Query/Mutation 约定（对齐 ARB-001 schema）

```typescript
// P2 Query（ARB-001 §1.2，5 个 Must Have）
import { gql } from "urql";

const DashboardQuery = gql`
  query Dashboard {
    dashboard {
      user { id email name roles dataScope }
      classes { id name gradeId studentCount }
      viewports { key label route icon sortOrder requiredPermission }
      stats { totalExams pendingGrading todayHomework }
    }
  }
`;

const ViewportsQuery = gql`query { viewports { key label route icon sortOrder requiredPermission } }`;
const MeQuery = gql`query { me { id email name roles dataScope } }`;
const ClassesQuery = gql`query { classes { id name gradeId studentCount } }`;

// P3+ Mutation（ARB-001 §1.2 P2 不包含）
const CreateExamMutation = gql`
  mutation CreateExam($input: CreateExamInput!) {
    createExam(input: $input) { ... on ExamCreated { id } }
  }
`;
```

**urql mutation + 乐观更新**：

```typescript
const [, createExam] = useMutation(CreateExamMutation);
// 乐观更新：onMutate 回滚 + invalidateQueries（见 03 §10.1）
```

### 4.3 错误处理（ActionState 信封 + GraphQL errors，ARB-001 §1.3）

| 场景                           | 处理                                                                                                            |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| GraphQL 错误（errors[]）       | 解析 `extensions.code`（BFF_TEACHER_* 前缀，G14），映射到 i18n key `error.teacher_bff.<code_snake>`（F11）     |
| 网络错误                       | 重试 1 次 → 提示网络异常 → 记录 trace_id                                                                        |
| 401                            | useAuth 静默 refresh 一次 → 重试；失败跳登录（见 03 §2.7）                                                      |
| 部分失败（data + errors 共存） | 渲染 data 可用部分 + errors 区域提示，不整体失败（ARB-001 §1.3 降级模式方案 B）                                 |

- BFF 始终返回 `ActionState` 信封（004 §11.5），前端统一走 `normalizeActionState()` 工具（packages/hooks）

## 5. 事件设计

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

### 5.1 WebSocket 推送（P5）

| 事件                    | 触发         | teacher-portal 前端动作                         |
| ----------------------- | ------------ | ----------------------------------------------- |
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1                  |
| `HomeworkSubmitted`     | 学生提交作业 | toast + urql cache invalidate 作业批改列表      |
| 角色变更（iam）         | iam Kafka    | urql cache invalidate 权限/视口（强制刷新 me Query） |

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

```
GET /api/v1/ai/generate-questions (SSE) 或 GraphQL Subscription（P6+ 评估）
data: {"delta": "题目"}\n\n
data: {"done": true}\n\n
```

前端用 `AsyncIterable<T>` 消费（非 GraphQL，TanStack Query 场景），Tiptap 逐字插入。

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

### 6.1 权限（前端等价，F7 命名 `<RESOURCE>_<ACTION>`）

| 路由                          | 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 维护，ai13 消费）。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。

### 6.2 错误码清单（前端 i18n 路由，对齐 [matrix.md §6](../../../docs/architecture/issues/matrix.md)）

| 前缀           | 来源服务    | 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 | `error.teacher_bff.{{code_snake}}` |
| `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_inp_seconds`  | INP  | 同上                                                |
| `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/，ai13 维护，ARB-002 暴露）

| 组件                                       | 用途                                                                                                | MF 暴露（ARB-002） |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------ |
| `AppShell`                                 | 左侧栏 + 主内容区布局                                                                               | ✅ `./AppShell`     |
| `RequirePermission`                        | L3 组件级视口控制（无权限不渲染 children）                                                          | ✅ `./RequirePermission` |
| `ErrorBoundary`                            | React 渲染异常兜底（fallback UI）                                                                   | ✅ `./ErrorBoundary` |
| `Loading`                                  | 骨架屏（Skeleton）                                                                                  | ✅ `./Loading`      |
| `Empty`                                    | 空态（插画 + 文案 + CTA）                                                                           | ✅ `./Empty`        |
| `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/，ai13 维护，ARB-002 暴露）

| Hook                  | 职责                                                | MF 暴露（ARB-002） |
| --------------------- | --------------------------------------------------- | ------------------ |
| `useGraphQLClient()`  | urql client 单例（Shell 注入，Remote 复用）         | ✅ `./useGraphQLClient` |
| `useAuth()`           | 会话状态（user/token/refresh/login/logout）         | ✅ `./useAuth`      |
| `usePermission()`     | 权限查询（hasPermission/hasAny/hasAll + dataScope） | ✅ `./usePermission` |
| `useViewports(scope)` | 视口列表（GraphQL viewports Query，按 scope 过滤）  | —                  |
| `useApi()`            | 非 GraphQL 场景（文件上传/SSE）ApiClient 实例       | —                  |
| `useA11yId()`         | 唯一 ARIA ID 生成                                   | —                  |
| `useAriaLive()`       | aria-live 区域管理                                  | —                  |
| `useToast()`          | 全局 toast（Zustand ui-store）                      | —                  |

## 9. 设计令牌三层（packages/ui-tokens/，ai13 维护）

```
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（GraphQL over HTTP） | `/api/v1/teacher/*` 代理到 teacher-bff `POST /graphql` | 全部 GraphQL 业务请求        | P2+  |
| 调用   | api-gateway  | HTTP               | `POST /api/auth/login`                              | 教师登录（非 MF，认证前提）  | P2   |
| 调用   | push-gateway | WebSocket          | `ws://push-gateway:8081/ws`                         | 实时推送                     | P5   |
| 调用   | ai           | SSE                | `GET /api/v1/ai/generate-questions`                 | AI 流式出题（非 GraphQL）    | P5   |
| 被调用 | —            | —                  | —                                                   | 前端不暴露接口给其他服务     | —    |
| 消费   | teacher-bff  | GraphQL（ARB-001 schema） | `dashboard`/`viewports`/`me`/`classes`/`class` Query（P2）+ P3+ Mutation | 教师场景聚合 | P2+  |
| 消费   | push-gateway | WebSocket          | `NotificationRequested`/`HomeworkSubmitted`/角色变更 | 实时推送                     | P5+  |
| 依赖   | coord 维护   | —                  | `packages/shared-proto`                             | TS 类型（仅 contracts 部分） | P1+  |
| 依赖   | coord 维护   | —                  | `packages/shared-ts` + `contracts/graphql/teacher-bff.graphql` | ApiClient/Logger/GraphQL schema/通用工具 | P2+  |
| 依赖   | ai13 维护    | —                  | `packages/ui-tokens`（已建，批次 0.15）              | 三层设计令牌                 | P2+  |
| 依赖   | ai13 维护    | —                  | `packages/ui-components`（已建，批次 0.15）         | shadcn + 共享组件（ARB-002 暴露） | P2+  |
| 依赖   | ai13 维护    | —                  | `packages/hooks`（已建，批次 0.15）                 | useGraphQLClient/usePermission/useAuth（ARB-002 暴露） | P2+  |
| 依赖   | coord 维护   | —                  | `packages/contracts`                                | Permissions 常量 + 类型       | P2+  |

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

## 11. 风险与假设

### 11.1 假设

1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**：包含 GraphQL schema、Logger、Permissions 常量、通用类型。✅ packages 骨架（ui-tokens/ui-components/hooks）已于批次 0.15 建立（ISSUE-039）。
2. **假设 teacher-bff 提供 `POST /graphql`（ARB-001 schema 第一版）**：返回 dashboard/viewports/me/classes/class 5 个 Query。⏳ 待 ai03 + coord 仲裁。
3. **假设 api-gateway 代理 `/api/v1/teacher/*` → teacher-bff:3003**：⏳ 待 ai01。
4. **假设 Next.js 14+ Module Federation 2.0 稳定**：`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定，降级为 4 端独立部署 + 各自 Shell（重复实现 AppShell）。
5. **假设 iam 提供 refresh cookie 端点**：P6 localStorage→httpOnly cookie 迁移前置。⏳ 待 ai06 P6。

### 11.2 技术风险

| 风险                     | 影响                                                        | 缓解                                                                               |
| ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| MF SSR 对齐复杂          | Remote 在 SSR 时需 Shell 提供上下文                         | 优先 CSR，SSR 仅用于首屏 dashboard；MF 2.0 支持 SSR                                |
| 共享依赖版本漂移         | Remote 与 Shell 的 react/react-dom/urql/graphql 版本不一致导致运行时错误 | MF `shared.singleton: true`（ARB-002）+ CI 检查版本对齐 |
| Token 刷新竞态           | 多请求同时 401 触发多次 refresh                             | useAuth 全局单例 + refresh promise 复用（03 §2.7）                                |
| 权限缓存陈旧             | 角色变更后前端仍用旧权限                                    | iam 角色变更发 Kafka → msg → push-gateway WebSocket → urql cache invalidate       |
| GraphQL schema 演进阻塞  | ai03 schema 变更未同步 ai13 导致前端 query 失效            | SDL-first + schema 注册表 `packages/shared-ts/contracts/graphql/`（ARB-001）      |
| 设计令牌迁移破坏现有样式 | teacher-portal 现有硬编码令牌迁移到三层模型后样式漂移      | 灰度迁移：先建 ui-tokens 包，teacher-portal 引入但不删除旧 globals.css，验证后切换 |
| urql cache 膨胀          | 长时间使用后缓存项过多                                      | staleTime 分级 + P3 评估 graphcache normalized cache                               |

### 11.3 已决策设计点（原未决决策，现已对齐仲裁）

| 决策点              | 结论                                                        | 裁决依据                        |
| ------------------- | ----------------------------------------------------------- | ------------------------------- |
| packages 归属        | ai13 维护 ui-tokens/ui-components/hooks；coord 维护 shared-ts/contracts | 总裁 §2.18（ISSUE-039）         |
| GraphQL vs REST     | **P2 起 all-in GraphQL（urql），无 REST 过渡**              | F9 + ARB-001（ISSUE-036/037）   |
| i18n key 命名       | `error.{{service}}.{{code_snake_case}}`，与错误码前缀对齐  | F11                             |
| MF 暴露粒度         | 暴露 AppShell 整体 + GraphQLProvider + hooks + UI 组件（ARB-002 清单） | 总裁 §2.17 + ARB-002（ISSUE-038） |
| GraphQL client 单例 | Shell 暴露 GraphQLProvider，Remote 复用（方案 A）            | 总裁 §2.17（ISSUE-038）         |
| P2 Remote 数量      | 0（NEXT_PUBLIC_MF_ENABLED=false），P3 student 首个接入      | ARB-002 §2.3                    |

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

### 12.1 端口矩阵

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

> 与 [matrix.md](../../../docs/architecture/issues/matrix.md) §1 端口矩阵对齐（teacher-portal :4000）。

### 12.2 依赖的共享包

| 包              | 路径                      | 维护方       | 内容                                              |
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
| `shared-ts`     | `packages/shared-ts/`     | coord        | ApiClient、Logger、GraphQL schema、通用工具       |
| `contracts`     | `packages/contracts/`     | coord        | Permissions 常量、ActionState 类型、UserInfo 类型 |
| `ui-tokens`     | `packages/ui-tokens/`     | ai13         | 三层设计令牌（✅ 已建，批次 0.15）                |
| `ui-components` | `packages/ui-components/` | ai13         | shadcn + ErrorBoundary + RequirePermission（✅ 已建，ARB-002 暴露） |
| `hooks`         | `packages/hooks/`         | ai13         | useGraphQLClient/usePermission/useAuth（✅ 已建，ARB-002 暴露） |

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

| 契约                                                               | 提供方             | 当前状态            |
| ------------------------------------------------------------------ | ------------------ | ------------------- |
| `POST /graphql` + dashboard/viewports/me/classes/class Query（ARB-001） | teacher-bff（ai03）| ⏳ 待 coord 仲裁    |
| `POST /api/auth/login`                                             | api-gateway（ai01）→ iam | ⏳ 待 ai01    |
| classExams/classHomework/studentGrades Query + Mutation             | teacher-bff（ai03 P3）| ⏳ 待 ai03 P3   |
| knowledgeGraph/studentAnalytics Query                               | teacher-bff（ai03 P4）| ⏳ 待 ai03 P4   |
| myNotifications/markAsRead Query + Mutation                         | teacher-bff（ai03 P5）| ⏳ 待 ai03 P5   |
| `ws://push-gateway:8081/ws` 推送                                   | push-gateway（ai02 P5）| ⏳ 待 ai02 P5 |
| `GET /api/v1/ai/generate-questions`（SSE）                         | ai（ai12 P5）      | ⏳ 待 ai12 P5        |

### 12.4 错误码前缀（前端 i18n 路由依赖，对齐 [matrix.md §6](../../../docs/architecture/issues/matrix.md)）

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

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

### 12.5 不产生 Kafka 事件

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

## 13. 实施路线（ai13 自用，详见 [workline.md](../../../docs/architecture/issues/worklines/teacher-portal_workline.md)）

### P2（MF Shell + GraphQL client + 基础页面，ARB-001/002）

1. ✅ packages 骨架（ui-tokens/ui-components/hooks，批次 0.15 已完成）
2. NextFederationPlugin 配置（ARB-002 exposes + shared，**无 remotes**）
3. GraphQLProvider + urql client 单例（Shell 暴露，§2.17 方案 A）
4. AppShell（Layout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层）
5. 登录页（`POST /api/auth/login` → JWT 存 localStorage，F12）
6. 权限上下文（usePermission + useViewports，权限点 `<RESOURCE>_<ACTION>` F7）
7. Dashboard 框架（`dashboard` GraphQL Query）+ 班级列表（`classes`/`class` Query）+ 学生列表 + 个人设置
8. 补 ErrorBoundary + /api/health route
9. 引入 ESLint flat config 自定义规则（no-hardcoded-fonts / design-tokens）
10. 补 Vitest 单测 + Playwright E2E（覆盖率 ≥ 80%）

### P3（考试/作业/成绩 + 乐观更新 + 多 Tab 同步）

1. 考试/作业/成绩管理页面（GraphQL Query + Mutation）
2. 乐观更新（urql mutation onMutate 回滚 + invalidate）
3. 多 Tab 会话同步（BroadcastChannel，03 §2.5）

### 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. localStorage → httpOnly Cookie 迁移
4. 性能优化（MF shared 单例验证、bundle 分析）

---

**AI Agent**: ai13 (teacher-portal shell)
**Branch**: feat-review-teacher-portal-docs-vErc0L
**Coordinator**: coord-ai
