feat(education): save practice answers idempotently
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 答案保存幂等性回滚
|
||||
-- Ticket #7 / Migration 003
|
||||
-- =============================================
|
||||
--
|
||||
-- WARNING: This file contains NO executable SQL.
|
||||
-- Destructive rollback requires manual operator verification.
|
||||
--
|
||||
-- Manual rollback procedure (operator must execute):
|
||||
-- 1. Verify no other tables depend on education_answer_idempotency:
|
||||
-- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
|
||||
-- FROM information_schema.KEY_COLUMN_USAGE
|
||||
-- WHERE REFERENCED_TABLE_NAME = 'education_answer_idempotency'
|
||||
-- AND TABLE_SCHEMA = DATABASE();
|
||||
-- Result MUST be empty before proceeding.
|
||||
--
|
||||
-- 2. Verify the table contains only data from this migration:
|
||||
-- SELECT COUNT(*) AS idempotency_count FROM education_answer_idempotency;
|
||||
-- Operator must confirm this count is acceptable to destroy.
|
||||
--
|
||||
-- 3. Verify no application code depends on client_sequence column:
|
||||
-- Search codebase for 'clientSequence' / 'client_sequence' references.
|
||||
--
|
||||
-- 4. After verification, execute:
|
||||
-- DROP TABLE IF EXISTS education_answer_idempotency;
|
||||
-- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
|
||||
--
|
||||
-- DO NOT uncomment or execute the lines below without operator verification.
|
||||
-- -- DROP TABLE IF EXISTS education_answer_idempotency;
|
||||
-- -- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
|
||||
97
sql/mysql/education/003-education-answer-idempotency.sql
Normal file
97
sql/mysql/education/003-education-answer-idempotency.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- =============================================
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user