Files
ruoyi-vue-pro/sql/postgresql/education/004-education-submit-report.sql

221 lines
12 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 (PostgreSQL)
-- Ticket #8: 交卷 CAS、保护性答案快照、评分与报告
-- Migration: 004
-- Prerequisites: 003-education-answer-idempotency.sql
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Operator is expected to verify:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN ('education_submit_idempotency',
-- 'education_practice_report',
-- 'education_practice_report_detail');
-- Result MUST be 0 before executing this migration.
--
-- Verify prerequisite tables exist:
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN ('education_practice_session',
-- 'education_practice_question',
-- 'education_answer_idempotency');
-- Result MUST be 3.
-- =============================================
-- PracticeQuestionDO: add protected answer snapshot columns
-- =============================================
-- Purpose: At session creation, snapshot correct_answer and explanation
-- from the full CatalogQuestionDTO. These fields are NEVER exposed
-- before submission (enforced by SafeQuestionRespVO allow-list and
-- PracticeQuestionRespVO which does not include them).
ALTER TABLE education_practice_question
ADD COLUMN correct_answer TEXT DEFAULT NULL,
ADD COLUMN explanation TEXT DEFAULT NULL;
COMMENT ON COLUMN education_practice_question.correct_answer IS '正确答案快照(不可在交卷前暴露)';
COMMENT ON COLUMN education_practice_question.explanation IS '解析快照(不可在交卷前暴露)';
-- =============================================
-- 交卷幂等表
-- =============================================
-- Purpose: Provide durable idempotency for submit-session commands.
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original report.
-- Same key + different request_hash → conflict.
-- Concurrent same-key inserts resolved by unique constraint race handling.
--
-- Indexes:
-- uk_submit_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
-- INSERT during submit. DuplicateKeyException catch for concurrent-create race resolution.
-- idx_submit_session — covers lookup by session for audit/debug.
CREATE TABLE education_submit_idempotency (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
operation VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_SESSION',
idempotency_key VARCHAR(64) NOT NULL,
request_hash VARCHAR(64) NOT NULL,
session_id BIGINT NOT NULL,
report_id BIGINT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
response_json TEXT NOT NULL,
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_submit_idempotency IS '教育-交卷幂等记录';
COMMENT ON COLUMN education_submit_idempotency.id IS '主键';
COMMENT ON COLUMN education_submit_idempotency.tenant_id IS '租户编号';
COMMENT ON COLUMN education_submit_idempotency.user_id IS '交卷用户编号';
COMMENT ON COLUMN education_submit_idempotency.operation IS '操作类型SUBMIT_SESSION';
COMMENT ON COLUMN education_submit_idempotency.idempotency_key IS '客户端幂等键UUID';
COMMENT ON COLUMN education_submit_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
COMMENT ON COLUMN education_submit_idempotency.session_id IS '会话 ID';
COMMENT ON COLUMN education_submit_idempotency.report_id IS '关联的报告 ID成功时有值';
COMMENT ON COLUMN education_submit_idempotency.status IS '状态ACCEPTED-已接受, CONFLICT-冲突';
COMMENT ON COLUMN education_submit_idempotency.response_json IS '首次成功响应 JSON用于重试重放';
COMMENT ON COLUMN education_submit_idempotency.creator IS '创建者';
COMMENT ON COLUMN education_submit_idempotency.create_time IS '创建时间';
COMMENT ON COLUMN education_submit_idempotency.updater IS '更新者';
COMMENT ON COLUMN education_submit_idempotency.update_time IS '更新时间';
COMMENT ON COLUMN education_submit_idempotency.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_submit_idempotency ON education_submit_idempotency (tenant_id, user_id, operation, idempotency_key);
CREATE INDEX idx_submit_session ON education_submit_idempotency (tenant_id, session_id);
-- =============================================
-- 练习报告表(会话级)
-- =============================================
-- Purpose: Store the computed scoring result for a submitted session.
-- One report per session. Immutable after creation.
-- Question snapshots (stem, selectedAnswer, correctAnswer, explanation)
-- are stored in report_details so source question edits don't affect history.
--
-- Indexes:
-- uk_report_session — one report per session (unique).
-- idx_report_tenant_user — covers paginated history queries for current tenant+user.
-- idx_report_create_time — covers time-sorted listing.
CREATE TABLE education_practice_report (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
session_id BIGINT NOT NULL,
question_count INT NOT NULL,
answered_count INT NOT NULL DEFAULT 0,
unanswered_count INT NOT NULL DEFAULT 0,
correct_count INT NOT NULL DEFAULT 0,
incorrect_count INT NOT NULL DEFAULT 0,
score INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED',
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_report IS '教育-练习报告';
COMMENT ON COLUMN education_practice_report.id IS '主键';
COMMENT ON COLUMN education_practice_report.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_report.user_id IS '用户编号';
COMMENT ON COLUMN education_practice_report.session_id IS '会话 ID';
COMMENT ON COLUMN education_practice_report.question_count IS '题目总数';
COMMENT ON COLUMN education_practice_report.answered_count IS '已答题数';
COMMENT ON COLUMN education_practice_report.unanswered_count IS '未答题数';
COMMENT ON COLUMN education_practice_report.correct_count IS '正确题数';
COMMENT ON COLUMN education_practice_report.incorrect_count IS '错误题数';
COMMENT ON COLUMN education_practice_report.score IS '得分(整数,满分 100 为基准)';
COMMENT ON COLUMN education_practice_report.status IS '报告状态SUBMITTED';
COMMENT ON COLUMN education_practice_report.creator IS '创建者';
COMMENT ON COLUMN education_practice_report.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_report.updater IS '更新者';
COMMENT ON COLUMN education_practice_report.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_report.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_report_session ON education_practice_report (session_id);
CREATE INDEX idx_report_tenant_user ON education_practice_report (tenant_id, user_id);
CREATE INDEX idx_report_create_time ON education_practice_report (create_time);
-- =============================================
-- 练习报告明细表(逐题结果)
-- =============================================
-- Purpose: Store per-question scoring results at submission time.
-- Includes snapshot of stem, selectedAnswer, correctAnswer, and explanation
-- so that history is stable even if source questions are later edited.
--
-- Indexes:
-- uk_report_sequence — per-report uniqueness for question sequence.
-- idx_detail_session — covers lookup by session for report assembly.
CREATE TABLE education_practice_report_detail (
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
report_id BIGINT NOT NULL,
session_id BIGINT NOT NULL,
question_id VARCHAR(64) NOT NULL,
sequence INT NOT NULL,
stem TEXT NOT NULL,
type VARCHAR(32) NOT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
selected_answer TEXT DEFAULT NULL,
correct_answer TEXT DEFAULT NULL,
is_correct BOOLEAN NOT NULL DEFAULT false,
explanation TEXT DEFAULT NULL,
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_report_detail IS '教育-练习报告明细';
COMMENT ON COLUMN education_practice_report_detail.id IS '主键';
COMMENT ON COLUMN education_practice_report_detail.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_report_detail.user_id IS '用户编号';
COMMENT ON COLUMN education_practice_report_detail.report_id IS '报告 ID';
COMMENT ON COLUMN education_practice_report_detail.session_id IS '会话 ID';
COMMENT ON COLUMN education_practice_report_detail.question_id IS '原始题目 ID';
COMMENT ON COLUMN education_practice_report_detail.sequence IS '题目序号1-based';
COMMENT ON COLUMN education_practice_report_detail.stem IS '题干快照';
COMMENT ON COLUMN education_practice_report_detail.type IS '题型快照';
COMMENT ON COLUMN education_practice_report_detail.difficulty IS '难度快照';
COMMENT ON COLUMN education_practice_report_detail.selected_answer IS '学生已选答案';
COMMENT ON COLUMN education_practice_report_detail.correct_answer IS '正确答案快照';
COMMENT ON COLUMN education_practice_report_detail.is_correct IS '是否正确';
COMMENT ON COLUMN education_practice_report_detail.explanation IS '解析快照';
COMMENT ON COLUMN education_practice_report_detail.creator IS '创建者';
COMMENT ON COLUMN education_practice_report_detail.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_report_detail.updater IS '更新者';
COMMENT ON COLUMN education_practice_report_detail.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_report_detail.deleted IS '是否删除';
CREATE UNIQUE INDEX uk_report_sequence ON education_practice_report_detail (report_id, sequence);
CREATE INDEX idx_detail_session ON education_practice_report_detail (session_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify new tables exist:
-- \d education_submit_idempotency
-- \d education_practice_report
-- \d education_practice_report_detail
-- Verify columns added to question table:
-- SELECT column_name, data_type, column_default
-- FROM information_schema.columns
-- WHERE table_catalog = current_database()
-- AND table_name = 'education_practice_question'
-- AND column_name IN ('correct_answer', 'explanation');
-- Verify unique keys are enforced:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_submit_idempotency' AND indexname = 'uk_submit_idempotency';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report' AND indexname = 'uk_report_session';
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report_detail' AND indexname = 'uk_report_sequence';
-- Verify no orphan data (should be 0 after fresh migration):
-- SELECT COUNT(*) FROM education_submit_idempotency;
-- SELECT COUNT(*) FROM education_practice_report;
-- SELECT COUNT(*) FROM education_practice_report_detail;