# 模块理解确认书 — teacher-portal

> AI：ai13（TS/React · 教学场景域前端 shell）
> 阶段：阶段 1 交付物
> 日期：2026-07-09（2026-07-10 审查回写：对齐 F9 GraphQL P2 起 + ARB-001/002 仲裁）
> 关联：[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §5 ai13、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)、[known-issues §2.12](../../../docs/troubleshooting/known-issues.md)、[coord 仲裁 ARB-001/002](../../../docs/architecture/issues/coord.md)

---

## 1. 我在架构中的位置

- **层级**：L2 微前端层（004 §3.1 六层架构中的前端层）
- **MF 角色**：**Shell 宿主**（主应用），其余 3 端（student/parent/admin-portal）作为 Remote 子应用挂载
- **上游（谁调用我）**：浏览器（教师 / 教导主任 / 教研组长）
- **下游（同步）**：api-gateway（经 Next.js `rewrites` 代理 `/api/v1/*`），再反向代理到 teacher-bff `POST /graphql`
- **下游（推送，P5）**：push-gateway（WebSocket/SSE）
- **BFF 对接**：teacher-bff（GraphQL Yoga + DataLoader，P2 起 all-in GraphQL，F9 裁决，无 REST 过渡）
- **通信方式**：GraphQL over HTTP（前端→Gateway→teacher-bff）+ WebSocket（前端→push-gateway，P5）+ SSE（前端→ai 服务，P5）
- **不直连**：前端不直连任何业务服务或 BFF 后端实例，全部经 api-gateway 代理

**说明**：

- 通过 `next.config.js` 的 `rewrites` 将 `/api/v1/*` 代理到 `api-gateway`，api-gateway 再反向代理到 teacher-bff GraphQL endpoint
- F9 裁决：P2 起 BFF 用 GraphQL（Yoga + DataLoader），前端用 urql client，**无 REST 过渡阶段**（详见 [03-long-term-architecture.md §1.4](./03-long-term-architecture.md)）
- MF 架构下，teacher-portal 作为 Shell 宿主提供 AppShell + GraphQLProvider 单例 + 共享组件库 + 权限 Hook（ARB-002 暴露清单）
- 场景域 BFF 复用策略（004 §5.4）：教导主任/教研组长复用 teacher-portal + 额外管理视口，不单独建 portal

## 2. 我的限界上下文

### 2.1 我负责的聚合 / 实体（前端视图模型）

- 班级、考试、作业、成绩、备课、AI 出题（教学场景域前端视图）
- 会话状态（Session）、视口（Viewport）、权限（Permission）

### 2.2 业务领域

- **D3 教学核心领域**（前端场景域：教学场景域）

### 2.3 不负责

- 学生作答界面（归 student-portal）
- 家长多子女切换（归 parent-portal）
- 用户/角色/权限 CRUD（归 admin-portal）

### 2.4 数据范围

- DataScope L1-L5（教师 L1 班级 / 教导主任 L2 年级 / 校管理员 L3 学校 / 区教研员 L4 / 系统管理员 L5）

## 3. 我与外部的契约

### 3.1 消费的后端 API（经 api-gateway 代理）

