-- ============================================= -- Education 模块 — 答案保存幂等性 DDL -- Ticket #7: 答案命令幂等、乐观锁并发控制、答案恢复 -- Migration: 003 -- Prerequisites: 002-education-practice-session.sql (session + question snapshots) -- ============================================= -- ============================================= -- Preconditions -- ============================================= -- Operator is expected to verify: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_schema = DATABASE() -- AND table_name = 'education_answer_idempotency'; -- 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'); -- Result MUST be 2. -- ============================================= -- 答案命令幂等表 -- ============================================= -- Purpose: Provide durable idempotency for answer save commands. -- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response. -- Same key + different request_hash → conflict. -- Concurrent same-key inserts are resolved by unique constraint race handling. -- -- Indexes: -- uk_answer_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key. -- INSERT during answer save. DuplicateKeyException catch for concurrent-create race resolution. -- idx_tenant_session — covers lookup by session for audit/debug. -- -- response_json: Stores the serialized answer response for replay after network timeout/retry. -- request_hash: SHA-256 of canonical payload (sorted JSON fields) for content-based dedup. CREATE TABLE `education_answer_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_ANSWER' COMMENT '操作类型:SUBMIT_ANSWER', `idempotency_key` VARCHAR(64) NOT NULL COMMENT '客户端幂等键(UUID)', `request_hash` VARCHAR(64) NOT NULL COMMENT '请求载荷 SHA-256 哈希', `session_id` BIGINT NOT NULL COMMENT '会话 ID', `question_id` VARCHAR(64) NOT NULL COMMENT '题目 ID', `selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案', `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_answer_idempotency` (`tenant_id`, `user_id`, `operation`, `idempotency_key`), KEY `idx_tenant_session` (`tenant_id`, `session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-答案命令幂等记录'; -- ============================================= -- PracticeQuestionDO: add client_sequence column -- ============================================= -- Purpose: Track the last accepted client command sequence per question. -- Rejects stale clientSequence: only sequences strictly greater than the -- stored value are accepted (monotonic forward progression). -- NULL means no answer has been accepted yet. ALTER TABLE `education_practice_question` ADD COLUMN `client_sequence` INT DEFAULT NULL COMMENT '最后接受的客户端命令序号', ADD INDEX `idx_client_sequence` (`client_sequence`); -- ============================================= -- PracticeSessionDO: add last_client_sequence column -- ============================================= -- Purpose: Session-wide monotonic counter for client commands. -- Rejects stale clientSequence across questions (not just per-question). -- CAS incrementVersion now updates this column alongside version. -- NULL means no answer has been accepted yet for this session. ALTER TABLE `education_practice_session` ADD COLUMN `last_client_sequence` INT DEFAULT NULL COMMENT '会话级最后接受的客户端命令序号(跨题目)'; -- ============================================= -- Post-migration verification queries -- ============================================= -- Verify new table exists: -- SHOW CREATE TABLE education_answer_idempotency; -- Verify unique key is enforced: -- SHOW INDEX FROM education_answer_idempotency WHERE Key_name = 'uk_answer_idempotency'; -- Verify column 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 = 'client_sequence'; -- Verify no orphan data (should be 0 after fresh migration): -- SELECT COUNT(*) FROM education_answer_idempotency;