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