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
This commit is contained in:
SpecialX
2026-07-10 17:40:27 +08:00
parent b54bfd101b
commit 5661938cc0
76 changed files with 9525 additions and 70 deletions

View File

@@ -434,24 +434,36 @@
> parent-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 parent-portal **特有**的"场景→技术"映射,通用前端映射见 §2.12。
| 场景 | 技术/规则 |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P4 端口分配 | parent-portal = **4002**004 §1.2 强制 4 端 4000-4003ai07 初稿误用 3002 与 iam 冲突ai15 修订) |
| P4 MF Remote 角色 | next.config.js 配 `@module-federation/nextjs-mf` 暴露 `./ChildSwitcher` 等家长端特有组件,复用 Shell 提供的 `AppShell`/`usePermission`/`ApiClient`/`RequirePermission` |
| P4 DataScope CHILDREN | 家长仅能查看自己绑定子女的数据004 §5.4 L0 SELF 变体),前端通过 ApiClient 透传 userId + childId下游服务按 DataScope 过滤 |
| P4 多子女切换 ChildSwitcher | Zustand slice`selectedChildId`+ 切换后 `queryClient.invalidateQueries({ queryKey: ['child', prevId] })`;多子女 Tab≤3或下拉≥4空态/单子女/多子女三态分支 |
| P4 多子女数量边界 | 0 子女EmptyChildState 引导绑定)→ 1 子女(隐藏切换器)→ 2-3Tab Bar→ 4-10下拉→ >10搜索 + 列表);超出 10 触发性能预警 |
| P4 通知偏好三维矩阵 | `Record<childId, Record<eventType, Record<channel, boolean>>>`react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 |
| P4 跨标签同步 | BroadcastChannel API`parent-portal-sync`+ localStorage 持久化 + LWWLast-Write-Wins冲突解决同步 selectedChildId / 通知偏好 / 已读状态 |
| P4 隐私合规COPPA/FERPA/PIPL | 未成年人数据最小化采集;通知偏好默认 opt-in数据保留 7 年后自动归档;前端不缓存敏感字段到 localStorage仅 childId + 已读位图) |
| P4 i18n 5 语言含 RTL | next-intl 支持 zh-CN / en-US / zh-TW / ja-JP / ar-SAar-SA 用 `dir="rtl"` + Tailwind `rtl:` 变体;翻译文件按域分 namespaceparent/grades/notifications/preferences |
| P4 PWA + Service Worker | manifest.jsonname/short_name/icons/theme_color+ Service Worker 缓存策略App Shell precache + API stale-while-revalidate + 离线兜底页P5+ 支持离线查看子女成绩缓存 |
| P4 API 契约版本管理 | 主版本 URL `/api/v1/*` → `/api/v2/*` + 子版本响应头 `X-API-Version` + Deprecation/Sunset 头埋点 + 低带宽场景 `X-Fields` 字段裁剪GraphQL 风格BFF REST 透传) |
| P4 性能预算 | Bundle ≤ 80KB JS / 20KB CSS / 200KB 总recharts 懒加载MF shared 单例避免 React 重复打包dynamic import 非首屏组件;预加载子女数据 |
| P4 监控与降级 | 10 项前端指标MF 加载失败率/ApiClient 错误率/Web Vitals/切换延迟等)+ 9 项降级策略MF 失败回退独立页/BFF 5xx 降级缓存/WS 重试指数退避等) |
| P4 模块演化与解耦 | 拆分触发条件Bundle > 120KB / 团队 > 3 人 / 独立部署需求);拆分后 MF 配置从 Shell 暴露状态管理迁移路径Zustand → Server Component + Server ActionsMF 升级路径MF 2.0 → 3.0 |
| P4 iam 家长-学生关联 P0 阻塞 | iam 缺 `GetChildrenByParent` proto RPC + `GET /iam/children` REST + `iam_student_guardians` 表ai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试 |
| P4 BFF 错误码前缀 | **BFF_PARENT\_=parent-bff**coord final §B5统一 `BFF_XXX_` 风格);前端按 `BFF_PARENT_*` 前缀路由 i18n key与 BFF_TEACHER_/BFF_STUDENT_ 对齐 |
| 场景 | 技术/规则 |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P4 端口分配 | parent-portal = **4002**004 §1.2 强制 4 端 4000-4003ai07 初稿误用 3002 与 iam 冲突ai15 修订) |
| P4 MF Remote 角色 | next.config.js 配 `@module-federation/nextjs-mf` 暴露 `./ChildSwitcher` 等家长端特有组件,复用 Shell 提供的 `AppShell`/`usePermission`/`ApiClient`/`RequirePermission` |
| P4 DataScope CHILDREN | 家长仅能查看自己绑定子女的数据004 §5.4 L0 SELF 变体),前端通过 ApiClient 透传 userId + childId下游服务按 DataScope 过滤 |
| P4 多子女切换 ChildSwitcher | Zustand slice`selectedChildId`+ 切换后 `queryClient.invalidateQueries({ queryKey: ['child', prevId] })`;多子女 Tab≤3或下拉≥4空态/单子女/多子女三态分支 |
| P4 多子女数量边界 | 0 子女EmptyChildState 引导绑定)→ 1 子女(隐藏切换器)→ 2-3Tab Bar→ 4-10下拉→ >10搜索 + 列表);超出 10 触发性能预警 |
| P4 通知偏好三维矩阵 | `Record<childId, Record<eventType, Record<channel, boolean>>>`react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 |
| P4 跨标签同步 | BroadcastChannel API`parent-portal-sync`+ localStorage 持久化 + LWWLast-Write-Wins冲突解决同步 selectedChildId / 通知偏好 / 已读状态 |
| P4 隐私合规COPPA/FERPA/PIPL | 未成年人数据最小化采集;通知偏好默认 opt-in数据保留 7 年后自动归档;前端不缓存敏感字段到 localStorage仅 childId + 已读位图) |
| P4 i18n 5 语言含 RTL | next-intl 支持 zh-CN / en-US / zh-TW / ja-JP / ar-SAar-SA 用 `dir="rtl"` + Tailwind `rtl:` 变体;翻译文件按域分 namespaceparent/grades/notifications/preferences |
| P4 PWA + Service Worker | manifest.jsonname/short_name/icons/theme_color+ Service Worker 缓存策略App Shell precache + API stale-while-revalidate + 离线兜底页P5+ 支持离线查看子女成绩缓存 |
| P4 API 契约版本管理 | 主版本 URL `/api/v1/*` → `/api/v2/*` + 子版本响应头 `X-API-Version` + Deprecation/Sunset 头埋点 + 低带宽场景 `X-Fields` 字段裁剪GraphQL 风格BFF REST 透传) |
| P4 性能预算 | Bundle ≤ 80KB JS / 20KB CSS / 200KB 总recharts 懒加载MF shared 单例避免 React 重复打包dynamic import 非首屏组件;预加载子女数据 |
| P4 监控与降级 | 10 项前端指标MF 加载失败率/ApiClient 错误率/Web Vitals/切换延迟等)+ 9 项降级策略MF 失败回退独立页/BFF 5xx 降级缓存/WS 重试指数退避等) |
| P4 模块演化与解耦 | 拆分触发条件Bundle > 120KB / 团队 > 3 人 / 独立部署需求);拆分后 MF 配置从 Shell 暴露状态管理迁移路径Zustand → Server Component + Server ActionsMF 升级路径MF 2.0 → 3.0 |
| P4 iam 家长-学生关联 P0 阻塞 | iam 缺 `GetChildrenByParent` proto RPC + `GET /iam/children` REST + `iam_student_guardians` 表ai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试 |
| P4 BFF 错误码前缀 | **BFF_PARENT\_=parent-bff**coord final §B5统一 `BFF_XXX_` 风格);前端按 `BFF_PARENT_*` 前缀路由 i18n key与 BFF_TEACHER_/BFF_STUDENT_ 对齐 |
| P4 认证 token 存储 | localStorageF12 裁决P4 用 localStorage非 cookierefreshAccessToken 竞态防护用 inflight Promise 复用,并发请求只触发一次 refresh fetch |
| P4 switchChild 纯前端 | ISSUE-009 裁决:切换子女不调后端,仅 Zustand slice + localStorage 持久化 selectedChildId切换后由前端 invalidateQueries 触发数据刷新 |
| P4 通知偏好 localStorage 降级 | ISSUE-033 裁决P4 localStorage 为准,更新先写本地再尝试 GraphQL mutation失败不阻塞P5+ 切换到后端为主存储 |
| P4 上游 mock 策略 | MSW 拦截 parent-bff GraphQL + iam RESTiam GetChildrenByParent P0 阻塞用 mock fixturesstudent-001 + student-002 双子女MSW server 在 providers 启动 |
| TS noUncheckedIndexedAccess | Record 索引访问返回 `T \| undefined`;内层 Record 改 `Partial<Record<...>>` 或空对象回退(`?? {}`)避免 undefined 传播 |
| urql fetchOptions headers 类型 | 返回类型须用 `Record<string, string>` 累积再展开,不可用条件对象(`{...(token?{Authorization}:{})}`)否则 TS 推断不兼容 |
| Vitest Promise 身份比较 | `expect(p1).toBe(p2)` 对 Promise 不可靠async 包装产生新 Promise改为验证行为副作用如 fetch 调用次数 === 1 证明竞态防护生效) |
| jsdom window.location.href | 设置触发 "Not implemented: navigation" stderr 警告非错误测试仍通过mock 用 `Object.defineProperty(window, "location", {...})` 替代 `vi.spyOn` |
| MSW 响应过快观察加载态 | MSW 默认同步响应,加载态测试看不到 "加载中";用 `server.use()` + `delay(ms)` 延迟响应再断言按钮文本 |
| Testing Library getByRole 误匹配 | `getByRole("button", { name: /未读/ })` 会匹配通知项 aria-label="未读";改用 `getAllByRole("button")` + find 文本内容过滤精确选中筛选按钮 |
| ESLint flat config 测试文件 | 放宽规则需含 `*.test.tsx`/`*.spec.tsx`/`__tests__/**` 模式,否则 tsx 测试文件按严格规则报错 |
| Dockerfile 多阶段构建 | G1 裁决首次即多阶段buildernode:20-alpine 跑 build+ runnernode:20-alpine 跑 server端口 4002HEALTHCHECK 指向 `/api/health`,非 root 用户 nextjs |
### 2.14 parent-bffTS/NestJSP4