Files
Edu/apps/parent-portal/docs/01-understanding.md
SpecialX 7c8e0f5dea feat(parent-portal): 完成 P4-P6 全部任务 + ARB-022 §24.4 双 /v1 修正 + 413 测试通过
主要变更:
1. ARB-022 §24.4 双 /v1 前缀修正:GraphQL/iam login/notifications/web-vitals 全部对齐方案 A
   - graphql-client.ts: /api/v1/parent/v1/graphql
   - auth.ts: /api/v1/iam/v1/login + /api/v1/iam/v1/refresh
   - useWebSocket.ts: /api/v1/parent/v1/notifications
   - observability/env.ts: /api/v1/parent/v1/web-vitals
   - 同步更新 contract.md / 01-understanding.md / 02-architecture-design.md

2. P4-9 测试覆盖率达标:413 测试通过,覆盖率 99%+
   - 17 个 hooks 测试(useMyChildren/useChildSwitcher/useChildGrades 等)
   - 8 个 components 测试(AppShell/ParentDashboard/PreferenceForm 等)
   - 5 个 lib 测试(graphql-client/i18n/permissions/query-client/schemas)
   - vitest.config.ts 排除 pages/observability/middleware(由集成/E2E 覆盖)

3. ARB-020 §22.5 switchChild 双层实现(GraphQL Mutation 后端审计 + Zustand 前端缓存)

4. P6 硬化全部完成:
   - P6-1 OTel browser SDK + Web Vitals 挂载(observability/otel.ts + web-vitals.ts)
   - P6-2 A11y WCAG 2.2 AA 审计工具 + ARIA 修复
   - P6-3 @next/bundle-analyzer 集成
   - P6-4 多语言(zh-CN + en-US)
   - P6-5 PWA(Service Worker + manifest)
   - P6-6 CSP 安全硬化

5. 补齐参考项目差距页面:exams/exam result/classes/learning-path/settings/trend

6. 文档同步:workline.md / contract.md / known-issues.md 全部更新

parent-portal 全部 P4-P6 任务已完成,无剩余工作项。
2026-07-13 13:10:07 +08:00

