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:
@@ -499,6 +499,71 @@ module.exports = (appInfo) => {
|
||||
maxDays: 14,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 节点备份调度(考试开始前 / 考试结束后 / 成绩发布后)
|
||||
// -----------------------------------------------------------------
|
||||
// 需求来源:用户 2026-10-16「系统在考试开始前,考试结束后,成绩发布后这三个
|
||||
// 节点应该执行数据备份」。触发层用**后端钩子 + Node 定时器**(用户裁定①:
|
||||
// 巡检脚本会占额外内存),实现见 app/lib/nodeBackupScheduler.js。
|
||||
//
|
||||
// 🔴 生产隔离:备份目标是**生产库**(用户裁定②)⇒ 本段仅在生产环境生效,
|
||||
// 非生产 enabled=false 且调度器构造时即封死。这是与上方
|
||||
// DEPLOY_ENV_LOCAL_SKIP_PREFIXES=['DB_',...] 同一纪律的延伸:
|
||||
// 非生产进程不得持有生产连接凭据、不得去 dump 生产库。
|
||||
//
|
||||
// ⛔ 与 scripts/backup_daily.sh 的分工:那个脚本被自身硬门禁锁死在**本地库**
|
||||
// (端口须 ∈ {3306,3307,33060}、BACKUP_ENV_FILE 匹配 *production* 一律拒绝),
|
||||
// 故本节点备份**不复用它的入口**,只复用它底层的 scripts/backup_db.py
|
||||
// (同一份 mysqldump + gzip + 完整性校验 + 保留期治理,避免两套校验口径漂移)。
|
||||
// ⛔ 绝不「放宽 backup_daily.sh 的门禁来复用」—— 那是把本地防线拆给生产用。
|
||||
// 🔴 路径纪律:生产机的目录布局**与本地仓库不一致**,⛔ 不许用 `__dirname` 推导
|
||||
// (R168 实测踩坑)。取证依据(2026-08-10 / 2026-09-24 生产实证):
|
||||
// 后端 baseDir = /home/root/production/agent-code/back-end/competition-website
|
||||
// (注意是 `back-end` 连字符,不是仓库里的 `backend`)
|
||||
// 脚本目录 = /home/root/production/agent-code/scripts/ ← 与 deploy/ 同级
|
||||
// 环境文件 = /home/root/production/agent-code/deploy/competition-website/.env.production
|
||||
// (systemd EnvironmentFile,见 deploy/competition-backend.service)
|
||||
// ⇒ `repoRoot = resolve(__dirname,'../..')` 在本地指向仓库根、在生产指向
|
||||
// `.../agent-code/back-end` ⇒ `join(root,'deploy','competition-website','scripts')`
|
||||
// 在生产解析成 `.../back-end/deploy/competition-website/scripts` =**不存在**。
|
||||
// 解法 = 全部走环境变量、可显式覆盖,默认值只覆盖本地开发:
|
||||
// BACKUP_SCRIPT / BACKUP_ENV_FILE / BACKUP_OUT_DIR / BACKUP_LOG_DIR。
|
||||
// ⚠️ 这四项须在 deploy/.env.production 中显式声明(部署检查清单会逐条核对),
|
||||
// 否则生产会落到下面的默认值 ⇒ **路径错 ⇒ 备份静默失败**(P1 级)。
|
||||
const nbPath = require('path');
|
||||
const localRepo = nbPath.resolve(__dirname, '..', '..');
|
||||
config.nodeBackup = {
|
||||
enabled: true,
|
||||
// 触发缓冲(用户裁定①的「留出缓冲」口径)
|
||||
beforeStartBufferMin: Number(process.env.BACKUP_BEFORE_START_MIN) || 30, // 考试开始前 30 分钟
|
||||
afterEndDelayMin: Number(process.env.BACKUP_AFTER_END_MIN) || 10, // 考试结束后 10 分钟
|
||||
// 🔴 备份脚本路径:⛔ 本地默认值在生产不存在 ⇒ 生产必须由 BACKUP_SCRIPT 覆盖
|
||||
scriptPath: process.env.BACKUP_SCRIPT
|
||||
|| nbPath.join(localRepo, 'scripts', 'backup_db.py'),
|
||||
// 环境文件(生产库凭据来源)
|
||||
envFile: process.env.BACKUP_ENV_FILE
|
||||
|| nbPath.join(localRepo, 'deploy', '.env.production'),
|
||||
// 节点备份独立目录:⛔ 不复用 backups/db —— 否则会被每日备份的 keep-days
|
||||
// 清理误删(每日 14 天 vs 节点备份需长期留存以备「赛后复盘/申诉」回溯)
|
||||
outDir: process.env.BACKUP_OUT_DIR
|
||||
|| nbPath.join(localRepo, 'backups', 'db-nodes'),
|
||||
logDir: process.env.BACKUP_LOG_DIR
|
||||
|| nbPath.join(localRepo, 'logs', 'backup', 'nodes'),
|
||||
keepDays: Number(process.env.BACKUP_KEEP_DAYS) || 90,
|
||||
// 跨进程锁 TTL(秒):残留锁(kill -9)会永久挡住后续备份 ⇒ 需能接管。
|
||||
// 取 2h:远大于一次 dump 的正常耗时(分钟级),又不会让崩溃残留锁永久生效。
|
||||
lockTtlSec: Number(process.env.BACKUP_LOCK_TTL_SEC) || 7200,
|
||||
// 服务器 Py 3.6.8 无 pymysql/openpyxl ⇒ 既有约定走 /opt/pytools/lib
|
||||
pythonPath: process.env.BACKUP_PYTHON_PATH || '/opt/pytools/lib',
|
||||
mysqldump: process.env.BACKUP_MYSQLDUMP || '',
|
||||
// ⚠️ spawn 的 cwd:只影响相对路径解析,backup_db.py 内部路径均基于 __dirname
|
||||
repoRoot: process.env.BACKUP_CWD || localRepo,
|
||||
};
|
||||
// 非生产强制关闭(即使误设 enabled=true 也不生效)
|
||||
if (!config.isProduction) {
|
||||
config.nodeBackup.enabled = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 生产环境安全守卫:禁止使用内置默认密钥上线
|
||||
// (由 APP_ENV=production 触发)
|
||||
|
||||
Reference in New Issue
Block a user