Files
competition-website/database/migrations/023-024-big-question-tables.sql
qubiaobiao 8ef18da395 feat(big-question): 中职/高职组「综合大题」在线作答 + 封堵 auth 成绩泄露三通道
## 一、需求变更:中职/高职组 Word 文件上传题 → 在线「综合大题」

按 2026-09-21 需求变更,中职组 / 高职组取消 Word 文件上传题,改为在线作答
综合大题(1 道大题 × 3 个小问 × 每小问 10 分 = 大题 30 分)。

- 新增表(迁移 023/024)
  · `big_questions`        —— 题库(题干富文本含表格 + 三小问提示语 + 评分口径)
  · `big_question_answers` —— 作答(抽题快照 + 三小问答案 + 按小问得分)
  两份迁移同时提供 `.sql`(生产直跑)与 `.js`(本地/CI 幂等执行)。
- 新增 model:`app/model/bigQuestion.js`、`app/model/bigQuestionAnswer.js`
- 新增 service:`app/service/bigQuestion.js`(抽题 / 保存答案 / 评分核心逻辑)
- 考生端接口(`app/controller/exam.js`)
  · `GET  /api/exam/:group/big-question` —— 首次抽题并落库题干快照,
    后续一律返回同一道题(**幂等**,防止同一考生拿到不同题干引发纠纷)
  · `POST /api/exam/:group/big-answer`   —— 保存三小问答案,带考试窗口门控
- 管理端接口(`app/controller/admin.js`)
  · 题库 CRUD:`/api/admin/big-questions/:group[/:id]`
  · 作答查证:`GET /api/admin/big-answers/:group`(考生 → 抽到的题 → 三小问 → 提交时间)
  · 按小问评分:`PUT /api/admin/big-answers/:group/:uid/scores`
- 服务端封堵越界数据:`app/controller/exam.js` 对中职/高职组的 Word 上传接口
  一并拒绝(前端入口已移除,但直调 API 仍可绕过)。

**所有新增读写仅落在新增的两张表,不触碰任何既有表结构。**

## 二、安全修复(铁律 15 成绩泄露 —— auth 通道缺口)

原有成绩发布门控只覆盖 4 条通道,遗漏了 auth 的**三条同源通道**:三处都直接
回传 `toJSON()`,在 `score_published_<group> = false`(未发布)时照样下发成绩,
而同期 `GET /api/score` 正确返回 `null`。

- `POST /api/auth/login`         —— 实测未发布态回传 `score`
- `GET  /api/auth/current-user`  —— 全字段 `toJSON`,连带下发分项
- `POST /api/auth/refresh`       —— 全字段 `toJSON`,且前端 `doSilentRefresh()`
  会把响应 `user` 写入 `localStorage` 与 Pinia,等效于登录通道;
  实测泄漏 `score` + `exam_score` + `word_score` + `ppt_score` 共 4 个字段

修复方式:新增 `AuthService.toClientUser()` 作为**唯一出口**,三处全部收敛,
禁止各自直接 `toJSON`。口径:
- 该组已发布 → 保留 `score`;未发布 / 查询异常 → 剔除 `score`(异常按未发布,安全默认)
- `exam_score` / `word_score` / `ppt_score` **任何情况下都不下发**
  (前端不使用;保留会形成"总分虽隐藏、分项可加总"的旁路)

门禁 `scripts/verify_score_leak_all_channels.py` 相应从 14 项扩到 20 项,
新增通道⑤/⑥/⑦断言(含"发布后 score 必须恢复"的**正向对照**,防止
"永不回传"式过度封堵造成假绿),并经变异注入自检证明断言可证伪。

## 三、移除已弃用的 Word 模板

中职 / 高职各 3 个历史 Word 模板(`app/public/templates/{zhongzhi,gaozhi}/*.docx`)
随需求一并移除;目录保留 `.gitkeep` 以维持路径存在。

---
验证:G10 铁律15 门禁 20/20 PASS(含变异自检,EXIT=0)、G9 铁律14 门禁 22/22 PASS、
窗口哨兵残留自检 6/6 PASS。前端配套改动见前端仓库提交 1bfe007。
2026-09-21 11:57:58 +08:00

