# student-portal 对接契约

> 负责人：ai14
> 关联：[matrix.md](./matrix.md)、[coord.md §1 ARB-001](../coord.md)、[coord.md §2 ARB-002](../coord.md)、[student-bff_contract.md](./student-bff_contract.md)、[teacher-portal_contract.md](./teacher-portal_contract.md)
> 版本：v2（ai14 接管审计与补全版，2026-07-10）
> 修订摘要：GraphQL endpoint 路径修正为 `/api/v1/student/graphql`（对齐 matrix.md §5）+ 补全 24 个 GraphQL query/mutation（对齐 02 §4.2）+ 补充 ARB-002 MF Shell 暴露清单 + 补充就绪信号明细

---

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

### 1.1 gRPC 接口（如有）

无。student-portal 是前端微前端 Remote。

### 1.2 HTTP 端点（如有）

| Method | Path           | 用途         | 认证                     |
| ------ | -------------- | ------------ | ------------------------ |
| GET    | /              | 学生门户首页 | JWT 必需（前端路由守卫） |
| GET    | /my-classes    | 我的班级     | JWT 必需                 |
| GET    | /my-exams      | 我的考试     | JWT 必需                 |
| GET    | /my-exams/[id]/take    | 考试作答页 | JWT 必需                 |
| GET    | /my-exams/[id]/result | 考试结果页 | JWT 必需                 |
| GET    | /my-homework   | 我的作业     | JWT 必需                 |
| GET    | /my-homework/[id]/submit | 作业提交页 | JWT 必需            |
| GET    | /my-grades     | 我的成绩     | JWT 必需                 |
| GET    | /my-attendance | 我的考勤     | JWT 必需                 |
| GET    | /learning-path | 学习路径     | JWT 必需                 |
| GET    | /dashboard     | 学生仪表盘   | JWT 必需                 |
| GET    | /dashboard/weakness | 学情诊断 | JWT 必需                 |
| GET    | /dashboard/trend    | 学习趋势 | JWT 必需                 |
| GET    | /textbooks     | 教材列表     | JWT 必需                 |
| GET    | /textbooks/[id]/chapters | 章节列表 | JWT 必需             |
| GET    | /notifications | 通知中心     | JWT 必需                 |
| GET    | /ai-tutor      | AI 辅助答疑（P5 可选） | JWT 必需       |

> **路由前缀**：无 `/student/` 前缀（student-portal 作为 MF Remote，由 Shell 路由 `/student/*` 加载，内部路由无前缀）。

### 1.3 GraphQL schema（如 BFF）

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

> **消费的 schema 文件**：`packages/shared-ts/contracts/graphql/student-bff.graphql`（待 ISSUE-014-02 仲裁后由 ai04 创建，对齐 ARB-001 §1.3 集中管理原则）

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

无。

### 1.5 错误码前缀

无（前端不定义错误码前缀，透传 BFF 错误码 `BFF_STUDENT_*`，见 [matrix.md §6](../matrix.md)）。

### 1.6 微前端架构（MF Remote，ARB-002 对齐）

| 角色                   | 说明                                                                                                      |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| MF Remote              | student-portal 是微前端远程模块（P3 首个 Remote，ARB-002 §2.3），由 teacher-portal AppShell 加载          |
| 暴露的 remote 模块     | `./StudentApp`（学生端完整应用）                                                                          |
| module federation 配置 | `apps/student-portal/next.config.js`（NextFederationPlugin）                                              |
| Shell 暴露清单（复用） | AppShell / GraphQLProvider / useAuth / usePermission / useGraphQLClient / ErrorBoundary / Loading / Empty / RequirePermission（ARB-002 §2.2） |
| shared singleton       | react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks / @edu/contracts / @edu/shared-ts（ARB-002 §2.2） |
| feature flag           | `NEXT_PUBLIC_MF_ENABLED`（P2=false 独立壳，P3=true 接入 Shell）                                           |
| 登录页                 | 不实现，未登录跳转 `http://localhost:4000/login?redirect=student`（ARB-002 §2.3 登录由 Shell 独占）       |

### 1.7 WebSocket 消费（push-gateway）

| 端点                  | 用途                    | 认证 | 事件类型                                                                 |
| --------------------- | ----------------------- | ---- | ------------------------------------------------------------------------ |
| `ws://push-gateway:8081/ws` | 实时通知推送       | JWT | 作业通知 / 考试通知 / 成绩通知 / 系统通知 / 考试延长（待 ISSUE-014-06 仲裁） / 考试强制提交（待 ISSUE-014-06 仲裁） |

