Merge worktree branch merge-15-modules-to-main-5ug5xJ

This commit is contained in:
SpecialX
2026-07-10 15:28:20 +08:00
parent 60d7173545
commit df62ffc176
51 changed files with 11559 additions and 1908 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
# 模块理解确认书 — teacher-portal
> AIai07TS/React · 教学场景域前端 shell
> AIai13TS/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)
> 日期2026-07-092026-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)
---
@@ -12,16 +12,17 @@
- **层级**L2 微前端层004 §3.1 六层架构中的前端层)
- **MF 角色****Shell 宿主**(主应用),其余 3 端student/parent/admin-portal作为 Remote 子应用挂载
- **上游(谁调用我)**:浏览器(教师 / 教导主任 / 教研组长)
- **下游(同步)**api-gatewayREST经 Next.js `rewrites` 代理 `/api/v1/*`
- **下游(同步)**api-gateway经 Next.js `rewrites` 代理 `/api/v1/*`,再反向代理到 teacher-bff `POST /graphql`
- **下游推送P5**push-gatewayWebSocket/SSE
- **BFF 对接**teacher-bffGraphQL Yoga + DataLoaderP2-P3 用 REST 过渡)
- **通信方式**HTTP/REST前端→Gateway+ WebSocket前端→push-gatewayP5
- **BFF 对接**teacher-bffGraphQL Yoga + DataLoaderP2 起 all-in GraphQLF9 裁决,无 REST 过渡)
- **通信方式**GraphQL over HTTP前端→Gateway→teacher-bff+ WebSocket前端→push-gatewayP5+ SSE前端→ai 服务P5
- **不直连**:前端不直连任何业务服务或 BFF 后端实例,全部经 api-gateway 代理
**说明**
- 通过 `next.config.js``rewrites``/api/v1/*` 代理到 `api-gateway`
- MF 架构下teacher-portal 作为 Shell 宿主提供 AppShell + 共享组件库 + 权限 Hook + API 请求层
- 通过 `next.config.js``rewrites``/api/v1/*` 代理到 `api-gateway`api-gateway 再反向代理到 teacher-bff GraphQL endpoint
- F9 裁决P2 起 BFF 用 GraphQLYoga + DataLoader前端用 urql client**无 REST 过渡阶段**(详见 [03-long-term-architecture.md §1.4](./03-long-term-architecture.md)
- MF 架构下teacher-portal 作为 Shell 宿主提供 AppShell + GraphQLProvider 单例 + 共享组件库 + 权限 HookARB-002 暴露清单)
- 场景域 BFF 复用策略004 §5.4):教导主任/教研组长复用 teacher-portal + 额外管理视口,不单独建 portal
## 2. 我的限界上下文
@@ -49,19 +50,21 @@
### 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-educlasses 模块) | 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/*` | aiSSE 流式) | P5 AI 辅助出题 |
| `/api/v1/notifications/*` | msg | 通知中心P5 |
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)。
### 3.2 统一响应契约
| 阶段 | 消费方式 | 下游 BFF/服务 | 关键 GraphQL OperationP2 Must Have 加粗) |
| ---- | ---------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| P2 | GraphQL Queryteacher-bffARB-001 第一版) | teacher-bff → iam/classes | **`dashboard`**、**`viewports`**、**`me`**、**`classes`**、**`class(id)`**5 个 QueryARB-001 §1.2 |
| P2 | HTTP登录MF 依赖认证前提,非 GraphQL | api-gateway → iam | `POST /api/auth/login`F12JWT 存 localStorageP6 迁移 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` |
所有后端响应遵循 `ActionState` 结构(迁移指南 §7.5
> P2 不包含exams/homework/grades QueryP3 core-edu 就绪后、classPerformance/studentWeakness/learningTrendP4 data-ana、notificationsP5 msg、所有 MutationP3+、SubscriptionP6+ 评估)——见 ARB-001 §1.2。
### 3.2 统一响应契约ActionState 信封 + GraphQL errors
BFF GraphQL 始终返回 `ActionState` 信封004 §11.5GraphQL `errors[]` 数组扩展 ActionState 字段(`extensions.code` = `BFF_TEACHER_*`G14 裁决):
```typescript
type ActionState<T> =
@@ -72,18 +75,18 @@ type ActionState<T> =
};
```
错误码前缀按服务名大写(如 `IAM_``CORE_EDU_``CONTENT_``MSG_``AI_``BFF_TEACHER_``GW_`)。前端 API 请求层根据 `error.code` 前缀路由到对应 i18n key。
错误码前缀按服务名大写(如 `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
| 协议 | 场景 |
| ------------------------- | -------------------------------------------- |
| WebSocketpush-gateway | 学生提交作业通知、考试成绩录入提醒、全校广播 |
| SSEai 服务 | AI 辅助出题流式响应 |
| SSEai 服务,经 teacher-bff 代理或直连) | AI 辅助出题流式响应 |
### 3.4 proto 不直接消费
前端不调用 gRPCBFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
前端不调用 gRPCBFF 把 gRPC 聚合为 GraphQL 暴露给前端F9 裁决)。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 4. 我的技术栈
@@ -91,11 +94,12 @@ type ActionState<T> =
| --------------------------- | -------------------------------------------------------- | ---------------------------------------------- |
| 框架 | 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 |
| 微前端 | Module Federation 2.0@module-federation/nextjs-mf | teacher-portal = ShellARB-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 | TanStack Query v5 | 服务端数据缓存、重试、乐观更新 |
| 状态管理 L2 Server | urql cacheExchangeGraphQL 数据)+ 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 | 表单状态 |
@@ -103,7 +107,7 @@ type ActionState<T> =
| 图表 | recharts | 学情、Dashboard |
| i18n | next-intl | BFF/服务返回 i18n key + 参数,前端翻译 |
| A11y | eslint-plugin-jsx-a11yerror 级) | WCAG 2.2 AA |
| 字体 | Intersans/ Frauncesserif/ JetBrains Monomono | next/font/google 加载CSS 变量暴露 |
| 字体 | sans/serif/mono通过 `var(--font-family-*)` 引用,禁止字面量 | next/font/google 加载CSS 变量暴露§3.10 强制) |
## 5. 我的阶段归属
@@ -201,5 +205,5 @@ apps/teacher-portal/
---
**AI Agent**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
**AI Agent**: ai13 (teacher-portal shell)
**Branch**: feat-review-teacher-portal-docs-vErc0L

