模块理解确认书 — student-portal
AI:ai14(TS/React · 学习场景域前端 remote)
阶段:阶段 1 交付物(v2 — ai14 接管审计与补全版)
初版日期:2026-07-09(ai07 起草)
审计日期:2026-07-10(ai14 修订:端口、所有权、协议、路由、错误码、长远架构遗漏补全)
关联:004 架构影响地图 §1.1a/1.1b/§5.4、AI 分配方案 §3.2 ai14、pending-features P3、teacher-portal 阶段 1、teacher-portal 阶段 2、teacher-portal 阶段 3 长远架构、known-issues §2.15、coord 仲裁 ARB-001、coord 仲裁 ARB-002、student-bff 契约
审计修订摘要(ai14 → ai07 初稿):
- 端口修订:3001 → 4001(004 §1.2 强制 4 端 4000-4003,port-allocation §4 硬约束;3001 已被 classes 历史占用)
- 所有权修订:ai07 → ai14(ai-allocation.md §3.2 L94;ai07=classes/core-edu 交接,非 student-portal)
- 协议修订:REST → GraphQL(ARB-001 已裁决 student-bff 走 GraphQL Yoga + ActionState 信封 + DataLoader;前端 all-in GraphQL,不再消费 REST)
- 路由修订:
/student/dashboard → /dashboard(student-portal 是独立 dev server :4001,路由前缀不带 /student,与 student-portal_contract.md §1.2 对齐)
- 登录路径修订:
/iam/login → POST /api/auth/login(与 student-portal_contract.md §2.3 对齐;iam 仅承担 gRPC,登录由 api-gateway 聚合)
- 错误码前缀修订:
EXAMS_/HOMEWORK_/GRADES_ → CORE_EDU_(known-issues §2.15 已确认 core-edu 子域统一前缀);STUDENT_BFF_ → BFF_STUDENT_(coord §5.2 裁决)
- MF 配置修订:按 ARB-002,Shell 暴露
GraphQLProvider/useGraphQLClient/urql/graphql 单例,student-portal 不再实现自己的 ApiClient
- 遗漏补全:考试作答边界场景、学情诊断与个性化推荐、学生隐私合规(COPPA/FERPA/PIPL/未成年人保护法)、测试策略分层、韧性模式、性能预算、CSP/前端安全、跨标签同步、API 版本演进、未来扩展铺垫、长远愿景
- 新增章节:§11 考试作答边界场景、§12 学生隐私与合规、§13 测试策略、§14 性能与预算、§15 前端安全、§16 跨标签与跨设备同步、§17 i18n 深化、§18 移动端与 PWA、§19 长远愿景与演进路径
1. 我在架构中的位置
- 层级:L2 微前端层(004 §3.1 六层架构中的前端层)
- MF 角色:Remote 子应用,挂载到 teacher-portal Shell(ARB-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-gateway(WebSocket,含 HTTP 长轮询降级)
- BFF 对接:student-bff(ai04 设计,端口 3009,GraphQL Yoga endpoint
POST /graphql)
- 通信方式:GraphQL over HTTP(前端→Gateway→student-bff)+ WebSocket(前端→push-gateway,P5)+ 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 |
通知中心(P5,HTTP 长轮询降级) |
协议说明(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 降级模式):
错误码前缀按服务名大写(如 IAM_、CORE_EDU_、BFF_STUDENT_、GW_、NETWORK_)。前端 GraphQL 请求层根据 extensions.code 前缀路由到对应的 i18n key。
3.3 推送契约(P5)
| 协议 |
场景 |
降级 |
| WebSocket(push-gateway) |
考试发布通知、成绩发布、作业截止提醒 |
HTTP 长轮询(60s 拉取通知列表) |
| HTTP 长轮询(P5+) |
WebSocket 重连 5 次失败后降级 |
通知延迟最多 60s,UI 顶部提示降级条 |
3.4 proto 不直接消费
前端不调用 gRPC,student-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 = Remote(ARB-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-a11y(error 级) |
WCAG 2.2 AA;考试作答页额外要求键盘可操作 + 屏幕阅读器友好 |
| 字体 |
Inter(sans)/ Fraunces(serif)/ JetBrains Mono(mono) |
复用 Shell 的 next/font/google 加载 |
| 考试作答专用 |
— |
倒计时(基于服务器时间)+ 自动保存(HTTP POST 每 30s + blur)+ 断网恢复(localStorage 草稿) |
5. 我的阶段归属
- 阶段:P3
- 当前状态:📐 待设计(待 core-edu + student-bff 就绪),apps/student-portal/ 目录为空(仅 docs/),依赖上游阶段 P3
- 依赖上游阶段:P1(api-gateway + iam)+ P2(teacher-portal Shell 就绪 + ARB-002 MF 配置完成)+ P3(core-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 + Sentry(P6) |
❌ 待建(复用 shared-ts Logger) |
| metrics |
prom-client /metrics |
前端 Web Vitals → Gateway 上报 |
❌ 待建 |
| tracer |
OTel SDK |
前端 OTel browser SDK(P6) |
❌ 待建 |
| /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 client(ARB-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 |
| GlobalErrorFilter(ErrorBoundary) |
❌ |
待建,复用 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 队列 + 重连重试 |
现有文件清单
student-portal 当前为空目录(仅 docs/),所有维度均为 ❌ 待建状态。无现有文件、无现有代码、无现有违规点。P3 阶段从零开始搭建。
主要待建项(必须在 P3 起步时建立)
- MF Remote 配置:
next.config.js 配置 name: 'student_app'、exposes、remotes: { teacher: ... }(ARB-002 MF URL 用 4000 端口)
- 路由表(对齐 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
- ExamTaking 组件:考试作答(倒计时基于服务器时间 + 自动保存每 30s + blur + 断网恢复)
- 作业提交表单:react-hook-form + zodResolver,不用 Tiptap
- Dockerfile 多阶段构建:builder + runtime + HEALTHCHECK
- Vitest + Playwright 测试:覆盖率 ≥ 80%(含考试作答边界场景 E2E)
/api/health + /api/ready route:健康检查端点
- 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) |
| 协议 |
GraphQL(urql) |
GraphQL(urql,复用 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 草稿 |
| 成绩列表缓存 |
30s(TanStack Query) |
永久 |
staleTime 30s 后自动失效 |
| 通知列表 |
30s |
90 天 |
同上 |
| 学情诊断数据 |
30s |
永久 |
同上 |
| 行为埋点(防作弊) |
内存队列 100 条 |
90 天 |
队列满后批量上报,上报后清空 |
13. 测试策略分层
ai07 初稿仅提到"覆盖率 ≥ 80%",未细化测试类型与覆盖率分目标。ai14 补全。
13.1 测试金字塔
| 层级 |
工具 |
覆盖率目标 |
测试范围 |
| 单元测试 |
Vitest + @testing-library/react |
≥ 85% |
所有组件渲染/交互、Hook 业务逻辑、Zod schema 校验、纯函数 utils、倒计时计算、草稿恢复逻辑 |
| 集成测试 |
Vitest + MSW(Mock GraphQL) |
≥ 75% |
urql query + TanStack Query hook 组合、ExamTaking + Zustand slice + 自动保存流程、断网恢复流程 |
| 视觉回归 |
Playwright + Percy/Applitools(P6 引入) |
关键页面 |
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 |
| 契约测试 |
Pact(student-bff ↔ student-portal 双向,P6 引入) |
关键 query/mutation |
防止 BFF schema 变更打破前端消费 |
13.2 关键 E2E 场景(必须覆盖)
13.3 Mock 数据策略
| 数据来源 |
Mock 方式 |
维护方 |
| GraphQL 响应 |
MSW handlers,按 apps/student-portal/src/mocks/handlers.ts 集中管理 |
ai14 |
| WebSocket 事件 |
Mock WebSocket Server(Playwright 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 代码分割策略
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 补全完整前端安全策略。
由 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=Strict(project_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 实现(考试作答防多标签)
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 配置):
- Next.js 中间件根据
Accept-Language 头自动重定向
- 用户主动切换语言时写入 Cookie
NEXT_LOCALE,下次访问直接命中
- student-portal 不维护 locale 路由,复用 Shell 中间件
17.3 翻译文件组织
按路由切分 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+ 引入)
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 阶段演进路线
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 + VAPID(P5+ 引入) |
P5+ |
| 学区/教育局版多租户 |
URL 路由前缀 /{tenantId}/student/* 预留;TanStack Query key 加 tenantId 维度 |
P8+ |
| lockdown 浏览器(防作弊) |
考试作答页检测 lockdown 环境;启用更严格策略 |
P6+ |
19.3 模块解耦与演化
| 演化方向 |
触发条件 |
迁移策略 |
| student-portal 拆分为多个 Remote |
bundle > 200KB 或团队规模 > 5 人 |
按场景域拆分:student-core-remote(dashboard/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 / tRPC;urql 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 接管审计与补全)