---

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

### 2.1 gRPC 调用（同步）

无。前端不直接调 gRPC。

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

无。前端不直接订阅 Kafka。

### 2.3 HTTP 调用（如有）

| 被调用方            | Method.Path                    | 用途                                          | mock 策略                                                         |
| ------------------- | ------------------------------ | --------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01)  | POST /api/v1/student/graphql   | 学生 GraphQL 查询（经网关代理到 student-bff :3009/graphql） | api-gateway/student-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01)  | POST /api/auth/login           | 学生登录（Shell 独占，student-portal 不直接调用，仅跳转） | api-gateway 就绪前由 Shell 处理                              |
| api-gateway (ai01)  | POST /api/v1/student/upload    | 作业附件上传（待 ISSUE-014-05 仲裁）          | 待仲裁后实现                                                      |
| push-gateway (ai02) | GET /ws（WebSocket）           | WebSocket 实时通知                            | push-gateway 就绪前使用 mock-socket 模拟 WS 推送                  |

> **路径说明**（ISSUE-014-01）：
> - `POST /api/v1/student/graphql` 经 api-gateway 反向代理到 student-bff `POST /graphql`（:3009）
> - 路径前缀 `/api/v1/student/*` 与 matrix.md §5、teacher-portal `/api/v1/teacher/*` 保持命名一致性
> - 由 ai01（api-gateway）确认路由配置：`/api/v1/student/*` → `student-bff:3009/*`

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

> 对齐 [02-architecture-design.md v2 §4.2](../../../apps/student-portal/docs/02-architecture-design.md) GraphQL 操作清单（共 24 个）

#### 2.4.1 Query（读，16 个）

| Query                          | 用途                  | mock 策略                                          |
| ------------------------------ | --------------------- | -------------------------------------------------- |
| currentUser                    | 当前学生信息          | MSW 返回固定学生（id=student-001, roles=[student]）|
| myClasses                      | 我的班级              | MSW 返回固定 1 个班级                              |
| myExams                        | 我的考试列表          | MSW 返回固定 2 个考试                              |
| examDetail(id: ID!)            | 考试详情（含题目）    | MSW 返回固定考试 + 5 道题                          |
| myHomework                     | 我的作业列表          | MSW 返回固定 3 个作业（1 个待提交）                |
| homeworkDetail(id: ID!)        | 作业详情              | MSW 返回固定作业 + 题目                            |
| myGrades                       | 我的成绩              | MSW 返回固定 5 个成绩                              |
| myAttendance                   | 我的考勤              | MSW 返回固定 10 条考勤                             |
| textbooks                      | 教材列表              | MSW 返回固定 5 个教材                              |
| chapters(textbookId: ID!)      | 章节列表              | MSW 返回固定章节树                                 |
| learningPath                   | 学习路径              | MSW 返回固定 8 个知识点推荐顺序                    |
| studentDashboard               | 学生仪表盘            | MSW 返回固定仪表盘（avg_score=85.0, class_rank=5） |
| myWeakness                     | 我的薄弱点            | MSW 返回固定 3 个 weak_points                      |
| myTrend                        | 学习趋势              | MSW 返回固定趋势数据                               |
| myNotifications(first: Int, after: String) | 通知列表   | MSW 返回固定 10 条通知                             |
| serverTime                     | 服务器时间（考试倒计时对齐） | MSW 返回当前时间 + 100ms 延迟                |

#### 2.4.2 Mutation（写，8 个）

| Mutation                                | 用途                | mock 策略                              |
| --------------------------------------- | ------------------- | -------------------------------------- |
| submitHomework(input: SubmitHomeworkInput!) | 提交作业        | MSW 返回 success=true                  |
| submitExam(input: SubmitExamInput!)     | 提交考试作答        | MSW 返回 success=true + submittedAt    |
| saveExamDraft(input: SaveExamDraftInput!) | 保存考试草稿      | MSW 返回 success=true                  |
| markAsRead(notificationId: ID!)         | 标记通知已读        | MSW 返回 success=true                  |
| markAllAsRead                           | 全部标记已读        | MSW 返回 success=true                  |
| recordExamViolation(input: RecordExamViolationInput!) | 记录防作弊违规（待 ISSUE-014-03 仲裁） | MSW 返回 success=true |
| recordPasteEvent(input: RecordPasteEventInput!) | 记录粘贴事件（待 ISSUE-014-04 仲裁） | MSW 返回 success=true           |
| updateNotificationPreference(input: UpdateNotificationPreferenceInput!) | 更新通知偏好（P5） | MSW 返回 success=true |

