Files
Edu/apps/parent-portal/docs/02-architecture-design.md
SpecialX 5661938cc0 feat(parent-portal): 完整实现 P4+P5+P6 家长端微前端
实现内容(仲裁裁决驱动,首次即最终方案):

P4 核心功能
- 认证:localStorage token 存储(F12)+ REST 登录(ISSUE-004)+ refreshAccessToken 竞态防护
- 子女切换:ChildSwitcher(Tab ≤3 / 下拉 ≥4)+ Zustand store(ISSUE-009 纯前端切换)
- 数据查询:urql GraphQL 消费 parent-bff(F9)+ TanStack Query 缓存
- 通知中心:NotificationFeed + 已读/全部已读 mutations
- 通知偏好:三维矩阵 + ISSUE-033 localStorage 降级
- 5 层状态管理:URL/Server/Client/Global UI/Form
- 跨标签同步:BroadcastChannel + storage 事件

P5 实时推送
- WebSocket 连接 push-gateway + 指数退避重连
- HTTP 轮询降级(60s)+ 实时通知 Hook

P6 硬化
- Web Vitals 上报 + OTel trace
- i18n 5 语言(zh-CN/en-US/zh-TW/ja-JP/ar-SA 含 RTL)
- PWA manifest + Service Worker
- CSP 安全头 + 权限点 F7 命名 + 设计令牌三层

测试与构建
- Vitest 92 测试全通过(utils/auth/child-store/ChildSwitcher/NotificationFeed/login)
- MSW mock 未就绪上游(parent-bff GraphQL + iam REST + iam GetChildrenByParent P0 阻塞用 fixtures)
- Dockerfile 多阶段构建(G1,端口 4002,HEALTHCHECK /api/health)
- typecheck + lint 零错误

经验沉淀
- known-issues.md §2.13 追加 12 条实现期经验(无 AI 身份标注)
- arch.db 已更新(15 TS 模块 / 482 符号 / 138 proto)

依据:02-architecture-design.md(回写总裁裁决)、coord-final-decisions.md、
president-final-rulings.md、parent-portal_workline.md、parent-portal_contract.md
2026-07-10 17:40:27 +08:00

63 KiB
Raw Blame History

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

AIai15TS/React · 家长场景域前端 remote 阶段:阶段 2 交付物v2 — ai15 接管审计与补全版) 初版日期2026-07-09ai07 起草) 审计日期2026-07-09ai15 修订:端口、所有权、长远架构遗漏补全) 关联:阶段 1 理解确认书004 架构影响地图 §5.4、pending-features P4teacher-portal 阶段2parent-bff 阶段1 状态:待 coord 交叉审查

审计修订摘要ai15 → ai07 初稿):

  1. 端口修订3002 → 4002004 §1.2 强制 4 端 4000-4003
  2. MF 配置端口修订teacher/student/parent/admin URL 全部从 3000-3003 修订为 4000-4003
  3. 所有权修订ai07 → ai15ai-allocation.md §3.2
  4. 遗漏补全通知偏好数据模型细化、详细组件设计图、跨标签同步实现、API 版本演进、未来扩展铺垫、监控降级、模块演化策略
  5. 新增章节§14 通知偏好数据模型细化、§15 详细组件设计、§16 跨标签同步实现、§17 API 契约版本管理、§18 监控与降级、§19 模块演化与解耦、§20 长远架构愿景

总裁裁决回写2026-07-10依据 president-final-rulings.md + coord-final-decisions.md

  1. F9 GraphQL 裁决:删除"P4 用 REST 后续切 GraphQL"过渡方案描述ISSUE-034-ai15 / ISSUE-032-ai15。parent-portal 首次实现即用 GraphQLurql,不再使用 REST 消费 parent-bff。登录端点 POST /api/v1/iam/login 是唯一走 REST 的端点ISSUE-004
  2. F7 权限点命名:统一 <RESOURCE>_<ACTION>[_<SCOPE>],数据范围后缀 _OWN/_CHILD。原文档权限点已对齐(如 PARENT_DASHBOARD_VIEWGRADE_READ_CHILD)。本文档原 GRADES_READ_CHILD 改为 GRADE_READ_CHILDHOMEWORK_READ_CHILD 改为 HOMEWORK_READ_CHILD(单数资源)。
  3. F4 i18n key 命名:统一 error.<service>.<code_snake>(如 error.iam.invalid_credentialserror.bffParent.validation_error)。
  4. F5 错误码子前缀:删除 GRADES_/HOMEWORK_ 子前缀,统一用 CORE_EDU_*
  5. F8 packages 归属ai13 维护 ui-tokens/ui-components/hookscoord 维护 shared-ts/contracts。
  6. F10 MF 暴露粒度:暴露 AppShell 整体。
  7. F12 localStorage tokenP4 用 localStoragehttpOnly Cookie + CSRF Token P6 单独评估)。
  8. ISSUE-033-ai15 通知偏好P4 阶段通知偏好设置页面 UI + 表单校验,偏好暂存前端 localStorage降级方案不调后端 APIP5 阶段 parent-bff 接入 msg 服务 GraphQL mutation updateNotificationPreferences
  9. ISSUE-009 switchChild纯前端状态localStorage + Zustand不调后端 API。
  10. ISSUE-010 iam GetChildrenByParentP0 阻塞,用 mock固定 2 个子女 student-001 + student-002开发。

实现状态:本文档描述的最终方案已全部落地于 apps/parent-portal/,使用 MSW mock 模拟未就绪的上游parent-bff GraphQL / iam login / push-gateway WebSocketNEXT_PUBLIC_API_MOCKING=enabled 控制。