81 lines
5.9 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.
-- ============================================================================
-- 023 / 024 · 综合大题改造(中职组 / 高职组取消 Word 上传题 → 随机抽取大题在线作答)
-- 日期:2026-09-21
-- 目标库:MariaDB 5.5.68 competition_db (字符集 utf8mb4 / utf8mb4_general_ci)
--
-- 本文件是**服务器同步用**的可直接执行 DDL(与 023-*.js / 024-*.js 迁移等价)。
-- 本改造为纯 ADD TABLE:
-- · 不改动任何现有表结构
-- · 不动 zhongzhi_submissions / gaozhi_submissions(客观题链路)
-- · 不动 benke_submissions / jiaoshi_submissions(本科/教师组)
-- · 不动成绩发布门控与总分公式(中职/高职总分仍 = exam_score + word_score)
--
-- 注意:MariaDB 5.5 无 JSON 列类型(JSON 一律用 TEXT / MEDIUMTEXT 存文本);
-- DATETIME 不支持 DEFAULT CURRENT_TIMESTAMP,时间戳由应用层写入。
-- ============================================================================
-- ---------------------------------------------------------------------------
-- 023 · 综合大题题库
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `big_questions` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '大题ID',
`question_uid` varchar(64) NOT NULL COMMENT '大题稳定标识(全局唯一,形如 big_12)',
`group` varchar(20) NOT NULL COMMENT '组别:zhongzhi/gaozhi',
`title` varchar(200) NOT NULL COMMENT '大题标题',
`description` mediumtext DEFAULT NULL COMMENT '题干富文本 HTML(含表格)',
`sub_prompt_1` text DEFAULT NULL COMMENT '第1小问题干',
`sub_prompt_2` text DEFAULT NULL COMMENT '第2小问题干',
`sub_prompt_3` text DEFAULT NULL COMMENT '第3小问题干',
`score_per_sub` int(11) NOT NULL DEFAULT '10' COMMENT '每小题分值',
`total_score` int(11) NOT NULL DEFAULT '30' COMMENT '大题总分',
`sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` int(11) NOT NULL DEFAULT '1' COMMENT '状态:1=启用(参与抽题),0=停用',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_big_question_uid` (`question_uid`),
KEY `idx_big_q_group_status` (`group`,`status`,`sort_order`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='综合大题题库(中职/高职组,替代原 Word 上传题)';
-- ---------------------------------------------------------------------------
-- 024 · 综合大题作答(一考生一行)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `big_question_answers` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`aggregation_uid` int(11) NOT NULL COMMENT '聚合报名ID(中职/高职组 == contestants.id),唯一 → 一考生一行',
`contestant_id` int(11) DEFAULT NULL COMMENT '选手ID(冗余,便于管理端查证与排障)',
`group` varchar(20) NOT NULL COMMENT '组别:zhongzhi/gaozhi',
`big_question_id` int(11) DEFAULT NULL COMMENT '抽中的大题ID',
`big_question_uid` varchar(64) DEFAULT NULL COMMENT '抽中的大题稳定标识',
`question_snapshot` mediumtext DEFAULT NULL COMMENT '抽题时刻的题干快照 JSON(防纠纷存证)',
`answer_1` text DEFAULT NULL COMMENT '第1小问作答(上限2000字,后端同步校验)',
`answer_2` text DEFAULT NULL COMMENT '第2小问作答(上限2000字)',
`answer_3` text DEFAULT NULL COMMENT '第3小问作答(上限2000字)',
`score_1` int(11) DEFAULT NULL COMMENT '第1小问得分(0-10,人工阅卷)',
`score_2` int(11) DEFAULT NULL COMMENT '第2小问得分(0-10,人工阅卷)',
`score_3` int(11) DEFAULT NULL COMMENT '第3小问得分(0-10,人工阅卷)',
`total_score` int(11) DEFAULT NULL COMMENT '三小问得分合计(回写 contestants.word_score 的依据)',
`submit_time` datetime DEFAULT NULL COMMENT '随整卷正式提交写入(未提交为 NULL)',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_big_answer_agg` (`aggregation_uid`),
KEY `idx_big_answer_group` (`group`),
KEY `idx_big_answer_q` (`big_question_uid`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='综合大题作答(一考生一行,含题干快照)';
-- ============================================================================
-- 回滚(仅在确认弃用时执行)
-- DROP TABLE IF EXISTS `big_question_answers`;
-- DROP TABLE IF EXISTS `big_questions`;
--
-- 历史数据清理(2026-09-21 用户确认:已弃用,可全部删除)
-- DELETE FROM zhongzhi_submissions; -- 84 行(孤儿遗留,aggregation_uid 与现有选手不匹配)
-- DELETE FROM gaozhi_submissions; -- 480 行
-- UPDATE competition_times SET word_template = NULL WHERE `group` IN ('zhongzhi','gaozhi');
-- -- 并删除磁盘旧文件:app/public/answers/{zhongzhi,gaozhi}/、app/public/templates/{zhongzhi,gaozhi}/
-- -- ⚠️ 仅限中职/高职;本科/教师组的 word_template 与作品文件一律不得触碰。
-- ============================================================================