> **DataScope L0 强制执行**（ISSUE-014-07）：
> - 所有学生端 Query 不传 `studentId` 参数，由 student-bff 在 Resolver 层从 JWT `x-user-id` 提取并强制过滤
> - 前端无法绕过 L0 边界（前端篡改 JWT 无效，gRPC 层会重新校验）

---

## §3 就绪信号

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

| 上游                              | 就绪标志                                                                  | 阻塞阶段 | 状态         |
| --------------------------------- | ------------------------------------------------------------------------- | -------- | ------------ |
| packages 骨架（ai13 批次 0.15）   | ui-tokens / ui-components / hooks 可 import                               | P2 启动  | ✅ 已就绪    |
| teacher-portal MF Shell（ai13 P2）| exposes AppShell/GraphQLProvider/useGraphQLClient/useAuth/usePermission + shared singleton | P2 启动 | ⏳ 待 ai13 P2 |
| api-gateway HTTP :8080（ai01 P3） | `/api/v1/student/*` 反向代理 student-bff 可用                             | P3 启动  | ⏳ 待 ai01 P3 |
| student-bff GraphQL（ai04 P3）    | `POST /graphql` :3009 + 核心 Query/Mutation 可执行                        | P3 启动  | ⏳ 待 ai04 P3 |
| student-bff GraphQL schema        | `packages/shared-ts/contracts/graphql/student-bff.graphql` 创建（待 ISSUE-014-02 仲裁） | P3 启动 | ⏳ 待仲裁 |
| core-edu gRPC 50053（ai08 P3）    | ExamService/HomeworkService/GradeService/AttendanceService/ClassService   | P3 启动  | ⏳ 待 ai08 P3 |
| iam gRPC 50052（ai06 P2）         | GetUserInfo + GetEffectivePermissions + GetViewports                      | P3 启动  | ⏳ 待 ai06 P2 |
| content gRPC 50054（ai09 P4）     | TextbookService + ChapterService + KnowledgeGraphService                  | P4 启动  | ⏳ 待 ai09 P4 |
| data-ana gRPC 50055（ai11 P4）    | AnalyticsService.GetStudentWeakness + GetLearningTrend                    | P4 启动  | ⏳ 待 ai11 P4 |
| push-gateway WebSocket :8081/ws（ai02 P5） | WS 连接可建立 + 推送可接收                                        | P5 启动  | ⏳ 待 ai02 P5 |
| msg gRPC 50056（ai10 P5）         | NotificationService.ListNotifications + MarkAsRead                        | P5 启动  | ⏳ 待 ai10 P5 |
| ai 服务 gRPC 50057（ai12 P5，可选） | AiService.Chat（SSE 流式）                                              | P5 启动  | ⏳ 待 ai12 P5 |

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

- [ ] student-portal dev server :4001 启用（`pnpm dev` 可访问）
- [ ] MF Remote 可被 AppShell 加载（暴露 `./StudentApp` 模块，teacher-portal Shell 可加载）
- [ ] 独立壳渲染（`NEXT_PUBLIC_MF_ENABLED=false` 时首页 + 导航 + 路由守卫独立可用）
- [ ] 登录流程可用（未登录跳转 Shell `/login`，登录后回跳 student）
- [ ] GraphQL 查询可执行（currentUser / studentDashboard / myClasses 返回数据）
- [ ] 考试作答链路通（进入作答 → 自动保存 → 提交 → 跳转结果页）
- [ ] WebSocket 通知可接收（通知中心实时更新）
- [ ] lint + typecheck 零错误

---

## §4 Mock 策略

### 4.1 我提供的 mock

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

- **Storybook**：各组件独立 story（`apps/student-portal/.storybook/`）
- **MSW handlers**：`apps/student-portal/src/mocks/handlers.ts`，拦截所有 GraphQL/HTTP 请求
- **Mock fixtures**：`apps/student-portal/src/mocks/fixtures/*.json`，与 student-bff mock 数据一致

### 4.2 我消费的 mock

在真实上游就绪前，student-portal 使用以下 mock：

#### 4.2.1 HTTP / GraphQL mock（MSW）

- `POST /api/v1/student/graphql` → 按 operationName 返回对应 mock 响应（见 §2.4）
- `POST /api/auth/login` → 返回固定 JWT + UserInfo（student 角色）由 Shell 处理
- `POST /api/v1/student/upload` → 返回固定 signed URL（待 ISSUE-014-05 仲裁后实现）
- 所有 mock 响应定义在 `apps/student-portal/src/mocks/fixtures/*.json`