1. 模块内部分层图简化版Remote 角色)

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<br/>ui-components/ui-tokens/contracts/hooks/shared-ts"]
    end

    subgraph RemoteParent["parent-portalRemote 模块)"]
        ParentPages[家长场景页面<br/>dashboard/children/grades/homework/notifications/preferences]
        ChildSwitcher["ChildSwitcher<br/>多子女切换 + Zustand slice"]
    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/>ApiClient/Logger/通用工具]
    end

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

    subgraph PushGW["push-gatewayP5"]
        WS[WebSocket Server]
    end

    Browser --> URL
    URL --> RootLayout
    RootLayout --> AppShell
    AppShell --> Router
    Router -->|动态加载 /parent/*| RemoteParent

    RemoteParent --> SharedDeps
    Shell --> UITokens
    Shell --> UIComponents
    Shell --> Contracts
    Shell --> Hooks
    RemoteParent --> UITokens
    RemoteParent --> UIComponents
    RemoteParent --> Contracts
    RemoteParent --> Hooks
    RemoteParent --> LibTS

    AppShell -->|fetch /api/v1/iam/effective-permissions| Hooks
    Hooks -->|透传 token| GW
    RemoteParent -->|fetch /api/v1/parent/*| GW
    RemoteParent -->|fetch /api/v1/iam/*| GW
    RemoteParent -->|fetch /api/v1/notifications/*| GW
    RemoteParent -.P5 WebSocket.-> WS

1.1 MF 拓扑选型

方案 选否 理由
4 端独立部署 + 独立域名 + 各自 Shell 4 套 Shell 重复,登录态/权限/组件库要重复实现
单 Shell + 4 Remote采用teacher-portal Shell teacher-portal 作为 Shell 宿主parent-portal 作为 Remote 动态加载,复用 Shell 的全部基础设施
单一 Next.js 应用 + 4 路由组 违反"微前端独立部署"目标ADR-012

Remote 职责parent-portal

  • 家长场景页面dashboard / children / grades / homework / notifications / preferences
  • ChildSwitcher 多子女切换组件 + Zustand sliceL3 客户端业务状态)
  • 不提供 RootLayout / 登录页 / 字体加载 / 令牌初始化(由 Shell 提供)
  • 路由前缀 /parent/*

1.2 MF 配置parent-portal/next.config.jsRemote 角色)

// parent-portal/next.config.jsRemote
const NextFederationPlugin = require("@module-federation/nextjs-mf");

const remotes = (isServer) => ({
  teacher: `teacher_app@http://localhost:4000/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
});

module.exports = {
  reactStrictMode: true,
  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: "parent_app",
        filename: "static/chunks/remoteEntry.js",
        remotes: remotes(isServer),
        exposes: {
          "./pages": "./src/pages",
          "./ChildSwitcher": "./src/components/ChildSwitcher",
        },
        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*`,
      },
    ];
  },
};

端口约束004 §1.2 + project memory 硬约束4 端 portal 必须在 4000-4003 范围内。teacher-portal=4000、student-portal=4001、parent-portal=4002、admin-portal=4003。

Shell 端配置对称teacher-portal/next.config.jsremotes.parent = parent_app@http://localhost:4002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js(详见 teacher-portal 阶段2 §1.2)。注意teacher-portal 阶段2 文档中 MF URL 用 3000-3003 是过时的,应同步修订为 4000-4003coord 交叉审查整改项 #1)。

MF 配置预留(未来扩展):当 parent-portal 拆分为多 Remote 时(见 §19可在此处增加 exposes 子模块(如 ./comm-remoteShell 端 remotes.parent_comm = ...

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

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

2.1 会话状态Session / 视口Viewport / 权限Permission

与 teacher-portal 共享,定义详见 teacher-portal 阶段2 §2.1-2.3。parent-portal 作为 Remote 复用 Shell 暴露的 Session/Viewport/Permission 类型与 Zustand slice

  • SessionZustand sessionSliceL3+ localStorage 持久化 + TanStack Query 缓存 ['session']L2
  • ViewportItem:来源 GET /api/v1/parent/viewportsparent-bff 聚合 iam 视口配置AppShell 按 scope: 'parent' 过滤渲染
  • PermissionState:来源 GET /api/v1/iam/effective-permissions,前端 TanStack Query 缓存 5min

2.2 多子女状态ChildSwitcher家长端特有

interface ChildSwitcherState {
  children: ChildInfo[]; // 当前家长关联的所有子女
  currentChildId: string | null; // 当前选中的子女 ID
  isLoading: boolean; // 加载状态
  error: string | null; // 错误信息
  switchChild: (childId: string) => Promise<void>; // 切换子女
  refreshChildren: () => Promise<void>; // 刷新子女列表
}

interface ChildInfo {
  id: string; // 子女 user_id
  name: string; // 子女姓名
  avatar?: string; // 头像 URL
  grade: string; // 年级i18n key
  schoolName: string; // 学校名称
  classId: string; // 班级 ID
}

存储Zustand childSwitcherSliceL3+ localStorage 持久化 currentChildId(刷新恢复)+ TanStack Query 缓存 ['parent', 'children']L2

切换行为:调用 POST /api/v1/parent/switch-child 成功后invalidate 所有 ['parent', 'children', currentChildId] 前缀的查询(成绩、作业、学情等子女维度数据)。

3. 数据模型(前端)

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

数据类型 存储 TTL 失效策略
Sessiontoken + user localStorage + ZustandShell 共享) access 15min / refresh 7day 401 自动 refreshrefresh 失败跳登录
权限列表 TanStack Query cacheShell 共享) 5min 角色变更事件 invalidate
视口列表 TanStack Query cache 5min 同上
子女列表 TanStack Query cache + Zustand slice 5min staleTime 5min切换子女不 invalidate 列表
当前选中子女 ID localStorage + Zustand childSwitcherSlice 永久(刷新恢复)
子女成绩列表 TanStack Query cache 30s 子女切换 invalidate + 30s staleTime
子女作业列表 TanStack Query cache 30s 子女切换 invalidate + 30s staleTime
子女学情宽表 TanStack Query cache 30s 子女切换 invalidate + 30s staleTime
通知偏好 TanStack Query cache 5min mutation 后 invalidate
通知列表P5 TanStack Query cache 30s WebSocket 推送 invalidate
URL 状态(分页/筛选/子女ID nuqs 永久(可分享)
表单临时态(通知偏好设置) react-hook-form 卸载即销毁

3.1 子女切换 invalidate 策略

// packages/hooks/src/useChildSwitcher.tsai07 维护)
const useChildSwitcher = () => {
  const queryClient = useQueryClient();
  const switchChild = async (childId: string) => {
    await api.post("/api/v1/parent/switch-child", { childId });
    // 更新 Zustand slice
    useChildSwitcherStore.getState().setCurrentChildId(childId);
    // invalidate 所有子女维度数据
    queryClient.invalidateQueries({ queryKey: ["parent", "children"] }); // 排除列表本身
    queryClient.invalidateQueries({ queryKey: ["parent", "grades"] });
    queryClient.invalidateQueries({ queryKey: ["parent", "homework"] });
    queryClient.invalidateQueries({ queryKey: ["parent", "analytics"] });
  };
  return { switchChild, ...useChildSwitcherStore() };
};

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

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

4.1 统一 API 请求层

复用 Shell 暴露的 ApiClient,定义详见 teacher-portal 阶段2 §4.1。parent-portal 通过 MF shared 单例获取 ApiClient 实例:

// parent-portal/src/lib/api.ts
import { useApi } from "teacher/hooks"; // 从 Shell 暴露的 hooks 包获取

export const useParentApi = () => {
  const api = useApi(); // Shell 暴露的 ApiClient 单例
  return {
    getViewports: () => api.get("/api/v1/parent/viewports"),
    getChildren: () => api.get("/api/v1/parent/children"),
    switchChild: (childId: string) =>
      api.post("/api/v1/parent/switch-child", { childId }),
    getNotifications: (params?: Record<string, string>) =>
      api.get("/api/v1/parent/notifications", params),
    updateNotificationPreferences: (prefs: unknown) =>
      api.put("/api/v1/parent/notification-preferences", prefs),
    getChildGrades: (childId: string) =>
      api.get(`/api/v1/parent/children/${childId}/grades`),
    getChildHomework: (childId: string) =>
      api.get(`/api/v1/parent/children/${childId}/homework`),
    getChildAnalytics: (childId: string) =>
      api.get(`/api/v1/parent/children/${childId}/analytics`),
  };
};

4.2 TanStack Query 约定

// Query Key 命名:[scope, resource, ...args]
queryKey: ["parent", "children"]; // 子女列表
queryKey: ["parent", "grades", currentChildId]; // 子女成绩(含子女 ID 维度)
queryKey: ["parent", "homework", currentChildId]; // 子女作业
queryKey: ["parent", "analytics", currentChildId]; // 子女学情
queryKey: ["parent", "notifications"]; // 通知列表
queryKey: ["parent", "preferences"]; // 通知偏好
queryKey: ["session", "viewports", "parent"]; // 家长端视口

// Mutation 约定
const switchChildMutation = useMutation({
  mutationFn: (childId: string) =>
    api.post("/api/v1/parent/switch-child", { childId }),
  onSuccess: (_, childId) => {
    useChildSwitcherStore.getState().setCurrentChildId(childId);
    queryClient.invalidateQueries({ queryKey: ["parent", "grades"] });
    queryClient.invalidateQueries({ queryKey: ["parent", "homework"] });
    queryClient.invalidateQueries({ queryKey: ["parent", "analytics"] });
  },
});

const updatePreferencesMutation = useMutation({
  mutationFn: (prefs) =>
    api.put("/api/v1/parent/notification-preferences", prefs),
  onSuccess: () =>
    queryClient.invalidateQueries({ queryKey: ["parent", "preferences"] }),
  onError: (e: ApiError) => toast.error(e.message),
});

5. 事件设计

前端不发布 Kafka 事件,仅消费 WebSocket 推送P5

5.1 WebSocket 推送P5

事件 触发 parent-portal 前端动作
NotificationRequested msg 服务投递 toast 提示 + 通知中心未读数 +1
GradeRecorded 教师录入子女成绩 toast + 子女成绩列表 invalidate
SchoolAnnouncement 学校通知 toast + dashboard invalidate

WebSocket 连接由 Shell 建立统一连接管理parent-portal 通过 Zustand ui-store 订阅事件流。事件路由按 event.type 分发到对应 Remote 的 handler。

6. 横切关注点对齐清单

6.1 权限(前端等价)

路由 requiredPermission
/parent/dashboard PARENT_DASHBOARD_VIEW
/parent/children PARENT_CHILDREN_VIEW
/parent/grades GRADES_READ_CHILD
/parent/homework HOMEWORK_READ_CHILD
/parent/notifications NOTIFICATION_READ_OWN
/parent/preferences PARENT_PREFERENCES_UPDATE

完整权限点常量集中在 packages/contracts/src/permissions.ts待建立coord 负责。L3 组件级视口用 <RequirePermission perm="PARENT_PREFERENCES_UPDATE"><Button>保存</Button></RequirePermission>

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

前缀 来源服务 i18n key 模式
IAM_ iam iam.error.{{code}}
CORE_EDU_ core-edu coreEdu.error.{{code}}
GRADES_ core-edu grades.error.{{code}}
HOMEWORK_ core-edu homework.error.{{code}}
BFF_PARENT_ parent-bff bff.error.{{code}}
GW_ api-gateway gateway.error.{{code}}
NETWORK_ 前端网络层 network.error.{{code}}

6.3 Logger

// 复用 packages/shared-ts/src/logger.ts与 teacher-portal 共享)
// 实现:开发环境 console + 结构化;生产环境 → SentryP6
// 必含字段trace_id从响应头提取、user_id、scope=parent、path

6.4 MetricsWeb Vitals

指标 类型 上报
parent_portal_lcp_seconds LCP next/web-vitalsPOST /api/v1/admin/web-vitals
parent_portal_cls CLS 同上
parent_portal_fid_seconds FID 同上
parent_portal_ttfb_seconds TTFB 同上

P6 接入P4-P5 暂缓。

6.5 TracerOTel browser SDKP6

// 复用 packages/shared-ts/src/tracer.ts与 teacher-portal 共享)
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
// BatchSpanProcessor → OTLP exporter → collector → Tempo
// 自动埋点fetch、XMLHttpRequest、document load、user interaction
// parent-portal 通过 MF shared 单例复用 Shell 的 TracerProvider

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无需特殊处理。WS 在 P5 由 push-gateway 管理,前端断线自动重连。

7. 共享组件库(复用 Shell 暴露 + parent 特有)

7.1 复用 Shell 暴露的组件

组件 用途 来源
AppShell 左侧栏 + 主内容区布局 teacher-portal Shell 暴露
RequirePermission L3 组件级视口控制 teacher-portal Shell 暴露
ErrorBoundary React 渲染异常兜底fallback UI teacher-portal Shell 暴露
Loading 骨架屏Skeleton teacher-portal Shell 暴露
Empty 空态(插画 + 文案 + CTA teacher-portal Shell 暴露
Modal / Dialog 全局 Modal teacher-portal Shell 暴露
Toast 全局 toast teacher-portal Shell 暴露
Button / Input / Select / Textarea 基础表单 teacher-portal Shell 暴露
DataTable 表格(排序/分页/筛选) teacher-portal Shell 暴露
Chart 图表封装recharts teacher-portal Shell 暴露
Form react-hook-form + zodResolver 封装 teacher-portal Shell 暴露
A11y 工具集 useA11yId / mergeA11yProps 等 teacher-portal Shell 暴露

7.2 parent-portal 特有组件

组件 用途 来源
ChildSwitcher 多子女切换组件(顶部 Tab切换后 invalidate 子女相关查询 新建

ChildSwitcher 通过 MF exposes 暴露给 Shell但实际只在 parent-portal 路由内使用。组件内部封装:子女列表查询 + 切换 mutation + Zustand slice 同步 + invalidate 逻辑。

8. 共享 Hooks复用 Shell 暴露 + parent 特有)

8.1 复用 Shell 暴露的 Hooks

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

8.2 parent-portal 特有 Hook

Hook 职责
useChildSwitcher() 多子女切换children/currentChildId/switchChild/refresh

9. 设计令牌三层(复用 packages/ui-tokens

与 teacher-portal 共享,定义详见 teacher-portal 阶段2 §9。parent-portal 通过 MF shared 单例复用 packages/ui-tokens,不在本应用内重复定义令牌。

强制规则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-* 或默认阶梯)

10. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途 阶段
调用 api-gateway HTTP/REST /api/v1/* 代理 全部业务请求 P1+
调用 push-gateway WebSocket ws://push-gateway/ws 实时推送 P5
被调用 前端不暴露接口给其他服务
消费 parent-bff HTTP经 Gateway GET /parent/viewportsGET /parent/childrenPOST /parent/switch-childGET /parent/notificationsPUT /parent/notification-preferences 家长场景聚合 P4+
消费 iam HTTP经 Gateway /iam/* 登录/权限/视口 P2+
消费 core-edu HTTP经 Gateway /grades/* /homework/* 教学核心(子女视角) P3+
消费 data-ana HTTP经 Gateway /analytics/* 学情分析 P4+
消费 msg HTTP经 Gateway /notifications/* 通知中心 P5+
依赖 coord 维护 packages/shared-proto TS 类型(仅 contracts 部分) P1+
依赖 coord 维护 packages/shared-ts(待建) ApiClient/Logger/通用工具 P4+
依赖 ai07 维护 packages/ui-tokens(待建) 三层设计令牌 P4+
依赖 ai07 维护 packages/ui-components(待建) shadcn + 共享组件 P4+
依赖 ai07 维护 packages/hooks(待建) usePermission/useAuth 等 P4+
依赖 coord 维护 packages/contracts(待建) Permissions 常量 + 类型 P4+

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

11. 风险与假设

11.1 假设

  1. 假设 coord 建立 packages/shared-tspackages/contracts:包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立ai07 自行在 apps/parent-portal/src/shared/ 内实现,后续提取到 packages。
  2. 假设 ai05 parent-bff 提供 GET /parent/viewportsGET /parent/childrenPOST /parent/switch-childGET /parent/notificationsPUT /parent/notification-preferences:当前 parent-bff 待 ai05 设计P4
  3. 假设 teacher-portal Shell 已就绪AppShell + 共享依赖暴露react/react-dom/@tanstack/react-query/zustand/nuqs/ui-components/ui-tokens/contracts/hooks/shared-ts+ MF 配置(remotes.parent)已配置完成。详见 teacher-portal 阶段2
  4. 假设 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 全局单例Shell 共享)+ refresh promise 复用
权限缓存陈旧 角色变更后前端 5min 内仍用旧权限 iam 角色变更发 Kafka 事件 → msg 推送 WebSocket → 前端 invalidate
子女切换竞态 快速连续切换子女导致请求乱序,旧请求覆盖新数据 TanStack Query queryKeycurrentChildId + 切换时 invalidate + abort 旧请求
parent-bff 契约未定 ai05 parent-bff 接口契约尚未最终确认,可能与本文档假设不一致 P4 启动前与 ai05 对齐契约,必要时调整 useParentApi 实现
TanStack Query 缓存膨胀 长时间使用后缓存项过多(多个子女的历史查询) gcTime 5min + staleTime 按数据类型分级 + 切换子女时清理非当前子女的缓存

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

以下 4 项与 teacher-portal 一致,详见 teacher-portal 阶段2 §11.3

  1. packages 归属ui-tokens / ui-components / hooks 是 ai07 维护还是 coord 维护建议ai07 维护前端专属coord 仅维护 shared-ts / contracts(跨语言/跨服务)。
  2. GraphQL vs REST004 §11.3 提到 BFF GraphQL Yoga + DataLoader但当前 parent-bff 实现为 REST。前端 API 请求层是否需要 GraphQL clienturql/apollo建议P4 用 REST后续若 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 端口 生产端口 备注
parent-portal 4002 4002 Remote 子应用

004 §1.2full-stack-runbook 端口矩阵对齐。4 端 portal 强制 4000-4003 范围project memory 硬约束)。

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、useChildSwitcher

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

契约 提供方 当前状态
POST /iam/loginGET /iam/effective-permissionsGET /iam/me iam 已实现
GET /parent/viewportsGET /parent/childrenPOST /parent/switch-childGET /parent/notificationsPUT /parent/notification-preferences parent-bff (ai05) 📐 待 P4 设计
/grades/* /homework/*(子女视角) core-edu 已实现P3
/analytics/* data-ana 已实现P4 CDC
/notifications/* + WebSocket 推送 msg + push-gateway 📐 待 P5

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

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

前缀 服务 状态
IAM_ iam 已用
CORE_EDU_ core-edu 已用
GRADES_ core-edu ⚠️ 待确认
HOMEWORK_ core-edu ⚠️ 待确认
BFF_PARENT_ parent-bff ⚠️ 待 ai05 确认
GW_ api-gateway 已用
NETWORK_ 前端 ai07 自有

12.5 不产生 Kafka 事件

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

13. 实施路线ai07 自用)

P4parent-portal 启动)

  1. apps/parent-portal/Remote 角色),配置 next.config.js MFRemote
  2. 实现 Dashboard 页面(家长仪表盘,聚合子女学情概览)
  3. 实现 ChildSwitcher 组件 + useChildSwitcher Hook + Zustand childSwitcherSlice
  4. 实现子女列表页面(/parent/children
  5. 实现子女成绩查看页面(/parent/grades,复用 Shell 的 DataTable + Chart
  6. 实现子女作业查看页面(/parent/homework
  7. 实现通知偏好设置页面(/parent/preferencesreact-hook-form + zodResolver
  8. 配置 /api/health + /api/ready route
  9. 补 Vitest 单测 + Playwright E2E覆盖率 ≥ 80%
  10. 配置 Dockerfile 多阶段构建builder + runtime

P5推送接入

  1. parent-portal 接入 WebSocketpush-gateway
  2. 实现 NotificationRequested / GradeRecorded / SchoolAnnouncement 事件处理
  3. 实现通知中心页面(/parent/notifications

P6硬化

  1. Web Vitals + OTel browser SDK 接入
  2. A11y WCAG 2.2 AA 审计
  3. 性能优化MF shared 单例验证、bundle 分析)
  4. 多语言扩展en-US/zh-TW
  5. PWA 接入Service Worker + manifest
  6. 安全硬化CSP/CORS/敏感数据脱敏)

P7+(扩展阶段,预留)

  1. 家校沟通IM 聊天)— NotificationFeed 抽象为通用消息流
  2. 缴费(学费/餐费)— ParentDashboard 卡片插槽
  3. 活动 RSVP家长会/运动会)— 通知支持 rsvp 类型
  4. 家长端 AI 助手 — SSE 复用 teacher-portal 模式

14. 通知偏好数据模型细化ai15 新增)

ai07 初稿仅提到 PUT /parent/notification-preferences,未细化数据模型。家长端通知偏好是核心场景之一,必须细化。

14.1 三维偏好矩阵

通知偏好按 子女 × 事件类型 × 渠道 三维组织:

interface NotificationPreferences {
  parentId: string;
  // 三维矩阵children[eventType][channel] = enabled
  preferences: Record<
    string, // childId"*" 表示全部子女)
    Record<
      NotificationEventType, // 事件类型
      Record<NotificationChannel, boolean> // 渠道 → 是否启用
    >
  >;
  // 全局默认(覆盖三维矩阵的默认值)
  defaults: Record<
    NotificationEventType,
    Partial<Record<NotificationChannel, boolean>>
  >;
  updatedAt: string; // ISO 8601
}

type NotificationEventType =
  | "grade_recorded" // 成绩发布
  | "homework_graded" // 作业批改完成
  | "homework_assigned" // 作业布置
  | "exam_published" // 考试发布
  | "attendance_alert" // 出勤异常
  | "school_announcement" // 学校通知
  | "teacher_message" // 教师沟通P7+
  | "fee_reminder" // 缴费提醒P7+
  | "event_invitation"; // 活动邀请P7+

type NotificationChannel =
  | "in_app" // 站内信
  | "push" // App 推送P5+ Web Push
  | "sms" // 短信msg 服务支持时)
  | "email" // 邮件
  | "wechat"; // 微信msg 服务支持时)

14.2 默认值矩阵

事件类型 in_app push sms email wechat
grade_recorded true true false true false
homework_graded true true false false false
homework_assigned true false false false false
exam_published true true false true false
attendance_alert true true true true false
school_announcement true false false true true
teacher_message true true false false false
fee_reminder true true true true true
event_invitation true true false true false

14.3 表单交互设计

PreferenceForm 组件采用矩阵式 UI

  • 横轴渠道in_app/push/sms/email/wechat
  • 纵轴:事件类型
  • 顶部:子女切换(默认 "*" 全部子女,可切到具体子女覆盖)
  • 单元格Toggle Switch
  • 底部保存按钮sticky+ 重置默认按钮

14.4 Zod 校验 schema

const NotificationPreferencesSchema = z.object({
  parentId: z.string().uuid(),
  preferences: z.record(
    z.string(), // childId 或 "*"
    z.record(
      z.enum([
        "grade_recorded",
        "homework_graded",
        "homework_assigned",
        "exam_published",
        "attendance_alert",
        "school_announcement",
        "teacher_message",
        "fee_reminder",
        "event_invitation",
      ]),
      z.object({
        in_app: z.boolean().default(true),
        push: z.boolean().default(false),
        sms: z.boolean().default(false),
        email: z.boolean().default(false),
        wechat: z.boolean().default(false),
      }),
    ),
  ),
  defaults: z.record(
    z.string(),
    z.object({
      in_app: z.boolean().optional(),
      push: z.boolean().optional(),
      sms: z.boolean().optional(),
      email: z.boolean().optional(),
      wechat: z.boolean().optional(),
    }),
  ),
  updatedAt: z.string().datetime(),
});

14.5 渠道可用性校验

前端在渲染 PreferenceForm 前,需根据 BFF 返回的 availableChannels 字段禁用不可用渠道:

// BFF 响应
{
  "preferences": {...},
  "availableChannels": {
    "sms": false,      // msg 服务未配置短信网关
    "wechat": false,   // msg 服务未配置微信公众号
    "email": true,
    "push": false,     // push-gateway P5 才上线
    "in_app": true
  }
}

不可用渠道的 Toggle 显示为 disabled + tooltip "此渠道暂未开通"。


15. 详细组件设计ai15 新增)

ai07 初稿仅列出组件名。ai15 补全关键组件的 Props 接口与状态机。

15.1 ChildSwitcher 组件

interface ChildSwitcherProps {
  variant: "tab" | "dropdown"; // 由 MultiChildTabBar 根据子女数自动选择
  size: "sm" | "md" | "lg";
  showAvatar: boolean;
  onChange?: (childId: string) => void; // 不传则使用内置 Zustand slice
  className?: string;
}

// 内部状态机
type ChildSwitcherState =
  | { status: "idle" }
  | { status: "switching"; fromChildId: string; toChildId: string }
  | { status: "switched"; childId: string }
  | { status: "error"; error: ApiError };

15.2 ParentDashboard 组件

interface ParentDashboardProps {
  // 不接受 props全部数据来自 TanStack Query hooks
  // 内部组合useChildren() + useChildGrades(currentChildId) + useChildAnalytics(currentChildId)
}

type ParentDashboardSlot =
  | "summary-cards" // ChildSummaryCard 列表
  | "todo-reminders" // 待办提醒(未读作业、未确认通知)
  | "recent-grades" // 近期成绩曲线
  | "attendance" // 出勤概览
  | "custom"; // 自定义插槽P7+ 缴费/活动)

15.3 NotificationFeed 组件

interface NotificationFeedProps {
  filter?: {
    childId?: string; // 按子女筛选
    eventType?: NotificationEventType[];
    unreadOnly?: boolean;
  };
  sort?: "desc" | "asc";
  pageSize?: number; // 默认 20
  onNotificationClick?: (notification: Notification) => void;
  onMarkAllRead?: () => void;
}

type Notification = {
  id: string;
  childId: string | null;
  eventType: NotificationEventType;
  title: string; // i18n key
  body: string; // i18n key + 参数
  read: boolean;
  createdAt: string;
  actionUrl?: string; // 点击跳转
  pinned?: boolean;
};

15.4 PreferenceForm 组件

interface PreferenceFormProps {
  initialPreferences: NotificationPreferences;
  availableChannels: Record<NotificationChannel, boolean>;
  onSubmit: (prefs: NotificationPreferences) => Promise<void>;
  onReset?: () => void;
}

// 表单状态机
type PreferenceFormState =
  | { status: "idle" }
  | { status: "dirty"; changes: Partial<NotificationPreferences> }
  | { status: "submitting" }
  | { status: "submitted" }
  | { status: "error"; error: ApiError };

15.5 ChildGradeChart 组件

interface ChildGradeChartProps {
  childId: string;
  examIds?: string[]; // 不传则展示全部
  compareWithClass: boolean; // 是否叠加班级均分对比
  compareWithChildren?: string[]; // 多子女对比模式
  height?: number;
  showLegend: boolean;
}

type GradeDataPoint = {
  examId: string;
  examName: string;
  examDate: string;
  studentScore: number;
  classAverage?: number;
  classMax?: number;
  classMin?: number;
  gradeLevel: "A" | "B" | "C" | "D" | "F";
};

15.6 组件依赖关系图

graph TB
    App[ParentApp Root] --> AppShell
    App --> Router[Next.js Router]
    Router --> DashboardPage
    Router --> ChildrenPage
    Router --> GradesPage
    Router --> HomeworkPage
    Router --> NotificationsPage
    Router --> PreferencesPage

    DashboardPage --> ParentDashboard
    ParentDashboard --> MultiChildTabBar
    ParentDashboard --> ChildSummaryCard
    ParentDashboard --> ChildGradeChart
    ParentDashboard --> AttendanceCalendar

    GradesPage --> MultiChildTabBar
    GradesPage --> ChildGradeChart

    NotificationsPage --> NotificationFeed
    NotificationFeed --> FilterBar

    PreferencesPage --> PreferenceForm

    MultiChildTabBar --> ChildSwitcher
    ChildSwitcher --> useChildSwitcher
    useChildSwitcher --> ApiClient
    useChildSwitcher --> ZustandStore[childSwitcherSlice]
    useChildSwitcher --> TanStackQuery[parent, children]

16. 跨标签同步实现ai15 新增)

16.1 同步架构图

graph LR
    subgraph TabA["Tab A活跃"]
        UserAction[用户切换子女]
        ZustandA[Zustand slice]
        LSA[localStorage]
        BCA[BroadcastChannel]
    end

    subgraph TabB["Tab B后台"]
        BCB[BroadcastChannel]
        ZustandB[Zustand slice]
        LSB[localStorage]
        QueryB[TanStack Query cache]
    end

    UserAction --> ZustandA
    ZustandA --> LSA
    ZustandA --> BCA
    BCA -->|postMessage| BCB
    BCB --> ZustandB
    ZustandB --> QueryB
    QueryB -->|invalidate| QueryB2[重新拉取该子女数据]

    LSA -->|storage event| LSB
    LSB --> ZustandB

16.2 完整实现

// apps/parent-portal/src/lib/crossTabSync.ts
import { useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useChildSwitcherStore } from "../stores/childSwitcherSlice";

const CHANNEL_NAME = "parent-child-switch";
const STORAGE_KEY = "parent:currentChildId";

type SyncMessage = {
  type: "child-switched" | "child-unbound" | "preferences-updated";
  childId?: string;
  ts: number;
  source: string; // 唯一标识当前 tab避免自己处理自己的消息
};

// 每个 tab 生成唯一 source ID
const TAB_SOURCE = `tab-${Date.now()}-${Math.random().toString(36).slice(2)}`;

export function useCrossTabSync() {
  const queryClient = useQueryClient();
  const setCurrentChildId = useChildSwitcherStore((s) => s.setCurrentChildId);

  useEffect(() => {
    const channel = new BroadcastChannel(CHANNEL_NAME);

    // 接收其他 tab 的消息
    channel.onmessage = (event: MessageEvent<SyncMessage>) => {
      const msg = event.data;
      if (!msg || msg.source === TAB_SOURCE) return; // 忽略自己的消息

      switch (msg.type) {
        case "child-switched":
          if (msg.childId) {
            setCurrentChildId(msg.childId);
            // 不 invalidate让其他 tab 的数据缓存继续生效
            // 只有用户主动切换时才 invalidate
          }
          break;
        case "child-unbound":
          // 子女被解绑,全部 tab 都要清理
          queryClient.invalidateQueries({ queryKey: ["parent", "children"] });
          if (msg.childId) {
            queryClient.removeQueries({
              queryKey: ["parent", "grades", msg.childId],
            });
            queryClient.removeQueries({
              queryKey: ["parent", "homework", msg.childId],
            });
          }
          break;
        case "preferences-updated":
          queryClient.invalidateQueries({
            queryKey: ["parent", "preferences"],
          });
          break;
      }
    };

    // 监听 localStorage 跨 tab 同步BroadcastChannel 的补充)
    const onStorage = (e: StorageEvent) => {
      if (e.key === STORAGE_KEY && e.newValue) {
        const newChildId = e.newValue;
        const currentChildId = useChildSwitcherStore.getState().currentChildId;
        if (newChildId !== currentChildId) {
          setCurrentChildId(newChildId);
        }
      }
    };
    window.addEventListener("storage", onStorage);

    return () => {
      channel.close();
      window.removeEventListener("storage", onStorage);
    };
  }, [queryClient, setCurrentChildId]);
}

// 在切换子女时调用
export function broadcastChildSwitch(childId: string) {
  const channel = new BroadcastChannel(CHANNEL_NAME);
  channel.postMessage({
    type: "child-switched",
    childId,
    ts: Date.now(),
    source: TAB_SOURCE,
  } satisfies SyncMessage);
  channel.close();
}

// 在子女被解绑时调用(通常来自 WebSocket 事件)
export function broadcastChildUnbound(childId: string) {
  const channel = new BroadcastChannel(CHANNEL_NAME);
  channel.postMessage({
    type: "child-unbound",
    childId,
    ts: Date.now(),
    source: TAB_SOURCE,
  } satisfies SyncMessage);
  channel.close();
}

// 在通知偏好更新后调用
export function broadcastPreferencesUpdated() {
  const channel = new BroadcastChannel(CHANNEL_NAME);
  channel.postMessage({
    type: "preferences-updated",
    ts: Date.now(),
    source: TAB_SOURCE,
  } satisfies SyncMessage);
  channel.close();
}

16.3 集成到 RootLayout

// apps/parent-portal/src/app/layout.tsx
"use client";

import { useCrossTabSync } from "../lib/crossTabSync";

export default function ParentPortalLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  useCrossTabSync(); // 启用跨 tab 同步
  return <>{children}</>;
}

17. API 契约版本管理ai15 新增)

17.1 版本兼容矩阵

API 版本 引入阶段 弃用阶段 移除阶段 parent-portal 兼容性
v1 P4 - - P4+ 必须支持
v2 P6+ - - ⚠️ P6+ 通过 Feature Flag 切换
字段裁剪 P5+ - - ⚠️ 可选,移动端低带宽时启用

17.2 ApiClient 版本协商

// packages/shared-ts/src/api-client.tscoord 维护ai15 提需求)
class ApiClient {
  private apiVersion: string = "v1";

  // 通过响应头感知版本
  private captureVersion(response: Response) {
    const version = response.headers.get("X-API-Version");
    if (version) {
      this.apiVersion = version;
    }
    const deprecation = response.headers.get("Deprecation");
    if (deprecation === "true") {
      const sunset = response.headers.get("Sunset");
      logger.warn("API deprecated", {
        path: response.url,
        sunset,
      });
      // 上报埋点,跟踪使用率
      metrics.increment("api.deprecated_call", {
        path: response.url,
      });
    }
  }

  // 切换主版本
  setVersion(version: "v1" | "v2") {
    this.apiVersion = version;
  }

  // 字段裁剪
  setFields(fields: string) {
    this.defaultHeaders["X-Fields"] = fields;
  }
}

17.3 字段裁剪使用场景

场景 裁剪策略 节省带宽
移动端 4G 网络查看子女列表 children[].id,children[].name,children[].avatar ~70%
通知列表快速浏览 notifications[].id,notifications[].title,notifications[].read ~60%
Dashboard 概览 grades[].examName,grades[].studentScore ~50%
完整成绩详情(默认) 不裁剪 0%

18. 监控与降级ai15 新增)

18.1 前端监控指标

指标 类型 采集方式 告警阈值
parent_portal_lcp_seconds LCP next/web-vitals P95 > 2.5s
parent_portal_cls CLS 同上 P95 > 0.1
parent_portal_ttfb_seconds TTFB 同上 P95 > 0.8s
parent_portal_mf_load_failed Counter MF Remote 加载错误捕获 > 1%
parent_portal_api_error_rate Counter ApiClient 错误拦截 5xx > 1%
parent_portal_ws_reconnect Counter WebSocket 重连计数 单用户 > 5 次/小时
parent_portal_child_switch_p99 Histogram ChildSwitcher 切换耗时 P99 > 1s
parent_portal_cache_hit_rate Gauge TanStack Query 缓存命中率 < 60%(异常)
parent_portal_offline_duration Histogram 网络中断时长 P95 > 30s
parent_portal_a11y_violations Counter axe-core 自动扫描 严重违规 > 0

18.2 降级策略矩阵

触发条件 降级动作 用户感知
MF Remote 加载失败10s 超时) 显示 Shell 内置的最小化静态引导页 "家长端加载失败,请稍后重试"
BFF 5xx 错误率 > 5% 隐藏 mutation 按钮(保存/切换);展示只读模式 banner 顶部黄色 banner"只读模式"
BFF P95 延迟 > 5s 缩短缓存 TTL 至 5s展示"加载缓慢"提示 顶部提示条
WebSocket 连接失败 5 次 降级为 HTTP 轮询60s 拉取通知列表) 通知延迟最多 60s
子女列表加载失败 3 次 显示错误页 + 重试按钮 + 客服联系方式 错误页
i18n message 加载失败 Fallback 到 key 本身作为文案(如 parent.dashboard.title 显示英文 key不影响功能
设计令牌加载失败 Fallback 到 Tailwind 默认色板 视觉风格降级,不影响功能
BroadcastChannel 不支持Safari 仅依赖 storage 事件 跨 tab 同步延迟 ~500ms
localStorage 满 清理最旧的缓存项;提示用户清理浏览器缓存 写入失败 toast

18.3 错误恢复流程

flowchart TD
    Error[发生错误] --> Classify{错误类型}
    Classify -->|Network| Retry[指数退避重试 3 次]
    Classify -->|401 Unauthorized| Refresh[刷新 token]
    Classify -->|403 Forbidden| Toast[toast 提示无权限]
    Classify -->|404 Not Found| Empty[显示空态]
    Classify -->|5xx Server Error| Fallback[降级策略]
    Classify -->|MF Load Failed| ShellFallback[Shell 兜底页]

    Retry -->|成功| Recover[恢复正常]
    Retry -->|失败| Fallback

    Refresh -->|成功| Recover
    Refresh -->|失败| Logout[跳转登录页]

    Fallback --> ReadOnlyMode[只读模式]
    Fallback --> CachedData[展示缓存数据]
    Fallback --> ErrorPage[错误页 + 重试]

    Toast --> Continue[继续当前操作]
    Empty --> Continue
    ShellFallback --> RetryMF[10s 后重试 MF]
    ReadOnlyMode --> WaitForRecover[等待 BFF 恢复]
    WaitForRecover --> Recover

19. 模块演化与解耦ai15 新增)

19.1 拆分触发条件

触发条件 拆分方向 阶段
parent-portal bundle > 200KBgzipped 按场景域拆分为 parent-core-remote + parent-comm-remote P7+
团队规模 > 5 人同时维护 parent-portal 同上 P7+
家校沟通功能复杂度提升 拆出 parent-comm-remoteIM + 通知中心) P7+
缴费功能引入 拆出 parent-finance-remote P7+
多租户支持 URL 前缀 /{tenantId}/parent/*TanStack Query key 加 tenantId P8+

19.2 拆分后的 MF 配置

// 拆分后parent-core-remote核心场景
new NextFederationPlugin({
  name: "parent_core_app",
  filename: "static/chunks/remoteEntry.js",
  exposes: {
    "./pages": "./src/pages", // dashboard/grades/homework/children
    "./ChildSwitcher": "./src/components/ChildSwitcher",
  },
  // ...
});

// 拆分后parent-comm-remote沟通场景
new NextFederationPlugin({
  name: "parent_comm_app",
  filename: "static/chunks/remoteEntry.js",
  exposes: {
    "./pages": "./src/pages", // notifications/preferences/chat
    "./NotificationFeed": "./src/components/NotificationFeed",
  },
  // ...
});

19.3 状态管理迁移路径

迁移场景 触发条件 迁移策略
Zustand → Jotai 性能瓶颈或团队偏好 逐 slice 迁移Hook 接口保持不变;新旧 slice 共存 1 个迭代周期
localStorage → IndexedDB 数据量 > 5MB 抽象存储层Hook 接口不变;迁移期双写 + 校验一致性
TanStack Query v5 → v6 TanStack 发布 v6 跟随社区升级Breaking Change 通过 codemod 自动迁移

19.4 MF 升级路径

升级场景 触发条件 风险与缓解
MF 2.0 → 3.0 MF 3.0 稳定且解决 SSR 问题 Shell 端升级parent-portal 仅改 name/filenameCI 验证 shared 单例
MF → 原生 SSR脱离 MF SEO 需求强烈或 MF 维护成本过高 保留 API 请求层和组件库;移除 MF 配置;独立部署为完整 Next.js 应用
Next.js 14 → 15 Next.js 15 稳定 跟随 Shell 升级App Router API 兼容性验证
React 18 → 19 React 19 稳定 跟随 Shell 升级use hook / Suspense 改进可简化 TanStack Query 集成

20. 长远架构愿景ai15 新增)

20.1 三年架构演进路线

graph LR
    Y1[Year 1<br/>P4-P6 单体 Remote<br/>MF 2.0] --> Y2[Year 2<br/>P7+ 多 Remote 拆分<br/>家校沟通+缴费+活动]
    Y2 --> Y3[Year 3<br/>P8+ 多租户+多区域<br/>MF 3.0 / 原生 SSR]

20.2 架构原则(始终不变)

  1. Remote 角色不变parent-portal 始终作为 teacher-portal Shell 的 Remote不独立 Shell
  2. 契约先行:所有 API 变更先 proto/contracts后实现
  3. 无状态前端:所有持久化状态走 localStorage/IDB不依赖服务端 session
  4. 复用优先:组件/Hook/工具优先复用 Shell 暴露的,避免重复实现
  5. 可降级任何依赖BFF/WebSocket/Shell故障都有降级方案
  6. 可观测:所有用户行为、错误、性能指标都可观测
  7. 可测试:所有组件/Hook/页面都有单测 + E2E 覆盖
  8. 可演化:模块可拆分、状态管理可迁移、框架可升级

20.3 与其他 portal 的协同演化

演化方向 parent-portal 角色 协同端
共享组件抽取到 packages/ 消费方(不再在 Remote 内自建组件) teacher-portalShell+ 其他 Remote
共享 Hook 抽取到 packages/hooks/ 消费方 同上
共享业务模型抽取到 packages/contracts/ 消费方(类型定义) 同上
多 portal 间通信(如教师↔家长) 通过后端 BFF/gRPC不直连其他 Remote teacher-portal / student-portal
跨 portal 路由跳转 通过 Shell 统一路由表 所有 portal

20.4 与后端架构的协同演化

后端演化 parent-portal 适配
BFF REST → GraphQL 引入 urql/apollo client保持 ApiClient 接口
BFF → BFF + 微前端 API Gateway parent-portal 不感知ApiClient baseUrl 不变
引入 Service MeshIstio parent-portal 不感知;网络层透明
引入 Feature Flag 服务 集成 FeatureFlagProvider组件按 flag 渲染
引入 A/B Testing 平台 集成 ABTestProviderUI 按 variant 渲染
后端多租户 URL 加 /{tenantId}/parent/*Query key 加维度
后端多区域 ApiClient 按 region 路由CDN 边缘缓存

20.5 关键技术债务预警

债务项 当前状态 紧急度 清理建议
ai07 初稿端口错误3002 vs 4002 已修订 coord 同步修订 teacher-portal docs
ai07 初稿所有权错误ai07 vs ai15 已修订 004 §15 文档矩阵已记录 ai15 = parent-portal
MF SSR 对齐 待验证 P4 启动前 PoC 验证 MF 2.0 SSR若失败降级 CSR
iam 家长-学生关联接口缺失P0 阻塞) 待 ai02 补 coord 协调 ai02 在 P3 收尾前补全
shared-ts / contracts 未建立 待 coord P4 启动前必须建立;否则 ai15 自行实现后续提取
ui-tokens / ui-components / hooks 未建立 待 ai07 P4 启动前必须建立ai07 在 P2 收尾交付
MF Remote 加载失败的兜底 待实现 P4 实现期落地 Shell 内置兜底页
跨 tab 同步在 Safari 的兼容性 待验证 P6 硬化期验证;降级为 storage 事件

AI Agent: ai15 (parent-portal remote) Branch: docs/parent-portal-stage1-stage2-design-ai15 Coordinator: coord-ai Predecessor: ai07初版起草ai15 接管审计与补全)