Files
Edu/apps/student-portal/docs/01-understanding.md

876 lines
64 KiB
Markdown
Raw Permalink 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.
# 模块理解确认书 — student-portal
> AIai14TS/React · 学习场景域前端 remote
> 阶段:阶段 1 交付物v2 — ai14 接管审计与补全版)
> 初版日期2026-07-09ai07 起草)
> 审计日期2026-07-10ai14 修订:端口、所有权、协议、路由、错误码、长远架构遗漏补全)
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §3.2 ai14、[pending-features P3](../../../docs/architecture/roadmap/pending-features.md)、[teacher-portal 阶段 1](../../teacher-portal/docs/01-understanding.md)、[teacher-portal 阶段 2](../../teacher-portal/docs/02-architecture-design.md)、[teacher-portal 阶段 3 长远架构](../../teacher-portal/docs/03-long-term-architecture.md)、[known-issues §2.15](../../../docs/troubleshooting/known-issues.md)、[coord 仲裁 ARB-001](../../../docs/architecture/issues/coord.md#1-arb-001teacher-bff-graphql-schema-第一版)、[coord 仲裁 ARB-002](../../../docs/architecture/issues/coord.md#2-arb-002mf-shell-暴露清单)、[student-bff 契约](../../../docs/architecture/issues/contracts/student-bff_contract.md)
> **审计修订摘要**ai14 → ai07 初稿):
>
> 1. **端口修订**3001 → **4001**004 §1.2 强制 4 端 4000-4003[port-allocation](../../../infra/port-allocation.md) §4 硬约束3001 已被 classes 历史占用)
> 2. **所有权修订**ai07 → **ai14**ai-allocation.md §3.2 L94ai07=classes/core-edu 交接,非 student-portal
> 3. **协议修订**REST → **GraphQL**ARB-001 已裁决 student-bff 走 GraphQL Yoga + ActionState 信封 + DataLoader前端 all-in GraphQL不再消费 REST
> 4. **路由修订**`/student/dashboard` → `/dashboard`student-portal 是独立 dev server :4001路由前缀不带 `/student`,与 student-portal_contract.md §1.2 对齐)
> 5. **登录路径修订**`/iam/login` → `POST /api/auth/login`(与 student-portal_contract.md §2.3 对齐iam 仅承担 gRPC登录由 api-gateway 聚合)
> 6. **错误码前缀修订**`EXAMS_`/`HOMEWORK_`/`GRADES_` → **`CORE_EDU_`**known-issues §2.15 已确认 core-edu 子域统一前缀);`STUDENT_BFF_` → **`BFF_STUDENT_`**coord §5.2 裁决)
> 7. **MF 配置修订**:按 ARB-002Shell 暴露 `GraphQLProvider`/`useGraphQLClient`/`urql`/`graphql` 单例student-portal 不再实现自己的 ApiClient
> 8. **遗漏补全**考试作答边界场景、学情诊断与个性化推荐、学生隐私合规COPPA/FERPA/PIPL/未成年人保护法、测试策略分层、韧性模式、性能预算、CSP/前端安全、跨标签同步、API 版本演进、未来扩展铺垫、长远愿景
> 9. **新增章节**§11 考试作答边界场景、§12 学生隐私与合规、§13 测试策略、§14 性能与预算、§15 前端安全、§16 跨标签与跨设备同步、§17 i18n 深化、§18 移动端与 PWA、§19 长远愿景与演进路径
---
## 1. 我在架构中的位置
- **层级**L2 微前端层004 §3.1 六层架构中的前端层)
- **MF 角色****Remote 子应用**,挂载到 teacher-portal ShellARB-002 裁决P3 首个 Remote
- **上游(谁调用我)**:浏览器(学生)— 含桌面 Chrome/Edge/Safari、移动端 iOS Safari/Android Chrome、考试机 lockdown 浏览器P6+ 评估)
- **下游(同步)**api-gateway**GraphQL over HTTP**,经 Next.js `rewrites` 代理 `/api/v1/*``/api/auth/*`
- **下游推送P5**push-gatewayWebSocket含 HTTP 长轮询降级)
- **BFF 对接**student-bffai04 设计,端口 3009GraphQL Yoga endpoint `POST /graphql`
- **通信方式****GraphQL over HTTP**前端→Gateway→student-bff+ WebSocket前端→push-gatewayP5+ HTTP 长轮询降级P5+
- **不直连**:前端不直连任何业务服务或 BFF 后端实例,全部经 api-gateway 代理
**说明**
- 通过 `next.config.js``rewrites``/api/v1/*``/api/auth/*` 代理到 `api-gateway`
- MF 架构下student-portal 作为 Remote 暴露页面入口,由 teacher-portal Shell 的 AppShell 动态加载ARB-002
- 复用 Shell 暴露的共享依赖react/react-dom/urql/graphql/@tanstack/react-query/zustand/nuqs/@edu/ui-components/@edu/ui-tokens/@edu/contracts/@edu/hooks/@edu/shared-ts
- 复用 Shell 暴露的 `GraphQLProvider`urql client 单例student-portal **不重复创建** GraphQL client
- 不独立提供 RootLayout / 字体加载 / 设计令牌 / i18n Provider / 登录页,全部由 Shell 提供ARB-002登录页 P2 不走 MF
## 2. 我的限界上下文
### 2.1 我负责的聚合 / 实体(前端视图模型)
- 考试作答ExamTaking、考试草稿ExamTakingDraft、作业提交HomeworkSubmit
- 学情诊断Diagnostic、错题本Weakness、学习路径LearningPath
- 会话状态Session、视口Viewport、权限Permission— 与 Shell 共享,引用 teacher-portal 文档
### 2.2 业务领域
- **D3 教学核心领域**(前端场景域:学习场景域,学生视角)
### 2.3 不负责
- 教师批改界面(归 teacher-portal
- AI 出题(归 teacher-portal
- 班级/考试/作业 CRUD 管理(归 teacher-portal
- 用户/角色/权限 CRUD归 admin-portal
- 家长多子女切换(归 parent-portal
- 教师沟通(归 teacher-portal P7+
- 成绩录入(归 teacher-portal学生仅查看
### 2.4 数据范围
- DataScope **L0仅本人**:学生只能看到自己的作业、考试、成绩、学情、错题、考勤
- 例外:班级排名、班级均分等聚合数据由 student-bff 聚合返回,学生不可见其他同学个体数据
## 3. 我与外部的契约
### 3.1 消费的后端 API经 api-gateway 代理)
| 路径前缀 | 下游 BFF/服务 | 关键端点 |
| ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/auth/login` | api-gateway | 学生登录api-gateway 聚合 iam gRPC返回 JWT + UserInfo |
| `POST /api/v1/student/graphql` | student-bff | GraphQL 端点dashboard / currentUser / myClasses / myExams / myHomework / submitHomework / myGrades / myAttendance / textbooks / chapters / learningPath / studentDashboard / myWeakness / myTrend / myNotifications / markAsRead |
| `GET /api/v1/notifications/*` | msg + push-gateway | 通知中心P5HTTP 长轮询降级) |
> **协议说明**ARB-001 已裁决student-bff 是 GraphQL 聚合层,前端 all-in GraphQL**不再消费 REST**。除登录(`POST /api/auth/login`和通知中心降级P5+)外,全部业务请求经 GraphQL endpoint。student-bff 由 ai04 设计,聚合 iam + core-edu + content + data-ana + msg为学生场景提供聚合视图。
### 3.1.1 API 契约版本演进策略
| 版本信号 | 携带位置 | 演进规则 |
| ----------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 主版本 | URL 路径 `/api/v1/*``/api/v2/*` | 破坏性变更升主版本student-portal 同时支持 v1 + v2 至少 1 个迭代周期4 周),通过 Feature Flag 切换 |
| GraphQL | schema 内 `@deprecated` + schema registry | 字段废弃用 `@deprecated` 标注student-portal 监控使用率,< 1% 后移除调用 |
| 子版本 | 响应头 `X-API-Version: 2026-07-10` | 向后兼容字段新增前端忽略未知字段Zod 默认行为) |
| Deprecation | 响应头 `Deprecation: true` + `Sunset: <date>` | 前端收到 Deprecation 头后上报埋点,跟踪使用率 |
> student-portal 不主动驱动 API 版本升级;契约变更由 coord 协调各业务 AI 落地。student-portal 仅负责消费侧的兼容与迁移。
### 3.2 统一响应契约
所有 GraphQL 响应遵循 **ActionState 信封**ARB-001 §1.3 + 总裁裁决 §3.4 方案 B 降级模式):
```typescript
// GraphQL 响应信封(与 teacher-bff 一致)
type GraphQLResponse<T> = {
data: T | null;
errors: Array<{
code: string; // 错误码前缀如 IAM_/CORE_EDU_/BFF_STUDENT_/GW_
message: string;
path: Array<string | number>;
extensions?: {
degraded?: boolean; // 降级模式标记
partial?: boolean; // 部分聚合失败
traceId?: string;
};
}> | null;
};
// 业务错误码(前端 i18n 路由)
type ActionError = {
code: string; // 如 BFF_STUDENT_UPSTREAM_UNAVAILABLE
message: string;
details?: unknown;
};
```
错误码前缀按服务名大写(如 `IAM_``CORE_EDU_``BFF_STUDENT_``GW_``NETWORK_`)。前端 GraphQL 请求层根据 `extensions.code` 前缀路由到对应的 i18n key。
### 3.3 推送契约P5
| 协议 | 场景 | 降级 |
| ------------------------- | ------------------------------------ | ----------------------------------- |
| WebSocketpush-gateway | 考试发布通知、成绩发布、作业截止提醒 | HTTP 长轮询60s 拉取通知列表) |
| HTTP 长轮询P5+ | WebSocket 重连 5 次失败后降级 | 通知延迟最多 60sUI 顶部提示降级条 |
### 3.4 proto 不直接消费
前端不调用 gRPCstudent-bff 把 gRPC 聚合为 GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 4. 我的技术栈
| 维度 | 选型 | 说明 |
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 框架 | Next.js 14+App Router | server components 默认client components 按需MF Remote 优先 CSR考试作答页禁用 SSR 防缓存) |
| 语言 | TypeScript 5.5+strict | 沿用 tsconfig.base.json |
| 微前端 | Module Federation 2.0@module-federation/nextjs-mf | student-portal = RemoteARB-002 P3 首个 Remote |
| 数据请求 | **urql GraphQL client**Shell 暴露单例ARB-002 | all-in GraphQL不再用 REST ApiClient |
| 样式 | Tailwind CSS 3.4+ | 配合设计令牌三层模型(复用 Shell 提供的令牌) |
| UI 组件库 | shadcn/ui迁移指南 §7.2 | 复用 Shell 暴露的 `packages/ui-components/` |
| 状态管理 L1 URL | nuqs | 可分享、可刷新状态(考试作答页 URL 不携带答案,防泄露) |
| 状态管理 L2 Server | TanStack Query v5 | GraphQL query 缓存、重试、乐观更新urql 与 TanStack Query 协同urql 作为 fetcher |
| 状态管理 L3 Client Business | Zustand slice | 客户端业务状态(考试作答草稿、倒计时、断网队列) |
| 状态管理 L4 Global UI | Zustand ui-store + ModalRoot | 全局 UI 状态(复用 Shell |
| 状态管理 L5 Form | react-hook-form + zodResolver | 表单状态(作业提交表单、附件上传) |
| 富文本 | **不使用**(学生不作答富文本,作业提交用表单) | 与 teacher-portal 差异点;客观题用单选/多选/填空,主观题用 textarea |
| 图表 | recharts | 学情诊断、Dashboard、错题本掌握度 |
| i18n | next-intl | 复用 Shell Provider按 scope=student 加载翻译 |
| A11y | eslint-plugin-jsx-a11yerror 级) | WCAG 2.2 AA考试作答页额外要求键盘可操作 + 屏幕阅读器友好 |
| 字体 | Intersans/ Frauncesserif/ JetBrains Monomono | 复用 Shell 的 next/font/google 加载 |
| 考试作答专用 | — | 倒计时(基于服务器时间)+ 自动保存HTTP POST 每 30s + blur+ 断网恢复localStorage 草稿) |
## 5. 我的阶段归属
- **阶段**P3
- **当前状态**:📐 待设计(待 core-edu + student-bff 就绪apps/student-portal/ 目录为空(仅 docs/),依赖上游阶段 P3
- **依赖上游阶段**P1api-gateway + iam+ P2teacher-portal Shell 就绪 + ARB-002 MF 配置完成)+ P3core-edu + student-bff
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
> 前端无 `@RequirePermission` 装饰器(后端概念),对齐项改造为前端等价物。
| 对齐项 | classes后端黄金模板 | student-portal 前端等价 | 当前状态 |
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------- |
| 权限校验 | `@RequirePermission(Permissions.XXX)` | `usePermission().hasPermission("XXX")` Hook + `<RequirePermission>` 组件 | ❌ 待建(复用 Shell 暴露的 hooks |
| 错误码前缀统一 | `CLASSES_*``IAM_*` | GraphQL 请求层根据 `extensions.code` 前缀路由 i18n | ❌ 待建(复用 Shell urql client |
| logger | pino | 前端 console + SentryP6 | ❌ 待建(复用 shared-ts Logger |
| 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 无长连接(除 WS无需 | ✅ 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 + GraphQL 请求层统一错误处理 | ❌ 待建(复用 Shell ErrorBoundary |
| 设计令牌三层 | — | 复用 Shell 提供的 primitive/semantic-light/dark/tailwind-theme | ❌ 待建(依赖 ui-tokens 包) |
| A11y 工具集 | — | useA11yId / mergeA11yProps / describeInput / focus-trap | ❌ 待建(复用 Shell 暴露的 hooks |
| GraphQL client 单例 | — | 复用 Shell 暴露的 urql clientARB-002 | ❌ 待建 |
---
## 附student-portal 现状审计(对齐黄金模板)
### 审计表
| 维度 | 状态 | 说明 |
| ------------------------------------ | ------ | ---------------------------------------------------- |
| 权限装饰器(前端等价 usePermission | ❌ | 待建,复用 Shell 暴露的 `usePermission` Hook |
| 错误码前缀 | ❌ | 待建,复用 Shell 暴露的 urql client + 错误扩展 |
| logger | ❌ | 待建,复用 shared-ts Logger |
| metrics | ❌ | 待建Web Vitals 上报 |
| tracer | ❌ | 待建OTel browser SDK |
| /healthz | ❌ | 待建Next.js `/api/health` route |
| /readyz | ❌ | 待建Next.js `/api/ready` route |
| 优雅关闭 | ✅ N/A | Next.js 无长连接 |
| 测试覆盖率 | ❌ | 0%,无测试文件(待建,目标 ≥ 80% |
| Dockerfile 多阶段 | ❌ | 待建builder + runtime |
| Zod 输入验证 | ❌ | 待建react-hook-form + zodResolver |
| GlobalErrorFilterErrorBoundary | ❌ | 待建,复用 Shell ErrorBoundary |
| 设计令牌三层 | ❌ | 待建,复用 ui-tokens 包 |
| A11y 工具集 | ❌ | 待建,复用 hooks 包 |
| Module Federation 配置 | ❌ | 待建Remote 角色ARB-002 |
| 5 层状态管理 | ❌ | 待建,复用 Shell Provider |
| 共享组件库 | ❌ | 待建,复用 Shell 暴露的 ui-components |
| i18n | ❌ | 待建,复用 Shell Provider + scope=student 翻译 |
| GraphQL 请求层 | ❌ | 待建,复用 Shell 暴露的 urql client 单例ARB-002 |
| ESLint flat config 自定义规则 | ❌ | 待建,与 Shell 共用配置 |
| 考试作答自动保存 | ❌ | 待建HTTP POST 每 30s + blur + localStorage 草稿 |
| 考试倒计时(服务器时间对齐) | ❌ | 待建,基于服务器 `expiresAt` |
| 断网恢复队列 | ❌ | 待建IDB 队列 + 重连重试 |
### 现有文件清单
```
apps/student-portal/
└─ docs/
├─ 01-understanding.md # 本文件v2
└─ 02-architecture-design.md # v2
```
> student-portal 当前为空目录(仅 docs/),所有维度均为 ❌ 待建状态。无现有文件、无现有代码、无现有违规点。P3 阶段从零开始搭建。
### 主要待建项(必须在 P3 起步时建立)
1. **MF Remote 配置**`next.config.js` 配置 `name: 'student_app'``exposes``remotes: { teacher: ... }`ARB-002 MF URL 用 4000 端口)
2. **路由表**(对齐 student-portal_contract.md §1.2`/dashboard``/my-homework``/my-homework/:id/submit``/my-exams``/my-exams/:id/take``/my-exams/:id/result``/my-grades``/my-attendance``/learning-path``/diagnostic``/weakness``/notifications``/my-schedule`
3. **ExamTaking 组件**:考试作答(倒计时基于服务器时间 + 自动保存每 30s + blur + 断网恢复)
4. **作业提交表单**react-hook-form + zodResolver不用 Tiptap
5. **Dockerfile 多阶段构建**builder + runtime + HEALTHCHECK
6. **Vitest + Playwright 测试**:覆盖率 ≥ 80%(含考试作答边界场景 E2E
7. **`/api/health` + `/api/ready` route**:健康检查端点
8. **urql GraphQL 查询**:复用 Shell 单例 client按 GraphQL 查询域封装 hooks
---
## 7. L1 导航菜单(视口)
| 视口 key | 文案 | 路由 | 权限 | 阶段 |
| --------------- | --------- | ------------------------ | ------------------------ | ---- |
| `dashboard` | Dashboard | `/dashboard` | `STUDENT_DASHBOARD_VIEW` | P3 |
| `my-homework` | 我的作业 | `/my-homework` | `HOMEWORK_READ_OWN` | P3 |
| `my-exams` | 我的考试 | `/my-exams` | `EXAMS_READ_OWN` | P3 |
| `my-grades` | 我的成绩 | `/my-grades` | `GRADES_READ_OWN` | P3 |
| `my-attendance` | 我的考勤 | `/my-attendance` | `ATTENDANCE_READ_OWN` | P3 |
| `learning-path` | 学习路径 | `/learning-path` | `LEARNING_PATH_VIEW` | P4 |
| `diagnostic` | 学情诊断 | `/diagnostic` | `DIAGNOSTIC_READ_OWN` | P4 |
| `weakness` | 错题本 | `/weakness` | `WEAKNESS_READ_OWN` | P4 |
| `notifications` | 通知中心 | `/notifications` | `NOTIFICATION_READ_OWN` | P5 |
| `my-schedule` | 我的课表 | `/my-schedule` | `SCHEDULE_READ_OWN` | P5+ |
> 来源student-bff GraphQL `viewports` query聚合 iam 视口配置AppShell 按 `scope=student` 过滤渲染。
## 8. L2 路由表
| 路由 | 页面 | 权限 | 阶段 |
| ------------------------------- | ---------- | ------------------------ | ---- |
| `/dashboard` | 学生仪表盘 | `STUDENT_DASHBOARD_VIEW` | P3 |
| `/my-homework` | 我的作业 | `HOMEWORK_READ_OWN` | P3 |
| `/my-homework/:id/submit` | 提交作业 | `HOMEWORK_SUBMIT` | P3 |
| `/my-exams` | 我的考试 | `EXAMS_READ_OWN` | P3 |
| `/my-exams/:id/take` | 作答考试 | `EXAMS_TAKE` | P3 |
| `/my-exams/:id/result` | 考试结果 | `EXAMS_RESULT_VIEW` | P3 |
| `/my-grades` | 我的成绩 | `GRADES_READ_OWN` | P3 |
| `/my-attendance` | 我的考勤 | `ATTENDANCE_READ_OWN` | P3 |
| `/learning-path` | 学习路径 | `LEARNING_PATH_VIEW` | P4 |
| `/diagnostic` | 学情诊断 | `DIAGNOSTIC_READ_OWN` | P4 |
| `/weakness` | 错题本 | `WEAKNESS_READ_OWN` | P4 |
| `/notifications` | 通知中心 | `NOTIFICATION_READ_OWN` | P5 |
| `/my-schedule` | 我的课表 | `SCHEDULE_READ_OWN` | P5+ |
> 路由前缀**不带** `/student/`student-portal 是独立 dev server :4001挂载到 Shell 后由 Shell 路由表统一前缀。L3 组件级视口用 `<RequirePermission perm="HOMEWORK_SUBMIT"><Button>提交作业</Button></RequirePermission>`。权限点后缀 `_OWN` 强调学生仅能操作自己的数据DataScope L0
## 9. L3 组件级差异student-portal 特有)
### 9.1 复用 Shell 暴露的组件
- `AppShell`(左栏导航 + 主内容区)
- `RequirePermission`L3 组件级视口控制)
- `ErrorBoundary`React 渲染异常兜底)
- `Loading`(骨架屏)
- `Empty`(空态)
- `DataTable`(表格)
- `Form`react-hook-form + zodResolver 封装)
- `Chart`recharts 封装)
- `GraphQLProvider`urql client 单例ARB-002
### 9.2 student-portal 特有组件
| 组件 | 用途 | 来源 | 是否 MF 暴露 |
| ----------------- | --------------------------------------------- | ---- | --------------------------------------- |
| `ExamTaking` | 考试作答(倒计时 + 自动保存 + 断网恢复) | 新建 | ✅ 暴露 `./ExamTaking`(供 Shell 路由复用) |
| `HomeworkSubmit` | 作业提交表单react-hook-form + zodResolver | 新建 | ❌ 内部使用 |
| `DiagnosticChart` | 学情诊断图表recharts 多维雷达 + 趋势线) | 新建 | ❌ 内部使用 |
| `WeaknessList` | 错题本列表(按知识点聚合 + 掌握度标签) | 新建 | ❌ 内部使用 |
| `LearningPathMap` | 学习路径图(知识点前置依赖可视化) | 新建 | ❌ 内部使用 |
| `ExamResultView` | 考试结果页(得分 + 错题分析 + 知识点掌握度) | 新建 | ❌ 内部使用 |
| `CountdownTimer` | 倒计时组件(基于服务器时间,避免客户端篡改) | 新建 | ❌ 内部使用 |
| `DraftRecovery` | 草稿恢复弹窗(断网恢复后提示是否恢复作答) | 新建 | ❌ 内部使用 |
### 9.3 不使用的组件(与 teacher-portal 差异)
- **不使用 `RichTextEditor`Tiptap**:学生不作答富文本,作业提交用表单
- 不使用 `SSEViewer`:学生不参与 AI 出题
- 不使用 `ChildSwitcher`:学生无多子女切换(家长端特有)
- 不使用 `UserManagementTable`:学生不管理用户
- 不使用 `RolePermissionMatrix`:学生不管理权限
## 10. L4 数据层差异
| 维度 | teacher-portal | student-portal |
| ---------- | ------------------------- | -------------------------------------------------- |
| 主要数据源 | teacher-bff聚合多服务 | student-bff聚合 iam + core-edu + content + data-ana + msg |
| 协议 | GraphQLurql | GraphQLurql复用 Shell 单例) |
| 缓存策略 | 5min 中等缓存 | 5-30s 短缓存,作业列表 30s考试作答页 0s禁缓存|
| 数据范围 | L1-L5按角色 | L0仅本人 |
| 特殊状态 | — | 考试作答草稿Zustand L3 + localStorage + IDB断网恢复+ 倒计时L3基于服务器时间+ 断网队列IDB |
| SSR 策略 | dashboard SSR | dashboard SSR首屏考试作答页强制 CSR防缓存 |
## 11. 考试作答边界场景(学生端特有,必须覆盖)
> 考试作答是学生端最复杂、最易出问题的场景,必须在架构中预留所有边界场景的处理。本节是 ai14 新增。
### 11.1 网络与设备边界
| 场景 | 触发条件 | 前端处理 | 后端契约依赖 |
| ------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 作答中断网 | 学生作答过程中网络中断 | 自动保存失败入 IDB 队列UI 显示"离线模式"标识;继续作答;网络恢复后批量重试 | `POST /api/v1/student/graphql` mutation `saveExamAnswer` |
| 作答中刷新页面 | 学生误刷新或浏览器崩溃 | Zustand L3 + localStorage + IDB 三级草稿恢复;进入考试页时检测未提交草稿,弹 `DraftRecovery` 提示是否恢复 | 无(纯前端恢复) |
| 作答中关闭浏览器 | 学生主动关闭 | 同上,下次进入考试页恢复草稿 | 无 |
| 作答中切到后台标签 | 学生切换标签(疑似作弊) | `visibilitychange` 事件记录切换次数 + 时长;超过阈值(如 3 次)警告;服务端最终判定(前端仅记录) | `mutation recordExamSuspiciousBehavior`(待 ai04 确认) |
| 作答中设备时间被篡改 | 学生修改系统时间影响倒计时 | 倒计时基于服务器返回的 `expiresAt`ISO 8601 UTC前端仅做展示提交以服务器时间为准 | `query myExams { expiresAt }` |
| 作答提交后网络超时 | 提交响应未到达但服务端已处理 | 客户端重试前先 query 提交状态;若已提交则跳转结果页,避免重复提交 | `query examSubmissionStatus(examId)` |
### 11.2 时间边界
| 场景 | 触发条件 | 前端处理 | 后端契约依赖 |
| ------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 考试时间到自动提交 | 倒计时归零 | 前端触发自动提交 mutation若失败入 IDB 队列重试UI 显示"时间到,正在提交" | `mutation submitExam` |
| 服务器时间与客户端偏差 | 客户端时间快/慢于服务器 | 进入考试页时同步服务器时间差(`serverTime - clientTime`),倒计时按校正后的时间计算;偏差 > 5s 时提示 | `query serverTime` |
| 考试开始前进入 | 早于 `startsAt` | 显示"考试未开始"倒计时;禁用作答区 | `query myExams { startsAt expiresAt }` |
| 考试结束后进入 | 晚于 `expiresAt` | 跳转结果页(若已提交)或"考试已结束"提示(若未提交,按缺考处理) | `query examResult(examId)` |
| 考试延时 | 教师临时延长考试时间 | WebSocket 事件 `ExamExtended` → 重新拉取 `expiresAt` → 更新倒计时 | WebSocket event `ExamExtended`(待 ai10 msg 确认) |
### 11.3 作答内容边界
| 场景 | 触发条件 | 前端处理 | 后端契约依赖 |
| ------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 客观题单选/多选/填空 | 默认题型 | Zustand L3 存 `answers: Record<questionId, AnswerInput>`UI 用 Radio/Checkbox/TextInput | `mutation saveExamAnswer` |
| 主观题文本作答 | 简答题/论述题 | textarea + 字数统计;不使用富文本(与 teacher-portal 差异) | 同上 |
| 附件上传(主观题照片) | 学生拍照上传手写答案 | 文件大小限制(≤ 10MB+ 类型限制jpg/png/pdf+ 分片上传;上传中 UI 显示进度 | `mutation uploadAttachment`(待 ai04 确认) |
| 答案序号变更 | 教师调整题目顺序(考试中) | WebSocket 事件 `ExamQuestionReordered` → 重新拉取题目 → 草稿按 questionId 映射(不依赖序号) | WebSocket event `ExamQuestionReordered`(待 ai10 确认) |
| 答案为空提交 | 学生未作答部分题目 | 提交前弹窗确认"还有 N 题未作答,确认提交?";学生确认后才提交 | 无(前端校验) |
### 11.4 防作弊边界(前端配合,服务端最终判定)
| 场景 | 触发条件 | 前端处理 | 备注 |
| ------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 切屏检测 | `visibilitychange` 到 hidden | 记录切换次数 + 时长 + 时间戳;超过阈值(如 3 次/分钟)警告;服务端最终判定是否违规 | 仅记录,不阻断作答 |
| 复制粘贴检测 | 监听 `copy`/`paste` 事件 | 阻止默认行为 + 警告;记录次数 | 主观题允许粘贴自己输入的内容(待产品确认) |
| 全屏退出检测 | `fullscreenchange` 事件 | 警告 + 记录;不强制全屏(避免影响体验) | P6+ lockdown 浏览器才强制 |
| 多标签检测 | `BroadcastChannel` 检测同源标签 | 若检测到同源标签,警告"请勿多开标签" | 防止学生开多个标签查答案 |
| 右键禁用 | `contextmenu` 事件 | 考试作答页禁用右键 | 防止查看源码/搜索 |
---
## 12. 学生隐私与合规
> 学生数据是高敏感数据含未成年人student-portal 必须在设计阶段预留合规框架。本节是 ai14 新增,覆盖 COPPA、FERPA、PIPL、未成年人保护法等法规对前端架构的要求。
### 12.1 适用法规矩阵
| 法规 | 适用范围 | 对前端的要求 | 阶段 |
| -------------- | ------------------ | --------------------------------------------------------------------------- | ----------- |
| COPPA | 美国 <13 岁儿童 | 收集前需家长可验证同意;展示同意记录入口;可删除数据请求入口 | P6 海外扩展 |
| FERPA | 美国教育记录 | 学生有权查看自己教育记录;学校有权限制访问 | P6 海外扩展 |
| PIPL | 中国个人信息保护法 | 隐私政策弹窗 + 同意按钮;敏感信息(成绩)展示前需二次确认;数据导出请求入口 | P3 起强制 |
| GDPR | 欧盟用户 | Cookie 同意管理;被遗忘权请求入口;数据可携带权导出 | P6 海外扩展 |
| 未成年人保护法 | 中国 <18 岁 | 14 岁以下需家长同意;展示适合年龄段的内容;防沉迷时间提醒 | P3 起强制 |
### 12.2 前端合规设计
| 合规点 | 实现位置 | 阶段 |
| ------------------------- | ----------------------------------------------- | ---- |
| 隐私政策同意弹窗 | Shell RootLayout 首次登录后弹窗 | P3 |
| Cookie 同意管理(按类别) | Shell + student-portal 复用 | P3 |
| 成绩展示二次确认 | `/my-grades` 默认遮罩,点击"查看"展示 | P3 |
| 数据导出请求入口 | `/settings#data-export` 页面 | P5 |
| 数据删除请求入口 | `/settings#data-deletion` 页面 | P5 |
| 同意记录查看 | `/settings#consent-history` 页面 | P5 |
| 防沉迷时间提醒 | 连续使用 > 2 小时弹窗提醒休息 | P3 |
| 敏感数据脱敏 | 截图/分享时自动遮罩成绩数字 | P5+ |
### 12.3 数据保留策略(前端配合)
| 数据类型 | 前端保留 | 后端保留 | 前端处理 |
| ------------------ | --------------------- | ---------- | ---------------------------- |
| 考试作答草稿 | IDB 永久(直至提交) | 永久 | 提交成功后清除 IDB 草稿 |
| 成绩列表缓存 | 30sTanStack Query | 永久 | staleTime 30s 后自动失效 |
| 通知列表 | 30s | 90 天 | 同上 |
| 学情诊断数据 | 30s | 永久 | 同上 |
| 行为埋点(防作弊) | 内存队列 100 条 | 90 天 | 队列满后批量上报,上报后清空 |
---
## 13. 测试策略分层
> ai07 初稿仅提到"覆盖率 ≥ 80%"未细化测试类型与覆盖率分目标。ai14 补全。
### 13.1 测试金字塔
| 层级 | 工具 | 覆盖率目标 | 测试范围 |
| --------- | ------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------- |
| 单元测试 | Vitest + @testing-library/react | ≥ 85% | 所有组件渲染/交互、Hook 业务逻辑、Zod schema 校验、纯函数 utils、倒计时计算、草稿恢复逻辑 |
| 集成测试 | Vitest + MSWMock GraphQL | ≥ 75% | urql query + TanStack Query hook 组合、ExamTaking + Zustand slice + 自动保存流程、断网恢复流程 |
| 视觉回归 | Playwright + Percy/ApplitoolsP6 引入) | 关键页面 | dashboard/my-homework/my-exams/take/my-grades/diagnostic/weakness 七个核心页面在 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 模拟);考试作答页 LCP < 1.5s |
| 契约测试 | Pactstudent-bff ↔ student-portal 双向P6 引入) | 关键 query/mutation | 防止 BFF schema 变更打破前端消费 |
### 13.2 关键 E2E 场景(必须覆盖)
```yaml
- name: exam_taking_normal_flow
steps:
- login as student
- navigate to /my-exams
- click exam "期末数学"
- assert /my-exams/:id/take loaded
- assert countdown timer running
- answer question 1 (single choice A)
- answer question 2 (multiple choice A,C)
- assert draft saved to IDB within 30s
- reload page
- assert DraftRecovery dialog shown
- click "恢复作答"
- assert answers restored
- click "提交"
- assert confirm dialog "还有 8 题未作答"
- click "确认提交"
- assert redirect to /my-exams/:id/result
- assert score displayed
- name: exam_taking_offline_recovery
steps:
- login as student
- enter exam
- answer 3 questions
- simulate offline (Playwright network condition)
- assert "离线模式" indicator shown
- answer 2 more questions
- assert auto-save failed but queued in IDB
- simulate online
- assert queued answers retried and saved
- submit exam
- assert IDB draft cleared
- name: exam_auto_submit_on_timeout
steps:
- login as student
- enter exam with 60s remaining (mock server time)
- wait 60s
- assert countdown reaches 0
- assert "时间到,正在提交" shown
- assert redirect to result page
- name: homework_submit_flow
steps:
- login as student
- navigate to /my-homework
- click homework "数学作业 5"
- assert /my-homework/:id/submit loaded
- fill answers in form
- upload attachment (mock file)
- click submit
- assert success toast
- assert homework list refreshed
- name: diagnostic_view
steps:
- login as student
- navigate to /diagnostic
- assert DiagnosticChart rendered (radar + trend)
- assert knowledge point mastery displayed
- name: anti_cheat_detection
steps:
- login as student
- enter exam
- switch to another tab (Playwright)
- switch back
- assert warning toast "请勿切换标签"
- assert suspicious behavior recorded
```
### 13.3 Mock 数据策略
| 数据来源 | Mock 方式 | 维护方 |
| -------------- | -------------------------------------------------------------------- | ------ |
| GraphQL 响应 | MSW handlers`apps/student-portal/src/mocks/handlers.ts` 集中管理 | ai14 |
| WebSocket 事件 | Mock WebSocket ServerPlaywright fixture | ai14 |
| i18n 文案 | 真实 next-intl messages不 Mock | coord |
| 设计令牌 | 真实 ui-tokens不 Mock | coord |
| 权限列表 | 按 fixture 角色预设student_with_exams 等) | ai14 |
| 服务器时间 | MSW 拦截 `serverTime` query 返回固定时间 | ai14 |
---
## 14. 性能预算与代码分割
> ai07 初稿未提及性能预算。ai14 补全。
### 14.1 性能预算Bundle Size
| 资源类型 | 预算gzipped | 备注 |
| ---------------------- | --------------- | ------------------------------------------------- |
| student-portal 首屏 JS | ≤ 80 KB | 含 ExamTaking + Dashboard + 共享依赖分摊 |
| student-portal 首屏 CSS | ≤ 20 KB | 含 Tailwind purged + 设计令牌 |
| 路由级懒加载 chunk | ≤ 30 KB/chunk | 每个二级路由单独 chunk考试作答页单独 chunk |
| 考试作答页 JS | ≤ 50 KB | 含倒计时 + 自动保存 + 断网恢复逻辑 |
| 图片 | ≤ 100 KB/页 | 学情图表渲染、空态插画 |
| 总下载量(首屏) | ≤ 200 KB | 4G 网络下 LCP < 2.5s;考试作答页 LCP < 1.5s |
### 14.2 代码分割策略
```typescript
// apps/student-portal/src/app/(app)/my-exams/[id]/take/page.tsx
import dynamic from "next/dynamic";
const ExamTaking = dynamic(() => import("./ExamTaking"), {
loading: () => <Skeleton rows={20} />,
ssr: false, // 考试作答页强制 CSR防缓存 + 防预渲染泄露答案
});
const DiagnosticPage = dynamic(() => import("./DiagnosticPage"), {
loading: () => <Skeleton rows={10} />,
});
// 学情诊断(重型 recharts独立 chunk
const WeaknessPage = dynamic(() => import("./WeaknessPage"), {
loading: () => <Skeleton rows={8} />,
});
```
### 14.3 预加载策略
| 触发时机 | 预加载内容 |
| ------------------------- | ----------------------------------------------------- |
| Dashboard 加载完成 | 预加载 my-homework chunk + my-exams chunk最常访问 |
| 鼠标 hover 考试列表项 | 预加载该考试的题目数据GraphQL prefetch |
| 通知未读数 > 0 | 预加载 notifications chunk |
| 用户进入 my-grades 页面 | 预加载 diagnostic chunk趋势图 next-step |
| 考试开始前 5 分钟 | 预加载 ExamTaking chunk + 题目数据 |
### 14.4 渲染策略
| 页面 | 渲染模式 | 理由 |
| ----------------------- | ------------------------ | ------------------------------------------- |
| `/dashboard` | SSR首屏+ CSR交互 | SEO 无关,但首屏速度 |
| `/my-homework` | CSR + Suspense | 认证后数据,无需 SEO |
| `/my-exams` | CSR | 认证后数据 |
| `/my-exams/:id/take` | **CSR 强制**ssr: false| 防缓存 + 防预渲染泄露答案 + 个性化数据 |
| `/my-exams/:id/result` | CSR | 认证后数据 |
| `/my-grades` | CSR + Suspense | 数据频变 |
| `/diagnostic` | CSR + 流式渲染 | 图表渲染慢,流式加载 |
| `/notifications` | CSR + 流式渲染 | 实时性要求 |
---
## 15. 前端安全
> ai07 初稿仅在横切关注点提到 401 处理。ai14 补全完整前端安全策略。
### 15.1 安全头HTTP Headers
由 teacher-portal Shell 在 `next.config.js` 配置student-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 参数 `?examId=` | Zod 校验为 UUID 格式,禁止任意字符 |
| localStorage 存储草稿 | 仅存答案数据,不存敏感信息;登出时清除 |
| 主观题文本作答 | textarea 默认转义;服务端二次清洗 |
### 15.3 CSRF 防护
- student-portal 仅消费 GraphQL POST + 登录 POST所有 mutation 经 urql client
- urql client 自动注入 `X-Requested-With: XMLHttpRequest`
- 后端 BFF 校验该头 + 同源 Cookie SameSite=Strictproject_rules §4
### 15.4 考试作答安全(前端配合)
| 安全点 | 前端措施 | 服务端最终判定 |
| ------------------------ | ------------------------------------------------------------ | -------------- |
| 防作弊(切屏/复制/多标签)| §11.4 边界场景记录 | 是 |
| 答案篡改 | 草稿仅前端临时存储,提交时服务端校验题目有效性 + 答案格式 | 是 |
| 时间篡改 | 倒计时基于服务器 `expiresAt`,不依赖客户端时间 | 是 |
| 重复提交 | 提交前 query 提交状态,避免重复 | 是 |
| 越权访问他人考试 | BFF DataScope=L0 校验,前端 URL 篡改 `?examId=` 无效 | 是 |
### 15.5 敏感数据处理
| 数据 | 敏感级别 | 前端处理 |
| -------------- | -------- | -------------------------------------------------- |
| 学生姓名 | 中 | 默认展示截图时脱敏P5+ |
| 学生成绩 | 高 | 默认遮罩,点击"查看"展示;页面离开 5s 后自动遮罩 |
| 考试作答内容 | 高 | 不缓存到 localStorage仅 IDB 草稿),提交后清除 |
| 学生 ID | 低 | URL 可携带,但 BFF 校验本人权限 |
| 通知内容 | 中 | 不缓存到 localStorage仅 TanStack Query 内存缓存 |
---
## 16. 跨标签与跨设备同步
### 16.1 同步机制矩阵
| 场景 | 同步机制 | 同步内容 | 冲突解决 |
| ----------------------- | ------------------------------- | ----------------------------------------- | ------------------- |
| 同浏览器多标签考试检测 | BroadcastChannel API | 考试作答中标签切换检测 | 阻止多标签作答 |
| 考试作答草稿跨标签 | BroadcastChannel + IDB | 草稿变更(仅同一考试) | 最后写入胜出LWW |
| 通知未读数跨标签同步 | BroadcastChannel | unread count | 服务端为准 |
| 跨设备状态同步 | WebSocket 事件P5 | 通知变更 | 服务端为准 |
| 网络恢复后状态对齐 | 重连后批量 invalidate + refetch | 全部学生维度数据 | 服务端为准 |
### 16.2 BroadcastChannel 实现(考试作答防多标签)
```typescript
// apps/student-portal/src/lib/examTabGuard.ts
const EXAM_CHANNEL_NAME = "student-exam-guard";
// 进入考试页时检测是否有其他标签正在作答同一考试
export function useExamTabGuard(examId: string) {
useEffect(() => {
const channel = new BroadcastChannel(EXAM_CHANNEL_NAME);
const tabId = `tab-${Date.now()}-${Math.random().toString(36).slice(2)}`;
// 广播:我正在作答此考试
channel.postMessage({ type: "exam-entered", examId, tabId, ts: Date.now() });
// 监听其他标签的响应
const onMessage = (event: MessageEvent) => {
const msg = event.data;
if (msg.tabId === tabId) return;
if (msg.type === "exam-entered" && msg.examId === examId) {
// 已有其他标签在作答,警告
toast.warning(t("exam.multiTabDetected"));
// 记录可疑行为
recordSuspiciousBehavior("multi-tab-exam");
}
};
channel.addEventListener("message", onMessage);
return () => {
channel.postMessage({ type: "exam-left", examId, tabId });
channel.close();
};
}, [examId]);
}
```
### 16.3 考试草稿跨标签恢复
若学生在 Tab A 开始作答,关闭 Tab A 后在 Tab B 重新打开:
- IDB 草稿跨标签共享(同源策略保证)
- Tab B 进入考试页时检测 IDB 草稿
-`DraftRecovery` 提示"检测到未完成的作答草稿,是否恢复?"
- 用户确认后从 IDB 加载草稿
---
## 17. i18n 深化
> ai07 初稿仅提到 next-intl。ai14 补全多语言策略。
### 17.1 支持语言矩阵
| 语言 | locale | 阶段 | 完成度要求 |
| --------------- | ------ | ----------- | ---------- |
| 简体中文 | zh-CN | P3 强制 | 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/dashboard
/en-US/dashboard
/dashboard → 默认重定向到浏览器首选语言
```
- Next.js 中间件根据 `Accept-Language` 头自动重定向
- 用户主动切换语言时写入 Cookie `NEXT_LOCALE`,下次访问直接命中
- student-portal 不维护 locale 路由,复用 Shell 中间件
### 17.3 翻译文件组织
```
apps/student-portal/src/i18n/messages/
├─ zh-CN/
│ ├─ common.json # 通用文案(确认/取消/加载中等)
│ ├─ dashboard.json
│ ├─ homework.json
│ ├─ exams.json
│ ├─ exam-taking.json # 考试作答专用文案(含倒计时/自动保存/断网提示)
│ ├─ grades.json
│ ├─ attendance.json
│ ├─ learning-path.json
│ ├─ diagnostic.json
│ ├─ weakness.json
│ ├─ notifications.json
│ └─ errors.json # 错误码 → i18n key 映射
├─ en-US/
│ └─ ... (镜像 zh-CN 结构)
└─ index.ts # 按需加载 messages
```
按路由切分 message bundle避免首屏加载全部翻译。
### 17.4 国际化格式
| 数据类型 | 库 | 示例zh-CN | 示例en-US |
| -------- | ------------------------------------- | --------------------- | ------------------- |
| 日期 | `Intl.DateTimeFormat` | 2026年7月10日 | July 10, 2026 |
| 时间 | 同上 | 下午3:30 | 3:30 PM |
| 数字 | `Intl.NumberFormat` | 1,234.56 | 1,234.56 |
| 百分比 | 同上 | 85.5% | 85.5% |
| 成绩等级 | 自定义映射表 | 优秀/良好/及格/不及格 | A/B/C/D/F |
| 时区 | `Intl.DateTimeFormat` with `timeZone` | Asia/Shanghai | America/Los_Angeles |
| 倒计时 | 自定义HH:mm:ss 格式) | 01:30:00 | 01:30:00 |
### 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
> 学生端在移动端使用比例较高(学生多在课后/自习时间查看作业/成绩/学情),移动端策略必须前置。考试作答移动端体验需要特别考虑。
### 18.1 响应式断点
| 断点 | 宽度 | 典型设备 | student-portal 布局变化 |
| -------------- | ----------- | ---------------------- | --------------------------------------------------- |
| `sm` (default) | < 640px | iPhone/Android 手机 | 单列布局;考试作答区全屏;侧栏导航抽屉化 |
| `md` | 640-1024px | iPad Mini/Android 平板 | 双列布局(侧栏 + 内容);考试作答区优化 |
| `lg` | 1024-1280px | iPad Pro/小笔记本 | 三列布局(侧栏 + 内容 + 详情);学情图表完整展示 |
| `xl` | > 1280px | 桌面 | 三列布局;考试作答区居中固定宽度 |
### 18.2 移动端交互优化
| 场景 | 移动端优化 |
| -------------- | ------------------------------------------ |
| 考试作答 | 题目纵向滚动;选项触摸友好;倒计时顶部固定 |
| 作业提交 | 附件拍照上传;底部固定提交按钮 |
| 查看成绩 | 卡片式纵向滚动;图表触摸缩放 |
| 通知列表 | 左滑标记已读、右滑删除iOS 风格) |
| 学情诊断 | 图表简化为单维度;横屏展示完整雷达图 |
| 表单提交 | 底部固定按钮;软键盘弹出时自动避让 |
| 长列表 | 无限滚动 + 骨架屏 |
### 18.3 PWA 配置P5+ 引入)
```json
// apps/student-portal/public/manifest.json
{
"name": "Edu 学生端",
"short_name": "EduStudent",
"start_url": "/dashboard",
"display": "standalone",
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#1677ff",
"icons": [
{ "src": "/icons/student-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/student-512.png", "sizes": "512x512", "type": "image/png" }
],
"shortcuts": [
{ "name": "我的作业", "url": "/my-homework" },
{ "name": "我的考试", "url": "/my-exams" },
{ "name": "我的成绩", "url": "/my-grades" }
]
}
```
### 18.4 Service Worker 策略P5+
| 资源类型 | 缓存策略 | TTL |
| ----------------------- | --------------------------- | ------- |
| 静态资源JS/CSS/图片) | Cache First + 网络更新 | 24 小时 |
| 学生 dashboard 数据 | Stale While Revalidate | 30 秒 |
| 考试作答页 | **Network Only** | — |
| 考试题目数据 | **Network Only** | — |
| 通知列表 | Network Only | — |
| API 401 响应 | 不缓存 | — |
> 考试作答相关资源强制 Network Only防止缓存泄露答案。
---
## 19. 长远愿景与演进路径
### 19.1 阶段演进路线
```mermaid
graph LR
P3[P3 核心教学<br/>学生端 MVP<br/>dashboard+homework+exams+take+grades] --> P4[P4 学情分析<br/>diagnostic+weakness+learning-path]
P4 --> P5[P5 推送<br/>通知中心+WebSocket+防作弊增强]
P5 --> P6[P6 硬化<br/>PWA+A11y+性能+安全+多语言]
P6 --> P7[P7+ 扩展<br/>AI 辅导+错题推荐+学习计划]
P7 --> P8[P8+ 多租户<br/>学区/教育局版]
```
### 19.2 未来功能铺垫(架构预留)
| 未来功能 | 架构预留点 | 启用阶段 |
| -------------------------- | ------------------------------------------------------------------------------ | -------- |
| AI 辅导(个性化答疑) | API 路由 `/api/v1/student/ai/*` 预留SSE 复用 teacher-portal 模式 | P7 |
| 错题智能推荐 | `WeaknessList` 组件抽象推荐策略GraphQL query `recommendedWeakness` 预留 | P7 |
| 学习计划(自动生成) | `LearningPathMap` 组件预留计划编辑模式GraphQL mutation `generatePlan` | P7 |
| 同学协作(学习小组) | `NotificationFeed` 抽象为通用消息流WebSocket 事件协议预留 `group.*` 类型 | P7+ |
| 移动端原生壳 | PWA → Capacitor 打包MF 不变 | P8+ |
| 离线模式(考试作答断网增强)| Service Worker + IDB 队列P5+ 引入) | P5+ |
| 推送通知Web Push | Service Worker PushManager + VAPIDP5+ 引入) | P5+ |
| 学区/教育局版多租户 | URL 路由前缀 `/{tenantId}/student/*` 预留TanStack Query key 加 tenantId 维度 | P8+ |
| lockdown 浏览器(防作弊) | 考试作答页检测 lockdown 环境;启用更严格策略 | P6+ |
### 19.3 模块解耦与演化
| 演化方向 | 触发条件 | 迁移策略 |
| ------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| student-portal 拆分为多个 Remote | bundle > 200KB 或团队规模 > 5 人 | 按场景域拆分student-core-remotedashboard/homework/grades+ student-exam-remote考试作答 |
| MF 2.0 → 3.0 升级 | MF 3.0 稳定且解决 SSR 问题 | Shell 端 `@module-federation/nextjs-mf` 升级student-portal 仅改 `name`/`filename` 字段 |
| 切换为原生 SSR脱离 MF | SEO 需求强烈或 MF 维护成本过高 | 保留 GraphQL 请求层和组件库;移除 MF 配置;独立部署为完整 Next.js 应用 |
| 状态管理迁移Zustand → Jotai | Zustand 性能瓶颈或团队偏好 | 逐 slice 迁移Hook 接口保持不变 |
| GraphQL → 更轻量协议 | GraphQL 性能瓶颈 | 评估 RPC / tRPCurql client 替换为对应 client |
### 19.4 监控与降级
| 监控项 | 阈值 | 触发动作 |
| ------------------------ | --------------- | ------------------------------------------------------ |
| student-portal 5xx 错误率 | > 1% | 告警 SRE自动切换到只读模式隐藏提交按钮 |
| MF Remote 加载失败 | 加载超时 10s | Fallback 到 Shell 内置的最小化 dashboard静态引导页 |
| WebSocket 连接失败 | 重试 5 次仍失败 | 降级为 HTTP 轮询(每 60s 拉取通知列表) |
| BFF 响应延迟 | P95 > 3s | 前端展示"加载缓慢"提示;自动缩短缓存 TTL |
| 考试作答自动保存失败率 | > 5% | 告警UI 提示学生手动保存;增加重试频率 |
| 考试提交失败 | 单次失败 | 自动重试 + 入 IDB 队列3 次失败后人工介入提示 |
---
**AI Agent**: ai14 (student-portal remote)
**Branch**: feat-review-student-portal-docs-9yN6Av
**Coordinator**: coord-ai
**Predecessor**: ai07初版起草ai14 接管审计与补全)