Skip to content

feat: 验收答题计时 + 学生排队验收(多助教共享队列,学生不能选择助教) #1

Open

Description

@44106

背景

这是对实验验收与分数登记网站提出的两个新功能。当前验收流程是:助教线下叫学生 → 验收页选择实验 → 定位学生 → 展示 demo → 现场抽题 → 录入成绩。存在两个缺口:

  1. 验收过程中没有时间参考,无法知道每位学生答了多久、每场验收用了多久;
  2. 学生没有线上排队手段,助教也无法看到「现在还有谁在等验收」,只能靠线下临时找人。

本 issue 给出两个功能的完整设计、实现与验证。

需求

  1. 验收时显示答题时间:验收过程中实时显示该学生的答题用时,并保留记录。
  2. 学生排队验收:
    • 学生端可以预约排队验收(加入 / 取消队列);
    • 助教端可以看到排队情况(谁、哪个实验、等了多久、排第几);
    • 验收结束后自动把该学生从队列中删除(实时);
    • 约束:学生不能自主选择助教——队列按实验、全班共享,不绑定助教;
    • 约束:线上只负责排队与验收后的实时删除,叫号仍由助教线下完成(不做线上叫号/轮到提醒)。

方案设计

1. 验收答题计时

  • 计时口径:助教在「定位学生」中选中某学生时开始计时(进入「展示 demo」步骤),步骤 3/4/5(展示 demo、现场抽题、录入成绩)期间显示。
  • 显示位置:验收页顶部操作区常驻「答题时间 mm:ss」芯片(超过 1 小时显示 h:mm:ss);「录入成绩」步骤的学生信息条右侧再显示一个紧凑芯片。
  • 保存记录:「保存并下一位」时把 durationMs + startedAt 一并提交,服务端为该 (学生, 实验) 写入一条 CheckoffSession 验收记录;保存成功提示追加「· 用时 mm:ss」。
  • 历史查看:「现在谁正在验收 → 最近保存」列表中,每条记录追加「· 用时 mm:ss」。
  • 清零时机:保存并下一位、返回「选择实验 / 定位学生」、切换实验时清零;重新选中学生重新开始计时。
  • 兼容:durationMs / startedAt 均为可选字段,旧客户端不带参数保存照常工作,只是不产生用时记录。

2. 学生排队验收(多助教共享、不绑定助教)

数据模型:CheckoffQueueEntry(按「实验 + 学生」唯一,记录班级与加入时间)。队列不记录助教 id,API 也不接收助教参数,从数据层面保证「学生不能选择助教」。

学生端(新增 /student-console/queue「排队验收」页):

  • 只列出当前开放验收的实验:publishTime 为空或已发布,且 checkoffDeadline 为空或未截止;
  • 每张卡片显示实验、验收截止时间、当前排队人数与本人状态;
  • 状态:未排队 →「加入队列」;排队中 →「第 N 位 / 前面还有 M 人 / 已等待 X 分钟」+「取消排队」;已完成验收 →「已完成验收」+「重新排队」;
  • 加入幂等(重复加入不刷新加入时间、不插队);只能取消本人记录;
  • 每 15 秒轮询 + 回到页面时刷新;助教保存成绩后下一次刷新会感知「已完成验收,已自动移出队列」并提示。

助教端(验收页新增「排队情况」面板):

  • 顶部「排队情况」按钮,带排队人数徽标(已选实验显示该实验人数,未选显示全班人数,>99 显示 99+);
  • 面板按实验分组(当前已选实验组置顶并高亮),每条显示 #位次 + 头像 + 姓名 + 学号 + 已等待时长,30s 刷新等待时长;
  • 点击某条:关闭面板 → 若属于其它实验则切换实验 → 把该学生学号预填到「定位学生」搜索框(不自动选中学生,避免叫错人或误保存);
  • 空态、刷新按钮齐全。

自动移出与实时广播:

  • 触发点:POST /api/checkoff(保存验收成绩)。任意助教保存该学生该实验的成绩后,服务端删除对应排队记录;
  • 仅当真的删除了记录(count > 0)才广播,避免无意义推送;
  • 通过 socket.io 向该班级的验收房间 checkoff-watch:<classId> 广播完整快照事件 checkoff:queue(学生加入 / 取消也会广播),打开验收页的所有助教实时看到队列变化;
  • 队列为单一共享队列:多名助教看到同一份顺序,谁验收完谁移出,无需抢占或指派。

3. 数据模型(be/prisma/schema.prisma)

/// 验收排队:学生预约排队验收(按实验、全班共享;学生不能选择助教)
model CheckoffQueueEntry {
  id           String   @id @default(uuid(4))
  classId      String
  experimentId String
  userId       String
  joinedAt     DateTime @default(now())
  updatedAt    DateTime @updatedAt

  @@unique([experimentId, userId])
  @@index([classId, experimentId, joinedAt])
  @@index([userId])
}

/// 验收答题记录:一次「保存并下一位」写入一条,记录答题用时
model CheckoffSession {
  id           String   @id @default(uuid(4))
  classId      String
  experimentId String
  userId       String
  graderId     String?
  durationMs   Int
  startedAt    DateTime
  createdAt    DateTime @default(now())

  @@index([experimentId, userId, createdAt])
  @@index([classId])
}

