Files
Edu/apps/teacher-portal/docs/02-architecture-design.md
SpecialX e691cd267d docs(teacher-portal): ai07 阶段1+2 拆分到4端portal的docs目录
删除合并版README,按portal拆分8份文档(每端01-understanding+02-architecture-design)

teacher-portal(shell/P2)+student-portal(remote/P3)+parent-portal(remote/P4)+admin-portal(remote/P6)

AI Agent: ai07 (4 portals)

Branch: docs/portals-stage1-stage2-design-ai07
2026-07-09 18:23:27 +08:00

31 KiB
Raw Blame History

模块架构设计文档 — teacher-portal

AIai07TS/React · 教学场景域前端 shell 阶段:阶段 2 交付物 日期2026-07-09 关联:阶段 1 理解确认书004 架构影响地图 §5.4、pending-features P2 状态:待 coord 交叉审查


1. 模块内部分层图4 端统一 MF 架构)

graph TB
    subgraph Browser["浏览器"]
        URL[URL 路由]
    end

    subgraph Shell["teacher-portalShell 宿主)"]
        AppShell[AppShell<br/>左栏导航 + 主内容区]
        RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
        Router[Next.js App Router]
        SharedDeps["共享依赖暴露<br/>react/react-dom/@tanstack/react-query/zustand/nuqs"]
    end

    subgraph RemoteTeacher["teacher-portal Remote 模块"]
        TeacherPages[教学场景页面<br/>dashboard/classes/exams/homework/grades/ai-assist]
    end

    subgraph RemoteStudent["student-portalRemote"]
        StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
    end

    subgraph RemoteParent["parent-portalRemote"]
        ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
    end

    subgraph RemoteAdmin["admin-portalRemote"]
        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/useA11y]
        LibTS[shared-ts<br/>通用工具]
    end

    subgraph Gateway["api-gateway"]
        GW[Gin 路由/鉴权/限流]
    end

    Browser --> URL
    URL --> RootLayout
    RootLayout --> AppShell
    AppShell --> Router
    Router -->|动态加载| RemoteTeacher
    Router -->|动态加载| RemoteStudent
    Router -->|动态加载| RemoteParent
    Router -->|动态加载| 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

    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

1.1 MF 拓扑选型

方案 选否 理由
4 端独立部署 + 独立域名 + 各自 Shell 4 套 Shell 重复,登录态/权限/组件库要重复实现
单 Shell + 4 Remote采用 teacher-portal 作为 Shell 宿主,提供 AppShell + 共享依赖;其余 3 端作为 Remote 动态加载
单一 Next.js 应用 + 4 路由组 违反"微前端独立部署"目标ADR-012

