-- ============================================= -- 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 = 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), -- DuplicateKeyException catch 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 next-key lock on uk_tenant_client_session unique key; -- concurrent inserts with same (tenant_id, client_session_id) serialize naturally. -- No additional table-level locks required. CREATE TABLE `education_practice_session` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '会话主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT 'Member 用户编号', `client_session_id` VARCHAR(36) NOT NULL COMMENT '客户端生成的会话标识(UUID),用于幂等创建', `status` VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '会话状态:ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消', `question_count` INT NOT NULL DEFAULT 0 COMMENT '题目总数', `collection_id` VARCHAR(64) DEFAULT NULL COMMENT '源题集 ID', `node_id` VARCHAR(64) DEFAULT NULL COMMENT '源目录节点 ID', `type` VARCHAR(32) DEFAULT NULL COMMENT '筛选题型', `difficulty` VARCHAR(32) DEFAULT NULL COMMENT '筛选难度', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_client_session` (`tenant_id`, `client_session_id`), KEY `idx_tenant_user_status` (`tenant_id`, `user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习会话'; -- ============================================= -- 练习会话题目快照表 -- ============================================= -- 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: JSON data type stores only label, content, order — never isCorrect. -- Application layer (optionsToSafeJson) strips correctness before storage. CREATE TABLE `education_practice_question` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `session_id` BIGINT NOT NULL COMMENT '会话 ID', `sequence` INT NOT NULL COMMENT '题目序号(1-based,服务端固定)', `question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID', `content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '快照时的题目内容版本', `stem` TEXT NOT NULL COMMENT '题干快照', `type` VARCHAR(32) NOT NULL COMMENT '题型快照', `difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照', `options` JSON NOT NULL COMMENT '选项快照 JSON(不含 isCorrect)', `selected_answer` TEXT DEFAULT NULL COMMENT '学生已选答案', `is_answered` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否已作答', `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_session_sequence` (`session_id`, `sequence`), KEY `idx_session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习会话题目快照'; -- ============================================= -- Post-migration verification queries -- ============================================= -- Verify tables exist with correct structure: -- SHOW CREATE TABLE education_practice_session; -- SHOW CREATE TABLE education_practice_question; -- Verify unique keys are enforced: -- SHOW INDEX FROM education_practice_session WHERE Key_name = 'uk_tenant_client_session'; -- SHOW INDEX FROM education_practice_question WHERE Key_name = '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;