Repository navigation
Open
Description
背景
这是对实验验收与分数登记网站提出的两个新功能。当前验收流程是:助教线下叫学生 → 验收页选择实验 → 定位学生 → 展示 demo → 现场抽题 → 录入成绩。存在两个缺口:
- 验收过程中没有时间参考,无法知道每位学生答了多久、每场验收用了多久;
- 学生没有线上排队手段,助教也无法看到「现在还有谁在等验收」,只能靠线下临时找人。
本 issue 给出两个功能的完整设计、实现与验证。
需求
- 验收时显示答题时间:验收过程中实时显示该学生的答题用时,并保留记录。
- 学生排队验收:
- 学生端可以预约排队验收(加入 / 取消队列);
- 助教端可以看到排队情况(谁、哪个实验、等了多久、排第几);
- 验收结束后自动把该学生从队列中删除(实时);
- 约束:学生不能自主选择助教——队列按实验、全班共享,不绑定助教;
- 约束:线上只负责排队与验收后的实时删除,叫号仍由助教线下完成(不做线上叫号/轮到提醒)。
方案设计
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:
Metadata
Metadata
Assignees
Labels
No labels
Activity