157 lines
9.0 KiB
SQL
157 lines
9.0 KiB
SQL
-- =============================================
|
||
-- 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 /* 错题记录 ID(upsert 后填充) */,
|
||
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;
|