feat(admin): 成绩复核——按姓名/身份证/电话检索考生并查看全卷客观题逐题作答与正确答案(R26)

- service.admin: 新增 reviewSearch / getContestantScore / _reviewWorks;移除同名的死代码旧实现
- 归集双键合并 [contestant.id] ∪ [真实 aggregation_uid],与 getExamSubmissions 口径逐字一致
- 题序还原:优先 exam_submissions.question_order,缺失则回退「选择→判断 + 题号升序」并回传 questionOrderFixed=false
- 未作答行 answerMatches 返回 null(非 false),避免界面把「未答」读成「答错」
- 组别以库中考生 group 为准,路由 :group 不符返回 409,防前端传错组照错人
- 异常可区分:questionMissing(题被删/改组)、answerMatches≠isCorrect(判分矛盾)
- 因回传正确答案,两条路由均挂 adminAuth —— 选手端可达即成绩泄露(与 verify_score_leak_all_channels.py 三通道门禁一致)
- 不改动 schema、不改动既有接口行为

controller.admin: 新增 reviewSearch / contestantScore(NOT_FOUND→404 / GROUP_MISMATCH→409 / 其它→400)
router.js: 注册 GET /api/admin/review/search 与 /api/admin/review/score/:group/:id,均 adminAuth
This commit is contained in:
2026-09-28 10:15:42 +08:00
parent 079c1b31c9
commit cbc05ed4e7
3 changed files with 405 additions and 38 deletions

View File

@@ -187,6 +187,14 @@ module.exports = app => {
router.post('/api/admin/toggle-test-flag', adminAuth, controller.admin.toggleTestFlag);
router.post('/api/admin/reset-test-data', adminAuth, controller.admin.resetTestData);
// ==================== 成绩复核(需管理员认证) ====================
// 场景:考生对成绩有异议 → 按「姓名 / 身份证号 / 电话」任一检索 → 核对全卷题目与作答。
// ⚠️ 本组会回传客观题正确答案,务必保持 adminAuth —— 选手端一旦可达即为成绩泄露。
// 注册顺序:两条路径段数不同(3 段 / 5 段),不存在通配段互相吞并的问题;
// 仍按「固定段在前」的惯例把 search 放前面。
router.get('/api/admin/review/search', adminAuth, controller.admin.reviewSearch);
router.get('/api/admin/review/score/:group/:id', adminAuth, controller.admin.contestantScore);
// ==================== 成绩查询(需认证) ====================
router.get('/api/score', auth, controller.score.index);