221 lines
12 KiB
SQL
221 lines
12 KiB
SQL
-- =============================================
|
||
-- 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;
|