Shell 职责teacher-portal

  • RootLayout字体、设计令牌、i18n Provider、TanStack QueryClientProvider、Zustand StoreProvider
  • AppShell左侧导航 + 主内容区 + 用户信息 + 登出)
  • 共享依赖暴露react、react-dom、@tanstack/react-query、zustand、nuqs、ui-components、ui-tokens、contracts、hooks
  • 路由表4 端路由前缀:/teacher/*/student/*/parent/*/admin/*
  • 登录页(统一登录入口,按角色重定向到对应 portal

1.2 MF 配置teacher-portal/next.config.js

// teacher-portal/next.config.jsShell
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`,
});

module.exports = {
  reactStrictMode: true,
  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: "teacher_app",
        filename: "static/chunks/remoteEntry.js",
        remotes: remotes(isServer),
        exposes: {
          "./AppShell": "./src/components/AppShell",
          "./shared-deps": "./src/shared/deps",
        },
        shared: {
          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 },
        },
        extraOptions: { exposePages: false },
      }),
    );
    return config;
  },
  async rewrites() {
    return [
      {
        source: "/api/v1/:path*",
        destination: `${process.env.API_GATEWAY_URL || "http://localhost:8080"}/api/v1/:path*`,
      },
    ];
  },
};

Remote 端配置对称:name: 'student_app'exposes: { './pages': './src/pages' }remotes: { teacher: 'teacher_app@...' }

2. 领域模型(前端视角)

前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。

2.1 会话状态Session

interface Session {
  user: UserInfo; // { id, email, name, roles, permissions, dataScope }
  tokens: { accessToken: string; refreshToken: string };
  viewports: ViewportItem[]; // L1 导航视口
  expiresAt: number; // access token 过期时间戳
}

存储Zustand sessionSliceL3+ localStorage 持久化(刷新恢复)+ TanStack Query 缓存 ['session']L2

2.2 视口模型Viewport

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
}

来源:GET /api/v1/{scope}/viewportsBFF 聚合 iam 视口配置。AppShell 按 scope 过滤渲染对应 portal 的导航。

2.3 权限模型Permission

interface PermissionState {
  permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]
  dataScope: DataScope; // L0-L5
  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。

3. 数据模型(前端)

前端无数据库,仅有缓存层:

数据类型 存储 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 卸载即销毁

4. API 设计(前端 → 后端)

前端不设计后端 API仅声明消费的端点。详见 01-understanding.md §3.1

4.1 统一 API 请求层lib/api.ts

// 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
}

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 流式
}

// 错误结构
interface ApiError {
  code: string; // 'IAM_INVALID_CREDENTIALS'
  message: string; // 已 i18n 翻译或后端原文
  details?: unknown;
  httpStatus: number;
}

职责

  • 自动注入 Authorization: Bearer ${token}
  • 401 自动 refresh token 一次,失败调 onUnauthorized
  • 解析 ActionStatesuccess=false 抛 ApiError
  • error.code 前缀路由 i18n key
  • 全局错误 toast除 401
  • 请求/响应 trace_id 透传(从响应头 X-Request-Id 提取)

4.2 TanStack Query 约定

// Query Key 命名:[scope, resource, ...args]
queryKey: ["teacher", "classes", { gradeId }];
queryKey: ["teacher", "exams", classId];
queryKey: ["session", "effective-permissions"];
queryKey: ["session", "viewports", "teacher"];

// 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),
});

5. 事件设计

前端不发布 Kafka 事件,仅消费 WebSocket 推送P5和 Server-Sent EventsAI 流式)。

5.1 WebSocket 推送P5

事件 触发 teacher-portal 前端动作
NotificationRequested msg 服务投递 toast 提示 + 通知中心未读数 +1
HomeworkSubmitted 学生提交作业 toast + 作业批改列表 invalidate

5.2 SSE 流式P5 AI 辅助出题)

GET /api/v1/ai/generate-questions (SSE)
data: {"delta": "题目"}\n\n
data: {"delta": "A. option1"}\n\n
data: {"done": true}\n\n

前端用 AsyncIterable<T> 消费Tiptap 逐字插入。

6. 横切关注点对齐清单

6.1 权限(前端等价)

路由 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 负责 shared-tsai07 负责调用。L3 组件级视口用 <RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>

6.2 错误码清单(前端 i18n 路由)

前缀 来源服务 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 bff.error.{{code}}
GW_ api-gateway gateway.error.{{code}}
NETWORK_ 前端网络层 network.error.{{code}}

6.3 Logger

// 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-vitalsPOST /api/v1/admin/web-vitals
teacher_portal_cls CLS 同上
teacher_portal_fid_seconds FID 同上
teacher_portal_ttfb_seconds TTFB 同上

P6 接入P2-P5 暂缓。

6.5 TracerOTel browser SDKP6

// 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/待建立ai07 维护)

组件 用途 来源
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 封装(备课/出题/反馈) 新建

8. 共享 Hookspackages/hooks/待建立ai07 维护)

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

9. 设计令牌三层packages/ui-tokens/待建立ai07 维护)

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 HTTP/REST /api/v1/* 代理 全部业务请求 P1+
调用 push-gateway WebSocket ws://push-gateway/ws 实时推送 P5
调用 ai SSE GET /api/v1/ai/generate-questions AI 流式出题 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+
依赖 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+

proto 不直接消费:前端不调用 gRPCBFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 packages/contracts/src/permissions.ts 中的权限点常量TS 文件,非 proto 生成)。

11. 风险与假设

11.1 假设

  1. 假设 coord 建立 packages/shared-tspackages/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

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 按数据类型分级

11.3 未决设计决策(需 coord 仲裁)

  1. packages 归属ui-tokens / ui-components / hooks 是 ai07 维护还是 coord 维护建议ai07 维护前端专属coord 仅维护 shared-ts / contracts(跨语言/跨服务)。
  2. GraphQL vs REST004 §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 自行决定内部布局。

12. coord 交叉审查所需信息

12.1 端口矩阵

dev 端口 生产端口 备注
teacher-portal 3000 3000 Shell 宿主

full-stack-runbook 端口矩阵对齐。

12.2 依赖的共享包(需 coord 建立)

路径 维护方 内容
shared-ts packages/shared-ts/ coord ApiClient、Logger、通用工具
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建议 usePermission、useAuth、useViewports

12.3 依赖的后端契约(需对应 AI 确认)

契约 提供方 当前状态
POST /iam/loginGET /iam/effective-permissionsGET /iam/me iam 已实现
GET /teacher/viewportsGET /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-questionsSSE ai 📐 待 P5

12.4 错误码前缀(前端 i18n 路由依赖)

前端不产生错误码,仅消费。需各服务确认错误码前缀不重叠:

前缀 服务 状态
IAM_ iam 已用
CLASSES_ core-edu 已用
EXAMS_/HOMEWORK_/GRADES_ core-edu ⚠️ 待确认
CONTENT_ content ⚠️ 待确认
MSG_ msg ⚠️ 待确认
AI_ ai ⚠️ 待确认
BFF_TEACHER_ teacher-bff ⚠️ 待确认
GW_ api-gateway 已用
NETWORK_ 前端 ai07 自有

12.5 不产生 Kafka 事件

前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。

13. 实施路线ai07 自用)

P2 收尾teacher-portal 审计对齐)

  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
  8. 补 ErrorBoundary + /api/health route
  9. 配置 next.config.js Module FederationShell 角色)
  10. 补 Vitest 单测 + Playwright E2E覆盖率 ≥ 80%

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. 性能优化MF shared 单例验证、bundle 分析)

AI Agent: ai07 (teacher-portal shell) Branch: docs/teacher-portal-stage1-stage2-design-ai07 Coordinator: coord-ai