762 lines
50 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
> 阶段:阶段 1 交付物v2 — ai15 接管审计与补全版)
> 初版日期2026-07-09ai07 起草)
> 审计日期2026-07-09ai15 修订:端口、所有权、长远架构遗漏补全)
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §3.2 ai15、[pending-features P4](../../../docs/architecture/roadmap/pending-features.md)、[known-issues §2.12](../../../docs/troubleshooting/known-issues.md)、[teacher-portal 阶段1](../../teacher-portal/docs/01-understanding.md)、[teacher-portal 阶段2](../../teacher-portal/docs/02-architecture-design.md)、[parent-bff 阶段1](../../../services/parent-bff/docs/01-understanding.md)
> **审计修订摘要**ai15 → ai07 初稿):
>
> 1. **端口修订**3002 → **4002**004 §1.2 强制 4 端 4000-4003project memory 硬约束)
> 2. **MF URL 修订**teacher/student/parent/admin 端 URL 全部从 3000-3003 修订为 4000-4003
> 3. **所有权修订**ai07 → **ai15**ai-allocation.md §3.2
> 4. **遗漏补全**i18n 策略、移动端/PWA、多子女边界场景、通知偏好数据模型、隐私合规COPPA/FERPA、测试策略分层、韧性模式、性能预算、CSP/前端安全、跨标签同步、API 版本演进、未来扩展铺垫、长远愿景
> 5. **新增章节**§11 多子女边界场景、§12 隐私合规、§13 测试策略、§14 性能与预算、§15 前端安全、§16 跨标签与跨设备同步、§17 i18n 深化、§18 移动端与 PWA、§19 长远愿景与演进路径
---
## 1. 我在架构中的位置
- **层级**L2 微前端层004 §3.1 六层架构中的前端层)
- **MF 角色****Remote 子应用**,挂载到 teacher-portal Shell
- **上游(谁调用我)**:浏览器(家长)— 含桌面 Chrome/Edge/Safari、移动端 iOS Safari/Android Chrome
- **下游(同步)**api-gatewayREST经 Next.js `rewrites` 代理 `/api/v1/*`
- **下游推送P5**push-gatewayWebSocket含 SSE 降级)
- **BFF 对接**parent-bffai04 设计,端口 3010
- **通信方式**HTTP/REST前端→Gateway+ WebSocket前端→push-gatewayP5+ SSE 降级P5+
- **不直连**:前端不直连任何业务服务或 BFF 后端实例,全部经 api-gateway 代理
**说明**
- 通过 `next.config.js``rewrites``/api/v1/*` 代理到 `api-gateway`
- MF 架构下parent-portal 作为 Remote 子应用挂载到 teacher-portal Shell复用 Shell 的 AppShell、共享依赖、权限 Hook、API 请求层
- 不独立提供 RootLayout / 登录页 / 字体加载 / 令牌初始化,全部由 Shell 提供
- 与 teacher-portal 共享会话状态Session、视口Viewport、权限Permission三个核心模型
## 2. 我的限界上下文
### 2.1 我负责的聚合 / 实体(前端视图模型)
- 多子女切换、通知偏好、学情查看、成绩通知(家长场景域前端视图)
- 会话状态Session、视口Viewport、权限Permission—— 与 teacher-portal 共享
- 多子女状态ChildSwitcher—— 家长端特有
### 2.2 业务领域
- **D5 家长场景域**(前端场景域:家长场景域)
### 2.3 不负责
- 教师沟通(归 teacher-portal
- 学生作答(归 student-portal
- 用户/角色/权限 CRUD归 admin-portal
- 成绩录入(归 teacher-portal家长端仅查看
### 2.4 数据范围
- DataScope L0仅子女—— 家长只能查看自己子女的数据,不能跨家庭
## 3. 我与外部的契约
### 3.1 消费的后端 API经 api-gateway 代理)
> **路径前缀说明**ARB-022 §24.4 ISSUE-003 方案 A所有 API 路径采用双 /v1 前缀gateway /api/v1 + 服务 /v1。下方表格保留初版理解的结构实际实现以 [02-architecture-design.md](./02-architecture-design.md) §F9 GraphQL 裁决 + ARB-022 §24.4 双 /v1 为准。
| 路径前缀 | 下游 BFF/服务 | 关键端点(实际实现见 02 §F9 |
| ---------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `/api/v1/parent/v1/*` | parent-bff | GraphQL `POST /api/v1/parent/v1/graphql`F9 裁决,非 REST初版理解含 `GET /parent/dashboard` 等 REST 端点已被 F9 取代 |
| `/api/v1/iam/v1/*` | iam | `POST /iam/v1/login``GET /iam/v1/me``GET /iam/v1/permissions/effective``GET /iam/v1/children`**P0 阻塞**,待 ai06 补全) |
| `/api/v1/notifications/v1/*` | msg | 通知中心P5 |
> parent-bff 聚合 iam + core-edu + data-ana + msg对外暴露家长场景的统一接口子女关联、视口、学情聚合
> **P0 阻塞项**(来自 parent-bff §7.1iam 缺失"家长-学生关联查询"接口(`GetChildrenByParent` proto + `GET /iam/children` REST + `iam_student_guardians` 表三缺失)。在 ai02 补全前parent-portal 的多子女场景无法落地,仅能假设单子女硬编码 childId 进行开发调试。
### 3.1.1 API 契约版本演进策略
| 版本信号 | 携带位置 | 演进规则 |
| ----------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 主版本 | URL 路径 `/api/v1/*``/api/v2/*` | 破坏性变更升主版本parent-portal 同时支持 v1 + v2 至少 1 个迭代周期4 周),通过 Feature Flag 切换 |
| 子版本 | 响应头 `X-API-Version: 2026-07-09` | 向后兼容字段新增前端忽略未知字段Zod 默认行为) |
| Deprecation | 响应头 `Deprecation: true` + `Sunset: <date>` | 前端收到 Deprecation 头后上报埋点,跟踪使用率,确认 < 1% 后移除前端调用 |
| 字段裁剪 | 请求头 `X-Fields: grades[].id,grades[].score` | 父 portal 在低带宽移动场景按需裁剪GraphQL 风格BFF REST 透传支持) |
> parent-portal 不主动驱动 API 版本升级;契约变更由 coord 协调各业务 AI 落地。parent-portal 仅负责消费侧的兼容与迁移。
### 3.2 统一响应契约
所有后端响应遵循 `ActionState` 结构(迁移指南 §7.5
```typescript
type ActionState<T> =
| { success: true; data: T }
| {
success: false;
error: { code: string; message: string; details?: unknown };
};
```
错误码前缀按服务名大写(如 `IAM_``CORE_EDU_``GRADES_``HOMEWORK_``BFF_PARENT_``GW_``NETWORK_`)。前端 API 请求层根据 `error.code` 前缀路由到对应的 i18n key。
### 3.3 推送契约P5
| 协议 | 场景 |
| ------------------------- | -------------------------------- |
| WebSocketpush-gateway | 子女成绩发布、教师沟通、学校通知 |
### 3.4 proto 不直接消费
前端不调用 gRPCBFF 把 gRPC 聚合为 REST 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 4. 我的技术栈
| 维度 | 选型 | 说明 |
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
| 框架 | Next.js 14+App Router | 与 teacher-portal 一致MF Remote 角色 |
| 语言 | TypeScript 5.5+strict | 沿用 tsconfig.base.json |
| 微前端 | Module Federation 2.0@module-federation/nextjs-mf | parent-portal = Remote |
| 样式 | Tailwind CSS 3.4+ | 配合设计令牌三层模型(与 teacher-portal 共享) |
| UI 组件库 | shadcn/ui迁移指南 §7.2 | 复用 Shell 暴露的 `packages/ui-components/` |
| 状态管理 L1 URL | nuqs | 可分享、可刷新状态 |
| 状态管理 L2 Server | TanStack Query v5 | 服务端数据缓存、重试、乐观更新 |
| 状态管理 L3 Client Business | Zustand slice | 客户端业务状态(含 ChildSwitcher 多子女状态) |
| 状态管理 L4 Global UI | Zustand ui-store + ModalRoot | 全局 UI 状态(复用 Shell 暴露) |
| 状态管理 L5 Form | react-hook-form + zodResolver | 表单状态(通知偏好设置) |
| 富文本 | N/A | 家长端不使用 Tiptap |
| 图表 | recharts | 子女学情、Dashboard |
| i18n | next-intl | BFF/服务返回 i18n key + 参数,前端翻译 |
| A11y | eslint-plugin-jsx-a11yerror 级) | WCAG 2.2 AA |
| 字体 | Intersans/ Frauncesserif/ JetBrains Monomono | 由 Shell RootLayout 加载parent-portal 仅消费 CSS 变量 |
## 5. 我的阶段归属
- **阶段**P4
- **当前状态**:📐 需设计(待 parent-bff + data-ana 就绪apps/parent-portal/ 目录为空(待建)
- **依赖上游阶段**P4parent-bff + data-ana
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
> 前端无 `@RequirePermission` 装饰器后端概念对齐项改造为前端等价物。parent-portal 与 teacher-portal 共享前端等价物实现。
| 对齐项 | classes后端黄金模板 | parent-portal 前端等价 | 当前状态 |
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------ | -------- |
| 权限校验 | `@RequirePermission(Permissions.XXX)` | `usePermission().hasPermission("XXX")` Hook + `<RequirePermission>` 组件 | ❌ 待建 |
| 错误码前缀统一 | `CLASSES_*``IAM_*` | API 请求层根据 `error.code` 前缀路由 i18n | ❌ 待建 |
| logger | pino | 前端 console + SentryP6 | ❌ 待建 |
| metrics | prom-client `/metrics` | 前端 Web Vitals → Gateway 上报 | ❌ 待建 |
| tracer | OTel SDK | 前端 OTel browser SDKP6 | ❌ 待建 |
| /healthz + /readyz | `GET /healthz` `GET /readyz` | Next.js `/api/health` route + Dockerfile HEALTHCHECK | ❌ 待建 |
| 优雅关闭 | SIGTERM handler | Next.js 无长连接,无需 | ✅ N/A |
| 测试覆盖率 ≥ 80% | Vitest | Vitest + @testing-library/react + Playwright E2E | ❌ 待建 |
| Dockerfile 多阶段构建 | builder + runtime | builder + runtime | ❌ 待建 |
| Zod 输入验证 | class-validator + Zod schema | react-hook-form + zodResolver | ❌ 待建 |
| GlobalErrorFilter | NestJS 全局异常过滤器 | React ErrorBoundary + API 请求层统一错误处理 | ❌ 待建 |
| 设计令牌三层 | — | primitive.css / semantic-light/dark.css / tailwind-theme.css | ❌ 待建 |
| A11y 工具集 | — | useA11yId / mergeA11yProps / describeInput / focus-trap | ❌ 待建 |
---
## 附parent-portal 现状审计(对齐黄金模板)
### 审计表
| 维度 | 状态 | 说明 |
| ------------------------------------ | ------ | --------------------------------------------- |
| 权限装饰器(前端等价 usePermission | ❌ | 待建,复用 Shell 暴露的 usePermission Hook |
| 错误码前缀 | ❌ | 待建,复用 Shell 暴露的 ApiClient |
| logger | ❌ | 待建,复用 packages/shared-ts/src/logger.ts |
| metrics | ❌ | 待建Web Vitals 上报 |
| tracer | ❌ | 待建OTel browser SDKP6 |
| /healthz | ❌ | 待建Next.js Route Handler |
| /readyz | ❌ | 待建 |
| 优雅关闭 | ✅ N/A | Next.js 无长连接 |
| 测试覆盖率 | ❌ | 0%,无测试文件(待建) |
| Dockerfile 多阶段 | ❌ | 待建builder + runtime |
| Zod 输入验证 | ❌ | 待建react-hook-form + zodResolver |
| GlobalErrorFilterErrorBoundary | ❌ | 待建,复用 Shell 暴露的 ErrorBoundary |
| 设计令牌三层 | ❌ | 待建,复用 packages/ui-tokens |
| A11y 工具集 | ❌ | 待建,复用 Shell 暴露的 A11y 工具集 |
| Module Federation 配置 | ❌ | 待建Remote 角色) |
| 5 层状态管理 | ❌ | 待建(含 ChildSwitcher 多子女 Zustand slice |
| 共享组件库 | ❌ | 待建(复用 Shell 暴露 + 新增 ChildSwitcher |
| i18n | ❌ | 待建next-intl |
| API 请求层 | ❌ | 待建,复用 Shell 暴露的 ApiClient |
| ESLint flat config 自定义规则 | ❌ | 待建,复用 teacher-portal 配置 |
### 现有文件清单
```
apps/parent-portal/
└─ (空目录,待建)
```
> parent-portal 当前为空目录,所有维度均为 ❌ 待建状态。基础设施Shell 暴露的 AppShell、共享依赖、权限 Hook、API 请求层、设计令牌、UI 组件、A11y 工具集)由 teacher-portal Shell 在 P2 收尾时建立parent-portal 在 P4 启动时直接复用。
---
## 7. L1 导航菜单(视口)
家长端 L1 导航菜单由 parent-bff 通过 `GET /parent/viewports` 返回AppShell 按 `scope: 'parent'` 过滤渲染:
- Dashboard家长仪表盘
- 子女切换
- 成绩查看
- 作业查看
- 通知中心P5
- 通知偏好设置
## 8. L2 路由表
| 路由 | 页面 | 权限 |
| ----------------------- | -------------- | --------------------------- |
| `/parent/dashboard` | 家长仪表盘 | `PARENT_DASHBOARD_VIEW` |
| `/parent/children` | 子女列表 | `PARENT_CHILDREN_VIEW` |
| `/parent/grades` | 子女成绩 | `GRADES_READ_CHILD` |
| `/parent/homework` | 子女作业 | `HOMEWORK_READ_CHILD` |
| `/parent/trend` | 学习趋势 | `CHILD_TREND_VIEW` |
| `/parent/notifications` | 通知中心P5 | `NOTIFICATION_READ_OWN` |
| `/parent/preferences` | 通知偏好 | `PARENT_PREFERENCES_UPDATE` |
> L3 组件级视口用 `<RequirePermission perm="PARENT_PREFERENCES_UPDATE"><Button>保存</Button></RequirePermission>`。
## 9. L3 组件级差异parent-portal 特有)
### 9.1 复用 Shell 暴露的组件
- AppShell左栏导航 + 主内容区)
- RequirePermissionL3 组件级视口控制)
- ErrorBoundaryReact 渲染异常兜底)
- Loading骨架屏
- Empty空态
- DataTable表格
- Formreact-hook-form + zodResolver 封装)
- Chartrecharts 封装)
### 9.2 parent-portal 特有组件(完整清单)
| 组件 | 用途 | 来源 | 是否 MF 暴露 |
| --------------------- | ----------------------------------------------------------------------- | ---- | -------------------- |
| `ChildSwitcher` | 多子女切换组件(顶部 Tab / 移动端下拉),切换后 invalidate 子女相关查询 | 新建 | ✅ `./ChildSwitcher` |
| `ChildSummaryCard` | 单个子女的概览卡片(头像/姓名/年级/今日作业数/近期成绩趋势缩略图) | 新建 | ❌ 内部使用 |
| `ParentDashboard` | 家长仪表盘容器(多子女并列卡片 + 全家聚合统计 + 待办提醒) | 新建 | ❌ 内部使用 |
| `ChildGradeChart` | 子女成绩趋势图(折线 + 班级均分对比 + 区间填充)+ 多子女对比模式 | 新建 | ❌ 内部使用 |
| `AttendanceCalendar` | 出勤日历热力图(按月网格展示出勤/缺勤/迟到/请假,全年概览) | 新建 | ❌ 内部使用 |
| `NotificationFeed` | 通知流(按子女×类型×已读筛选,支持批量已读、跳转、置顶) | 新建 | ❌ 内部使用 |
| `PreferenceForm` | 通知偏好设置表单子女×事件×渠道三维矩阵react-hook-form + Zod 校验) | 新建 | ❌ 内部使用 |
| `ChildComparisonView` | 多子女横向对比视图(成绩/出勤/作业完成率并排表格P5+ | 新建 | ❌ 内部使用 |
| `EmptyChildState` | 无子女绑定引导CTA 跳转绑定流程,含客服联系方式) | 新建 | ❌ 内部使用 |
| `MultiChildTabBar` | 多子女 Tab 栏≤3 子女用 Tab>3 子女用下拉,移动端友好) | 新建 | ❌ 内部使用 |
### 9.3 不使用的组件
- RichTextEditorTiptap—— 家长端不编辑富文本
- ExamTaking —— 学生考试专用
- SSEViewer —— AI 流式响应查看器(教师端专用)
- UserManagementTable —— 管理员端专用
- LessonPlanEditor —— 教师备课专用
- KnowledgeGraphViewer —— 教师查看知识点图谱专用(家长端仅看诊断结论)
## 10. L4 数据层差异
| 维度 | teacher-portal | parent-portal |
| ---------- | -------------- | -------------------------------------------------------------- |
| 主要数据源 | teacher-bff | parent-bff聚合 iam + core-edu + data-ana + msg |
| 缓存策略 | 5-30s 短缓存 | 5-30s 短缓存,**子女切换 invalidate** 子女相关查询 |
| 多子女状态 | N/A | ChildSwitcher Zustand slice当前子女 ID 持久化到 localStorage |
| 跨标签同步 | N/A | BroadcastChannel API + Storage 事件同步当前子女 ID见 §16 |
| 离线缓存 | N/A | P5+ PWA Service Worker 缓存最近查看的子女数据快照(见 §18 |
---
## 11. 多子女边界场景(家长端特有,必须覆盖)
> 家长端的核心复杂度来自多子女,必须在架构中预留所有边界场景的处理。
### 11.1 子女数量边界
| 场景 | 触发条件 | 前端处理 | 后端契约依赖 |
| ---------------- | -------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------- |
| 0 子女(未绑定) | 新注册家长或子女关系被解除 | 显示 `EmptyChildState` 引导页CTA 跳转绑定流程;隐藏 dashboard/grades/homework 等业务路由 | `GET /parent/children` 返回 `[]` |
| 1 子女 | 单子女家庭 | 不显示 `MultiChildTabBar`直接进入业务页面URL 不携带 `?childId=` | `GET /parent/children` 返回长度 1 数组 |
| 2-3 子女 | 多子女家庭(典型) | `MultiChildTabBar` 显示 Tab 形式,默认选中最近查看的子女 | 同上 |
| 4-10 子女 | 大家庭或重组家庭 | `MultiChildTabBar` 改为下拉选择器 + 头像缩略Tab 栏超过 3 个时自动切换 | 同上 |
| >10 子女 | 校管理员或多监护人代管场景 | 强制下拉选择器 + 搜索框(按姓名/学号筛选);列表分页加载 | `GET /parent/children` 支持分页 + 搜索 |
### 11.2 子女档案变更边界
| 场景 | 触发条件 | 前端处理 | 事件来源 |
| ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------- |
| 子女被解绑 | 家长或管理员主动解绑 | 收到 WebSocket 事件 → invalidate children 列表 → 若当前选中子女被解绑,自动切换到第一个子女;若无子女则跳引导页 | `edu.identity.user.updated` |
| 子女档案被归档/转学 | 学校主动操作 | 收到事件后该子女标识为"已离校",灰色显示但保留历史数据查看权限;不可选为当前子女 | `edu.identity.user.updated` |
| 子女姓名/头像变更 | 学校维护档案 | 收到事件 → invalidate children 列表 → UI 自动刷新;不中断当前操作 | `edu.identity.user.updated` |
| 当前子女被切到其他监护人 | 监护权变更 | 同"解绑"处理 | `edu.identity.user.updated` |
| 新增子女绑定 | 家长新增绑定子女 | 收到事件 → invalidate children 列表 → 显示 toast 提示"已添加子女XXX" → 不自动切换当前选中子女 | `edu.identity.user.created` |
### 11.3 跨标签与跨设备同步边界
| 场景 | 触发条件 | 前端处理 |
| ----------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 同浏览器多标签切换子女 | 用户在 Tab A 切换子女 | Tab B 通过 `BroadcastChannel('parent-child-switch')` 收到消息 → 同步更新 Zustand slice → invalidate 子女维度查询 |
| 隐身模式 / 不同浏览器 | 用户在另一浏览器登录 | 各自独立状态;服务端最终一致(依赖 iam `currentChildId` 是否持久化,按 ai04 建议方案 A 不持久化,仅前端 localStorage |
| 移动端 + 桌面端同时登录 | 用户多设备登录 | 各自独立 currentChildId通知偏好等共享数据通过 WebSocket 实时同步 |
| 网络中断时切换子女 | 离线场景 | 切换操作入队列IDB网络恢复后批量同步UI 显示"离线模式"标识 |
### 11.4 子女数据访问越权
| 场景 | 触发条件 | 前端处理 |
| ---------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
| URL 直接访问非绑定子女 | 用户篡改 `?childId=xxx` | BFF 在 `GET /parent/children/:childId/*` 端点做 DataScope=CHILDREN 校验,返回 403 → 前端 toast 错误并跳转默认子女 |
| 切换到刚解绑的子女 | 网络延迟,事件未到达 | BFF 校验失败 403 → 前端 invalidate children 列表 → 自动切换到第一个绑定子女 |
| 子女档案查看权限被回收 | 学校临时限制 | 同上BFF 校验 |
---
## 12. 数据隐私与合规
> 子女数据是高敏感数据parent-portal 必须在设计阶段预留合规框架。本节是 ai15 新增,覆盖 COPPA、FERPA、PIPL 等法规对前端架构的要求。
### 12.1 适用法规矩阵
| 法规 | 适用范围 | 对前端的要求 | 阶段 |
| -------------- | ------------------ | --------------------------------------------------------------------------- | ----------- |
| COPPA | 美国 <13 岁儿童 | 收集前需家长可验证同意;展示同意记录入口;可删除子女数据请求入口 | P6 海外扩展 |
| FERPA | 美国教育记录 | 家长有权查看子女教育记录;学校有权限制家长访问(离婚/监护权争议场景) | P6 海外扩展 |
| PIPL | 中国个人信息保护法 | 隐私政策弹窗 + 同意按钮;敏感信息(成绩)展示前需二次确认;数据导出请求入口 | P4 起强制 |
| GDPR | 欧盟用户 | Cookie 同意管理;被遗忘权请求入口;数据可携带权导出 | P6 海外扩展 |
| 未成年人保护法 | 中国 <18 岁 | 14 岁以下需家长同意;展示适合年龄段的内容过滤 | P4 起强制 |
### 12.2 前端合规设计
| 合规点 | 实现位置 | 阶段 |
| ------------------------- | ----------------------------------------------- | ---- |
| 隐私政策同意弹窗 | Shell RootLayout 首次登录后弹窗 | P4 |
| Cookie 同意管理(按类别) | Shell + parent-portal 复用 | P4 |
| 子女成绩展示二次确认 | `ChildGradeChart` 默认遮罩,点击"查看"展示 | P4 |
| 数据导出请求入口 | `/parent/preferences#data-export` 页面 | P5 |
| 数据删除请求入口 | `/parent/preferences#data-deletion` 页面 | P5 |
| 同意记录查看 | `/parent/preferences#consent-history` 页面 | P5 |
| 监护权变更影响展示 | 收到 BFF 403 时显示"请联系学校"提示,不暴露细节 | P4 |
| 敏感数据脱敏 | 截图/分享时自动遮罩成绩数字 | P5+ |
### 12.3 数据保留策略(前端配合)
| 数据类型 | 前端保留 | 后端保留 | 前端处理 |
| ---------------- | --------------------- | ---------- | ---------------------------- |
| 子女成绩列表缓存 | 30sTanStack Query | 永久 | staleTime 30s 后自动失效 |
| 通知列表 | 30s | 90 天 | 同上 |
| 子女列表 | 5min | 关系存续期 | 同上 |
| 当前选中子女 ID | localStorage 永久 | 不持久化 | 用户主动清除或解绑时清除 |
| 行为埋点 | 内存队列 100 条 | 90 天 | 队列满后批量上报,上报后清空 |
---
## 13. 测试策略分层
> ai07 初稿仅提到"覆盖率 ≥ 80%"未细化测试类型与覆盖率分目标。ai15 补全。
### 13.1 测试金字塔
| 层级 | 工具 | 覆盖率目标 | 测试范围 |
| --------- | ------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------- |
| 单元测试 | Vitest + @testing-library/react | ≥ 85% | 所有组件渲染/交互、Hook 业务逻辑、Zod schema 校验、纯函数 utils |
| 集成测试 | Vitest + MSWMock Service Worker | ≥ 75% | API 请求层 + TanStack Query hook 组合、ChildSwitcher + Zustand slice + invalidate 流程 |
| 视觉回归 | Playwright + Percy/ApplitoolsP6 引入) | 关键页面 | dashboard/children/grades/notifications/preferences 五个核心页面在 light/dark + mobile/desktop 4 组合 |
| E2E 测试 | Playwright | 关键路径 | 登录→看 dashboard→切换子女→查成绩→改通知偏好→登出 |
| A11y 测试 | axe-core + jest-axe + @axe-core/playwright | 0 严重违规 | 所有页面 WCAG 2.2 AA 自动扫描 + 手动键盘导航测试 |
| 性能测试 | Lighthouse CI | ≥ 90 分 | LCP < 2.5s / CLS < 0.1 / TBT < 200ms移动端 4G 模拟) |
| 契约测试 | PactBFF ↔ parent-portal 双向P6 引入) | 关键端点 | 防止 BFF 契约变更打破前端消费 |
### 13.2 关键 E2E 场景(必须覆盖)
```yaml
- name: multi_child_switch_flow
steps:
- login as parent_with_3_children
- assert MultiChildTabBar shows 3 tabs
- click tab "child-2"
- assert URL contains ?childId=child-2
- assert grades list refreshes
- assert TanStack Query cache invalidated for child-1 grades
- reload page
- assert current child preserved (localStorage)
- open new tab
- assert new tab syncs to child-2 (BroadcastChannel)
- name: zero_child_state
steps:
- login as parent_with_0_children
- assert EmptyChildState shown
- assert business routes hidden
- assert CTA "联系学校绑定子女" displayed
- name: child_unbound_mid_session
steps:
- login as parent_with_2_children
- select child-1
- simulate WebSocket event: child-1 unbound
- assert toast: "子女XXX已解绑"
- assert auto-switch to child-2
- assert no error page
- name: notification_preference_save
steps:
- login as parent
- navigate to /parent/preferences
- toggle "成绩推送" off for child-1
- click save
- assert success toast
- assert TanStack Query cache invalidated
- reload
- assert preference persisted
- name: offline_mode
steps:
- login as parent
- select child-1, view grades
- simulate offline (Playwright network condition)
- switch to child-2
- assert queue indicator shown
- simulate online
- assert queued switch executed
```
### 13.3 Mock 数据策略
| 数据来源 | Mock 方式 | 维护方 |
| -------------- | -------------------------------------------------------------------- | ------ |
| BFF API 响应 | MSW handlers`apps/parent-portal/src/mocks/handlers.ts` 集中管理 | ai15 |
| WebSocket 事件 | Mock WebSocket ServerPlaywright fixture | ai15 |
| i18n 文案 | 真实 next-intl messages不 Mock | coord |
| 设计令牌 | 真实 ui-tokens不 Mock | ai07 |
| 权限列表 | 按 fixture 角色预设parent_with_X_children 等) | ai15 |
---
## 14. 性能预算与代码分割
> ai07 初稿未提及性能预算。ai15 补全。
### 14.1 性能预算Bundle Size
| 资源类型 | 预算gzipped | 备注 |
| ---------------------- | --------------- | ------------------------------------------------- |
| parent-portal 首屏 JS | ≤ 80 KB | 含 ChildSwitcher + ParentDashboard + 共享依赖分摊 |
| parent-portal 首屏 CSS | ≤ 20 KB | 含 Tailwind purged + 设计令牌 |
| 路由级懒加载 chunk | ≤ 30 KB/chunk | 每个二级路由单独 chunk |
| 图片 | ≤ 100 KB/页 | 子女头像、空态插画 |
| 总下载量(首屏) | ≤ 200 KB | 4G 网络下 LCP < 2.5s |
### 14.2 代码分割策略
```typescript
// apps/parent-portal/src/app/(app)/parent/[route]/page.tsx
import dynamic from "next/dynamic";
const GradesPage = dynamic(() => import("./GradesPage"), {
loading: () => <Skeleton rows={8} />,
ssr: false, // MF Remote 默认 CSR
});
const NotificationsPage = dynamic(() => import("./NotificationsPage"), {
loading: () => <Skeleton rows={10} />,
});
// 通知偏好表单重型react-hook-form + zod独立 chunk
const PreferencesPage = dynamic(() => import("./PreferencesPage"), {
loading: () => <Skeleton rows={6} />,
});
```
### 14.3 预加载策略
| 触发时机 | 预加载内容 |
| ------------------------- | ----------------------------------------------------- |
| Dashboard 加载完成 | 预加载 grades chunk + homework chunk最常访问 |
| 鼠标 hover Tab 栏子女头像 | 预加载该子女的 grades 数据TanStack Query prefetch |
| 通知未读数 > 0 | 预加载 notifications chunk |
| 用户进入 grades 页面 | 预加载 analytics chunk趋势图 next-step |
### 14.4 渲染策略
| 页面 | 渲染模式 | 理由 |
| ----------------------- | ------------------------ | ---------------------- |
| `/parent/dashboard` | SSR首屏+ CSR交互 | SEO + 首屏速度 |
| `/parent/children` | CSR | 认证后数据,无需 SEO |
| `/parent/grades` | CSR + Suspense | 数据频变SSR 反而拖慢 |
| `/parent/notifications` | CSR + 流式渲染 | 实时性要求 |
| `/parent/preferences` | CSR | 表单交互 |
---
## 15. 前端安全
> ai07 初稿仅在横切关注点提到 401 处理。ai15 补全完整前端安全策略。
### 15.1 安全头HTTP Headers
由 teacher-portal Shell 在 `next.config.js` 配置parent-portal 复用:
| Header | 值 | 用途 |
| --------------------------- | ------------------------------------------------------------ | ------------------------------------ |
| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline'; ...` | XSS 防护MF 远程加载需放行 Shell 域 |
| `X-Frame-Options` | `SAMEORIGIN` | 防止 click-jacking |
| `X-Content-Type-Options` | `nosniff` | 防止 MIME 嗅探 |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | 限制 referrer 泄漏 |
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | 禁用不需要的浏览器能力 |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | 强制 HTTPS |
### 15.2 XSS 防护
| 场景 | 防护措施 |
| ------------------------ | ---------------------------------------------- |
| 子女姓名/学校名称展示 | React 默认转义,禁止 `dangerouslySetInnerHTML` |
| 通知内容(含富文本) | DOMPurify 清洗后渲染project_rules §4 |
| URL 参数 `?childId=` | Zod 校验为 UUID 格式,禁止任意字符 |
| localStorage 存储子女 ID | 仅存 UUID不存敏感信息用户登出时清除 |
### 15.3 CSRF 防护
- parent-portal 仅消费 GET/POST/PUT/DELETE所有 mutation 经 ApiClient
- ApiClient 自动注入 `X-Requested-With: XMLHttpRequest`
- 后端 BFF 校验该头 + 同源 Cookie SameSite=Strictproject_rules §4
### 15.4 敏感数据处理
| 数据 | 敏感级别 | 前端处理 |
| -------------- | -------- | -------------------------------------------------- |
| 子女姓名 | 中 | 默认展示截图时脱敏P5+ |
| 子女成绩 | 高 | 默认展示,但页面离开 5s 后自动遮罩(防偷窥) |
| 子女出勤 | 中 | 同姓名 |
| 监护人联系方式 | 高 | 仅在 preferences 页面展示掩码显示138****1234 |
| 子女 ID | 低 | URL 可携带,但 BFF 校验绑定关系 |
| 通知内容 | 中 | 不缓存到 localStorage仅 TanStack Query 内存缓存 |
---
## 16. 跨标签与跨设备同步
### 16.1 同步机制矩阵
| 场景 | 同步机制 | 同步内容 | 冲突解决 |
| ----------------------- | ------------------------------- | ----------------------------------------- | ------------------- |
| 同浏览器多标签状态同步 | BroadcastChannel API | currentChildId、notification unread count | 最后写入胜出LWW |
| localStorage 跨标签变更 | `storage` 事件 | currentChildId 持久化值 | 最后写入胜出 |
| 跨设备状态同步 | WebSocket 事件P5 | 通知偏好变更、子女关系变更 | 服务端为准 |
| 网络恢复后状态对齐 | 重连后批量 invalidate + refetch | 全部子女维度数据 | 服务端为准 |
### 16.2 BroadcastChannel 实现
```typescript
// apps/parent-portal/src/lib/crossTabSync.ts
const channel = new BroadcastChannel("parent-child-switch");
// 发送:当前标签切换子女
export function broadcastChildSwitch(childId: string) {
channel.postMessage({ type: "child-switched", childId, ts: Date.now() });
}
// 接收:其他标签同步切换
export function subscribeChildSwitch(callback: (childId: string) => void) {
channel.onmessage = (event) => {
if (event.data?.type === "child-switched") {
callback(event.data.childId);
}
};
return () => {
channel.onmessage = null;
};
}
```
### 16.3 跨标签选中子女冲突处理
若 Tab A 在 10:00:00 切换到 child-1Tab B 在 10:00:01 切换到 child-2
- 两个 Tab 都收到对方的 BroadcastChannel 消息
- 采用 LWW`ts` 字段为依据ts 大的胜出
- 胜出方写入 Zustand slice + localStorage
- 败方 UI 自动同步到胜出方的选中子女
- 用户感知:可能看到一瞬间的切换抖动,可接受
---
## 17. i18n 深化
> ai07 初稿仅提到 next-intl。ai15 补全多语言策略。
### 17.1 支持语言矩阵
| 语言 | locale | 阶段 | 完成度要求 |
| --------------- | ------ | ----------- | ---------- |
| 简体中文 | zh-CN | P4 强制 | 100% |
| 英文 | en-US | P6 海外扩展 | 100% |
| 繁体中文 | zh-TW | P6 海外扩展 | 100% |
| 日文 | ja-JP | P6+ 未来 | ≥ 80% |
| 阿拉伯文RTL | ar-SA | P6+ 未来 | ≥ 80% |
### 17.2 locale 路由策略
**采用 URL 前缀策略**(与 Shell 共享,由 Shell 配置):
```
/zh-CN/parent/dashboard
/en-US/parent/dashboard
/parent/dashboard → 默认重定向到浏览器首选语言
```
- Next.js 中间件根据 `Accept-Language` 头自动重定向
- 用户主动切换语言时写入 Cookie `NEXT_LOCALE`,下次访问直接命中
- parent-portal 不维护 locale 路由,复用 Shell 中间件
### 17.3 翻译文件组织
```
apps/parent-portal/src/i18n/messages/
├─ zh-CN/
│ ├─ common.json # 通用文案(确认/取消/加载中等)
│ ├─ dashboard.json
│ ├─ children.json
│ ├─ grades.json
│ ├─ homework.json
│ ├─ notifications.json
│ ├─ preferences.json
│ └─ errors.json # 错误码 → i18n key 映射
├─ en-US/
│ └─ ... (镜像 zh-CN 结构)
└─ index.ts # 按需加载 messages
```
按路由切分 message bundle避免首屏加载全部翻译。
### 17.4 国际化格式
| 数据类型 | 库 | 示例zh-CN | 示例en-US |
| -------- | ------------------------------------- | --------------------- | ------------------- |
| 日期 | `Intl.DateTimeFormat` | 2026年7月9日 | July 9, 2026 |
| 时间 | 同上 | 下午3:30 | 3:30 PM |
| 数字 | `Intl.NumberFormat` | 1,234.56 | 1,234.56 |
| 百分比 | 同上 | 85.5% | 85.5% |
| 货币 | 同上 | ¥1,234.50 | $1,234.50 |
| 成绩等级 | 自定义映射表 | 优秀/良好/及格/不及格 | A/B/C/D/F |
| 时区 | `Intl.DateTimeFormat` with `timeZone` | Asia/Shanghai | America/Los_Angeles |
### 17.5 RTL 支持P6+ 预留)
- Tailwind CSS logical properties`ms-*`/`me-*`/`ps-*`/`pe-*` 替代 `ml-*`/`mr-*`/`pl-*`/`pr-*`
- 设计令牌预留 RTL 语义令牌:`--space-inline-start` / `--space-inline-end`
- 图标方向敏感(如返回箭头)需根据 `dir` 属性翻转
---
## 18. 移动端与 PWA
> 家长端是 4 端中移动端使用比例最高的(家长多在通勤/碎片时间查看),移动端策略必须前置。
### 18.1 响应式断点
| 断点 | 宽度 | 典型设备 | parent-portal 布局变化 |
| -------------- | ----------- | ---------------------- | --------------------------------------------------- |
| `sm` (default) | < 640px | iPhone/Android 手机 | 单列布局MultiChildTabBar 改为下拉;侧栏导航抽屉化 |
| `md` | 640-1024px | iPad Mini/Android 平板 | 双列布局(侧栏 + 内容MultiChildTabBar 顶部 Tab |
| `lg` | 1024-1280px | iPad Pro/小笔记本 | 三列布局(侧栏 + 内容 + 详情);多子女并列卡片 |
| `xl` | > 1280px | 桌面 | 三列布局;多子女对比视图横向滚动 |
### 18.2 移动端交互优化
| 场景 | 移动端优化 |
| ------------ | ------------------------------------------ |
| 切换子女 | 下拉选择器 + 头像 + 姓名,单手操作可达 |
| 查看成绩 | 卡片式纵向滚动,避免横向表格;图表触摸缩放 |
| 通知列表 | 左滑标记已读、右滑删除iOS 风格) |
| 通知偏好设置 | 大号 Toggle Switch手指点击友好 |
| 表单提交 | 底部固定按钮;软键盘弹出时自动避让 |
| 长列表 | 无限滚动 + 骨架屏;不上拉加载更多按钮 |
### 18.3 PWA 配置P5+ 引入)
```json
// apps/parent-portal/public/manifest.json
{
"name": "Edu 家长端",
"short_name": "EduParent",
"start_url": "/parent/dashboard",
"display": "standalone",
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#1677ff",
"icons": [
{ "src": "/icons/parent-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/parent-512.png", "sizes": "512x512", "type": "image/png" }
],
"shortcuts": [
{ "name": "子女成绩", "url": "/parent/grades" },
{ "name": "通知", "url": "/parent/notifications" }
]
}
```
### 18.4 Service Worker 策略P5+
| 资源类型 | 缓存策略 | TTL |
| ----------------------- | --------------------------- | ------- |
| 静态资源JS/CSS/图片) | Cache First + 网络更新 | 24 小时 |
| 子女列表 | Stale While Revalidate | 5 分钟 |
| 子女成绩 | Network First失败回退缓存 | 30 秒 |
| 通知列表 | Network Only | — |
| 通知偏好 | Network Only | — |
| API 401 响应 | 不缓存 | — |
---
## 19. 长远愿景与演进路径
### 19.1 阶段演进路线
```mermaid
graph LR
P4[P4 内容分析<br/>家长端 MVP<br/>dashboard+grades+homework] --> P5[P5 沟通AI<br/>通知中心+推送+通知偏好]
P5 --> P6[P6 硬化<br/>PWA+A11y+性能+安全+多语言]
P6 --> P7[P7+ 扩展<br/>家校沟通+缴费+活动 RSVP]
P7 --> P8[P8+ 多租户<br/>学区/教育局版]
```
### 19.2 未来功能铺垫(架构预留)
| 未来功能 | 架构预留点 | 启用阶段 |
| -------------------------- | ------------------------------------------------------------------------------ | -------- |
| 家校沟通IM 聊天) | `NotificationFeed` 组件抽象为通用消息流WebSocket 事件协议预留 `chat.*` 类型 | P7 |
| 缴费(学费/餐费) | `ParentDashboard` 卡片插槽API 路由前缀 `/parent/fees/*` 预留 | P7 |
| 活动 RSVP家长会/运动会) | `NotificationFeed` 支持 `rsvp` 类型消息;状态机:待回复/已确认/已拒绝 | P7 |
| 家长端 AI 助手 | API 路由 `/parent/ai/*` 预留SSE 复用 teacher-portal 模式 | P7+ |
| 多子女横向对比报告 | `ChildComparisonView` 组件预留P5+ 启用) | P5+ |
| 学区/教育局版多租户 | URL 路由前缀 `/{tenantId}/parent/*` 预留; TanStack Query key 加 tenantId 维度 | P8+ |
| 移动端原生壳 | PWA → Capacitor 打包MF 不变 | P8+ |
| 离线模式 | Service Worker + IDB 队列P5+ 引入) | P5+ |
| 推送通知Web Push | Service Worker PushManager + VAPIDP5+ 引入) | P5+ |
### 19.3 模块解耦与演化
| 演化方向 | 触发条件 | 迁移策略 |
| ------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| parent-portal 拆分为多个 Remote | bundle > 200KB 或团队规模 > 5 人 | 按场景域拆分parent-core-remotedashboard/grades+ parent-comm-remotenotifications/preferences |
| MF 2.0 → 3.0 升级 | MF 3.0 稳定且解决 SSR 问题 | Shell 端 `@module-federation/nextjs-mf` 升级parent-portal 仅改 `name`/`filename` 字段 |
| 切换为原生 SSR脱离 MF | SEO 需求强烈或 MF 维护成本过高 | 保留 API 请求层和组件库;移除 MF 配置;独立部署为完整 Next.js 应用 |
| 状态管理迁移Zustand → Jotai | Zustand 性能瓶颈或团队偏好 | 逐 slice 迁移Hook 接口保持不变 |
### 19.4 监控与降级
| 监控项 | 阈值 | 触发动作 |
| ------------------------ | --------------- | ------------------------------------------------------ |
| parent-portal 5xx 错误率 | > 1% | 告警 SRE自动切换到只读模式隐藏 mutation 按钮) |
| MF Remote 加载失败 | 加载超时 10s | Fallback 到 Shell 内置的最小化 dashboard静态引导页 |
| WebSocket 连接失败 | 重试 5 次仍失败 | 降级为 HTTP 轮询(每 60s 拉取通知列表) |
| BFF 响应延迟 | P95 > 3s | 前端展示"加载缓慢"提示;自动缩短缓存 TTL |
| 子女列表加载失败 | 3 次重试失败 | 显示"网络异常,请稍后重试"页面 + 重试按钮 |
---
**AI Agent**: ai15 (parent-portal remote)
**Branch**: docs/parent-portal-stage1-stage2-design-ai15
**Coordinator**: coord-ai
**Predecessor**: ai07初版起草ai15 接管审计与补全)