-- ============================================= -- Education 模块 — 交卷提交与成绩报告 DDL -- Ticket #8: 交卷 CAS、保护性答案快照、评分与报告 -- Migration: 004 -- Prerequisites: 003-education-answer-idempotency.sql -- ============================================= -- ============================================= -- Preconditions -- ============================================= -- Operator is expected to verify: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_schema = DATABASE() -- AND table_name IN ('education_submit_idempotency', -- 'education_practice_report', -- 'education_practice_report_detail'); -- Result MUST be 0 before executing this migration. -- -- Verify prerequisite tables exist: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_schema = DATABASE() -- AND table_name IN ('education_practice_session', -- 'education_practice_question', -- 'education_answer_idempotency'); -- Result MUST be 3. -- ============================================= -- PracticeQuestionDO: add protected answer snapshot columns -- ============================================= -- Purpose: At session creation, snapshot correct_answer and explanation -- from the full CatalogQuestionDTO. These fields are NEVER exposed -- before submission (enforced by SafeQuestionRespVO allow-list and -- PracticeQuestionRespVO which does not include them). ALTER TABLE `education_practice_question` ADD COLUMN `correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照(不可在交卷前暴露)', ADD COLUMN `explanation` TEXT DEFAULT NULL COMMENT '解析快照(不可在交卷前暴露)'; -- ============================================= -- 交卷幂等表 -- ============================================= -- Purpose: Provide durable idempotency for submit-session commands. -- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original report. -- Same key + different request_hash → conflict. -- Concurrent same-key inserts resolved by unique constraint race handling. -- -- Indexes: -- uk_submit_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key. -- INSERT during submit. DuplicateKeyException catch for concurrent-create race resolution. -- idx_submit_session — covers lookup by session for audit/debug. CREATE TABLE `education_submit_idempotency` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT '交卷用户编号', `operation` VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_SESSION' COMMENT '操作类型:SUBMIT_SESSION', `idempotency_key` VARCHAR(64) NOT NULL COMMENT '客户端幂等键(UUID)', `request_hash` VARCHAR(64) NOT NULL COMMENT '请求载荷 SHA-256 哈希', `session_id` BIGINT NOT NULL COMMENT '会话 ID', `report_id` BIGINT DEFAULT NULL COMMENT '关联的报告 ID(成功时有值)', `status` VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED' COMMENT '状态:ACCEPTED-已接受, CONFLICT-冲突', `response_json` TEXT NOT NULL COMMENT '首次成功响应 JSON(用于重试重放)', `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_submit_idempotency` (`tenant_id`, `user_id`, `operation`, `idempotency_key`), KEY `idx_submit_session` (`tenant_id`, `session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-交卷幂等记录'; -- ============================================= -- 练习报告表(会话级) -- ============================================= -- Purpose: Store the computed scoring result for a submitted session. -- One report per session. Immutable after creation. -- Question snapshots (stem, selectedAnswer, correctAnswer, explanation) -- are stored in report_details so source question edits don't affect history. -- -- Indexes: -- uk_report_session — one report per session (unique). -- idx_report_tenant_user — covers paginated history queries for current tenant+user. -- idx_report_create_time — covers time-sorted listing. CREATE TABLE `education_practice_report` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT '用户编号', `session_id` BIGINT NOT NULL COMMENT '会话 ID', `question_count` INT NOT NULL COMMENT '题目总数', `answered_count` INT NOT NULL DEFAULT 0 COMMENT '已答题数', `unanswered_count` INT NOT NULL DEFAULT 0 COMMENT '未答题数', `correct_count` INT NOT NULL DEFAULT 0 COMMENT '正确题数', `incorrect_count` INT NOT NULL DEFAULT 0 COMMENT '错误题数', `score` INT NOT NULL DEFAULT 0 COMMENT '得分(整数,满分 100 为基准)', `status` VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED' COMMENT '报告状态:SUBMITTED', `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_report_session` (`session_id`), KEY `idx_report_tenant_user` (`tenant_id`, `user_id`), KEY `idx_report_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习报告'; -- ============================================= -- 练习报告明细表(逐题结果) -- ============================================= -- Purpose: Store per-question scoring results at submission time. -- Includes snapshot of stem, selectedAnswer, correctAnswer, and explanation -- so that history is stable even if source questions are later edited. -- -- Indexes: -- uk_report_sequence — per-report uniqueness for question sequence. -- idx_detail_session — covers lookup by session for report assembly. CREATE TABLE `education_practice_report_detail` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT '用户编号', `report_id` BIGINT NOT NULL COMMENT '报告 ID', `session_id` BIGINT NOT NULL COMMENT '会话 ID', `question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID', `sequence` INT NOT NULL COMMENT '题目序号(1-based)', `stem` TEXT NOT NULL COMMENT '题干快照', `type` VARCHAR(32) NOT NULL COMMENT '题型快照', `difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照', `selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案', `correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照', `is_correct` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否正确', `explanation` TEXT DEFAULT NULL COMMENT '解析快照', `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_report_sequence` (`report_id`, `sequence`), KEY `idx_detail_session` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习报告明细'; -- ============================================= -- Post-migration verification queries -- ============================================= -- Verify new tables exist: -- SHOW CREATE TABLE education_submit_idempotency; -- SHOW CREATE TABLE education_practice_report; -- SHOW CREATE TABLE education_practice_report_detail; -- Verify columns added to question table: -- SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT -- FROM information_schema.COLUMNS -- WHERE TABLE_SCHEMA = DATABASE() -- AND TABLE_NAME = 'education_practice_question' -- AND COLUMN_NAME IN ('correct_answer', 'explanation'); -- Verify unique keys are enforced: -- SHOW INDEX FROM education_submit_idempotency WHERE Key_name = 'uk_submit_idempotency'; -- SHOW INDEX FROM education_practice_report WHERE Key_name = 'uk_report_session'; -- SHOW INDEX FROM education_practice_report_detail WHERE Key_name = 'uk_report_sequence'; -- Verify no orphan data (should be 0 after fresh migration): -- SELECT COUNT(*) FROM education_submit_idempotency; -- SELECT COUNT(*) FROM education_practice_report; -- SELECT COUNT(*) FROM education_practice_report_detail;