# 模块架构设计文档 — admin-portal
> AI:ai16(TS/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.5(ISSUE-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-portal(Shell 宿主,端口 4000)"]
AppShell[AppShell
左栏导航 + 主内容区
按 scope=admin 过滤视口]
RootLayout[RootLayout
字体/令牌/i18n Provider]
Router[Next.js App Router
动态加载 admin Remote]
SharedDeps["共享依赖暴露(singleton)
react/react-dom/urql/graphql
@edu/ui-tokens/@edu/ui-components/@edu/hooks/@edu/contracts"]
GraphQLProviderShell[GraphQLProvider
Shell 暴露 urql client 单例]
end
subgraph RemoteAdmin["admin-portal(Remote 子应用,端口 4003)"]
AdminApp[AdminApp
MF Remote 入口 exposes ./AdminApp]
AdminPages[管理场景页面
dashboard/users/roles/permissions/viewports/organization
classes/teachers/students/audit-logs/system]
AdminComponents["admin 特有组件
UserManagementTable/RolePermissionMatrix
ViewportConfigEditor/OrganizationTree/NotificationPanel"]
AdminHooks["admin 业务 Hooks
useUsers/useRoles/usePermissions/useViewports
useOrganization/useClasses/useTeachers/useStudents
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
用户/角色/权限/视口 CRUD + AuditEvent 发布]
BFF[teacher-bff
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-004);standalone 模式 `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.js,Remote 角色)
```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-gateway),standalone 模式可独立运行
## 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 缓存层。
| 数据类型 | 存储 | 缓存策略 |
| ----------------------- | ---------------------------- | --------------------------------------------- |
| Session(token + 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 Client(urql,ARB-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`(singleton,ARB-004)。
**urql 类型断言技巧**:urql 泛型与纯字符串 query 兼容性有限,用 `as unknown as` 从 unknown 转换:
```typescript
// use-graphql.ts
const [executeMutation] = useMutation(mutation) as unknown as [
(variables: V) => { toPromise: () => Promise> },
unknown,
];
```
### 4.2 通用 GraphQL Hooks(`src/hooks/use-graphql.ts`)
```typescript
export function useGraphQuery>(
query: string,
variables?: V,
): QueryResult { ... }
export function useGraphMutation(
mutation: string,
): [MutationFn, MutationResult] { ... }
```
### 4.3 业务 Hooks 清单
| Hook | 职责 |
| ---------------------------- | ------------------------------------------------ |
| `useUsers(filter)` | 用户列表查询(含筛选/分页) |
| `useUser(userId)` | 用户详情 |
| `useCreateUser()` | 创建用户 mutation |
| `useUpdateUser()` | 更新用户 mutation |
| `useToggleUserStatus()` | 启用/禁用用户 mutation |
| `useUserFilter()` | 用户筛选状态(搜索/角色/状态/组织) |
| `useRoles()` | 角色列表查询 |
| `useCreateRole()` | 创建角色 mutation |
| `useUpdateRolePermissions()` | 更新角色权限 mutation(RolePermissionMatrix 用) |
| `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 A11y(WCAG 2.2 AA)
- `eslint-plugin-jsx-a11y`(error 级)
- skip-link(`admin-shell.tsx` 跳过到主内容区)
- `focus-visible` 样式(`globals.css`)
- 组织树:`