Files
competition-website/app/model/examSubmission.js
qubiaobiao 2ab0991310 fix(admin): 复核页选择题选项渲染修复 + 阅卷导出 Word + 评分两位小数(R28)
针对测试人员反馈的四项:
1) 复核明细「选择题大量空白」:options 为 text 列存 JSON 字符串,原先被原样透传,
   前端 v-for 对字符串逐字符迭代(op.key/op.text 皆 undefined)⇒ 每字符渲染一行。
   新增 parseOptions()(仅 string 才 JSON.parse,失败降级 null)。
2) 选项与答案不随机:仅题目用 RAND() 抽题,normalizeQuestion 无 shuffle,字母无歧义。
3) 评分精度:DECIMAL(6,1) -> DECIMAL(6,2),migration 026 覆盖 17 列(与 025 同范围),
   lib/decimal_score.js 与 11 个 model 注释同步;汇总收敛 round2(×100/100)。
4) 阅卷导出:GET /api/admin/big-answers/:group/export?ids=
   单选考生 -> 直接 .docx;多位/全部 -> 每位考生单独 .docx 打包 .zip。
   路由为 3 段,注册在 :group 通配段之前。

验证:verify_big_question 51 PASS/1 FAIL(唯一 FAIL=测试人员新数据,非本次改动)、
verify_score_leak_all_channels 24/24、verify_question_no_answer_leak 4/3/0/1。
2026-09-28 15:50:46 +08:00

108 lines
3.1 KiB
JavaScript
Raw Permalink 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.
'use strict';
/**
* 答题记录模型 - ExamSubmission
*
* 存储选手的试卷答题记录,包括答题内容、提交时间和得分。
* 每个选手在其组别的比赛中只允许提交一次。
* 表名:exam_submissions
*/
module.exports = app => {
const { STRING, INTEGER, DATE, TEXT, JSON: JSON_TYPE } = app.Sequelize;
// 分数列统一走共享工厂(DECIMAL(6,2) + Number 兜底,见 app/lib/decimal_score.js)
const decimalScore = require('../lib/decimal_score')(app);
const ExamSubmission = app.model.define('exam_submission', {
// 主键 ID,自增
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '答题记录唯一标识',
},
// 选手 ID,外键关联 contestants 表
contestant_id: {
type: INTEGER,
allowNull: false,
comment: '选手 ID',
references: {
model: 'contestants',
key: 'id',
},
},
// 答题内容(JSON 格式存储,如 [{question_id: 1, answer: "..."}, ...])
answers: {
type: JSON_TYPE,
allowNull: false,
comment: '答题内容(JSON)',
},
// 该考生固化后的题目顺序(question_uid 数组 JSON 字符串):首次进入随机生成并持久化,
// 之后重进复用,避免每次进入都 RAND() 重新打乱顺序(重进题目顺序变乱 Bug 修复)。
question_order: {
type: TEXT,
allowNull: true,
defaultValue: null,
comment: '该考生固化题目顺序(question_uid JSON 数组),重进复用',
},
// Word 答案文件路径
word_file_path: {
type: STRING(500),
allowNull: true,
defaultValue: null,
comment: 'Word答案文件路径',
},
// Word 答案文件名
word_file_name: {
type: STRING(255),
allowNull: true,
defaultValue: null,
comment: 'Word答案文件名',
},
// 提交时间
submit_time: {
type: DATE,
allowNull: true,
comment: '提交时间(占位/未提交为 NULL,正式提交后为具体时间)',
},
// 得分(管理员评分后更新,NULL 表示尚未评分)
score: decimalScore('score', '试卷得分', { defaultValue: null }),
// 创建时间
created_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '创建时间',
},
// 更新时间
updated_at: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '更新时间',
},
}, {
tableName: 'exam_submissions',
timestamps: false,
underscored: true,
indexes: [
{
// 唯一约束:每选手仅一条答题提交记录,并发创建时折叠为一条(B12)。
unique: true,
name: 'uk_exam_submission_contestant',
fields: ['contestant_id'],
},
],
});
// 关联关系:一条答题记录属于一名选手
ExamSubmission.associate = () => {
app.model.ExamSubmission.belongsTo(app.model.Contestant, {
foreignKey: 'contestant_id',
as: 'contestant',
});
};
return ExamSubmission;
};