Files
Edu/apps/teacher-portal/docs/02-architecture-design.md

618 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块架构设计文档 — teacher-portal
> AIai13TS/React · 教学场景域前端 shell
> 阶段:阶段 2 交付物
> 日期2026-07-092026-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)
> 状态:✅ 已对齐 F9GraphQL P2 起)+ ARB-001schema 第一版)+ ARB-002MF Shell 暴露清单)
---
## 1. 模块内部分层图4 端统一 MF + GraphQL 架构)
```mermaid
graph TB
subgraph Browser["浏览器"]
URL[URL 路由]
end
subgraph Shell["teacher-portalShell 宿主 :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-portalRemoteP3+"]
StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
end
subgraph RemoteParent["parent-portalRemoteP4+"]
ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
end
subgraph RemoteAdmin["admin-portalRemoteP6+"]
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 /graphqlARB-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-portalARB-002 §2.2
- RootLayout字体、设计令牌、i18n Provider、GraphQLProvider、Zustand StoreProvider
- AppShell左侧导航 + 主内容区 + 用户信息 + 登出 + 路由守卫)
- GraphQLProviderurql client 单例,总裁 §2.17 方案 ARemote 复用)
- 共享依赖暴露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.jsARB-002 §2.2 裁决)
```javascript
// teacher-portal/next.config.jsShellARB-002 第一版暴露清单)
const NextFederationPlugin = require("@module-federation/nextjs-mf");
// P2remotes 为空NEXT_PUBLIC_MF_ENABLED=falseARB-002 §2.3P3+ 逐步接入
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 GraphQLF9 裁决)。
### 2.1 会话状态Session
```typescript
interface Session {
user: UserInfo; // { id, email, name, roles, dataScope }GraphQL me Query
tokens: { accessToken: string; refreshToken: string }; // F12: localStorageP6 迁移 cookie
viewports: ViewportItem[]; // L1 导航视口GraphQL viewports Query
permissions: string[]; // 权限点GraphQL me Query 内嵌或 effective-permissions
expiresAt: number; // access token 过期时间戳
}
```
存储Zustand sessionSliceL3+ localStorage 持久化刷新恢复F12+ urql cacheL2GraphQL document cache
### 2.2 视口模型Viewport
```typescript
// 对齐 ARB-001 §1.2 schemaViewportItem
interface ViewportItem {
key: string; // 'dashboard' | 'classes' | ...
label: string; // i18n key 或显式文案
route: String; // '/teacher/dashboard'schema 为 String
icon: String | null; // 图标 keyschema 可空)
sortOrder: String; // 排序schema 为 String
requiredPermission: String | null; // 'CLASSES_READ' 等
}
```
来源GraphQL `viewports` QueryARB-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 | ALLARB-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 | 失效策略 |
| ----------------------- | ----------------------------- | ---------------------------------- | --------------------------------------------- |
| Sessiontoken + user | localStorage + ZustandF12 | access 15min / refresh 7day | 401 自动 refreshrefresh 失败跳登录 |
| 权限列表 + 视口 | urql cacheExchange | 0always 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 cache03 §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 GraphQLARB-001 schema 第一版)。
### 4.1 urql client 单例packages/hooks + Shell GraphQLProvider
```typescript
// packages/hooks/src/use-graphql-client.tsShell 暴露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 注入给所有 RemoteARB-002 方案 A
- token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链,见 [03 §2.7](./03-long-term-architecture.md)
- MF shared singletonreact/urql/graphql避免 Remote 多实例 + 缓存不一致
- 请求/响应 trace_id 透传(`X-Trace-Id` header
### 4.2 GraphQL Query/Mutation 约定(对齐 ARB-001 schema
```typescript
// P2 QueryARB-001 §1.25 个 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+ MutationARB-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 errorsARB-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 EventsAI 流式)。
### 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 SubscriptionP6+ 评估)
data: {"delta": "题目"}\n\n
data: {"done": true}\n\n
```
前端用 `AsyncIterable<T>` 消费(非 GraphQLTanStack 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 + 结构化;生产环境 → SentryP6
// 必含字段trace_id从响应头提取、user_id、scope、path
```
### 6.4 MetricsWeb 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 TracerOTel browser SDKP6
```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` | 全局 ModalModalRoot + 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. 共享 Hookspackages/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 场景(文件上传/SSEApiClient 实例 | — |
| `useA11yId()` | 唯一 ARIA ID 生成 | — |
| `useAriaLive()` | aria-live 区域管理 | — |
| `useToast()` | 全局 toastZustand 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 | HTTPGraphQL 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 | GraphQLARB-001 schema | `dashboard`/`viewports`/`me`/`classes`/`class` QueryP2+ 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/useAuthARB-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 提供上下文 | 优先 CSRSSR 仅用于首屏 dashboardMF 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/hookscoord 维护 shared-ts/contracts | 总裁 §2.18ISSUE-039 |
| GraphQL vs REST | **P2 起 all-in GraphQLurql无 REST 过渡** | F9 + ARB-001ISSUE-036/037 |
| i18n key 命名 | `error.{{service}}.{{code_snake_case}}`,与错误码前缀对齐 | F11 |
| MF 暴露粒度 | 暴露 AppShell 整体 + GraphQLProvider + hooks + UI 组件ARB-002 清单) | 总裁 §2.17 + ARB-002ISSUE-038 |
| GraphQL client 单例 | Shell 暴露 GraphQLProviderRemote 复用(方案 A | 总裁 §2.17ISSUE-038 |
| P2 Remote 数量 | 0NEXT_PUBLIC_MF_ENABLED=falseP3 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 QueryARB-001 | teacher-bffai03| ⏳ 待 coord 仲裁 |
| `POST /api/auth/login` | api-gatewayai01→ iam | ⏳ 待 ai01 |
| classExams/classHomework/studentGrades Query + Mutation | teacher-bffai03 P3| ⏳ 待 ai03 P3 |
| knowledgeGraph/studentAnalytics Query | teacher-bffai03 P4| ⏳ 待 ai03 P4 |
| myNotifications/markAsRead Query + Mutation | teacher-bffai03 P5| ⏳ 待 ai03 P5 |
| `ws://push-gateway:8081/ws` 推送 | push-gatewayai02 P5| ⏳ 待 ai02 P5 |
| `GET /api/v1/ai/generate-questions`SSE | aiai12 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)
### P2MF 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. AppShellLayout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层)
5. 登录页(`POST /api/auth/login` → JWT 存 localStorageF12
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 会话同步BroadcastChannel03 §2.5
### P5推送 + AI 接入)
1. teacher-portal 接入 WebSocketpush-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