# parent-portal 对接契约

> 负责人：ai15
> 关联：[matrix.md](./matrix.md)、[parent-bff_contract.md](./parent-bff_contract.md)、[iam_contract.md](./iam_contract.md)、[push-gateway_contract.md](./push-gateway_contract.md)
> 依据：ARB-001（BFF GraphQL）、ARB-002（MF Shell 暴露清单）、[port-allocation.md](../../../infra/port-allocation.md) §4
> 待仲裁：ISSUE-001 ~ ISSUE-010（见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md)），仲裁前本契约按 ARB-001 GraphQL 方向编写

---

## §1 我提供什么（对外接口）

### 1.1 gRPC 接口（如有）

无。parent-portal 是前端微前端 Remote，不提供 gRPC。

### 1.2 HTTP 端点（如有）

无对外 HTTP API 端点。parent-portal 是 Next.js 前端应用（MF Remote），不对外暴露 REST API。

> **说明**（ISSUE-006）：parent-portal 的页面路由（`/parent/dashboard`、`/parent/grades` 等）是前端 SSR/CSR 路由，不是 HTTP API 端点。页面路由清单见 [01-understanding.md §8 L2 路由表](../../../apps/parent-portal/docs/01-understanding.md#8-l2-路由表)。
>
> parent-portal 仅提供两个内部健康检查端点（非业务 API）：

| Method | Path        | 用途                   | 认证 |
| ------ | ----------- | ---------------------- | ---- |
| GET    | /api/health | Dockerfile HEALTHCHECK | 无   |
| GET    | /api/ready  | K8s readinessProbe     | 无   |

### 1.3 GraphQL schema（如 BFF）

不适用。parent-portal 消费 parent-bff GraphQL，自身不提供 schema。

### 1.4 Kafka 事件发布（如有）

无。前端不发布 Kafka 事件。

### 1.5 错误码前缀

parent-portal 不产生错误码前缀（前端不定义错误码）。消费侧错误码前缀见 §2.5。

### 1.6 微前端架构

| 角色                   | 说明                                                                                                                                                                                                      |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MF 角色                | Remote（Shell = teacher-portal :4000）                                                                                                                                                                    |
| Remote name            | `parent_app`                                                                                                                                                                                              |
| remoteEntry 路径       | `static/chunks/remoteEntry.js`                                                                                                                                                                            |
| 暴露模块               | `./pages`（家长场景页面）、`./ChildSwitcher`（多子女切换组件）                                                                                                                                            |
| MF 配置文件            | `apps/parent-portal/next.config.js`（NextFederationPlugin，见 [02-architecture-design §1.2](../../../apps/parent-portal/docs/02-architecture-design.md#12-mf-配置parent-portalnextconfigjs-remote-角色)） |
| MF shared（singleton） | react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks（ARB-002）                                                                 |
| dev/prod 端口          | 4002（[port-allocation.md](../../../infra/port-allocation.md) §4）                                                                                                                                        |
| feature flag           | `NEXT_PUBLIC_MF_ENABLED`（ARB-002，P4 默认开）                                                                                                                                                            |

> **注**（ISSUE-007）：MF 配置文件统一为 `next.config.js`，不使用 `module-federation.config.ts`（与 02-architecture-design + teacher-portal Shell 一致）。

---

## §2 我消费什么（依赖上游）

### 2.1 gRPC 调用（同步）

无。前端不直接调 gRPC。

### 2.2 Kafka 事件订阅（异步）

无。前端不直接订阅 Kafka。

### 2.3 HTTP 调用（非 GraphQL）

| 被调用方           | Method.Path                 | 用途     | mock 策略                                          |
| ------------------ | --------------------------- | -------- | -------------------------------------------------- |
| api-gateway (ai01) | POST /api/v1/iam/v1/login   | 家长登录 | api-gateway 就绪前 MSW 返回固定 JWT（parent 角色） |
| api-gateway (ai01) | POST /api/v1/iam/v1/refresh | 刷新令牌 | MSW 返回新 access/refresh token                    |

> **注**（ISSUE-004 + ARB-022 §24.4）：
>
> - 登录端点统一为 `POST /api/v1/iam/v1/login`（双 /v1 前缀，ARB-022 §24.4 ISSUE-003 方案 A，与 matrix.md §5 `/api/v1/iam/v1/*` 一致）
> - 登录是 parent-portal 唯一走 REST（非 GraphQL）的端点：登录前无 JWT，GraphQL endpoint 需鉴权
> - ARB-020 §22.3 早期描述为单 /v1，ARB-022 §24.4 已修正为双 /v1（以 ARB-022 为准）

### 2.4 GraphQL 查询域（经 api-gateway 代理到 parent-bff）

> **依据**：ARB-001（BFF GraphQL）+ [parent-bff_contract.md](./parent-bff_contract.md) §1.3 + ARB-022 §24.4 方案 A
>
> **端点**：`POST /api/v1/parent/v1/graphql`（双 /v1 前缀，ARB-022 §24.4 ISSUE-003 方案 A；api-gateway 代理 `/api/v1/parent/v1/*` → parent-bff :3010 `/parent/v1/*`）

> **注**（ISSUE-001 / ISSUE-008 + ARB-022 §24.4）：
>
> - 仲裁前本表按 GraphQL 方向编写（与 parent-bff contract + matrix.md 一致）
> - 路径前缀统一为 `/api/v1/parent/v1/graphql`（双 /v1，ARB-022 §24.4 方案 A，与 matrix.md §5 一致）
> - ARB-020 §22.3 早期描述为单 /v1，ARB-022 §24.4 已修正为双 /v1（以 ARB-022 为准）

| Query/Mutation                | 类型     | 用途                 | 对应 parent-bff 聚合                                     | mock 策略                                          |
| ----------------------------- | -------- | -------------------- | -------------------------------------------------------- | -------------------------------------------------- |
| currentUser                   | Query    | 当前家长信息         | iam.GetUserInfo + GetEffectivePermissions + GetViewports | MSW 返回固定家长（parent-001 王家长）              |
| myChildren                    | Query    | 我的子女列表（核心） | iam.GetChildrenByParent（I3/ISSUE-047 裁决）             | MSW 返回固定 2 个子女（student-001 + student-002） |
| childSummary(childId)         | Query    | 子女仪表盘概览       | data-ana.GetParentDashboard                              | MSW 返回固定仪表盘                                 |
| childGrades(childId)          | Query    | 子女成绩             | core-edu.ListGradesByStudent                             | MSW 返回固定 5 个成绩                              |
| childAttendance(childId)      | Query    | 子女考勤             | core-edu.ListAttendanceByStudent                         | MSW 返回固定 10 条考勤                             |
| childHomework(childId)        | Query    | 子女作业             | core-edu.ListHomeworkByClass                             | MSW 返回固定 3 个作业                              |
| childWeakness(childId)        | Query    | 子女薄弱点           | data-ana.GetStudentWeakness                              | MSW 返回固定 3 个 weak_points                      |
| childTrend(childId)           | Query    | 子女学习趋势         | data-ana.GetLearningTrend                                | MSW 返回固定趋势数据                               |
| myNotifications               | Query    | 通知列表（P5）       | msg.ListNotifications                                    | MSW 返回固定 10 条通知                             |
| markAsRead(notificationId)    | Mutation | 标记已读（P5）       | msg.MarkAsRead                                           | MSW 返回 success=true                              |
| updateNotificationPreferences | Mutation | 更新通知偏好         | msg（待 ai05 确认）                                      | MSW 返回 success=true                              |
| switchChild(childId)          | Mutation | 切换当前子女         | 待 ISSUE-009 仲裁确认                                    | 见 ISSUE-009                                       |

> **switchChild 说明**（ISSUE-009）：
>
> - parent-bff_contract.md §1.3 未列 switchChild Mutation
> - 待 coord 仲裁：switchChild 是 GraphQL Mutation（后端记录当前子女）还是纯前端状态（localStorage + Zustand）
> - 若纯前端：本表移除 switchChild，切换逻辑在 `useChildSwitcher` 内直接写 Zustand + localStorage

### 2.5 消费的错误码前缀（前端 i18n 路由）

parent-portal 不产生错误码，仅消费。前端 API 请求层根据 `error.code` 前缀路由到对应 i18n key：

| 前缀          | 来源服务    | i18n key 模式               |
| ------------- | ----------- | --------------------------- |
| `IAM_`        | iam         | `error.iam.{{code}}`        |
| `CORE_EDU_`   | core-edu    | `error.core_edu.{{code}}`   |
| `BFF_PARENT_` | parent-bff  | `error.bff_parent.{{code}}` |
| `GW_`         | api-gateway | `error.gw.{{code}}`         |
| `NETWORK_`    | 前端网络层  | `error.network.{{code}}`    |

> **注**：与 [matrix.md](./matrix.md) §6 错误码前缀矩阵对齐。`BFF_PARENT_` 前缀由 parent-bff 定义（见 [parent-bff_contract.md](./parent-bff_contract.md) §1.5）。

### 2.6 WebSocket 推送（P5）

| 被调用方            | 协议        | 路径 | 用途     | mock 策略                               |
| ------------------- | ----------- | ---- | -------- | --------------------------------------- |
| push-gateway (ai02) | WebSocket   | /ws  | 实时推送 | mock-socket 模拟 WS 推送（每 30s 1 条） |
| push-gateway (ai02) | SSE（降级） | /sse | SSE 降级 | —                                       |

> WebSocket 连接由 Shell 建立（统一连接管理），parent-portal 通过 Zustand ui-store 订阅事件流。

### 2.7 消费的 MF Shell 暴露（ARB-002）

| 暴露模块          | 来源                   | 用途                        |
| ----------------- | ---------------------- | --------------------------- |
| AppShell          | teacher-portal Shell   | 左栏导航 + 主内容区布局     |
| GraphQLProvider   | teacher-portal Shell   | urql client 单例（ARB-002） |
| useAuth           | packages/hooks         | 会话状态                    |
| usePermission     | packages/hooks         | 权限查询                    |
| useGraphQLClient  | packages/hooks         | urql client 获取            |
| ErrorBoundary     | packages/ui-components | React 渲染异常兜底          |
| Loading / Empty   | packages/ui-components | 骨架屏 / 空态               |
| RequirePermission | packages/ui-components | L3 组件级视口控制           |

> MF shared（singleton）：react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks（ARB-002 裁决，见 [coord.md](../coord.md) §2）

---

## §3 就绪信号

### 3.1 我依赖的上游就绪标志

| 上游                              | 就绪信号                                                                        | 提供方    | 状态       |
| --------------------------------- | ------------------------------------------------------------------------------- | --------- | ---------- |
| api-gateway                       | HTTP :8080 启用 + JWT 验签 + `/api/v1/parent/v1/*` 代理（ARB-022 §24.4 双 /v1） | ai01      | ⏳         |
| parent-bff GraphQL                | `POST /graphql` :3010 + currentUser/myChildren/childSummary/childGrades Query   | ai05      | ⏳ P4      |
| iam GetChildrenByParent           | gRPC 50052 + `iam_student_guardians` 表（I3/ISSUE-047 裁决）                    | ai06      | ⏳ P3 补全 |
| teacher-portal Shell              | MF exposes（AppShell + GraphQLProvider + hooks + UI 组件）+ shared singleton    | ai13      | ⏳ P2      |
| push-gateway                      | WebSocket :8081/ws                                                              | ai02      | ⏳ P5      |
| msg                               | gRPC 50056 + NotificationService                                                | ai10      | ⏳ P5      |
| shared-ts / contracts             | ApiClient / Logger / Permissions 常量                                           | coord     | ⏳         |
| ui-tokens / ui-components / hooks | 三层令牌 + shadcn + usePermission/useAuth                                       | ai07/ai13 | ⏳ P2 收尾 |

> **P0 阻塞**（ISSUE-010）：iam `GetChildrenByParent` 缺失，多子女场景无法落地。补全前用 mock（固定 2 个子女 student-001 + student-002）开发。

### 3.2 我的就绪标志（供下游消费）

> 与 [matrix.md](./matrix.md) §8 就绪信号跟踪表对齐

| 信号                                | 说明                                                                              | 阶段  |
| ----------------------------------- | --------------------------------------------------------------------------------- | ----- |
| parent-portal dev server :4002 启用 | MF Remote 可被 Shell 加载                                                         | P4-1  |
| MF Remote remoteEntry.js 可加载     | Shell 端 `remotes.parent = parent_app@http://localhost:4002/...` 可解析           | P4-1  |
| 独立壳渲染                          | 首页 + 导航 + 路由守卫                                                            | P4-1  |
| 登录流程可用                        | `POST /api/v1/iam/v1/login` 获取 JWT 存入 httpOnly cookie（ARB-022 §24.4 双 /v1） | P4-2  |
| GraphQL 查询可执行                  | currentUser / myChildren / childSummary 返回数据（mock 或真实）                   | P4-2  |
| 多子女切换可用                      | ChildSwitcher + invalidate 流程通过                                               | P4-3  |
| 数据范围校验生效                    | 前端路由守卫校验 childId 是否在 myChildren 返回列表中                             | P4-3  |
| Dashboard 可访问                    | 家长登录 → 看到 Dashboard（含子女卡片）                                           | P4-4  |
| 健康检查通过                        | `GET /api/health` + `GET /api/ready` 200                                          | P4-1  |
| 测试覆盖率达标                      | 单元 ≥ 85% + 集成 ≥ 75%                                                           | P4-9  |
| Docker 镜像可构建                   | `docker build` 成功                                                               | P4-10 |
| WebSocket 通知可接收                | push-gateway WS 事件正确处理                                                      | P5-1  |

---

## §4 Mock 策略

### 4.1 我提供的 mock

parent-portal 是前端，无下游消费方。但对开发体验提供：

- **Storybook**：各组件独立 story
- **MSW handlers**：`apps/parent-portal/src/mocks/handlers.ts`，拦截所有 GraphQL/HTTP 请求

### 4.2 我消费的 mock

在真实上游就绪前，parent-portal 使用以下 mock（由 `NEXT_PUBLIC_API_MOCKING=enabled` 控制）：

- **GraphQL mock**：MSW 拦截 `POST /api/v1/parent/v1/graphql`（双 /v1，ARB-022 §24.4）
  - 按 operationName 返回对应 mock 响应（与 parent-bff mock 数据一致）
  - currentUser → 固定家长（id="parent-001", name="王家长", roles=["parent"]）
  - myChildren → 固定 2 个子女（id="student-001" 李同学 + id="student-002" 李妹妹）
  - childSummary → 固定仪表盘（child_avg_score=85.0, child_class_rank=5）
  - childGrades → 固定 5 个成绩
  - childAttendance → 固定 10 条考勤
  - childHomework → 固定 3 个作业
  - myNotifications → 固定 10 条通知
  - 所有 mock 响应定义在 `apps/parent-portal/src/mocks/fixtures/*.json`
- **HTTP mock**：MSW 拦截 `POST /api/v1/iam/v1/login`（双 /v1，ARB-022 §24.4） → 返回固定 JWT + UserInfo（parent 角色）
- **WebSocket mock**：mock-socket 库，连接后每 30 秒推送 1 条 mock 通知
- **JWT mock**：固定 mock JWT，存入 httpOnly cookie
- **环境切换**：`NEXT_PUBLIC_API_MOCKING=enabled`（开发）/ `disabled`（上游就绪后）
- **数据一致性**：myChildren mock 必须返回固定 2 个孩子（student-001 + student-002），与所有 child* 查询的 student_id 一致（否则前端数据范围校验失败）

---

## §5 待协调事项（指向 objections）

以下事项已提请 coord 仲裁，仲裁结果可能影响本契约：

| ISSUE                                     | 影响章节 | 当前处理                                                                          |
| ----------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| ISSUE-001（REST vs GraphQL）              | §2.4     | ✅ 已裁决 ARB-020 §22：GraphQL（`POST /api/v1/parent/v1/graphql`）                |
| ISSUE-004（登录端点）                     | §2.3     | ✅ 已裁决 ARB-020 §22 + ARB-022 §24.4：`POST /api/v1/iam/v1/login`（双 /v1）      |
| ISSUE-006（HTTP 端点分类）                | §1.2     | ✅ 已修正为"无对外 HTTP API"                                                      |
| ISSUE-007（MF 配置文件名）                | §1.6     | ✅ 已修正为 `next.config.js`                                                      |
| ISSUE-008（GraphQL 路径前缀）             | §2.4     | ✅ 已修正为 `/api/v1/parent/v1/graphql`（双 /v1，ARB-022 §24.4 方案 A）           |
| ISSUE-009（switchChild Mutation）         | §2.4     | ✅ 已裁决 ARB-020 §22.5：双层实现（GraphQL Mutation 后端审计 + Zustand 前端缓存） |
| ISSUE-010（iam GetChildrenByParent 缺失） | §3.1     | ✅ 已裁决 ARB-020 §22：P0 阻塞，用 mock 开发（固定 2 子女）                       |

详见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md)。
