-- ============================================= -- Education 模块 — 错题本 DDL -- Ticket #9: 错题自动收集、复习练习创建 -- Migration: 005 -- Prerequisites: 004-education-submit-report.sql (report + detail tables) -- ============================================= -- ============================================= -- Preconditions -- ============================================= -- Operator is expected to verify: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_schema = DATABASE() -- AND table_name IN ('education_wrong_question', -- 'education_wrong_question_idempotency'); -- Result MUST be 0 before executing this migration. -- -- Verify prerequisite tables exist: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_schema = DATABASE() -- AND table_name IN ('education_practice_report', -- 'education_practice_report_detail'); -- Result MUST be 2. -- ============================================= -- PracticeReportDetailDO: add options snapshot column -- ============================================= -- PracticeReportDetailDO: add content_version + options snapshot columns -- ============================================= -- Purpose: At submit time, snapshot question content version and options -- (without isCorrect) so wrong-question book and review sessions have -- stable display data. The options are already stripped of isCorrect -- by the submit flow. ALTER TABLE `education_practice_report_detail` ADD COLUMN `content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '题目内容版本快照', ADD COLUMN `options` TEXT DEFAULT NULL COMMENT '选项快照 JSON(不含 isCorrect)'; -- ============================================= -- PracticeSessionDO: add review fingerprint column -- ============================================= -- Purpose: Persist the canonical fingerprint of wrong-question IDs used -- to create a review session. On idempotent replay, the fingerprint -- is compared: same tenant+clientSessionId+sameUser+sortedIDs match -- returns the existing session; different ID set returns -- SESSION_IDEMPOTENCY_MISMATCH. ALTER TABLE `education_practice_session` ADD COLUMN `review_fingerprint` VARCHAR(64) DEFAULT NULL COMMENT '复习会话题目指纹(SHA-256 of sorted unique wrongQuestionIds)'; -- 错题表 -- ============================================= -- Purpose: Persistent wrong-question book per student. -- Each (tenant, user, question) is a unique entry. -- Repeated wrong answers on the SAME question increment wrong_count -- and update last_wrong_time. The idempotency guard table ensures -- each (tenant, user, question, report) can upsert at most once. -- -- master_status values: 'PENDING' (default) | 'MASTERED' -- Marking mastered retains the full history and count; it does NOT -- delete or archive the record. Students can optionally un-master. -- -- Snapshot fields (stem, type, difficulty, options, content_version): -- populated from the latest report detail that touched this question. -- These are for listing/detail display without joining report details. -- -- latest_correct_answer, latest_explanation: -- also from the latest report detail; available for detail display -- post-submit (not exposed in review session creation pre-submit). -- -- Indexes: -- uk_tenant_user_question — per-tenant, per-user, per-question uniqueness. -- INSERT ... ON DUPLICATE KEY UPDATE is the primary write path. -- idx_tenant_user_status — covers filtered list queries (page with status filter). -- idx_tenant_user_last_wrong — covers time-sorted listing. CREATE TABLE `education_wrong_question` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT '学生用户编号', `question_id` VARCHAR(64) NOT NULL COMMENT '原始题目 ID', -- snapshot fields for listing / detail (from latest report detail) `stem` TEXT NOT NULL COMMENT '题干快照(最新)', `type` VARCHAR(32) NOT NULL COMMENT '题型快照', `difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度快照', `options` JSON NOT NULL COMMENT '选项快照 JSON(不含 isCorrect)', `content_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '题目内容版本', `latest_correct_answer` TEXT DEFAULT NULL COMMENT '正确答案快照(最新,供详情展示)', `latest_explanation` TEXT DEFAULT NULL COMMENT '解析快照(最新,供详情展示)', -- timing & count `first_wrong_time` DATETIME NOT NULL COMMENT '首次错误时间', `last_wrong_time` DATETIME NOT NULL COMMENT '最近错误时间', `wrong_count` INT NOT NULL DEFAULT 1 COMMENT '累计错误次数', -- mastery `master_status` VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '掌握状态:PENDING-待掌握, MASTERED-已掌握', `mastered_time` DATETIME DEFAULT NULL COMMENT '标记掌握时间', -- provenance `last_report_id` BIGINT DEFAULT NULL COMMENT '最近关联的报告 ID', `last_session_id` BIGINT DEFAULT NULL COMMENT '最近关联的会话 ID', -- audit `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_user_question` (`tenant_id`, `user_id`, `question_id`), KEY `idx_tenant_user_status` (`tenant_id`, `user_id`, `master_status`), KEY `idx_tenant_user_last_wrong` (`tenant_id`, `user_id`, `last_wrong_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-错题本'; -- ============================================= -- 错题流水幂等表 -- ============================================= -- Purpose: Ensure each (tenant, user, question, report) upserts the -- wrong-question book exactly once. The submitSession transaction -- INSERT IGNOREs into this table BEFORE the wrong question upsert; -- a duplicate means this report already contributed to the count. -- This guards against: -- - Replayed submit (idempotent resubmit) double-counting -- - Concurrent submit races where both threads evaluate the -- same report details -- -- Indexes: -- uk_tenant_user_question_report — per (tenant, user, question, report) uniqueness. -- INSERT IGNORE provides the idempotency guard BEFORE upserting. -- wrong_question_id is filled after upsert for audit purposes. -- idx_report — fast lookup by report for audit/debug. CREATE TABLE `education_wrong_question_idempotency` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` BIGINT NOT NULL COMMENT '租户编号', `user_id` BIGINT NOT NULL COMMENT '学生用户编号', `wrong_question_id` BIGINT DEFAULT NULL COMMENT '错题记录 ID(upsert 后填充)', `report_id` BIGINT NOT NULL COMMENT '报告 ID', `question_id` VARCHAR(64) NOT NULL COMMENT '题目 ID', `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_user_question_report` (`tenant_id`, `user_id`, `question_id`, `report_id`), KEY `idx_report` (`report_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-错题流水幂等'; -- ============================================= -- Post-migration verification queries -- ============================================= -- Verify new tables exist: -- SHOW CREATE TABLE education_wrong_question; -- SHOW CREATE TABLE education_wrong_question_idempotency; -- Verify unique keys are enforced: -- SHOW INDEX FROM education_wrong_question WHERE Key_name = 'uk_tenant_user_question'; -- SHOW INDEX FROM education_wrong_question_idempotency WHERE Key_name = 'uk_tenant_user_question_report'; -- Verify no orphan data (should be 0 after fresh migration): -- SELECT COUNT(*) FROM education_wrong_question; -- SELECT COUNT(*) FROM education_wrong_question_idempotency;