104 lines
5.6 KiB
SQL
104 lines
5.6 KiB
SQL
-- =============================================
|
||
-- Education 模块 — 统一幂等表 DDL (PostgreSQL)
|
||
-- Ticket: 合并 Answer + Submit 幂等表为统一 education_idempotency
|
||
-- Migration: 009
|
||
-- Prerequisites: 002-education-practice-session.sql
|
||
-- =============================================
|
||
|
||
-- =============================================
|
||
-- Preconditions
|
||
-- =============================================
|
||
-- Verify table does not already exist:
|
||
-- SELECT COUNT(*) FROM information_schema.tables
|
||
-- WHERE table_catalog = current_database()
|
||
-- AND table_name = 'education_idempotency';
|
||
-- Result MUST be 0 before executing this migration.
|
||
--
|
||
-- NOTE: This table replaces education_answer_idempotency and education_submit_idempotency.
|
||
-- Those tables should be dropped after this migration is verified:
|
||
-- DROP TABLE IF EXISTS education_answer_idempotency;
|
||
-- DROP TABLE IF EXISTS education_submit_idempotency;
|
||
|
||
-- =============================================
|
||
-- 统一幂等表
|
||
-- =============================================
|
||
-- Purpose: Unified idempotency store for all education write operations.
|
||
-- operation field distinguishes: SUBMIT_ANSWER vs SUBMIT_SESSION.
|
||
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
|
||
-- Same key + different request_hash → conflict.
|
||
-- PostgreSQL ON CONFLICT DO NOTHING resolves concurrent same-key inserts.
|
||
--
|
||
-- Indexes:
|
||
-- uk_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
|
||
-- Used by: IdempotencyStoreMapper.insertIgnore() ON CONFLICT resolution.
|
||
-- idx_tenant_session — covers lookup by session for audit/debug.
|
||
--
|
||
-- Fields specific to SUBMIT_ANSWER: question_id, selected_answer (nullable for SUBMIT_SESSION).
|
||
-- Fields specific to SUBMIT_SESSION: report_id (nullable for SUBMIT_ANSWER).
|
||
-- response_json: stores serialized response for replay after timeout/retry.
|
||
-- request_hash: SHA-256 of canonical payload for content-based dedup.
|
||
CREATE TABLE education_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,
|
||
idempotency_key VARCHAR(64) NOT NULL,
|
||
request_hash VARCHAR(64) NOT NULL,
|
||
session_id BIGINT NOT NULL,
|
||
question_id VARCHAR(64) DEFAULT NULL,
|
||
selected_answer TEXT DEFAULT NULL,
|
||
report_id BIGINT DEFAULT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||
response_json TEXT DEFAULT NULL,
|
||
business_payload JSONB DEFAULT 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_idempotency IS '教育-统一幂等记录(合并 answer + submit)';
|
||
COMMENT ON COLUMN education_idempotency.id IS '主键';
|
||
COMMENT ON COLUMN education_idempotency.tenant_id IS '租户编号';
|
||
COMMENT ON COLUMN education_idempotency.user_id IS '用户编号';
|
||
COMMENT ON COLUMN education_idempotency.operation IS '操作类型:SUBMIT_ANSWER-答题, SUBMIT_SESSION-交卷';
|
||
COMMENT ON COLUMN education_idempotency.idempotency_key IS '客户端幂等键(UUID)';
|
||
COMMENT ON COLUMN education_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
|
||
COMMENT ON COLUMN education_idempotency.session_id IS '会话 ID';
|
||
COMMENT ON COLUMN education_idempotency.question_id IS '题目 ID(SUBMIT_ANSWER)';
|
||
COMMENT ON COLUMN education_idempotency.selected_answer IS '学生已选答案(SUBMIT_ANSWER)';
|
||
COMMENT ON COLUMN education_idempotency.report_id IS '关联报告 ID(SUBMIT_SESSION)';
|
||
COMMENT ON COLUMN education_idempotency.status IS '状态:ACCEPTED-已接受, CONFLICT-冲突';
|
||
COMMENT ON COLUMN education_idempotency.response_json IS '首次成功响应 JSON(用于重试重放)';
|
||
COMMENT ON COLUMN education_idempotency.business_payload IS '业务扩展载荷(JSONB)';
|
||
COMMENT ON COLUMN education_idempotency.creator IS '创建者';
|
||
COMMENT ON COLUMN education_idempotency.create_time IS '创建时间';
|
||
COMMENT ON COLUMN education_idempotency.updater IS '更新者';
|
||
COMMENT ON COLUMN education_idempotency.update_time IS '更新时间';
|
||
COMMENT ON COLUMN education_idempotency.deleted IS '是否删除';
|
||
|
||
CREATE UNIQUE INDEX uk_idempotency ON education_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||
CREATE INDEX idx_idempotency_tenant_session ON education_idempotency (tenant_id, session_id);
|
||
|
||
-- =============================================
|
||
-- PracticeSessionDO: add review_fingerprint column
|
||
-- =============================================
|
||
-- Purpose: Store canonical fingerprint for wrong-question review session idempotency.
|
||
-- SHA-256 of comma-joined sorted wrong_question IDs for deterministic comparison.
|
||
ALTER TABLE education_practice_session
|
||
ADD COLUMN review_fingerprint VARCHAR(64) DEFAULT NULL;
|
||
|
||
COMMENT ON COLUMN education_practice_session.review_fingerprint IS '错题复习指纹(SHA-256),用于幂等创建';
|
||
|
||
-- =============================================
|
||
-- Post-migration verification queries
|
||
-- =============================================
|
||
-- Verify new table exists:
|
||
-- \d education_idempotency
|
||
-- Verify unique index:
|
||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_idempotency';
|
||
-- Verify column added:
|
||
-- SELECT column_name, data_type FROM information_schema.columns
|
||
-- WHERE table_name = 'education_practice_session' AND column_name = 'review_fingerprint';
|