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

> AI：ai07（TS/React · 教学场景域前端 shell）
> 阶段：阶段 1 交付物
> 日期：2026-07-09
> 关联：[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §5 ai13/ai07、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)、[known-issues §2.12](../../../docs/troubleshooting/known-issues.md)

---

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

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

**说明**：

- 通过 `next.config.js` 的 `rewrites` 将 `/api/v1/*` 代理到 `api-gateway`
- MF 架构下，teacher-portal 作为 Shell 宿主提供 AppShell + 共享组件库 + 权限 Hook + API 请求层
- 场景域 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 代理）

| 路径前缀                                                                 | 下游 BFF/服务            | 关键端点                                                                                                                                                   |
| ------------------------------------------------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/api/v1/iam/*`                                                          | iam                      | `POST /iam/login`、`POST /iam/register`、`POST /iam/refresh`、`GET /iam/me`、`GET /iam/rbac/...`、`GET /iam/effective-permissions`                         |
| `/api/v1/teacher/*`                                                      | teacher-bff              | `GET /teacher/viewports`、`GET /teacher/dashboard`、`GET /teacher/classes/:id/exams`、`GET /teacher/classes/:id/homework`、`GET /teacher/exams/:id/grades` |
| `/api/v1/classes/*`                                                      | core-edu（classes 模块） | CRUD（黄金模板）                                                                                                                                           |
| `/api/v1/exams/*` `/api/v1/homework/*` `/api/v1/grades/*`                | core-edu                 | P3 教学核心                                                                                                                                                |
| `/api/v1/textbooks/*` `/api/v1/knowledge-points/*` `/api/v1/questions/*` | content                  | P4 内容                                                                                                                                                    |
| `/api/v1/ai/*`                                                           | ai（SSE 流式）           | P5 AI 辅助出题                                                                                                                                             |
| `/api/v1/notifications/*`                                                | msg                      | 通知中心（P5）                                                                                                                                             |

### 3.2 统一响应契约

所有后端响应遵循 `ActionState` 结构（迁移指南 §7.5）：

```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_`）。前端 API 请求层根据 `error.code` 前缀路由到对应的 i18n key。

### 3.3 推送契约（P5）

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

### 3.4 proto 不直接消费

前端不调用 gRPC，BFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `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                         |
| 样式                        | Tailwind CSS 3.4+                                        | 配合设计令牌三层模型                           |
| UI 组件库                   | shadcn/ui（迁移指南 §7.2）                               | 平移至 `packages/ui-components/`，MF 共享      |
| 状态管理 L1 URL             | nuqs                                                     | 可分享、可刷新状态                             |
| 状态管理 L2 Server          | TanStack Query v5                                        | 服务端数据缓存、重试、乐观更新                 |
| 状态管理 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                                    |
| 字体                        | Inter（sans）/ Fraunces（serif）/ JetBrains Mono（mono） | next/font/google 加载，CSS 变量暴露            |

## 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**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