View File

@@ -1,14 +1,14 @@
# 模块架构设计文档 — teacher-portal
> AIai07TS/React · 教学场景域前端 shell
> AIai13TS/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 交叉审查
> 日期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 架构)
## 1. 模块内部分层图4 端统一 MF + GraphQL 架构)
```mermaid
graph TB
@@ -16,26 +16,27 @@ graph TB
URL[URL 路由]
end
subgraph Shell["teacher-portalShell 宿主)"]
AppShell[AppShell<br/>左栏导航 + 主内容区]
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/@tanstack/react-query/zustand/nuqs"]
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-portalRemote"]
subgraph RemoteStudent["student-portalRemoteP3+"]
StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
end
subgraph RemoteParent["parent-portalRemote"]
subgraph RemoteParent["parent-portalRemoteP4+"]
ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
end
subgraph RemoteAdmin["admin-portalRemote"]
subgraph RemoteAdmin["admin-portalRemoteP6+"]
AdminPages[管理场景页面<br/>users/roles/permissions/viewports/monitoring]
end
@@ -43,22 +44,27 @@ graph TB
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/>通用工具]
Hooks[hooks<br/>usePermission/useAuth/useGraphQLClient]
LibTS[shared-ts<br/>GraphQL schema + 通用工具]
end
subgraph Gateway["api-gateway"]
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 --> AppShell
RootLayout --> GraphQLProvider
GraphQLProvider --> AppShell
AppShell --> Router
Router -->|动态加载| RemoteTeacher
Router -->|动态加载| RemoteStudent
Router -->|动态加载| RemoteParent
Router -->|动态加载| RemoteAdmin
Router -->|动态加载 P3+| RemoteStudent
Router -->|动态加载 P4+| RemoteParent
Router -->|动态加载 P6+| RemoteAdmin
RemoteTeacher --> SharedDeps
RemoteStudent --> SharedDeps
@@ -74,41 +80,43 @@ graph TB
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
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 重复,登录态/权限/组件库要重复实现 |
| 单 Shell + 4 Remote**采用** | ✅ | teacher-portal 作为 Shell 宿主,提供 AppShell + 共享依赖;其余 3 端作为 Remote 动态加载 |
| 4 端独立部署 + 独立域名 + 各自 Shell | ❌ | 4 套 Shell 重复,登录态/权限/组件库/GraphQL client 要重复实现 |
| 单 Shell + 4 Remote**采用** | ✅ | teacher-portal 作为 Shell 宿主,提供 AppShell + GraphQLProvider 单例 + 共享依赖;其余 3 端作为 Remote 动态加载 |
| 单一 Next.js 应用 + 4 路由组 | ❌ | 违反"微前端独立部署"目标ADR-012 |
**Shell 职责**teacher-portal
**Shell 职责**teacher-portalARB-002 §2.2
- RootLayout字体、设计令牌、i18n Provider、TanStack QueryClientProvider、Zustand StoreProvider
- AppShell左侧导航 + 主内容区 + 用户信息 + 登出)
- 共享依赖暴露react、react-dom、@tanstack/react-query、zustand、nuqs、ui-components、ui-tokens、contracts、hooks
- 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/*`
- 登录页(统一登录入口,按角色重定向到对应 portal
- 登录页(统一登录入口 `POST /api/auth/login`,非 MF登录是认证前提
### 1.2 MF 配置teacher-portal/next.config.js
### 1.2 MF 配置teacher-portal/next.config.jsARB-002 §2.2 裁决
```javascript
// teacher-portal/next.config.jsShell
// teacher-portal/next.config.jsShellARB-002 第一版暴露清单
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`,
});
// 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,
@@ -117,17 +125,28 @@ module.exports = {
new NextFederationPlugin({
name: "teacher_app",
filename: "static/chunks/remoteEntry.js",
remotes: remotes(isServer),
remotes: remotes(isServer), // P2 为 {}ARB-002 §2.3
exposes: {
"./AppShell": "./src/components/AppShell",
"./shared-deps": "./src/shared/deps",
// 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" },
"@tanstack/react-query": { singleton: true },
zustand: { singleton: true },
nuqs: { singleton: true },
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 },
}),
@@ -145,154 +164,181 @@ module.exports = {
};
```
> Remote 端配置对称:`name: 'student_app'``exposes: { './pages': './src/pages' }``remotes: { teacher: 'teacher_app@...' }`
> **P2 Remote 数量 = 0**ARB-002 §2.3`NEXT_PUBLIC_MF_ENABLED=false` 默认关。P3 student-portal 作为首个 Remote 接入(总裁 §1.3 step 4。feature flag 控制回退单体路由
## 2. 领域模型(前端视角)
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。数据来源统一为 teacher-bff GraphQLF9 裁决)。
### 2.1 会话状态Session
```typescript
interface Session {
user: UserInfo; // { id, email, name, roles, permissions, dataScope }
tokens: { accessToken: string; refreshToken: string };
viewports: ViewportItem[]; // L1 导航视口
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 持久化(刷新恢复+ TanStack Query 缓存 `['session']`L2)。
存储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'
icon: string | null; // 图标 key按需
sortOrder: number; // 排序
requiredPermission: string | null; // 'CLASSES_READ' 等
scope: "teacher" | "student" | "parent" | "admin"; // 标记归属哪个 portal
route: String; // '/teacher/dashboard'schema 为 String
icon: String | null; // 图标 keyschema 可空
sortOrder: String; // 排序schema 为 String
requiredPermission: String | null; // 'CLASSES_READ' 等
}
```
来源:`GET /api/v1/{scope}/viewports`BFF 聚合 iam 视口配置。AppShell 按 `scope` 过滤渲染对应 portal 的导航。
来源:GraphQL `viewports` QueryARB-001 §1.2。AppShell 按 `requiredPermission` + `usePermission().hasPermission()` 过滤渲染导航。
### 2.3 权限模型Permission
```typescript
interface PermissionState {
permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]
dataScope: DataScope; // L0-L5
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;
}
```
来源:`GET /api/v1/iam/effective-permissions``{ permissions, viewports, dataScope }`。Redis 缓存 5miniam 侧),前端 TanStack Query 缓存 5min角色变更主动 invalidate
来源: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. 数据模型(前端)
## 3. 数据模型(前端缓存层
前端无数据库,仅有缓存层
前端无数据库,仅有 urql 缓存层03 §1.4.4 缓存策略)
| 数据类型 | 存储 | TTL | 失效策略 |
| ----------------------- | ---------------------- | --------------------------- | -------------------------------------- |
| Sessiontoken + user | localStorage + Zustand | access 15min / refresh 7day | 401 自动 refreshrefresh 失败跳登录 |
| 权限列表 | TanStack Query cache | 5min | 角色变更事件 invalidate |
| 视口列表 | TanStack Query cache | 5min | 同上 |
| 班级/年级列表 | TanStack Query cache | 5min | staleTime 5minmutation 后 invalidate |
| 教学资源详情 | TanStack Query cache | 30s | staleTime 30s |
| 学情宽表 | TanStack Query cache | 30s | staleTime 30s(实时性由 BFF 决定) |
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
| 表单临时态 | react-hook-form | — | 卸载即销毁 |
| 数据类型 | 存储 | 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 | — | 卸载即销毁 |
## 4. API 设计(前端 → 后端)
> urql 默认 document cache 适合简单场景复杂关联班级↔考试↔成绩P3 起评估切 `@urql/exchange-graphcache` normalized cache03 §1.4.4)。不引入 TanStack Query 做 GraphQL 缓存避免双缓存层TanStack Query 仅用于非 GraphQL 场景(文件上传/SSE
前端不设计后端 API仅声明消费的端点。详见 [01-understanding.md §3.1](./01-understanding.md)。
## 4. API 设计(前端 → teacher-bff GraphQL
### 4.1 统一 API 请求层lib/api.ts
F9 裁决P2 起 all-in GraphQL。前端不再设计 REST 请求层,统一通过 urql client 消费 teacher-bff GraphQLARB-001 schema 第一版)。
### 4.1 urql client 单例packages/hooks + Shell GraphQLProvider
```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
}
// packages/hooks/src/use-graphql-client.tsShell 暴露Remote 复用ARB-002 §2.17 方案 A
import { createClient, Client } from "urql";
import { cacheExchange, fetchExchange } from "urql";
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 流式
}
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],
});
// 错误结构
interface ApiError {
code: string; // 'IAM_INVALID_CREDENTIALS'
message: string; // 已 i18n 翻译或后端原文
details?: unknown;
httpStatus: number;
}
// Shell: src/app/providers.tsx 包裹 <Provider value={client}>,所有 Remote 复用同一 client
```
**职责**
- 自动注入 `Authorization: Bearer ${token}`
- 401 自动 refresh token 一次,失败调 `onUnauthorized`
- 解析 `ActionState`success=false 抛 `ApiError`
- `error.code` 前缀路由 i18n key
- 全局错误 toast除 401
- 请求/响应 trace_id 透传(从响应头 `X-Request-Id` 提取)
- 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 TanStack Query 约定
### 4.2 GraphQL Query/Mutation 约定(对齐 ARB-001 schema
```typescript
// Query Key 命名:[scope, resource, ...args]
queryKey: ["teacher", "classes", { gradeId }];
queryKey: ["teacher", "exams", classId];
queryKey: ["session", "effective-permissions"];
queryKey: ["session", "viewports", "teacher"];
// P2 QueryARB-001 §1.25 个 Must Have
import { gql } from "urql";
// 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),
});
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 + 作业批改列表 invalidate |
| 事件 | 触发 | 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)
GET /api/v1/ai/generate-questions (SSE) 或 GraphQL SubscriptionP6+ 评估)
data: {"delta": "题目"}\n\n
data: {"delta": "A. option1"}\n\n
data: {"done": true}\n\n
```
前端用 `AsyncIterable<T>` 消费Tiptap 逐字插入。
前端用 `AsyncIterable<T>` 消费(非 GraphQLTanStack Query 场景)Tiptap 逐字插入。
## 6. 横切关注点对齐清单
### 6.1 权限(前端等价)
### 6.1 权限(前端等价F7 命名 `<RESOURCE>_<ACTION>`
| 路由 | requiredPermission |
| ----------------------------- | ------------------------ |
@@ -308,9 +354,9 @@ data: {"done": true}\n\n
| `/teacher/lesson-prep` | `LESSON_PREP_VIEW` |
| `/teacher/knowledge-graph` | `CONTENT_READ` |
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`待建立,coord 负责 shared-tsai07 负责调用。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`coord 维护ai13 消费。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。
### 6.2 错误码清单(前端 i18n 路由)
### 6.2 错误码清单(前端 i18n 路由,对齐 [matrix.md §6](../../../docs/architecture/issues/matrix.md)
| 前缀 | 来源服务 | i18n key 模式 |
| -------------- | ----------- | ------------------------ |
@@ -320,7 +366,7 @@ data: {"done": true}\n\n
| `CONTENT_` | content | `content.error.{{code}}` |
| `MSG_` | msg | `msg.error.{{code}}` |
| `AI_` | ai | `ai.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `error.teacher_bff.{{code_snake}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
@@ -333,7 +379,6 @@ interface Logger {
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, meta?: Record<string, unknown>): void;
}
// 实现:开发环境 console + 结构化;生产环境 → SentryP6
// 必含字段trace_id从响应头提取、user_id、scope、path
```
@@ -344,7 +389,7 @@ interface Logger {
| ----------------------------- | ---- | --------------------------------------------------- |
| `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_inp_seconds` | INP | 同上 |
| `teacher_portal_ttfb_seconds` | TTFB | 同上 |
P6 接入P2-P5 暂缓。
@@ -369,37 +414,38 @@ import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
Next.js 无长连接(除 SSE/WS无需特殊处理。SSE/WS 在 P5 由 push-gateway 管理,前端断线自动重连。
## 7. 共享组件库packages/ui-components/待建立ai07 维护
## 7. 共享组件库packages/ui-components/ai13 维护ARB-002 暴露
| 组件 | 用途 | 来源 |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------ |
| `AppShell` | 左侧栏 + 主内容区布局 | teacher-portal 现有 → 抽取共享 |
| `RequirePermission` | L3 组件级视口控制(无权限不渲染 children | 新建 |
| `ErrorBoundary` | React 渲染异常兜底fallback UI | 新建 |
| `Loading` | 骨架屏Skeleton | 新建 |
| `Empty` | 空态(插画 + 文案 + CTA | 新建 |
| `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 封装(备课/出题/反馈) | 新建 |
| 组件 | 用途 | 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/待建立ai07 维护
## 8. 共享 Hookspackages/hooks/ai13 维护ARB-002 暴露
| 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()` | 全局 toastZustand ui-store |
| 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/待建立ai07 维护)
## 9. 设计令牌三层packages/ui-tokens/ai13 维护)
```
packages/ui-tokens/
@@ -429,52 +475,54 @@ packages/ui-tokens/
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
| ------ | ------------ | ------------------ | --------------------------------------------------- | ---------------------------- | ---- |
| 调用 | 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 |
| 调用 | 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 | 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+ |
| 消费 | 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`(待建) | 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+ |
| 依赖 | 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+ |
> **proto 不直接消费**:前端不调用 gRPCBFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
> **前端不直接调 gRPC**teacher-bff 把 gRPC 聚合为 GraphQL 暴露给前端F9 裁决)。前端仅消费 `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
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 版本不一致导致运行时错误 | 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` 按数据类型分级 |
| 共享依赖版本漂移 | 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 未决设计决策(需 coord 仲裁)
### 11.3 已决策设计点(原未决决策,现已对齐仲裁)
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 clienturql/apollo建议P2-P3 用 RESTP4 起若 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 自行决定内部布局。
| 决策点 | 结论 | 裁决依据 |
| ------------------- | ----------------------------------------------------------- | ------------------------------- |
| 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 交叉审查所需信息
@@ -482,68 +530,74 @@ packages/ui-tokens/
| 端 | dev 端口 | 生产端口 | 备注 |
| -------------- | -------- | -------- | ---------- |
| teacher-portal | 3000 | 3000 | Shell 宿主 |
| teacher-portal | 4000 | 4000 | Shell 宿主 |
> 与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐。
> 与 [matrix.md](../../../docs/architecture/issues/matrix.md) §1 端口矩阵对齐teacher-portal :4000
### 12.2 依赖的共享包(需 coord 建立)
### 12.2 依赖的共享包
| 包 | 路径 | 维护方 | 内容 |
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、通用工具 |
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、GraphQL schema、通用工具 |
| `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建议 | usePermissionuseAuth、useViewports |
| `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 /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 |
| `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 路由依赖)
### 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 | ⚠️ 待确认 |
| `BFF_TEACHER_` | teacher-bff | ✅ 已裁决G14 |
| `GW_` | api-gateway | ✅ 已用 |
| `NETWORK_` | 前端 | ai07 自有 |
| `NETWORK_` | 前端 | ai13 自有 |
### 12.5 不产生 Kafka 事件
前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。
## 13. 实施路线ai07 自用)
## 13. 实施路线ai13 自用,详见 [workline.md](../../../docs/architecture/issues/worklines/teacher-portal_workline.md)
### P2 收尾teacher-portal 审计对齐
### P2MF Shell + GraphQL client + 基础页面ARB-001/002
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
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. 配置 next.config.js Module FederationShell 角色
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
@@ -553,10 +607,11 @@ packages/ui-tokens/
1. Web Vitals + OTel browser SDK 接入
2. A11y WCAG 2.2 AA 审计
3. 性能优化MF shared 单例验证、bundle 分析)
3. localStorage → httpOnly Cookie 迁移
4. 性能优化MF shared 单例验证、bundle 分析)
---
**AI Agent**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
**AI Agent**: ai13 (teacher-portal shell)
**Branch**: feat-review-teacher-portal-docs-vErc0L
**Coordinator**: coord-ai