# 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`(已裁决 ARB-019 §21,由 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 | 作业通知 / 考试通知 / 成绩通知 / 系统通知 / 考试延长(ExamExtended) / 考试强制提交(ExamForceSubmitted)(ARB-019 §21.6 已裁决) | --- ## §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 | 作业附件上传(ARB-019 §21.5 方案 A) | 按方案 A 实现:REST + 对象存储 + GraphQL 提交 URL | | 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!) | 记录防作弊违规(ARB-019 §21.4) | MSW 返回 success=true | | recordPasteEvent(input: RecordPasteEventInput!) | 记录粘贴事件(ARB-019 §21.4) | 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` 创建(ARB-019 §21 已裁决) | 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 50058(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(ARB-019 §21.5 方案 A 已裁决) - 所有 mock 响应定义在 `apps/student-portal/src/mocks/fixtures/*.json` #### 4.2.2 WebSocket mock(mock-socket) - 连接 `ws://localhost:8081/ws` 后每 30 秒推送 1 条 mock 通知 - 通知类型轮询:作业通知 / 考试通知 / 成绩通知 / 系统通知 - 支持模拟考试延长事件 ExamExtended + 考试强制提交 ExamForceSubmitted(ARB-019 §21.6 已裁决) #### 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`(ARB-019 §21 已裁决,统一为集中管理) | ✅ 已裁决 | | 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 | 已裁决 ARB-019 §21.7(方案 A:Resolver 从 JWT 提取) | ✅ 已裁决 | --- ## §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