Files
ruoyi-vue-pro/sql/postgresql/education/002-education-practice-session.sql

106 lines
6.6 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
-- Ticket #6: 练习会话创建、题目快照、恢复与状态机
-- Migration: 002
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- This migration MUST fail if either table already exists (no IF NOT EXISTS).
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = current_database()
-- AND table_name IN ('education_practice_session', 'education_practice_question');
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 练习会话表
-- =============================================
-- Indexes:
-- uk_tenant_client_session — per-tenant uniqueness for clientSessionId idempotency.
-- Used by: selectByTenantAndClientSessionId (idempotent create check),
-- INSERT ... ON CONFLICT ... for concurrent-create race resolution.
-- idx_tenant_user_status — covers getCurrentSession (latest ACTIVE by tenant+user)
-- and ownership queries. Column order: (tenant_id, user_id, status) so the
-- index supports both filtering by tenant+user and tenant+user+status.
-- Lock impact: INSERT acquires a row-level lock on the unique index entry;
-- concurrent inserts with same (tenant_id, client_session_id) serialize naturally.
-- No additional table-level locks required.
CREATE TABLE education_practice_session (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 会话主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* Member 用户编号 */,
client_session_id VARCHAR(36) NOT NULL /* 客户端生成的会话标识UUID用于幂等创建 */,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
/* 会话状态ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消 */,
question_count INT NOT NULL DEFAULT 0 /* 题目总数 */,
collection_id VARCHAR(64) DEFAULT NULL /* 源题集 ID */,
node_id VARCHAR(64) DEFAULT NULL /* 源目录节点 ID */,
type VARCHAR(32) DEFAULT NULL /* 筛选题型 */,
difficulty VARCHAR(32) DEFAULT NULL /* 筛选难度 */,
version INT NOT NULL DEFAULT 0 /* 乐观锁版本号 */,
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_practice_session IS '教育-练习会话';
CREATE UNIQUE INDEX uk_tenant_client_session ON education_practice_session (tenant_id, client_session_id);
CREATE INDEX idx_tenant_user_status ON education_practice_session (tenant_id, user_id, status);
-- =============================================
-- 练习会话题目快照表
-- =============================================
-- Indexes:
-- uk_session_sequence — per-session uniqueness for question sequence numbers.
-- Used by: insertBatch to ensure no duplicate sequences within a session.
-- idx_session_id — covers selectBySessionIdOrderBySequence (load all questions
-- for a session, ordered by sequence). Also used by cascade delete lookups.
-- Lock impact: INSERT acquires gap locks within session_id range on uk_session_sequence;
-- concurrent inserts into different sessions are independent.
-- Options column: JSONB data type stores only label, content, order — never isCorrect.
-- Application layer (optionsToSafeJson) strips correctness before storage.
CREATE TABLE education_practice_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
session_id BIGINT NOT NULL /* 会话 ID */,
sequence INT NOT NULL /* 题目序号1-based服务端固定 */,
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 快照时的题目内容版本 */,
stem TEXT NOT NULL /* 题干快照 */,
type VARCHAR(32) NOT NULL /* 题型快照 */,
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
options JSONB NOT NULL /* 选项快照 JSON不含 isCorrect */,
correct_answer TEXT DEFAULT NULL /* 正确答案快照 */,
explanation TEXT DEFAULT NULL /* 解析快照 */,
selected_answer TEXT DEFAULT NULL /* 学生已选答案 */,
is_answered BOOLEAN NOT NULL DEFAULT false /* 是否已作答 */,
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_practice_question IS '教育-练习会话题目快照';
CREATE UNIQUE INDEX uk_session_sequence ON education_practice_question (session_id, sequence);
CREATE INDEX idx_session_id ON education_practice_question (session_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify tables exist with correct structure:
-- \d education_practice_session
-- \d education_practice_question
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_session' AND indexname = 'uk_tenant_client_session';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_question' AND indexname = 'uk_session_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_practice_session;
-- SELECT COUNT(*) FROM education_practice_question;