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

1271 lines
63 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.
# 模块架构设计文档 — parent-portal
> AIai15TS/React · 家长场景域前端 remote
> 阶段:阶段 2 交付物v2 — ai15 接管审计与补全版)
> 初版日期2026-07-09ai07 起草)
> 审计日期2026-07-09ai15 修订:端口、所有权、长远架构遗漏补全)
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[pending-features P4](../../../docs/architecture/roadmap/pending-features.md)、[teacher-portal 阶段2](../../teacher-portal/docs/02-architecture-design.md)、[parent-bff 阶段1](../../../services/parent-bff/docs/01-understanding.md)
> 状态:待 coord 交叉审查
> **审计修订摘要**ai15 → ai07 初稿):
>
> 1. **端口修订**3002 → **4002**004 §1.2 强制 4 端 4000-4003
> 2. **MF 配置端口修订**teacher/student/parent/admin URL 全部从 3000-3003 修订为 4000-4003
> 3. **所有权修订**ai07 → **ai15**ai-allocation.md §3.2
> 4. **遗漏补全**通知偏好数据模型细化、详细组件设计图、跨标签同步实现、API 版本演进、未来扩展铺垫、监控降级、模块演化策略
> 5. **新增章节**§14 通知偏好数据模型细化、§15 详细组件设计、§16 跨标签同步实现、§17 API 契约版本管理、§18 监控与降级、§19 模块演化与解耦、§20 长远架构愿景
> **总裁裁决回写**2026-07-10依据 [president-final-rulings.md](../../../docs/architecture/president-final-rulings.md) + [coord-final-decisions.md](../../../docs/architecture/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_VIEW`、`GRADE_READ_CHILD`)。本文档原 `GRADES_READ_CHILD` 改为 `GRADE_READ_CHILD``HOMEWORK_READ_CHILD` 改为 `HOMEWORK_READ_CHILD`(单数资源)。
> 3. **F4 i18n key 命名**:统一 `error.<service>.<code_snake>`(如 `error.iam.invalid_credentials`、`error.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 token**P4 用 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 GetChildrenByParent**P0 阻塞,用 mock固定 2 个子女 student-001 + student-002开发。
> **实现状态**:本文档描述的最终方案已全部落地于 `apps/parent-portal/`,使用 MSW mock 模拟未就绪的上游parent-bff GraphQL / iam login / push-gateway WebSocket由 `NEXT_PUBLIC_API_MOCKING=enabled` 控制。
---
## 1. 模块内部分层图简化版Remote 角色)
```mermaid
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 角色)
```javascript
// 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.js` 的 `remotes.parent = parent_app@http://localhost:4002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`(详见 [teacher-portal 阶段2 §1.2](../../teacher-portal/docs/02-architecture-design.md#12-mf-配置teacher-portalnextconfigjs))。**注意**teacher-portal 阶段2 文档中 MF URL 用 3000-3003 是过时的,应同步修订为 4000-4003**coord 交叉审查整改项 #1**)。
>
> **MF 配置预留**(未来扩展):当 parent-portal 拆分为多 Remote 时(见 §19可在此处增加 `exposes` 子模块(如 `./comm-remote`Shell 端 `remotes.parent_comm = ...`。
## 2. 领域模型(前端视角)
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。
### 2.1 会话状态Session / 视口Viewport / 权限Permission
**与 teacher-portal 共享**,定义详见 [teacher-portal 阶段2 §2.1-2.3](../../teacher-portal/docs/02-architecture-design.md#21-会话状态session)。parent-portal 作为 Remote 复用 Shell 暴露的 Session/Viewport/Permission 类型与 Zustand slice
- `Session`Zustand sessionSliceL3+ localStorage 持久化 + TanStack Query 缓存 `['session']`L2
- `ViewportItem`:来源 `GET /api/v1/parent/viewports`parent-bff 聚合 iam 视口配置AppShell 按 `scope: 'parent'` 过滤渲染
- `PermissionState`:来源 `GET /api/v1/iam/effective-permissions`,前端 TanStack Query 缓存 5min
### 2.2 多子女状态ChildSwitcher家长端特有
```typescript
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 策略
```typescript
// 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](./01-understanding.md#31-消费的后端-api经-api-gateway-代理)。
### 4.1 统一 API 请求层
**复用 Shell 暴露的 ApiClient**,定义详见 [teacher-portal 阶段2 §4.1](../../teacher-portal/docs/02-architecture-design.md#41-统一-api-请求层lib-apits)。parent-portal 通过 MF `shared` 单例获取 ApiClient 实例:
```typescript
// 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 约定
```typescript
// 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
```typescript
// 复用 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-vitals``POST /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
```typescript
// 复用 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](../../teacher-portal/docs/02-architecture-design.md#9-设计令牌三层packagesui-tokens待建立ai07-维护)。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/viewports``GET /parent/children``POST /parent/switch-child``GET /parent/notifications``PUT /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-ts`、`packages/contracts`**:包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立ai07 自行在 `apps/parent-portal/src/shared/` 内实现,后续提取到 packages。
2. **假设 ai05 parent-bff 提供 `GET /parent/viewports`、`GET /parent/children`、`POST /parent/switch-child`、`GET /parent/notifications`、`PUT /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](../../teacher-portal/docs/02-architecture-design.md)。
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 `queryKey``currentChildId` + 切换时 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](../../teacher-portal/docs/02-architecture-design.md#113-未决设计决策需-coord-仲裁)。
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但当前 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.2](../../../docs/architecture/004_architecture_impact_map.md#12-服务清单) 与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐。**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/login``GET /iam/effective-permissions``GET /iam/me` | iam | ✅ 已实现 |
| `GET /parent/viewports``GET /parent/children``POST /parent/switch-child``GET /parent/notifications``PUT /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/preferences`react-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 三维偏好矩阵
通知偏好按 **子女 × 事件类型 × 渠道** 三维组织:
```typescript
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
```typescript
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` 字段禁用不可用渠道:
```typescript
// 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 组件
```typescript
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 组件
```typescript
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 组件
```typescript
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 组件
```typescript
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 组件
```typescript
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 组件依赖关系图
```mermaid
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 同步架构图
```mermaid
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 完整实现
```typescript
// 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
```typescript
// 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 版本协商
```typescript
// 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 错误恢复流程
```mermaid
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-remote`IM + 通知中心) | P7+ |
| 缴费功能引入 | 拆出 `parent-finance-remote` | P7+ |
| 多租户支持 | URL 前缀 `/{tenantId}/parent/*`TanStack Query key 加 tenantId | P8+ |
### 19.2 拆分后的 MF 配置
```javascript
// 拆分后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`/`filename`CI 验证 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 三年架构演进路线
```mermaid
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 接管审计与补全)