Files
ruoyi-vue-pro/sql/postgresql/education/003-education-answer-idempotency.sql

114 lines
6.1 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.

-- =============================================
-- 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;