#### 4.2.2 WebSocket mock（mock-socket）

- 连接 `ws://localhost:8081/ws` 后每 30 秒推送 1 条 mock 通知
- 通知类型轮询：作业通知 / 考试通知 / 成绩通知 / 系统通知
- 支持模拟考试延长事件（待 ISSUE-014-06 仲裁后实现）

#### 4.2.3 JWT mock

- 使用固定 mock JWT（`eyJhbGciOiJSUzI1NiIs...`），payload 含 `sub=student-001, roles=[student], dataScope=SELF`
- 存入 httpOnly cookie（由 Shell 登录流程设置）

#### 4.2.4 环境切换

- 通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制
- 上游就绪后设为 `disabled`，切换到真实请求
- MF 切换：`NEXT_PUBLIC_MF_ENABLED=false`（P2 独立壳）→ `true`（P3 接入 Shell）

#### 4.2.5 IDB 草稿恢复（真实 idb-keyval）

- 考试作答草稿使用真实 `idb-keyval` 存储（前端可独立测试断网恢复逻辑）
- 不需要 mock，IDB 在浏览器原生支持

---

## §5 与 student-bff 契约对齐核查

> 参考 [student-bff_contract.md](./student-bff_contract.md)（ai04 维护）

| 对齐项                  | student-portal 期望                                  | student-bff 提供                                                  | 状态 |
| ----------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- | ---- |
| GraphQL endpoint        | `POST /api/v1/student/graphql`（经 api-gateway 代理）| `POST /graphql` :3009                                             | ✅ 对齐（api-gateway 代理） |
| GraphQL schema 文件     | `packages/shared-ts/contracts/graphql/student-bff.graphql` | `apps/student-bff/src/schema/*.graphql`（待 ISSUE-014-02 仲裁） | ⏳ 待仲裁 |
| Query 域（16 个）       | 见 §2.4.1                                            | 见 student-bff §1.3（auth/myClasses/myExams/myHomework/myGrades/myAttendance/content/dashboard/weakness/trend/notifications） | ⏳ 待 ai04 确认 serverTime/examDetail/homeworkDetail |
| Mutation 域（8 个）     | 见 §2.4.2                                            | student-bff §1.3 仅列 submitHomework + markAsRead                 | ⏳ 待 ai04 补全 submitExam/saveExamDraft/recordExamViolation/recordPasteEvent/updateNotificationPreference |
| 错误码前缀              | 透传 `BFF_STUDENT_*`                                 | `BFF_STUDENT_`（student-bff §1.5）                                | ✅ 对齐 |
| ActionState 信封        | success/errors/data + extensions.degraded            | 待 ai04 实现（ARB-001 §1.3 原则）                                  | ⏳ 待 ai04 |
| DataLoader 防 N+1       | 依赖 student-bff 实现                                | 待 ai04 实现（ARB-001 §1.3 原则）                                  | ⏳ 待 ai04 |
| DataScope L0 强制执行   | student-bff Resolver 层从 JWT 提取 studentId         | 待 ISSUE-014-07 仲裁                                              | ⏳ 待仲裁 |

---

## §6 异议引用

> 详见 [objections/student-portal_issue.md](../objections/student-portal_issue.md)

| 编号         | 标题                                                     | 影响                                          |
| ------------ | -------------------------------------------------------- | --------------------------------------------- |
| ISSUE-014-01 | GraphQL endpoint 路径不一致                              | 影响 §2.3 路径配置                            |
| ISSUE-014-02 | student-bff GraphQL schema 存放位置不一致                | 影响 §1.3 schema 文件路径                     |
| ISSUE-014-03 | 考试作答页全屏策略与防作弊检测边界                       | 影响 §2.4.2 recordExamViolation mutation 实现 |
| ISSUE-014-04 | 主观题粘贴策略（防作弊 vs 学生体验）                     | 影响 §2.4.2 recordPasteEvent mutation 实现    |
| ISSUE-014-05 | 作业附件上传协议（GraphQL mutation vs REST multipart）   | 影响 §2.3 `/api/v1/student/upload` 端点       |
| ISSUE-014-06 | 考试延长/题目重排等实时事件命名未确认                    | 影响 §1.7 WebSocket 事件类型                  |
| ISSUE-014-07 | 学生端 DataScope L0 边界的强制执行层                     | 影响 §2.4 GraphQL 查询域参数设计              |

---

**AI Agent**: ai14（student-portal）
**Branch**: feat-review-student-portal-docs-9yN6Av
**Coordinator**: coord-ai
