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

705 lines
32 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.
# 模块架构设计文档 — admin-portal
> AIai16TS/React · 管理场景域前端 remote
> 阶段:阶段 2 交付物(仲裁后修订版 v2
> 日期2026-07-10
> 关联:
>
> - [阶段 1 理解确认书](./01-understanding.md)
> - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4
> - [总统最终裁决](../../../docs/architecture/president-final-rulings.md) §5.1-5.5ISSUE-044~048
> - [admin-portal 对接契约](../../architecture/issues/contracts/admin-portal_contract.md)
> - [pending-features P6](../../../docs/architecture/roadmap/pending-features.md)
> - [known-issues §2.16](../../../docs/troubleshooting/known-issues.md)
> 状态已实现P2-P6 全阶段交付完成)
---
## 1. 模块内部分层图admin-portal Remote + Shell 引用 + Gateway + push-gateway
```mermaid
graph TB
subgraph Browser["浏览器(系统/校管理员)"]
URL[URL 路由 /admin/*]
end
subgraph Shell["teacher-portalShell 宿主,端口 4000"]
AppShell[AppShell<br/>左栏导航 + 主内容区<br/>按 scope=admin 过滤视口]
RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
Router[Next.js App Router<br/>动态加载 admin Remote]
SharedDeps["共享依赖暴露singleton<br/>react/react-dom/urql/graphql<br/>@edu/ui-tokens/@edu/ui-components/@edu/hooks/@edu/contracts"]
GraphQLProviderShell[GraphQLProvider<br/>Shell 暴露 urql client 单例]
end
subgraph RemoteAdmin["admin-portalRemote 子应用,端口 4003"]
AdminApp[AdminApp<br/>MF Remote 入口 exposes ./AdminApp]
AdminPages[管理场景页面<br/>dashboard/users/roles/permissions/viewports/organization<br/>classes/teachers/students/audit-logs/system]
AdminComponents["admin 特有组件<br/>UserManagementTable/RolePermissionMatrix<br/>ViewportConfigEditor/OrganizationTree/NotificationPanel"]
AdminHooks["admin 业务 Hooks<br/>useUsers/useRoles/usePermissions/useViewports<br/>useOrganization/useClasses/useTeachers/useStudents<br/>useAuditLogs/useDashboard/useSystemSettings/useWebSocket"]
end
subgraph Gateway["api-gateway"]
GW[Gin 路由/鉴权/限流]
end
subgraph PushGateway["push-gateway"]
WS[WebSocket :8081/ws]
end
subgraph Backend["后端服务"]
IAM[iam<br/>用户/角色/权限/视口 CRUD + AuditEvent 发布]
BFF[teacher-bff<br/>GraphQL admin 命名空间 聚合]
end
Browser --> URL
URL --> RootLayout
RootLayout --> AppShell
AppShell --> Router
Router -->|动态加载 /admin/*| AdminApp
AdminApp --> AdminPages
AdminPages -->|消费 singleton| SharedDeps
SharedDeps --> GraphQLProviderShell
AdminPages --> AdminComponents
AdminPages --> AdminHooks
AdminHooks -->|useGraphQuery/useGraphMutation| GraphQLProviderShell
GraphQLProviderShell -->|POST /api/admin/graphql| GW
GW --> BFF
BFF --> IAM
AdminHooks -->|useWebSocket| WS
IAM -.->|Kafka edu.iam.audit.created| BFF
AppShell -->|fetch /api/auth/login| GW
```
### 1.1 Remote 与 Shell 的职责边界
| 职责 | 归属 | 说明 |
| -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------ |
| RootLayout字体/令牌/Provider | teacher-portal Shell | admin-portal MF 模式复用standalone 模式自建 |
| AppShell左栏 + 主内容区) | admin-portal 内部 | `admin-shell.tsx`11 项导航 + 跳过链接 A11y |
| 路由表 `/admin/*` | admin-portal | `src/app/admin/*/page.tsx` |
| 登录页 | admin-portal | standalone 模式 `src/app/login/page.tsx`mock 登录MF 模式复用 Shell `/login` |
| rewrites `/api/admin/graphql` | admin-portal | `next.config.js` rewrites 代理到 api-gateway |
| GraphQLProvider | Shell 暴露 / 自建 | MF 模式复用 Shell 单例ARB-004standalone 模式 `graphql-provider.tsx` 自建 urql client |
| 管理场景页面 | admin-portal Remote | `/admin/*` 下的所有 page.tsx |
| 管理特有组件 | admin-portal Remote | UserManagementTable 等 6 个 |
| 管理业务 Hooks | admin-portal Remote | 13 个业务 Hook + useWebSocket |
| 共享组件库 | Shell 暴露 | `@edu/ui-components` |
| 共享 Hooks | Shell 暴露 | `@edu/hooks` |
### 1.2 MF 配置admin-portal/next.config.jsRemote 角色)
```javascript
const NextFederationPlugin = require("@module-federation/nextjs-mf");
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@edu/ui-tokens", "@edu/ui-components", "@edu/hooks"],
async rewrites() {
const gatewayUrl = process.env.API_GATEWAY_URL || "http://localhost:8080";
return [
{
source: "/api/admin/graphql",
destination: `${gatewayUrl}/api/admin/graphql`,
},
{ source: "/api/:path*", destination: `${gatewayUrl}/api/:path*` },
];
},
webpack(config, { isServer }) {
if (process.env.NEXT_PUBLIC_MF_ENABLED === "true") {
config.plugins.push(
new NextFederationPlugin({
name: "admin_app",
filename: "static/chunks/remoteEntry.js",
exposes: { "./AdminApp": "./src/app/admin-app.tsx" },
remotes: {
teacher: `teacher_app@http://localhost:4000/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
},
shared: {
react: { singleton: true, requiredVersion: "^18.3.0" },
"react-dom": { singleton: true, requiredVersion: "^18.3.0" },
urql: { singleton: true },
graphql: { singleton: true },
"@edu/ui-tokens": { singleton: true },
"@edu/ui-components": { singleton: true },
"@edu/hooks": { singleton: true },
},
extraOptions: { exposePages: false },
}),
);
}
return config;
},
};
module.exports = nextConfig;
```
> **说明**
>
> - `NEXT_PUBLIC_MF_ENABLED` 环境变量控制 MF 启用standalone 模式默认 `false`MF 模式设 `true`
> - admin-portal 暴露 `./AdminApp` 模块(非旧文档的 `./pages`),供 Shell 动态 import
> - `shared` 全部 `singleton: true`,确保 urql/graphql/react 单例(避免多实例报错)
> - admin-portal 自身实现 `rewrites`(代理 `/api/admin/graphql` 到 api-gatewaystandalone 模式可独立运行
## 2. 领域模型(前端视角)
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。定义于 `src/types/view-models.ts`
### 2.1 视图模型清单
```typescript
// 数据范围
type DataScope = "ALL" | "SCHOOL" | "GRADE" | "CLASS" | "DISTRICT";
// 用户管理
interface UserViewModel {
id: string;
email: string;
name: string;
roles: { id: string; name: string; code: string }[];
status: "active" | "disabled" | "locked";
dataScope: DataScope;
organizationId: string | null;
schoolName?: string;
lastLoginAt: number | null;
createdAt: number;
updatedAt: number;
}
// 角色管理
interface RoleViewModel {
id: string;
name: string;
code: string;
description: string;
permissions: PermissionViewModel[];
userCount: number;
dataScope: DataScope;
isSystem: boolean;
createdAt: number;
updatedAt: number;
}
// 权限管理
interface PermissionViewModel {
id: string;
code: string;
name: string;
description: string;
resource: string;
action: string;
isSystem: boolean;
}
// 视口配置
interface ViewportConfigViewModel {
id: string;
key: string;
label: string;
route: string;
icon: string | null;
sortOrder: number;
requiredPermission: string | null;
scope: "teacher" | "student" | "parent" | "admin";
isVisible: boolean;
}
// 组织树
interface OrganizationNode {
id: string;
name: string;
type: "school" | "grade" | "class";
parentId: string | null;
childrenCount: number;
path: string;
}
// 学校设置
interface SystemSettingsViewModel {
schoolName: string;
schoolYear: string;
semester: string;
timezone: string;
locale: string;
// ... 更多设置项
}
// 班级/教师/学生admin 全局视角)
interface AdminClassViewModel {
id: string;
name: string;
grade: string;
headTeacher: string;
studentCount: number;
status: string;
}
interface AdminTeacherViewModel {
id: string;
email: string;
name: string;
subjects: string[];
classes: string[];
status: string;
}
interface AdminStudentViewModel {
id: string;
email: string;
name: string;
className: string;
grade: string;
status: string;
}
// 审计日志
interface AuditLogViewModel {
id: string;
userId: string;
userName: string;
action: string;
resourceType: string;
resourceId: string;
beforeState: string | null;
afterState: string | null;
ip: string;
userAgent: string;
occurredAt: number;
}
// 管理仪表盘
interface AdminDashboardViewModel {
totalUsers: number;
totalTeachers: number;
totalStudents: number;
totalClasses: number;
activeSessions: number;
serviceHealth: {
serviceName: string;
status: "healthy" | "degraded" | "down";
latencyMs: number;
}[];
recentActivity: {
timestamp: number;
action: string;
user: string;
resource: string;
}[];
}
// WebSocket 通知
interface WsNotification {
id: string;
type: "audit_alert" | "abnormal_login" | "system_error" | "info";
severity: "info" | "warning" | "error";
title: string;
message: string;
timestamp: number;
}
```
### 2.2 当前用户与会话
```typescript
interface CurrentUser {
id: string;
email: string;
name: string;
roles: string[];
permissions: string[];
dataScope: DataScope;
schoolId: string | null;
schoolName: string | null;
}
```
会话状态由 `AuthProvider` 管理token 存 `localStorage``edu_access_token`),用户信息存 `localStorage``edu_user`)。
## 3. 数据模型(前端缓存层)
admin-portal 无数据库,仅有 urql GraphQL client 缓存层。
| 数据类型 | 存储 | 缓存策略 |
| ----------------------- | ---------------------------- | --------------------------------------------- |
| Sessiontoken + user | localStorage + React Context | access token 持久化;用户信息持久化;登出清除 |
| GraphQL 查询缓存 | urql document cache | 默认缓存mutation 后手动 invalidate |
| 视口配置(拖拽态) | React useState | 本地编辑,保存时批量 mutation |
| 表单临时态 | react-hook-form | 卸载即销毁 |
| WebSocket 通知 | React useState限 20 条) | 新通知头部插入,超 20 条裁剪 |
> **缓存策略说明**admin 数据低频变urql 默认 document cache 已满足;监控指标经 WebSocket 实时推送(不再轮询);视口配置支持本地编辑 + 批量保存。
## 4. API 设计GraphQL 为主)
### 4.1 GraphQL ClienturqlARB-004
**standalone 模式**`src/lib/graphql-client.ts`
```typescript
import { createClient, fetchExchange, type Client } from "@urql/core";
const GRAPHQL_ENDPOINT = "/api/admin/graphql";
export function createGraphQLClient(): Client {
return createClient({
url: GRAPHQL_ENDPOINT,
exchanges: [fetchExchange],
fetchOptions: () => {
const token =
typeof window !== "undefined"
? localStorage.getItem("edu_access_token")
: null;
return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
},
});
}
```
**MF 模式**:复用 Shell 暴露的 `GraphQLProvider` + `useGraphQLClient`singletonARB-004
**urql 类型断言技巧**urql 泛型与纯字符串 query 兼容性有限,用 `as unknown as` 从 unknown 转换:
```typescript
// use-graphql.ts
const [executeMutation] = useMutation(mutation) as unknown as [
(variables: V) => { toPromise: () => Promise<OperationResult<T, V>> },
unknown,
];
```
### 4.2 通用 GraphQL Hooks`src/hooks/use-graphql.ts`
```typescript
export function useGraphQuery<T, V = Record<string, never>>(
query: string,
variables?: V,
): QueryResult<T> { ... }
export function useGraphMutation<T, V>(
mutation: string,
): [MutationFn<T, V>, MutationResult] { ... }
```
### 4.3 业务 Hooks 清单
| Hook | 职责 |
| ---------------------------- | ------------------------------------------------ |
| `useUsers(filter)` | 用户列表查询(含筛选/分页) |
| `useUser(userId)` | 用户详情 |
| `useCreateUser()` | 创建用户 mutation |
| `useUpdateUser()` | 更新用户 mutation |
| `useToggleUserStatus()` | 启用/禁用用户 mutation |
| `useUserFilter()` | 用户筛选状态(搜索/角色/状态/组织) |
| `useRoles()` | 角色列表查询 |
| `useCreateRole()` | 创建角色 mutation |
| `useUpdateRolePermissions()` | 更新角色权限 mutationRolePermissionMatrix 用) |
| `usePermissions()` | 全量权限列表 |
| `useViewports()` | 视口配置列表 |
| `useUpdateViewport()` | 更新视口配置 mutation |
| `useOrganization(parentId)` | 组织树查询(按 parentId 递归) |
| `useClasses(filter)` | 班级列表查询 |
| `useClassFilter()` | 班级筛选状态 |
| `useTeachers(filter)` | 教师列表查询 |
| `useTeacherFilter()` | 教师筛选状态 |
| `useStudents(filter)` | 学生列表查询 |
| `useStudentFilter()` | 学生筛选状态 |
| `useAuditLogs(filter)` | 审计日志查询 |
| `useAuditLogFilter()` | 审计日志筛选状态 |
| `exportAuditLogsCsv(logs)` | 审计日志 CSV 导出BOM + UTF-8 |
| `useDashboard()` | 管理仪表盘聚合查询 |
| `useSystemSettings()` | 学校设置查询 |
| `useUpdateSystemSettings()` | 学校设置更新 mutation |
| `useWebSocket(maxItems)` | WebSocket 实时通知mock 模式 30s 定时推送) |
## 5. 事件设计
### 5.1 WebSocket 实时通知ARB-006
admin-portal 接入 push-gateway `GET /ws`,消费 3 类通知:
```typescript
// src/hooks/use-websocket.ts
export function useWebSocket(maxItems = 20) {
// 真实模式:连接 push-gateway NEXT_PUBLIC_WS_URL
// mock 模式30s 定时器模拟推送
// 返回:{ notifications, connected, dismiss, clear }
}
```
| 通知类型 | severity | 触发场景 |
| ---------------- | -------- | ----------------------------- |
| `audit_alert` | warning | 敏感操作(权限变更/批量删除) |
| `abnormal_login` | error | 异地登录/异常时段登录 |
| `system_error` | error | 服务降级/宕机 |
### 5.2 不消费 Kafka
admin-portal 不直接订阅 Kafka。审计日志经 teacher-bff 聚合后通过 GraphQL `auditLogs` Query 消费ARB-005
**链路**iam → Kafka `edu.iam.audit.created`**teacher-bff 消费** → GraphQL `auditLogs` Query → admin-portal
> **不再用轮询**:旧文档的"60s 轮询监控 + 5min 轮询统计"已被 ARB-006 的 WebSocket 推送替换。
## 6. 横切关注点对齐清单
### 6.1 权限前端等价ARB-003
权限点常量定义于 `src/lib/permissions.ts`
- `ADMIN_PERMISSIONS`admin 自身资源(`ADMIN_DASHBOARD_VIEW` / `ADMIN_SYSTEM_MANAGE` / `ADMIN_AUDIT_READ` / `ADMIN_CLASS_READ` / `ADMIN_TEACHER_READ` / `ADMIN_STUDENT_READ` / `ADMIN_ORG_MANAGE` 等)
- `IAM_PERMISSIONS`:跨服务资源(`IAM_USER_READ` / `IAM_ROLE_READ` / `IAM_PERMISSION_READ` / `IAM_VIEWPORT_READ` 等)
- `ROUTE_PERMISSIONS`11 路由 → 权限点映射
权限校验:`usePermission().hasPermission("XXX")` Hook + `AuthGuard` 组件(路由级)。
### 6.2 错误码清单(前端 i18n 路由ARB-002
| 前缀 | 来源服务 | i18n key 模式 |
| -------------- | ----------- | ------------------------ |
| `IAM_` | iam | `iam.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
| `ADMIN_` | admin 域 | `error.admin.*` |
### 6.3 Web Vitals`src/lib/web-vitals.ts`
| 指标 | 类型 | 上报方式 |
| ---- | -------------- | ---------------------- |
| LCP | 最大内容绘制 | `navigator.sendBeacon` |
| CLS | 累积布局偏移 | 同上 |
| FCP | 首次内容绘制 | 同上 |
| INP | 交互到下一绘制 | 同上 |
| TTFB | 首字节时间 | 同上 |
仅 production 启用(`WebVitalsInitializer` 组件控制)。
### 6.4 健康检查
| 端点 | 用途 | 实现 |
| ----------------- | --------- | --------------------------------------------------- |
| `GET /api/health` | Liveness | 返回 `200 { status: "ok" }` |
| `GET /api/ready` | Readiness | 检查 gateway 可达性2s 超时mock 模式直接 ready |
### 6.5 A11yWCAG 2.2 AA
- `eslint-plugin-jsx-a11y`error 级)
- skip-link`admin-shell.tsx` 跳过到主内容区)
- `focus-visible` 样式(`globals.css`
- 组织树:`<button role="treeitem" aria-expanded aria-selected onKeyDown>`
- 通知面板:`<li>` 内嵌 `<button aria-label>`
- Toast`aria-live="polite"` + `aria-atomic="true"`
- 注:`@next/eslint-plugin-next` 14.x 与 ESLint 9 不兼容(`context.getAncestors` 已移除),已移除
### 6.6 横切关注点对齐清单(汇总)
| 对齐项 | 实现 |
| --------------------- | --------------------------------------------------------------- |
| 权限校验 | `usePermission()` + `AuthGuard``ADMIN_*`/`IAM_*` 前缀 |
| 错误码前缀 | 5 个前缀(`IAM_`/`BFF_TEACHER_`/`GW_`/`NETWORK_`/`ADMIN_` |
| logger | console + 结构化(生产上报) |
| metrics | Web Vitals v4 → `navigator.sendBeacon` |
| /health + /ready | Next.js Route Handler |
| 优雅关闭 | N/ANext.js 无长连接) |
| 测试覆盖率 ≥ 80% | Vitest + @testing-library/reactvitest.config.ts 已配置) |
| Dockerfile 多阶段构建 | `node:22-alpine` builder + runnerstandalone 输出EXPOSE 4003 |
| Zod 输入验证 | react-hook-form + zod |
| GlobalErrorFilter | React ErrorBoundary + Toast 错误处理 |
| 设计令牌三层 | `globals.css`primitive/semantic+ tailwind-theme |
| A11y | eslint-plugin-jsx-a11y + skip-link + focus-visible |
| Module Federation | `next.config.js` Remote`NEXT_PUBLIC_MF_ENABLED` 控制 |
## 7. 共享组件库(复用 Shell + admin 特有)
### 7.1 复用 Shell 暴露的组件(`@edu/ui-components`
AppShell 风格、ErrorBoundary、RequirePermission、Loading、Empty、Button/Input/Select 等。
### 7.2 admin-portal 特有组件(新建)
| 组件 | 用途 | 实现要点 |
| ---------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- |
| `AdminShell` | 左栏 11 项导航 + 主内容区 + 跳过链接 | 内部组件11 项导航按 `ROUTE_PERMISSIONS` 过滤;纸面风格 |
| `UserManagementTable` | 用户管理表格(列表/筛选/分页) | 列:邮箱/姓名/角色/状态/数据范围/最后登录;筛选:搜索/角色/状态 |
| `UserFormModal` | 用户表单弹窗(创建/编辑) | react-hook-form + zod字段邮箱/姓名/角色/数据范围/组织 |
| `RolePermissionMatrix` | 角色-权限矩阵编辑器checkbox 网格) | 行=角色,列=权限(按 resource 分组);全选/半选/单选;系统预置角色只读 |
| `OrganizationTree` | 组织树(递归 + A11y treeitem | 按 parentId 懒加载;`<button role="treeitem" aria-expanded aria-selected onKeyDown>` |
| `NotificationPanel` | 通知面板WebSocket 实时通知) | 固定右上角;显示最近 20 条;点击触发 Toast + dismiss`<li>` 内嵌 `<button aria-label>` |
### 7.3 通用 UI 组件(`src/components/ui.tsx`
PaperCard / PageHeader / Button4 variant/ Input / Select / Badge / Table / TableRow / TableCell / EmptyState / LoadingState / ErrorState / Pagination
### 7.4 不使用的组件(明确排除)
| 组件 | 归属 | 排除原因 |
| ----------------------------------- | -------------- | ------------------------------------- |
| `RichTextEditor` | teacher-portal | 管理端无富文本场景 |
| `ExamTaking` | student-portal | 管理端无作答场景 |
| `ChildSwitcher` | parent-portal | 管理端无多子女切换 |
| `PlatformMonitor`Grafana iframe | — | 已移除,改为管理仪表盘 + 服务健康表格 |
## 8. Mock 策略MSW + mock-socket
### 8.1 MSW handlers`src/mocks/handlers.ts`
开发期拦截全部 GraphQL/HTTP 请求:
- `/api/auth/login` → mock 登录admin@edu.test / admin123
- `graphql.operation` → 按 operationName 分发 18 个 GraphQL operation mock
- CurrentUser / AdminUsers / AdminUser / CreateUser / UpdateUser / ToggleUserStatus
- AdminRoles / CreateRole / UpdateRolePermissions
- AdminPermissions / AdminViewports / UpdateViewport
- AdminOrganization / AdminClasses / AdminTeachers / AdminStudents
- AuditLogs / AdminDashboard / SystemSettings / UpdateSystemSettings
- 内存可变副本:支持增删改 + 自动审计记录(`recordAudit`
- 分页:`paginate(items, page, pageSize)` 辅助函数
### 8.2 mock fixtures`src/mocks/fixtures.ts`
- `mockCurrentUser`admin 角色permissions=["*"]
- `mockPermissions`24 项权限点
- `mockRoles`4 个角色admin/teacher/student/parent
- `mockUsers`6 个用户
- `mockViewports`7 个视口配置
- `mockOrganization`12 节点school/grade/class
- `mockClasses` / `mockTeachers` / `mockStudents`6/6/5 条
- `mockAuditLogs`5 条审计日志
- `mockDashboard`:仪表盘聚合数据
- `mockSystemSettings`:学校设置
### 8.3 WebSocket mock`src/hooks/use-websocket.ts`
MSW 不拦截 WebSocket用定时器模拟推送
- mock 模式30s 定时器推送 1 条通知(轮换 3 类audit_alert/abnormal_login/system_error
- 真实模式:连接 `NEXT_PUBLIC_WS_URL`push-gateway `GET /ws`
### 8.4 环境切换
`NEXT_PUBLIC_API_MOCKING=enabled` 启用 MSW`disabled` 使用真实后端。
## 9. 设计令牌三层(`src/app/globals.css`
### 9.1 Layer 1 Primitive原始色板
```css
:root {
--paper: 0 0% 100%; /* 白纸 */
--ink: 30 10% 15%; /* 墨色 */
--ink-muted: 30 5% 45%;
--rule: 30 10% 90%; /* 分隔线 */
--accent: 210 60% 40%; /* 强调色 */
--accent-light: 210 60% 95%;
--danger: 0 70% 50%;
--success: 140 50% 40%;
--warning: 40 80% 50%;
}
```
### 9.2 Layer 2 Semantic
```css
:root {
--bg-paper: hsl(var(--paper));
--color-ink: hsl(var(--ink));
--color-ink-muted: hsl(var(--ink-muted));
--color-rule: hsl(var(--rule));
--color-accent: hsl(var(--accent));
--color-accent-light: hsl(var(--accent-light));
--color-danger: hsl(var(--danger));
--color-success: hsl(var(--success));
--color-warning: hsl(var(--warning));
}
```
### 9.3 Layer 3 Tailwind Theme`tailwind.config.js`
paper / ink / accent / rule / danger / success / warning 色板映射为 `bg-*` / `text-*` 类。
### 9.4 强制规则project_rules §3.10
- 禁止 `#hex` 字面量
- 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
- 禁止 `font-size: Npx`
- 禁止 Tailwind 任意值 `w-[Npx]`
## 10. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
| ---- | --------------------- | --------------------- | ---------------------------------------- | ---------------------------- | ---- |
| 调用 | api-gateway | HTTP/GraphQL | `POST /api/admin/graphql`(经 rewrites | 全部业务 GraphQL 查询 | P6+ |
| 调用 | api-gateway | HTTP | `POST /api/auth/login` | 登录standalone 模式) | P6+ |
| 调用 | push-gateway | WebSocket | `GET /ws` | 实时通知ARB-006 | P6+ |
| 消费 | teacher-bff | GraphQL经 Gateway | admin 命名空间 18 operations | 全部业务数据 | P6+ |
| 消费 | iam经 teacher-bff | GraphQL间接 | 用户/角色/权限/视口 CRUD + AuditEvent | 管理数据 | P6+ |
| 依赖 | coord 维护 | — | `packages/contracts` | Permissions 常量 + 类型 | P6+ |
| 依赖 | coord 维护 | — | `packages/shared-proto` | TS 类型(仅 contracts 部分) | P6+ |
| 依赖 | teacher-portal Shell | — | Shell 暴露的 GraphQLProvider + 共享依赖 | 宿主环境MF 模式) | P6+ |
> **proto 不直接消费**:前端不调用 gRPCteacher-bff 把 gRPC 聚合为 GraphQL 暴露给前端。
## 11. 风险与假设
### 11.1 假设
1. **假设 teacher-bff admin 命名空间 schema 就绪**ARB-001contract §2.4 18 operations若 ai03 未补齐admin-portal 用 MSW mock 开发,上游就绪后切换。
2. **假设 teacher-portal Shell 已就绪**MF 模式依赖 Shell 暴露 GraphQLProvider + 共享依赖。standalone 模式可独立运行。
3. **假设 push-gateway WebSocket 就绪**ARB-006若未就绪用 mock-socket + 30s 定时器模拟。
4. _\*假设 `packages/contracts` ADMIN_* 权限点常量就绪_*coord 维护admin-portal `src/lib/permissions.ts` 引用,若 coord 未建立则本地定义。
### 11.2 技术风险
| 风险 | 影响 | 缓解 |
| ----------------------------- | -------------------------------------- | --------------------------------------------------------- |
| Shell 未就绪阻塞 admin | MF 模式无法挂载 | standalone 模式自建 Provider 链,独立可运行 |
| teacher-bff admin schema 缺失 | GraphQL 查询失败 | MSW mock 全覆盖 18 operations上游就绪后切换 |
| urql 类型兼容性 | TypeScript 编译错误 | `as unknown as` 从 unknown 转换(见 §4.1 |
| ESLint 9 兼容性 | `@next/eslint-plugin-next` 14.x 不兼容 | 移除该插件,用 jsx-a11y + tseslint 替代 |
| WebSocket mock 不真实 | 开发期通知体验与生产不一致 | 30s 定时器 + 3 类通知轮换,接近真实推送频率 |
| 权限缓存陈旧 | 角色变更后前端仍用旧权限 | 登录时拉取最新权限admin-portal 本身是管理端,可主动刷新 |
## 12. 端口矩阵
| 端 | dev 端口 | 生产端口 | 备注 |
| -------------------- | -------- | -------- | ---------------------- |
| admin-portal | 4003 | 4003 | Remote挂载到 Shell |
| teacher-portal Shell | 4000 | 4000 | Shell 宿主 |
| api-gateway | 8080 | 8080 | GraphQL 代理 |
| push-gateway | 8081 | 8081 | WebSocket |
| teacher-bff | 3003 | 3003 | GraphQL admin 命名空间 |
## 13. 实施路线(已完成)
### P2-P6 全阶段交付(已完成)
1.**P2 骨架 Phase 1**:配置文件 + 设计令牌 + 类型定义
2.**P2 骨架 Phase 2**GraphQL client + auth + permissions + providers + AdminShell + MF Remote 入口 + standalone 壳
3.**P2 骨架 Phase 3**MSW mockhandlers + fixtures + browser+ mock-socket WebSocket mock
4.**P3 业务**用户管理useUsers/useUser/CRUD hooks + UserManagementTable + /admin/users 页面)
5.**P3 业务**:角色权限矩阵 + 权限点管理 + 视口配置
6.**P4 业务**:组织管理 + 学校设置 + 班级/教师/学生全局管理
7.**P5 业务**:审计日志 + WebSocket 实时通知 + 管理仪表盘
8.**P6 硬化**A11y + Web Vitals + Dockerfile + /api/health + /api/ready + 质量校验
9.**文档回写**01/02 文档对齐仲裁结果 + known-issues §2.16 + arch:scan
### 验收标准
- ✅ 所有 11 个路由页面可访问,权限校验生效
- ✅ MF Remote 可被 Shell 加载(暴露 `./AdminApp`
- ✅ standalone 模式独立可运行(自建 Provider 链 + mock 登录)
- ✅ GraphQL 18 operations 全部实现MSW mock + 真实切换)
- ✅ WebSocket 实时通知mock 模式 30s 定时推送)
- ✅ Web Vitals 采集LCP/CLS/FCP/INP/TTFB → sendBeacon
- ✅ A11y WCAG 2.2 AAeslint-plugin-jsx-a11y error 级 + skip-link + focus-visible
-`/api/health` + `/api/ready` 返回 200
- ✅ Dockerfile 多阶段构建node:22-alpinestandalone 输出EXPOSE 4003
-`pnpm run typecheck` + `pnpm run lint` 通过0 errors
---
**AI Agent**: ai16 (admin-portal remote)
**Branch**: admin-portal-arbitration-done-TaFkyb
**Coordinator**: coord-ai