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:
@@ -159,7 +159,8 @@ function firstLineTwips(node) {
|
||||
* 深度优先遍历,产出「块」序列:
|
||||
* { type:'p', runs:[...], align, firstLine }
|
||||
* { type:'table', rows:[{ cells:[{ blocks:[...] }] }] }
|
||||
* run 形态:{ text, bold, italic } 或 { br: true }
|
||||
* run 形态:{ text, bold, italic, size, color } 或 { br: true }
|
||||
* color 为 6 位十六进制(可带 `#` 前缀),如 'FF0000' / '#FF0000';非法值忽略。
|
||||
*/
|
||||
function walk(nodes, fmt, out) {
|
||||
const flush = () => {
|
||||
@@ -274,11 +275,31 @@ function textToBlocks(text) {
|
||||
|
||||
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
||||
|
||||
/**
|
||||
* 规范化 run 的颜色值。
|
||||
*
|
||||
* ⛔ 只接受 6 位十六进制(可带 `#`),其余一律返回 '':
|
||||
* OOXML 的 `w:val` 若被填入 `red` / `#F00` / 任意字符串,Word 会拒绝打开整个文档,
|
||||
* 而调用方(导出服务)无法感知 —— 表现为「导出的 docx 打不开」,排查成本极高。
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns {string} 6 位大写十六进制或 ''
|
||||
*/
|
||||
function normalizeColor(v) {
|
||||
if (v === null || v === undefined) return '';
|
||||
const s = String(v).trim().replace(/^#/, '');
|
||||
return /^[0-9a-fA-F]{6}$/.test(s) ? s.toUpperCase() : '';
|
||||
}
|
||||
|
||||
function rPrOf(run) {
|
||||
const parts = [];
|
||||
if (run.bold) parts.push('<w:b/><w:bCs/>');
|
||||
if (run.italic) parts.push('<w:i/><w:iCs/>');
|
||||
parts.push('<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:eastAsia="宋体" w:cs="Times New Roman"/>');
|
||||
// ⚠️ 顺序硬约束:CT_RPr 是 sequence,`w:color` 必须排在 rFonts 之后、`w:sz` 之前。
|
||||
// 顺序错乱(如放到 sz 之后)Word 同样会判文档损坏 —— 与「pPr 必须先于 runs」同理。
|
||||
const color = normalizeColor(run.color);
|
||||
if (color) parts.push(`<w:color w:val="${color}"/>`);
|
||||
const sz = Number(run.size) > 0 ? Number(run.size) : 21;
|
||||
parts.push(`<w:sz w:val="${sz}"/><w:szCs w:val="${sz}"/>`);
|
||||
return `<w:rPr>${parts.join('')}</w:rPr>`;
|
||||
@@ -464,6 +485,6 @@ module.exports = {
|
||||
textToBlocks,
|
||||
decodeEntities,
|
||||
esc,
|
||||
// 导出内部件便于单测
|
||||
_internal: { parseHtml, buildDocumentXml, CONTENT_TYPES, ROOT_RELS, STYLES_XML, DOC_RELS },
|
||||
// 导出内部件便于单测(`scripts/verify_big_score_import.py` 会断言 color 渲染与序列位置)
|
||||
_internal: { parseHtml, buildDocumentXml, rPrOf, normalizeColor, CONTENT_TYPES, ROOT_RELS, STYLES_XML, DOC_RELS },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user