Files
ruoyi-vue-pro/sql/postgresql/education/005-education-wrong-question.sql

157 lines
9.0 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 #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 = current_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 = current_database()
-- AND table_name IN ('education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 2.
-- =============================================
-- 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 '' /* 题目内容版本快照 */,
ADD COLUMN options TEXT DEFAULT NULL /* 选项快照 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 /* 复习会话题目指纹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 CONFLICT ... DO 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 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* 学生用户编号 */,
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
-- snapshot fields for listing / detail (from latest report detail)
stem TEXT NOT NULL /* 题干快照(最新) */,
type VARCHAR(32) NOT NULL /* 题型快照 */,
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
options JSONB NOT NULL /* 选项快照 JSON不含 isCorrect */,
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 题目内容版本 */,
latest_correct_answer TEXT DEFAULT NULL /* 正确答案快照(最新,供详情展示) */,
latest_explanation TEXT DEFAULT NULL /* 解析快照(最新,供详情展示) */,
-- timing & count
first_wrong_time TIMESTAMP NOT NULL /* 首次错误时间 */,
last_wrong_time TIMESTAMP NOT NULL /* 最近错误时间 */,
wrong_count INT NOT NULL DEFAULT 1 /* 累计错误次数 */,
-- mastery
master_status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
/* 掌握状态PENDING-待掌握, MASTERED-已掌握 */,
mastered_time TIMESTAMP DEFAULT NULL /* 标记掌握时间 */,
-- provenance
last_report_id BIGINT DEFAULT NULL /* 最近关联的报告 ID */,
last_session_id BIGINT DEFAULT NULL /* 最近关联的会话 ID */,
-- audit
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_wrong_question IS '教育-错题本';
CREATE UNIQUE INDEX uk_tenant_user_question ON education_wrong_question (tenant_id, user_id, question_id);
CREATE INDEX idx_tenant_user_status ON education_wrong_question (tenant_id, user_id, master_status);
CREATE INDEX idx_tenant_user_last_wrong ON education_wrong_question (tenant_id, user_id, last_wrong_time);
-- =============================================
-- 错题流水幂等表
-- =============================================
-- Purpose: Ensure each (tenant, user, question, report) upserts the
-- wrong-question book exactly once. The submitSession transaction
-- INSERT ... ON CONFLICT DO NOTHING 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 ... ON CONFLICT DO NOTHING 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 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
tenant_id BIGINT NOT NULL /* 租户编号 */,
user_id BIGINT NOT NULL /* 学生用户编号 */,
wrong_question_id BIGINT DEFAULT NULL /* 错题记录 IDupsert 后填充) */,
report_id BIGINT NOT NULL /* 报告 ID */,
question_id VARCHAR(64) NOT NULL /* 题目 ID */,
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_wrong_question_idempotency IS '教育-错题流水幂等';
CREATE UNIQUE INDEX uk_tenant_user_question_report ON education_wrong_question_idempotency (tenant_id, user_id, question_id, report_id);
CREATE INDEX idx_report ON education_wrong_question_idempotency (report_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- \d education_wrong_question
-- \d education_wrong_question_idempotency
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question' AND indexname = 'uk_tenant_user_question';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question_idempotency' AND indexname = '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;