两表均为纯记录表(不建外键关系),与仓库既有的 McpAuditLog 风格一致,避免改动 User 关系图。

4. 接口

方法 路径 权限 说明
GET /api/checkoff/queue?classId= staff 本班排队快照 { entries, serverTime }
GET /api/checkoff/records?classId= staff 既有接口,每条记录新增 durationMs
POST /api/checkoff staff 既有接口,新增可选 durationMs / startedAt;写分数后创建 CheckoffSession 并自动移出队列
GET /api/student/checkoff-queue student 开放验收的实验 + 本人排队状态(位次 / 前面人数 / 是否已验收)
POST /api/student/checkoff-queue student 加入排队(幂等;未开放 / 已截止分别返回 400 NOT_OPEN / CLOSED)
DELETE /api/student/checkoff-queue/:experimentId student 取消本人排队

be/src/index.ts 的学生接口白名单同步加入两条新路由模板。

5. 实时事件

checkoff:queue  →  { classId, entries: CheckoffQueueEntryDto[], at }

仅发送给该班级验收房间 checkoff-watch:<classId>(打开验收页的 staff),复用仓库既有房间与 socket 单例,不新增连接通道。

6. 前端改动

  • 新增 CheckoffTimer(计时 hook + 格式化 + 芯片)、CheckoffQueuePanel(排队面板)、StudentQueue(学生排队页);
  • 新增 useCheckoffQueue(classId):REST 首次加载 + checkoff:queue 实时替换 + 断线重连重拉;
  • 验收页接入计时、排队按钮/弹窗、点击定位、「最近保存」用时;
  • 学生端导航新增「排队验收」,路由 /student-console/queue;
  • 文档:docs/4-checkoff.md 新增 4.12 答题计时、4.13 排队情况;docs/11-student.md 新增 11.5 排队验收;README 目录与 12.4 路径速查表同步。

兼容性与风险

  • 接口向后兼容:两个新参数可选;不带参数的老客户端行为不变。
  • 数据库:新增 2 张表,SQLite 下 prisma db push / 迁移即可,不影响既有表。
  • 性能:队列快照批量查询(3 次 in 查询,无 N+1);队列规模为班级量级,全量快照广播成本可忽略。
  • 实时删除的边界:学生端采用 15s 轮询感知「已移出」;助教端是 socket 实时。若要求两端都严格实时,可后续给学生端开 socket 房间(本次按需求只要求助教端实时)。
  • 误操作保护:从排队面板点击只做「切换实验 + 预填学号」,不自动选中学生、不自动保存;若当前正在验收别的学生,会先回到「定位学生」步骤(未保存录入会被放弃)。

改动清单

 be/prisma/schema.prisma                           |  37 ++
 be/src/index.ts                                   |   2 +
 be/src/lib/checkoff-queue.ts                      |  74 ++(新增)
 be/src/lib/realtime.ts                            |   6 +
 be/src/routes/checkoff.ts                         |  70 +-
 be/src/routes/student.ts                          | 153 ++
 docs/11-student.md                                |  24 +-
 docs/12-misc.md                                   |   1 +
 docs/4-checkoff.md                                |  53 +
 docs/README.md                                    |   2 +-
 fe/src/App.tsx                                    |   2 +
 fe/src/components/checkoff/CheckoffQueuePanel.tsx | 169 ++(新增)
 fe/src/components/checkoff/CheckoffTimer.tsx      |  63 ++(新增)
 fe/src/components/checkoff/ScoreForm.tsx          |  22 +-
 fe/src/components/checkoff/StudentFinder.tsx      |  10 +-
 fe/src/components/layout/StudentShell.tsx         |   2 +
 fe/src/lib/api.ts                                 |  75 +
 fe/src/lib/checkoff-socket.ts                     |  78 +-
 fe/src/pages/Checkoff.tsx                         |  96 +
 fe/src/pages/StudentQueue.tsx                     | 351 ++(新增)
 20 files changed, 1280 insertions(+), 10 deletions(-)

验证

在本地 SQLite + 真实后端 + socket.io 客户端上完成:

  • pnpm --filter be typecheck:0 错误;prisma validate:schema 合法;
  • pnpm build:fe(tsc -b && vite build):构建成功;pnpm lint:0 error(仓库既有 warning 数量不变);
  • REST E2E(全部断言通过):学生可见开放实验且已截止实验不出现 → 加入排队(幂等)→ 位次 1 → 助教看到条目 → 保存成绩(带 durationMs)→ 队列自动移除 → /records 返回 durationMs → 学生端 checkedOff: true、myEntry: null → 取消排队 → 越权(学生调 staff 接口 / 助教调学生接口)返回 403 → 不带 durationMs 的旧请求仍可保存;
  • socket E2E(全部断言通过):助教 socket 加入验收房间后,学生加入排队实时收到含该学生的快照;助教保存成绩后实时收到移出后的快照。

关联

实现已推送到 fork 分支,PR 会关联到本 issue:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions