Files
competition-website/database/migrations/025-scores-decimal-one-place.sql
qubiaobiao 079c1b31c9 R20:客观题分值口径 + 大题一位小数评分 + 页面刷新修复 + 大题导出 Word
A1/A2 客观题:
- 清理题库占位题(答案恒 A/true、分值恒 5),客观题分值硬约束为 1 分
- 新增 scripts/check_question_bank_health.py(带变异注入自检,已验证判别力)
- ci-gates.sh 由 9 步扩为 10 步,纳入题库健康门禁

B 大题评分一位小数:
- 新增 app/lib/decimal_score.js:DECIMAL(6,1) + getter 兜底,单点收口
  「DB 列 / model 声明 / dialectOptions.decimalNumbers」三处必须同时成立
- 10 个 model 的 score 字段改走该工厂
- database/migrations/025:17 列 INT → DECIMAL(6,1)(6 列题目分值保持 int)
- service/bigQuestion.js:setSubScores 用 Math.round(n*10)/10 收敛,不再抹成整数

C 页面状态刷新:
- 修复 onQuestionTypeChange 的 `bigQuestions.length === 0` 守卫(切组后旧数组仍非空
  ⇒ 永不重新拉取)
- 修复评分后只刷一半(saveBigScores / saveExamScore 各只刷一侧列表)
- 收敛为唯一刷新入口 refreshQuestionData();ensureBigData 改按「数据属于哪个组」
  判定;fetchBigQuestions/fetchBigAnswers 增加组别竞态守卫

D 大题导出 Word(单题 / 批量 / 全部):
- 新增 app/lib/docx.js:零新依赖手写 OOXML(复用已有 archiver@8),支持 p/strong/
  span/a/br/table/ul-ol 子集,具名捕获组词法解析,pPr 严格排在 runs 之前
- service.bigQuestion.exportQuestions + controller.exportBigQuestions +
  GET /api/admin/big-questions/:group/export
- 前端导出选中/全部两按钮 + 表格多选(reserve-selection)

计划外发现并修复(已在报告中披露):
- 管理端跨组写分越权:setSubScores 定位作答行时缺 group 约束,可用任意组的路径
  改写他组考生成绩(读写不对称:读取侧有 where:{group},写入侧没有)
  ⇒ 已补 group 约束,并区分「不属于本组」与「尚未抽题」两种文案
- 导出路由注册顺序:export 原注册在 :group/:id 之后(当时因后者仅 PUT/DELETE 而
  未暴露问题),已前移以消除「将来新增 GET /:group/:id 会吞掉 export」的隐患

验证(全部实测,非仅看退出码):
- ci-gates.sh 10/10 全绿;四连门禁 52/0 · 22/0 · 20/0 · 46/0
- 端到端探针 probe_d_03(导出+小数)ALL PASS;probe_e_01(切组刷新)22/22 ALL PASS
- 前端构建 VITE_EXIT=0

未 push:须先上线验证通过再单独授权。
2026-09-23 18:29:50 +08:00

100 lines
6.1 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 025 一位小数评分支持:分数列 int(11) → DECIMAL(6,1)
--
-- 背景(2026-09-23)
-- 需求:中职 / 高职「综合大题」的人工评分允许保留小数点后一位(如 7.5)。
-- 现状:全库 23 个 score 类列**全部**为 int(11) ⇒ 小数落库即被静默截断;
-- 且 service/bigQuestion.js:337 另有一处 Math.round() 主动取整。
-- 本迁移把「得分」语义的列放宽为 DECIMAL(6,1)(上限 99999.9,保留 1 位小数)。
--
-- 范围(17 列)
-- 改(得分 / 分数落点):
-- big_question_answers.score_1 / score_2 / score_3 / total_score
-- contestants.exam_score / word_score / ppt_score / score
-- accounts.score
-- zhongzhi_submissions.score / gaozhi_submissions.score / exam_submissions.score
-- benke_submissions.score / jiaoshi_submissions.score / work_submissions.score
-- big_questions.score_per_sub / total_score(大题分值配置,随评分口径)
-- 不改:
-- choice_questions.score / judge_questions.score
-- —— 客观题口径已固定为每题 1 分(service/admin.js OBJECTIVE_SCORE_PER_QUESTION),
-- 且 scripts/check_question_bank_health.py 门禁断言其恒为 1;
-- word_questions.score、exam_questions/zhongzhi_questions/gaozhi_questions.score
-- —— 属题目分值配置或历史遗留题表,无小数需求。
--
-- 兼容性
-- INT → DECIMAL(6,1) 是**放宽**:既有整数值原样保留,读写语义不变。
-- ⚠️ 应用侧必须同步:
-- ① config/config.default.js 的 sequelize.dialectOptions.decimalNumbers = true
-- (否则 mysql2 把 DECIMAL 读成**字符串**);
-- ② 相关 model 字段类型声明为 DECIMAL,且 getter 转 Number
-- (否则 Sequelize 的 INTEGER.parse 会把 7.5 二次截断成 7)。
--
-- 执行(在 database/ 目录下):
-- mysql -uroot -p competition_db < migrations/025-scores-decimal-one-place.sql
-- 幂等:MODIFY 到同一类型可重复执行。
-- ============================================================================
-- ---------------------------------------------------------------- 大题作答
ALTER TABLE `big_question_answers`
MODIFY COLUMN `score_1` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '第 1 小问得分(可含 1 位小数)',
MODIFY COLUMN `score_2` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '第 2 小问得分(可含 1 位小数)',
MODIFY COLUMN `score_3` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '第 3 小问得分(可含 1 位小数)',
MODIFY COLUMN `total_score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '大题合计得分(可含 1 位小数)';
-- ---------------------------------------------------------------- 大题配置
ALTER TABLE `big_questions`
MODIFY COLUMN `score_per_sub` DECIMAL(6,1) NOT NULL DEFAULT 10 COMMENT '每小题分值',
MODIFY COLUMN `total_score` DECIMAL(6,1) NOT NULL DEFAULT 30 COMMENT '大题总分 = 每小题分值 × 小问数';
-- ---------------------------------------------------------------- 选手成绩
ALTER TABLE `contestants`
MODIFY COLUMN `exam_score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '卷面考试成绩',
MODIFY COLUMN `word_score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT 'Word题/综合大题得分(可含 1 位小数)',
MODIFY COLUMN `ppt_score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT 'PPT得分(本科/教师组,可含 1 位小数)',
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '选手总分(exam_score + word_score / word_score + ppt_score)';
-- ---------------------------------------------------------------- 账号分(历史兼容)
ALTER TABLE `accounts`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '总分(历史兼容字段)';
-- ---------------------------------------------------------------- 提交表分项
ALTER TABLE `zhongzhi_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '本题得分';
ALTER TABLE `gaozhi_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '本题得分';
ALTER TABLE `exam_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '卷面得分';
ALTER TABLE `benke_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '作品得分';
ALTER TABLE `jiaoshi_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '作品得分';
ALTER TABLE `work_submissions`
MODIFY COLUMN `score` DECIMAL(6,1) NULL DEFAULT NULL COMMENT '作品得分';
-- ============================================================================
-- 回滚(⚠️ DECIMAL → INT 会**丢失**既有小数位,执行前务必确认无小数值:
-- SELECT COUNT(*) FROM contestants WHERE score <> FLOOR(score); -- 应为 0)
--
-- ALTER TABLE `big_question_answers`
-- MODIFY COLUMN `score_1` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `score_2` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `score_3` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `total_score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `big_questions`
-- MODIFY COLUMN `score_per_sub` INT(11) NOT NULL DEFAULT 10,
-- MODIFY COLUMN `total_score` INT(11) NOT NULL DEFAULT 30;
-- ALTER TABLE `contestants`
-- MODIFY COLUMN `exam_score` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `word_score` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `ppt_score` INT(11) NULL DEFAULT NULL,
-- MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `accounts` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `zhongzhi_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `gaozhi_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `exam_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `benke_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `jiaoshi_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ALTER TABLE `work_submissions` MODIFY COLUMN `score` INT(11) NULL DEFAULT NULL;
-- ============================================================================