F9 裁决：P2 起 all-in GraphQL，前端不再消费 REST 端点（登录除外）。teacher-bff GraphQL endpoint `POST /graphql`（经 api-gateway `/api/v1/teacher/*` 代理），schema 详见 [coord ARB-001](../../../docs/architecture/issues/coord.md#§1) + [teacher-portal_contract.md §2.4](../../../docs/architecture/issues/contracts/teacher-portal_contract.md)。

| 阶段 | 消费方式                                       | 下游 BFF/服务            | 关键 GraphQL Operation（P2 Must Have 加粗）                                                                                                        |
| ---- | ---------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| P2   | GraphQL Query（teacher-bff，ARB-001 第一版）   | teacher-bff → iam/classes | **`dashboard`**、**`viewports`**、**`me`**、**`classes`**、**`class(id)`**（5 个 Query，ARB-001 §1.2）                                             |
| P2   | HTTP（登录，MF 依赖认证前提，非 GraphQL）       | api-gateway → iam        | `POST /api/auth/login`（F12：JWT 存 localStorage，P6 迁移 httpOnly cookie）                                                                         |
| P3   | GraphQL Query + Mutation                       | teacher-bff → core-edu    | `classExams` / `createExam` / `classHomework` / `assignHomework` / `studentGrades` / `recordGrade`                                                 |
| P4   | GraphQL Query                                  | teacher-bff → content/data-ana | `knowledgeGraph` / `studentAnalytics`                                                                                                              |
| P5   | GraphQL Query + Mutation + SSE/WS              | teacher-bff → msg/ai + push-gateway | `myNotifications` / `markAsRead` / `generateQuestion`（SSE 流式）/ `generateLessonPlan`                                                            |

> P2 不包含：exams/homework/grades Query（P3 core-edu 就绪后）、classPerformance/studentWeakness/learningTrend（P4 data-ana）、notifications（P5 msg）、所有 Mutation（P3+）、Subscription（P6+ 评估）——见 ARB-001 §1.2。

### 3.2 统一响应契约（ActionState 信封 + GraphQL errors）

BFF GraphQL 始终返回 `ActionState` 信封（004 §11.5），GraphQL `errors[]` 数组扩展 ActionState 字段（`extensions.code` = `BFF_TEACHER_*`，G14 裁决）：

```typescript
type ActionState<T> =
  | { success: true; data: T }
  | {
      success: false;
      error: { code: string; message: string; details?: unknown };
    };
```

错误码前缀按服务名大写（如 `IAM_`、`CORE_EDU_`、`CONTENT_`、`MSG_`、`AI_`、`BFF_TEACHER_`、`GW_`，见 [matrix.md §6](../../../docs/architecture/issues/matrix.md)）。前端 urql client 解析 `errors[].extensions.code` 前缀路由到对应 i18n key（详见 [03 §1.4.3](./03-long-term-architecture.md)）。

### 3.3 推送契约（P5）

| 协议                      | 场景                                         |
| ------------------------- | -------------------------------------------- |
| WebSocket（push-gateway） | 学生提交作业通知、考试成绩录入提醒、全校广播 |
| SSE（ai 服务，经 teacher-bff 代理或直连） | AI 辅助出题流式响应                          |

### 3.4 proto 不直接消费

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

## 4. 我的技术栈

| 维度                        | 选型                                                     | 说明                                           |
| --------------------------- | -------------------------------------------------------- | ---------------------------------------------- |
| 框架                        | Next.js 14+（App Router）                                | server components 默认，client components 按需 |
| 语言                        | TypeScript 5.5+（strict）                                | 沿用 tsconfig.base.json                        |
| 微前端                      | Module Federation 2.0（@module-federation/nextjs-mf）    | teacher-portal = Shell（ARB-002 暴露清单）     |
| **GraphQL client**          | **urql**（F9 裁决 P2 起，shell 单例，ARB-002 shared singleton） | cacheExchange + fetchExchange，详见 [03 §1.4](./03-long-term-architecture.md) |
| 样式                        | Tailwind CSS 3.4+                                        | 配合设计令牌三层模型                           |
| UI 组件库                   | shadcn/ui（迁移指南 §7.2）                               | 平移至 `packages/ui-components/`，MF 共享      |
| 状态管理 L1 URL             | nuqs                                                     | 可分享、可刷新状态                             |
| 状态管理 L2 Server          | urql cacheExchange（GraphQL 数据）+ TanStack Query（非 GraphQL 场景：文件上传/SSE） | F9 起不再双缓存层，详见 [03 §1.4.4](./03-long-term-architecture.md) |
| 状态管理 L3 Client Business | Zustand slice                                            | 客户端业务状态                                 |
| 状态管理 L4 Global UI       | Zustand ui-store + ModalRoot                             | 全局 UI 状态                                   |
| 状态管理 L5 Form            | react-hook-form + zodResolver                            | 表单状态                                       |
| 富文本                      | Tiptap（备课、出题、反馈）                               | SSR 安全                                       |
| 图表                        | recharts                                                 | 学情、Dashboard                                |
| i18n                        | next-intl                                                | BFF/服务返回 i18n key + 参数，前端翻译         |
| A11y                        | eslint-plugin-jsx-a11y（error 级）                       | WCAG 2.2 AA                                    |
| 字体                        | sans/serif/mono（通过 `var(--font-family-*)` 引用，禁止字面量） | next/font/google 加载，CSS 变量暴露（§3.10 强制） |

## 5. 我的阶段归属

- **阶段**：P2
- **当前状态**：✅ 已实现 P1 测试页 + P2 骨架（登录/AppShell/Dashboard/classes CRUD）；⚠️ 待审计对齐黄金模板 + 引入 MF + 共享组件库
- **依赖上游阶段**：P1（api-gateway + classes + iam）

## 6. 我需要对齐的黄金模板项（对照 classes 服务）

> 前端无 `@RequirePermission` 装饰器（后端概念），对齐项改造为前端等价物。

| 对齐项                | classes（后端黄金模板）               | teacher-portal 前端等价                                                  | 当前状态                                     |
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------- |
| 权限校验              | `@RequirePermission(Permissions.XXX)` | `usePermission().hasPermission("XXX")` Hook + `<RequirePermission>` 组件 | ❌ 缺失，直接硬编码 `user.roles.join(", ")`  |
| 错误码前缀统一        | `CLASSES_*`、`IAM_*`                  | API 请求层根据 `error.code` 前缀路由 i18n                                | ❌ 缺失统一请求层                            |
| logger                | pino                                  | 前端 console + Sentry（P6）                                              | ⚠️ 仅 console.error                          |
| metrics               | prom-client `/metrics`                | 前端 Web Vitals → Gateway 上报                                           | ❌ 缺失                                      |
| tracer                | OTel SDK                              | 前端 OTel browser SDK（P6）                                              | ❌ 缺失                                      |
| /healthz + /readyz    | `GET /healthz` `GET /readyz`          | Next.js `/api/health` route + Dockerfile HEALTHCHECK                     | ⚠️ Dockerfile 有 HEALTHCHECK，无 /api/health |
| 优雅关闭              | SIGTERM handler                       | Next.js 无长连接，无需                                                   | ✅ N/A                                       |
| 测试覆盖率 ≥ 80%      | Vitest                                | Vitest + @testing-library/react + Playwright E2E                         | ❌ 0%                                        |
| Dockerfile 多阶段构建 | builder + runtime                     | 已有多阶段                                                               | ✅ 已对齐                                    |
| Zod 输入验证          | class-validator + Zod schema          | react-hook-form + zodResolver                                            | ❌ 缺失                                      |
| GlobalErrorFilter     | NestJS 全局异常过滤器                 | React ErrorBoundary + API 请求层统一错误处理                             | ❌ 缺失                                      |
| 设计令牌三层          | —                                     | primitive.css / semantic-light/dark.css / tailwind-theme.css             | ❌ 硬编码在 globals.css + tailwind.config.js |
| A11y 工具集           | —                                     | useA11yId / mergeA11yProps / describeInput / focus-trap                  | ❌ 缺失                                      |

---

## 附：teacher-portal 现状审计（对齐黄金模板）

### 审计表

| 维度                                 | 状态   | 说明                                                                   |
| ------------------------------------ | ------ | ---------------------------------------------------------------------- |
| 权限装饰器（前端等价 usePermission） | ❌     | AppShell.tsx 直接 `user.roles.join(", ")`，违反 project_rules §3.8     |
| 错误码前缀                           | ❌     | 无统一 API 请求层，错误处理散落在每个 page.tsx                         |
| logger                               | ⚠️     | 仅 `console.error`，无结构化、无 trace_id                              |
| metrics                              | ❌     | 无 Web Vitals 采集                                                     |
| tracer                               | ❌     | 无 OTel browser SDK                                                    |
| /healthz                             | ⚠️     | Dockerfile 有 `HEALTHCHECK wget /`，但无 `/api/health` route           |
| /readyz                              | ❌     | 无                                                                     |
| 优雅关闭                             | ✅ N/A | Next.js 无长连接                                                       |
| 测试覆盖率                           | ❌     | 0%，无测试文件                                                         |
| Dockerfile 多阶段                    | ✅     | builder + runtime，非 root 用户，HEALTHCHECK                           |
| Zod 输入验证                         | ❌     | 表单直接 useState，无 zodResolver                                      |
| GlobalErrorFilter（ErrorBoundary）   | ❌     | 无 React ErrorBoundary                                                 |
| 设计令牌三层                         | ❌     | 硬编码在 globals.css（`:root` 变量）+ tailwind.config.js（hex 字面量） |
| A11y 工具集                          | ❌     | 无 useA11yId、focus-trap 等                                            |
| Module Federation 配置               | ❌     | next.config.js 仅有 rewrites，无 MF                                    |
| 5 层状态管理                         | ❌     | 仅 useState + localStorage，无 nuqs/TanStack Query/Zustand             |
| 共享组件库                           | ❌     | 仅 AppShell，无 ErrorBoundary/Loading/Empty/RequirePermission          |
| i18n                                 | ❌     | 中文硬编码在 JSX                                                       |
| API 请求层                           | ❌     | 每页重复 fetch + authHeaders + try/catch                               |
| ESLint flat config 自定义规则        | ❌     | 未配置 no-hardcoded-fonts / design-tokens 规则                         |

### 现有文件清单

```
apps/teacher-portal/
├─ src/
│  ├─ app/
│  │  ├─ (app)/                    # 受保护路由组（套 AppShell）
│  │  │  ├─ classes/page.tsx       # 班级 CRUD（P1 测试页）
│  │  │  ├─ dashboard/page.tsx     # 教师仪表盘
│  │  │  ├─ exams/page.tsx         # 考试列表
│  │  │  ├─ grades/page.tsx        # 成绩查询
│  │  │  ├─ homework/page.tsx      # 作业列表
│  │  │  └─ layout.tsx             # 套 AppShell
│  │  ├─ login/page.tsx            # 登录页（不套壳）
│  │  ├─ globals.css               # 全局样式 + 设计令牌（硬编码）
│  │  ├─ layout.tsx                # 根布局（字体加载）
│  │  └─ page.tsx                  # 根路径重定向
│  ├─ components/
│  │  └─ AppShell.tsx              # 左侧栏 + 主内容区
│  └─ lib/
│     └─ auth.ts                   # token + userInfo localStorage 管理
├─ Dockerfile                      # 多阶段构建 ✅
├─ next.config.js                  # 仅 rewrites，无 MF ❌
├─ package.json                    # 仅 next/react/react-dom，无 MF/Query/Zustand ❌
├─ tailwind.config.js              # 硬编码 hex ❌
├─ postcss.config.js
└─ tsconfig.json
```

### 主要违规点（必须在 P2 收尾或 P3 起步时修复）

1. **权限硬编码**：[AppShell.tsx:143](../src/components/AppShell.tsx) `user.roles.join(", ")` 违反 project_rules §3.8，必须改为 `usePermission().hasPermission()`
2. **设计令牌硬编码**：[globals.css:6-13](../src/app/globals.css) 与 [tailwind.config.js:7-19](../tailwind.config.js) 出现 `hsl(...)` 字面量与 `'Fraunces'`/`'Inter'` 字面量，违反 project_rules §3.10
3. **无统一 API 请求层**：4 个 page.tsx 重复 `authHeaders()` + `fetch` + `try/catch` + `setError`，必须抽取到 `lib/api.ts`
4. **无权限 Hook**：缺少 `usePermission().hasPermission()`，无法做 L3 组件级视口控制
5. **无 ErrorBoundary**：React 渲染异常会白屏
6. **无 5 层状态管理**：登录态用 localStorage（L3），但无 TanStack Query（L2）导致每页重复 fetch
7. **字体名硬编码**：[layout.tsx:3-7](../src/app/layout.tsx) 直接 import `Inter/Fraunces/JetBrains_Mono`，应改为 `var(--font-family-sans/serif/mono)`

---

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