feat(score): R173/R174 大题成绩 Excel 导入(三小问四列制)+ 三节点备份调度器与四项修复

成绩导入(R173 落地 → R174 改为三小问四列制)
- app/lib/bigScoreImport.js(新增 · 判据唯一真相源 · 零依赖:不 require Egg / 不连库 / 不读文件)
  · R174 删除 splitScoreAcrossSubs(均分),改为 scoreSubs 逐项取值
  · 留空 SUB_MISSING(整行拒绝,⛔ 不按 0 分)· 非数值 SUB_INVALID · 超单问上限 SUB_OUT_OF_RANGE
  · 文件级 LEGACY_TEMPLATE(旧两列模板)与 SUB_COLUMNS_MISSING(缺一小问整列)⇒ 整份拒绝
  · 定位键必须带 group:(group, team_uid),因 team_uid 跨组重号
- app/service/bigQuestion.js
  · buildScoreTemplate 改四列 + 说明书重写(删均分段,补"三列必须全填"与"旧模板不再支持")
  · previewScoreImport 回带 subRaws/subs/total;importScores 逐项写 setSubScores
  · ⛔ 不新增第二条写成绩通路:仍只走 setSubScores → admin.updateScores
- app/lib/docx.js:run 支持 color(6 位 hex);w:color 必须排在 w:sz 之前(CT_RPr sequence)
- app/controller/admin.js + app/router.js:import-template / import-preview(零写入)/ import-scores
  · 上传内存解析不落盘(app/public 对静态服务开放 ⇒ 成绩表可被猜名下载)

节点备份调度器(R168/R170)
- app/lib/nodeBackupScheduler.js(新增):考试开始前 30min / 结束后 10min / 成绩发布瞬间
  · 幂等键 group:node:fireAtISO(时间点即身份);overdue 判定用 clamp:false 的数学时刻
  · 非生产 enabled 强制 false 且 fail-safe(与 DEPLOY_ENV_LOCAL_SKIP_PREFIXES 同源)
- config/config.default.js:config.nodeBackup 四项路径全走环境变量,默认值只覆盖本地
  (生产目录布局与仓库不一致,⛔ 不许用 __dirname 推导)
- app.js:启动构造单例 + hydrate 未到期计划

缺陷修复
- app/controller/admin.js:R161 临时文件路径移到 try 外声明,否则导入抛错即跳过清理
- app/service/admin.js:R166「已答 N / N 题」恒真显示(原 totalQuestions = answeredCount)
- app/service/upload.js + app/controller/upload.js:R157/R162 删除结果计数 + @no-temp-cleanup 标注

测试
- .gitignore + test/lib.test.js:R134 候选 ㊴ —— test/* + !test/*.test.js,
  使第 26 步裸 mocha 在干净检出上可复现(改前 rc=1 ↔ 改后 23 passing)
This commit is contained in:
2026-10-19 13:03:48 +08:00
parent 192b43bc67
commit 5392345078
13 changed files with 2400 additions and 20 deletions

View File

@@ -139,6 +139,10 @@ module.exports = app => {
router.get('/api/admin/times', adminAuth, controller.admin.getTimes);
router.put('/api/admin/times', adminAuth, controller.admin.setTimes);
// 节点备份调度器状态(P1「生产备份静默失败」可见性):只读,不触发备份。
// ⚠️ 路由段 node-backup 与 /api/admin/* 其余段无前缀冲突;GET 方法在此段唯一。
router.get('/api/admin/node-backup/status', adminAuth, controller.admin.getNodeBackupStatus);
// 分数管理
router.get('/api/admin/scores/:group', adminAuth, controller.admin.getScores);
router.put('/api/admin/scores/:group', adminAuth, controller.admin.setScores);
@@ -169,6 +173,12 @@ module.exports = app => {
router.get('/api/admin/big-answers/:group', adminAuth, controller.admin.getBigAnswers);
// 考生答卷导出(R28):3 段路径,与上方 2 段列表、下方 PUT /:uid/scores 均不冲突
router.get('/api/admin/big-answers/:group/export', adminAuth, controller.admin.exportBigAnswers);
// 成绩 Excel 导入(R172):两段式(预览零写入 → 确认导入),均为 3 段路径,与 PUT /:uid/scores 不冲突。
// ⚠️ 具体段(import-template / import-preview / import-scores)不得与将来的 `/:uid` 通配段冲突 ⇒
// 保持与 export 同等注册位置(在 PUT 之前)。
router.get('/api/admin/big-answers/:group/import-template', adminAuth, controller.admin.downloadBigScoreTemplate);
router.post('/api/admin/big-answers/:group/import-preview', adminAuth, controller.admin.previewBigScoreImport);
router.post('/api/admin/big-answers/:group/import-scores', adminAuth, controller.admin.importBigScores);
router.put('/api/admin/big-answers/:group/:uid/scores', adminAuth, controller.admin.setBigAnswerScores);
// 备赛资料管理