feat(education): complete Flyway migration and atomic submit
This commit is contained in:
113
sql/postgresql/education/003-education-answer-idempotency.sql
Normal file
113
sql/postgresql/education/003-education-answer-idempotency.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 答案保存幂等性 DDL (PostgreSQL)
|
||||
-- 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_catalog = current_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_catalog = current_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 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_ANSWER',
|
||||
idempotency_key VARCHAR(64) NOT NULL,
|
||||
request_hash VARCHAR(64) NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_id VARCHAR(64) NOT NULL,
|
||||
selected_answer TEXT DEFAULT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||||
response_json TEXT NOT NULL,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_answer_idempotency IS '教育-答案命令幂等记录';
|
||||
COMMENT ON COLUMN education_answer_idempotency.id IS '主键';
|
||||
COMMENT ON COLUMN education_answer_idempotency.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_answer_idempotency.user_id IS '答题用户编号';
|
||||
COMMENT ON COLUMN education_answer_idempotency.operation IS '操作类型:SUBMIT_ANSWER';
|
||||
COMMENT ON COLUMN education_answer_idempotency.idempotency_key IS '客户端幂等键(UUID)';
|
||||
COMMENT ON COLUMN education_answer_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
|
||||
COMMENT ON COLUMN education_answer_idempotency.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_answer_idempotency.question_id IS '题目 ID';
|
||||
COMMENT ON COLUMN education_answer_idempotency.selected_answer IS '学生已选答案';
|
||||
COMMENT ON COLUMN education_answer_idempotency.status IS '状态:ACCEPTED-已接受, CONFLICT-冲突';
|
||||
COMMENT ON COLUMN education_answer_idempotency.response_json IS '首次成功响应 JSON(用于重试重放)';
|
||||
COMMENT ON COLUMN education_answer_idempotency.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_answer_idempotency.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_answer_idempotency.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_answer_idempotency.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_answer_idempotency.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_answer_idempotency ON education_answer_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||||
CREATE INDEX idx_tenant_session ON education_answer_idempotency (tenant_id, session_id);
|
||||
|
||||
-- =============================================
|
||||
-- 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;
|
||||
CREATE INDEX idx_client_sequence ON education_practice_question (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;
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new table exists:
|
||||
-- \d education_answer_idempotency
|
||||
-- Verify unique key is enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_answer_idempotency' AND indexname = 'uk_answer_idempotency';
|
||||
-- Verify column added to question table:
|
||||
-- SELECT column_name, data_type, column_default
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_catalog = current_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