forked from wangziqi/ruoyi-vue-pro
feat(education): complete Flyway migration and atomic submit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- Education 模块 Flyway 起始迁移(PostgreSQL)
|
||||
--
|
||||
-- 既有数据库在首次启动时会建立 V4009 baseline,本文件作为其后的首个受管版本。
|
||||
-- 当前版本仅验证 Flyway 接管链路;后续数据库变更请新增更高版本的 migration,
|
||||
-- 不要修改已经在共享环境执行过的脚本。
|
||||
|
||||
SELECT 1;
|
||||
@@ -0,0 +1,401 @@
|
||||
-- Education 原生题库目录(PostgreSQL)
|
||||
--
|
||||
-- 目录数据采用显式 scope:
|
||||
-- TENANT_OWNED - 仅归属租户可见;
|
||||
-- PUBLIC - 所有已认证租户可读,tenant_id 固定为 0。
|
||||
-- 应用层只能通过受控目录查询接缝读取 PUBLIC 数据,不得全局关闭租户隔离。
|
||||
|
||||
CREATE TABLE education_region (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
legacy_id VARCHAR(64),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(32),
|
||||
short_name VARCHAR(50),
|
||||
full_name VARCHAR(200),
|
||||
icon VARCHAR(500),
|
||||
pinyin VARCHAR(200),
|
||||
is_hot BOOLEAN NOT NULL DEFAULT false,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_region_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
)
|
||||
);
|
||||
COMMENT ON TABLE education_region IS '教育-地区';
|
||||
COMMENT ON COLUMN education_region.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_region_tenant_active_order
|
||||
ON education_region (tenant_id, is_active, sort_order) WHERE deleted = false;
|
||||
CREATE INDEX idx_education_region_public_active_order
|
||||
ON education_region (is_active, sort_order) WHERE deleted = false AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_school (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
legacy_id VARCHAR(64),
|
||||
region_id BIGINT,
|
||||
module_id BIGINT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
professional_exam_date VARCHAR(200),
|
||||
metadata JSONB,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_school_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_school_region FOREIGN KEY (region_id) REFERENCES education_region (id)
|
||||
);
|
||||
COMMENT ON TABLE education_school IS '教育-院校';
|
||||
COMMENT ON COLUMN education_school.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_school_tenant_region_order
|
||||
ON education_school (tenant_id, region_id, sort_order) WHERE deleted = false AND is_active = true;
|
||||
CREATE INDEX idx_education_school_public_region_order
|
||||
ON education_school (region_id, sort_order) WHERE deleted = false AND is_active = true AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_major (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
legacy_id VARCHAR(64),
|
||||
region_id BIGINT,
|
||||
school_id BIGINT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
study_tips VARCHAR(500),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_major_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_major_region FOREIGN KEY (region_id) REFERENCES education_region (id),
|
||||
CONSTRAINT fk_education_major_school FOREIGN KEY (school_id) REFERENCES education_school (id)
|
||||
);
|
||||
COMMENT ON TABLE education_major IS '教育-专业';
|
||||
COMMENT ON COLUMN education_major.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_major_tenant_school_order
|
||||
ON education_major (tenant_id, school_id, sort_order) WHERE deleted = false AND is_active = true;
|
||||
CREATE INDEX idx_education_major_public_school_order
|
||||
ON education_major (school_id, sort_order) WHERE deleted = false AND is_active = true AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_subject (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
region_id BIGINT,
|
||||
school_id BIGINT,
|
||||
major_id BIGINT,
|
||||
module_id BIGINT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_subject_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_subject_region FOREIGN KEY (region_id) REFERENCES education_region (id),
|
||||
CONSTRAINT fk_education_subject_school FOREIGN KEY (school_id) REFERENCES education_school (id),
|
||||
CONSTRAINT fk_education_subject_major FOREIGN KEY (major_id) REFERENCES education_major (id)
|
||||
);
|
||||
COMMENT ON TABLE education_subject IS '教育-科目';
|
||||
COMMENT ON COLUMN education_subject.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_subject_tenant_filters
|
||||
ON education_subject (tenant_id, region_id, school_id, major_id, type, sort_order)
|
||||
WHERE deleted = false AND is_active = true;
|
||||
CREATE INDEX idx_education_subject_public_filters
|
||||
ON education_subject (region_id, school_id, major_id, type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_category (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
subject_id BIGINT,
|
||||
legacy_node_id VARCHAR(64),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_category_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_category_subject FOREIGN KEY (subject_id) REFERENCES education_subject (id)
|
||||
);
|
||||
COMMENT ON TABLE education_category IS '教育-题目分类';
|
||||
COMMENT ON COLUMN education_category.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_category_tenant_subject_order
|
||||
ON education_category (tenant_id, subject_id, sort_order) WHERE deleted = false AND is_active = true;
|
||||
CREATE INDEX idx_education_category_public_subject_order
|
||||
ON education_category (subject_id, sort_order) WHERE deleted = false AND is_active = true AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_content_entry (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
legacy_id VARCHAR(64),
|
||||
region_id BIGINT,
|
||||
entry_key VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
entry_type VARCHAR(50) NOT NULL,
|
||||
icon VARCHAR(500),
|
||||
route VARCHAR(200),
|
||||
description VARCHAR(500),
|
||||
access_rules JSONB,
|
||||
layout_config JSONB,
|
||||
is_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_content_entry_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_content_entry_region FOREIGN KEY (region_id) REFERENCES education_region (id),
|
||||
CONSTRAINT uk_education_content_entry_scope_key UNIQUE (tenant_id, entry_key)
|
||||
);
|
||||
COMMENT ON TABLE education_content_entry IS '教育-内容入口';
|
||||
COMMENT ON COLUMN education_content_entry.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
COMMENT ON COLUMN education_content_entry.is_hidden IS '是否对学生端隐藏';
|
||||
CREATE INDEX idx_education_content_entry_tenant_filters
|
||||
ON education_content_entry (tenant_id, region_id, entry_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false;
|
||||
CREATE INDEX idx_education_content_entry_public_filters
|
||||
ON education_content_entry (region_id, entry_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_content_node (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
entry_id BIGINT NOT NULL,
|
||||
parent_id BIGINT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
title VARCHAR(200),
|
||||
node_type VARCHAR(50) NOT NULL,
|
||||
marker_type VARCHAR(50),
|
||||
depth INTEGER NOT NULL DEFAULT 0,
|
||||
is_leaf BOOLEAN NOT NULL DEFAULT false,
|
||||
is_selectable BOOLEAN NOT NULL DEFAULT true,
|
||||
is_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
metadata JSONB,
|
||||
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,
|
||||
CONSTRAINT ck_education_content_node_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_content_node_entry FOREIGN KEY (entry_id) REFERENCES education_content_entry (id),
|
||||
CONSTRAINT fk_education_content_node_parent FOREIGN KEY (parent_id) REFERENCES education_content_node (id)
|
||||
);
|
||||
COMMENT ON TABLE education_content_node IS '教育-内容节点';
|
||||
COMMENT ON COLUMN education_content_node.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
COMMENT ON COLUMN education_content_node.is_hidden IS '是否对学生端隐藏';
|
||||
CREATE INDEX idx_education_content_node_tenant_tree
|
||||
ON education_content_node (tenant_id, entry_id, parent_id, marker_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false;
|
||||
CREATE INDEX idx_education_content_node_public_tree
|
||||
ON education_content_node (entry_id, parent_id, marker_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_question_collection (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
entry_id BIGINT,
|
||||
node_id BIGINT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
title VARCHAR(200),
|
||||
collection_type VARCHAR(50) NOT NULL,
|
||||
question_count INTEGER NOT NULL DEFAULT 0,
|
||||
duration_minutes INTEGER,
|
||||
access_rules JSONB,
|
||||
is_hidden BOOLEAN NOT NULL DEFAULT false,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
metadata JSONB,
|
||||
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,
|
||||
CONSTRAINT ck_education_question_collection_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_question_collection_entry FOREIGN KEY (entry_id) REFERENCES education_content_entry (id),
|
||||
CONSTRAINT fk_education_question_collection_node FOREIGN KEY (node_id) REFERENCES education_content_node (id)
|
||||
);
|
||||
COMMENT ON TABLE education_question_collection IS '教育-题集';
|
||||
COMMENT ON COLUMN education_question_collection.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
COMMENT ON COLUMN education_question_collection.is_hidden IS '是否对学生端隐藏';
|
||||
CREATE INDEX idx_education_question_collection_tenant_filters
|
||||
ON education_question_collection (tenant_id, entry_id, node_id, collection_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false;
|
||||
CREATE INDEX idx_education_question_collection_public_filters
|
||||
ON education_question_collection (entry_id, node_id, collection_type, sort_order)
|
||||
WHERE deleted = false AND is_active = true AND is_hidden = false AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_question (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
content_version INTEGER NOT NULL DEFAULT 1,
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
type_label VARCHAR(50),
|
||||
difficulty VARCHAR(32),
|
||||
question_content JSONB,
|
||||
options JSONB NOT NULL,
|
||||
correct_answer VARCHAR(500),
|
||||
explanation TEXT,
|
||||
analysis TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED',
|
||||
is_published BOOLEAN NOT NULL DEFAULT true,
|
||||
subject_id BIGINT,
|
||||
node_id BIGINT,
|
||||
tags JSONB,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
metadata JSONB,
|
||||
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,
|
||||
CONSTRAINT ck_education_question_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT ck_education_question_version CHECK (content_version > 0),
|
||||
CONSTRAINT ck_education_question_status CHECK (status IN ('DRAFT', 'PUBLISHED', 'HIDDEN', 'INACTIVE')),
|
||||
CONSTRAINT fk_education_question_subject FOREIGN KEY (subject_id) REFERENCES education_subject (id),
|
||||
CONSTRAINT fk_education_question_node FOREIGN KEY (node_id) REFERENCES education_content_node (id)
|
||||
);
|
||||
COMMENT ON TABLE education_question IS '教育-题目';
|
||||
COMMENT ON COLUMN education_question.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
COMMENT ON COLUMN education_question.options IS '选项 JSONB;学生端响应必须剥离 isCorrect';
|
||||
COMMENT ON COLUMN education_question.correct_answer IS '正确答案;提交前不得返回';
|
||||
COMMENT ON COLUMN education_question.explanation IS '答案解析;提交前不得返回';
|
||||
COMMENT ON COLUMN education_question.analysis IS '深度解析;提交前不得返回';
|
||||
CREATE INDEX idx_education_question_tenant_filters
|
||||
ON education_question (tenant_id, node_id, subject_id, type, difficulty, sort_order)
|
||||
WHERE deleted = false AND is_published = true AND status = 'PUBLISHED';
|
||||
CREATE INDEX idx_education_question_public_filters
|
||||
ON education_question (node_id, subject_id, type, difficulty, sort_order)
|
||||
WHERE deleted = false AND is_published = true AND status = 'PUBLISHED' AND scope = 'PUBLIC';
|
||||
|
||||
CREATE TABLE education_practice_blueprint (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
mode VARCHAR(50) NOT NULL,
|
||||
entry_id BIGINT,
|
||||
node_id BIGINT,
|
||||
collection_id BIGINT,
|
||||
question_limit INTEGER,
|
||||
duration_minutes INTEGER,
|
||||
eligible_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
available_types JSONB,
|
||||
available_difficulties JSONB,
|
||||
min_questions INTEGER NOT NULL DEFAULT 1,
|
||||
max_questions INTEGER NOT NULL DEFAULT 200,
|
||||
suggested_count INTEGER NOT NULL DEFAULT 20,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
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,
|
||||
CONSTRAINT ck_education_practice_blueprint_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT ck_education_practice_blueprint_counts CHECK (
|
||||
min_questions > 0 AND max_questions >= min_questions AND suggested_count BETWEEN min_questions AND max_questions
|
||||
),
|
||||
CONSTRAINT fk_education_practice_blueprint_entry FOREIGN KEY (entry_id) REFERENCES education_content_entry (id),
|
||||
CONSTRAINT fk_education_practice_blueprint_node FOREIGN KEY (node_id) REFERENCES education_content_node (id),
|
||||
CONSTRAINT fk_education_practice_blueprint_collection FOREIGN KEY (collection_id) REFERENCES education_question_collection (id)
|
||||
);
|
||||
COMMENT ON TABLE education_practice_blueprint IS '教育-练习蓝图';
|
||||
COMMENT ON COLUMN education_practice_blueprint.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_practice_blueprint_tenant_filters
|
||||
ON education_practice_blueprint (tenant_id, entry_id, node_id, collection_id, mode)
|
||||
WHERE deleted = false AND is_active = true;
|
||||
CREATE INDEX idx_education_practice_blueprint_public_filters
|
||||
ON education_practice_blueprint (entry_id, node_id, collection_id, mode)
|
||||
WHERE deleted = false AND is_active = true AND scope = 'PUBLIC';
|
||||
|
||||
-- 题集-题目关联是题集成员关系的唯一事实源;题目表不保存 collection_id。
|
||||
CREATE TABLE education_question_collection_question (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'TENANT_OWNED',
|
||||
collection_id BIGINT NOT NULL,
|
||||
question_id BIGINT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
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,
|
||||
CONSTRAINT ck_education_collection_question_scope CHECK (
|
||||
(scope = 'PUBLIC' AND tenant_id = 0) OR
|
||||
(scope = 'TENANT_OWNED' AND tenant_id > 0)
|
||||
),
|
||||
CONSTRAINT fk_education_collection_question_collection FOREIGN KEY (collection_id) REFERENCES education_question_collection (id),
|
||||
CONSTRAINT fk_education_collection_question_question FOREIGN KEY (question_id) REFERENCES education_question (id),
|
||||
CONSTRAINT uk_education_collection_question UNIQUE (tenant_id, collection_id, question_id)
|
||||
);
|
||||
COMMENT ON TABLE education_question_collection_question IS '教育-题集题目关联,题集成员关系唯一事实源';
|
||||
COMMENT ON COLUMN education_question_collection_question.scope IS '内容范围:PUBLIC、TENANT_OWNED';
|
||||
CREATE INDEX idx_education_collection_question_order
|
||||
ON education_question_collection_question (tenant_id, collection_id, sort_order) WHERE deleted = false;
|
||||
CREATE INDEX idx_education_collection_question_public_order
|
||||
ON education_question_collection_question (collection_id, sort_order) WHERE deleted = false AND scope = 'PUBLIC';
|
||||
|
||||
-- Verification examples:
|
||||
-- SELECT table_name FROM information_schema.tables
|
||||
-- WHERE table_schema = 'public' AND table_name LIKE 'education_%' ORDER BY table_name;
|
||||
-- SELECT conname FROM pg_constraint
|
||||
-- WHERE conrelid IN ('education_question'::regclass, 'education_question_collection_question'::regclass)
|
||||
-- ORDER BY conname;
|
||||
@@ -0,0 +1,398 @@
|
||||
-- Education Practice core-loop schema and forward adoption (PostgreSQL)
|
||||
--
|
||||
-- Fresh databases receive the final unified schema. Existing compatible manual
|
||||
-- installations are adopted in place. Legacy answer/submit idempotency tables
|
||||
-- are read for backfill when present, but are never created or dropped here.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_practice_session (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
client_session_id VARCHAR(36) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
question_count INTEGER NOT NULL DEFAULT 0,
|
||||
collection_id VARCHAR(64),
|
||||
node_id VARCHAR(64),
|
||||
type VARCHAR(32),
|
||||
difficulty VARCHAR(32),
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
last_client_sequence INTEGER,
|
||||
review_fingerprint VARCHAR(64),
|
||||
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_session IS '教育-练习会话';
|
||||
ALTER TABLE education_practice_session
|
||||
ADD COLUMN IF NOT EXISTS last_client_sequence INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS review_fingerprint VARCHAR(64);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_practice_session_tenant_client
|
||||
ON education_practice_session (tenant_id, client_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_practice_session_tenant_user_status
|
||||
ON education_practice_session (tenant_id, user_id, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_practice_question (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
sequence INTEGER NOT NULL,
|
||||
question_id VARCHAR(64) NOT NULL,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
difficulty VARCHAR(32),
|
||||
options JSONB NOT NULL,
|
||||
correct_answer TEXT,
|
||||
explanation TEXT,
|
||||
selected_answer TEXT,
|
||||
is_answered BOOLEAN NOT NULL DEFAULT false,
|
||||
client_sequence INTEGER,
|
||||
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_question IS '教育-练习会话题目快照';
|
||||
ALTER TABLE education_practice_question
|
||||
ADD COLUMN IF NOT EXISTS correct_answer TEXT,
|
||||
ADD COLUMN IF NOT EXISTS explanation TEXT,
|
||||
ADD COLUMN IF NOT EXISTS client_sequence INTEGER;
|
||||
DO $$
|
||||
DECLARE
|
||||
options_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO options_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_practice_question'
|
||||
AND column_name = 'options';
|
||||
IF options_type = 'text' THEN
|
||||
BEGIN
|
||||
ALTER TABLE education_practice_question
|
||||
ALTER COLUMN options TYPE JSONB USING options::JSONB;
|
||||
EXCEPTION WHEN invalid_text_representation THEN
|
||||
RAISE EXCEPTION 'education_practice_question.options contains invalid JSON and cannot be adopted';
|
||||
END;
|
||||
ELSIF options_type <> 'jsonb' THEN
|
||||
RAISE EXCEPTION 'education_practice_question.options has incompatible type %', options_type;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_practice_question_session_sequence
|
||||
ON education_practice_question (session_id, sequence);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_practice_question_session
|
||||
ON education_practice_question (session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_practice_report (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_count INTEGER NOT NULL,
|
||||
answered_count INTEGER NOT NULL DEFAULT 0,
|
||||
unanswered_count INTEGER NOT NULL DEFAULT 0,
|
||||
correct_count INTEGER NOT NULL DEFAULT 0,
|
||||
incorrect_count INTEGER NOT NULL DEFAULT 0,
|
||||
score INTEGER 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 '教育-练习报告';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_practice_report_tenant_session
|
||||
ON education_practice_report (tenant_id, session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_practice_report_tenant_user
|
||||
ON education_practice_report (tenant_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_practice_report_create_time
|
||||
ON education_practice_report (create_time);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_practice_report_detail (
|
||||
id BIGINT 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 INTEGER NOT NULL,
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
difficulty VARCHAR(32),
|
||||
selected_answer TEXT,
|
||||
correct_answer TEXT,
|
||||
is_correct BOOLEAN NOT NULL DEFAULT false,
|
||||
explanation TEXT,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
options JSONB,
|
||||
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 '教育-练习报告明细';
|
||||
ALTER TABLE education_practice_report_detail
|
||||
ADD COLUMN IF NOT EXISTS content_version VARCHAR(64) NOT NULL DEFAULT '';
|
||||
DO $$
|
||||
DECLARE
|
||||
options_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO options_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_practice_report_detail'
|
||||
AND column_name = 'options';
|
||||
IF options_type IS NULL THEN
|
||||
ALTER TABLE education_practice_report_detail ADD COLUMN options JSONB;
|
||||
ELSIF options_type = 'text' THEN
|
||||
BEGIN
|
||||
ALTER TABLE education_practice_report_detail
|
||||
ALTER COLUMN options TYPE JSONB USING options::JSONB;
|
||||
EXCEPTION WHEN invalid_text_representation THEN
|
||||
RAISE EXCEPTION 'education_practice_report_detail.options contains invalid JSON and cannot be adopted';
|
||||
END;
|
||||
ELSIF options_type <> 'jsonb' THEN
|
||||
RAISE EXCEPTION 'education_practice_report_detail.options has incompatible type %', options_type;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_practice_report_detail_report_sequence
|
||||
ON education_practice_report_detail (report_id, sequence);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_practice_report_detail_session
|
||||
ON education_practice_report_detail (session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
difficulty VARCHAR(32),
|
||||
options JSONB NOT NULL,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
latest_correct_answer TEXT,
|
||||
latest_explanation TEXT,
|
||||
first_wrong_time TIMESTAMP NOT NULL,
|
||||
last_wrong_time TIMESTAMP NOT NULL,
|
||||
wrong_count INTEGER NOT NULL DEFAULT 1,
|
||||
master_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
mastered_time TIMESTAMP,
|
||||
last_report_id BIGINT,
|
||||
last_session_id BIGINT,
|
||||
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 '教育-错题本';
|
||||
DO $$
|
||||
DECLARE
|
||||
options_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO options_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_wrong_question'
|
||||
AND column_name = 'options';
|
||||
IF options_type = 'text' THEN
|
||||
BEGIN
|
||||
ALTER TABLE education_wrong_question
|
||||
ALTER COLUMN options TYPE JSONB USING options::JSONB;
|
||||
EXCEPTION WHEN invalid_text_representation THEN
|
||||
RAISE EXCEPTION 'education_wrong_question.options contains invalid JSON and cannot be adopted';
|
||||
END;
|
||||
ELSIF options_type <> 'jsonb' THEN
|
||||
RAISE EXCEPTION 'education_wrong_question.options has incompatible type %', options_type;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_wrong_question_tenant_user_question
|
||||
ON education_wrong_question (tenant_id, user_id, question_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_wrong_question_tenant_user_status
|
||||
ON education_wrong_question (tenant_id, user_id, master_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_wrong_question_tenant_user_last_wrong
|
||||
ON education_wrong_question (tenant_id, user_id, last_wrong_time);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
report_id BIGINT NOT NULL,
|
||||
question_id VARCHAR(64) 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_wrong_question_idempotency IS '教育-错题流水幂等';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_wrong_idempotency_tenant_user_question_report
|
||||
ON education_wrong_question_idempotency (tenant_id, user_id, question_id, report_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_wrong_question_idempotency_report
|
||||
ON education_wrong_question_idempotency (report_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_favorite (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL,
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
stem TEXT,
|
||||
type VARCHAR(32),
|
||||
difficulty VARCHAR(32),
|
||||
options JSONB,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
available BOOLEAN NOT NULL DEFAULT true,
|
||||
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_favorite IS '教育-收藏夹';
|
||||
DO $$
|
||||
DECLARE
|
||||
options_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO options_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_favorite'
|
||||
AND column_name = 'options';
|
||||
IF options_type = 'text' THEN
|
||||
BEGIN
|
||||
ALTER TABLE education_favorite
|
||||
ALTER COLUMN options TYPE JSONB USING options::JSONB;
|
||||
EXCEPTION WHEN invalid_text_representation THEN
|
||||
RAISE EXCEPTION 'education_favorite.options contains invalid JSON and cannot be adopted';
|
||||
END;
|
||||
ELSIF options_type <> 'jsonb' THEN
|
||||
RAISE EXCEPTION 'education_favorite.options has incompatible type %', options_type;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_favorite_tenant_user_target
|
||||
ON education_favorite (tenant_id, user_id, target_type, target_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_favorite_tenant_user
|
||||
ON education_favorite (tenant_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_favorite_tenant_user_type
|
||||
ON education_favorite (tenant_id, user_id, target_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS education_idempotency (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL,
|
||||
idempotency_key VARCHAR(64) NOT NULL,
|
||||
request_hash VARCHAR(64) NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_id VARCHAR(64),
|
||||
selected_answer TEXT,
|
||||
report_id BIGINT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||||
response_json TEXT,
|
||||
business_payload JSONB,
|
||||
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_idempotency IS '教育-统一幂等记录';
|
||||
DO $$
|
||||
DECLARE
|
||||
payload_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type INTO payload_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_idempotency'
|
||||
AND column_name = 'business_payload';
|
||||
IF payload_type IS NULL THEN
|
||||
ALTER TABLE education_idempotency ADD COLUMN business_payload JSONB;
|
||||
ELSIF payload_type = 'text' THEN
|
||||
BEGIN
|
||||
ALTER TABLE education_idempotency
|
||||
ALTER COLUMN business_payload TYPE JSONB USING business_payload::JSONB;
|
||||
EXCEPTION WHEN invalid_text_representation THEN
|
||||
RAISE EXCEPTION 'education_idempotency.business_payload contains invalid JSON and cannot be adopted';
|
||||
END;
|
||||
ELSIF payload_type <> 'jsonb' THEN
|
||||
RAISE EXCEPTION 'education_idempotency.business_payload has incompatible type %', payload_type;
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_education_idempotency_tenant_user_operation_key
|
||||
ON education_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_idempotency_tenant_session
|
||||
ON education_idempotency (tenant_id, session_id);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('education_answer_idempotency') IS NOT NULL THEN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM education_answer_idempotency
|
||||
GROUP BY tenant_id, user_id, idempotency_key
|
||||
HAVING COUNT(DISTINCT request_hash) > 1
|
||||
) THEN
|
||||
RAISE EXCEPTION 'education_answer_idempotency contains conflicting duplicate request_hash values';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM education_answer_idempotency legacy
|
||||
JOIN education_idempotency unified
|
||||
ON unified.tenant_id = legacy.tenant_id
|
||||
AND unified.user_id = legacy.user_id
|
||||
AND unified.operation = 'SUBMIT_ANSWER'
|
||||
AND unified.idempotency_key = legacy.idempotency_key
|
||||
WHERE unified.request_hash <> legacy.request_hash
|
||||
) THEN
|
||||
RAISE EXCEPTION 'education_answer_idempotency conflicts with education_idempotency request_hash';
|
||||
END IF;
|
||||
INSERT INTO education_idempotency
|
||||
(tenant_id, user_id, operation, idempotency_key, request_hash, session_id,
|
||||
question_id, selected_answer, status, response_json,
|
||||
creator, create_time, updater, update_time, deleted)
|
||||
SELECT tenant_id, user_id, 'SUBMIT_ANSWER', idempotency_key, request_hash, session_id,
|
||||
question_id, selected_answer, status, response_json,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
FROM education_answer_idempotency
|
||||
ON CONFLICT (tenant_id, user_id, operation, idempotency_key) DO NOTHING;
|
||||
END IF;
|
||||
|
||||
IF to_regclass('education_submit_idempotency') IS NOT NULL THEN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM education_submit_idempotency
|
||||
GROUP BY tenant_id, user_id, idempotency_key
|
||||
HAVING COUNT(DISTINCT request_hash) > 1
|
||||
) THEN
|
||||
RAISE EXCEPTION 'education_submit_idempotency contains conflicting duplicate request_hash values';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM education_submit_idempotency legacy
|
||||
JOIN education_idempotency unified
|
||||
ON unified.tenant_id = legacy.tenant_id
|
||||
AND unified.user_id = legacy.user_id
|
||||
AND unified.operation = 'SUBMIT_SESSION'
|
||||
AND unified.idempotency_key = legacy.idempotency_key
|
||||
WHERE unified.request_hash <> legacy.request_hash
|
||||
) THEN
|
||||
RAISE EXCEPTION 'education_submit_idempotency conflicts with education_idempotency request_hash';
|
||||
END IF;
|
||||
INSERT INTO education_idempotency
|
||||
(tenant_id, user_id, operation, idempotency_key, request_hash, session_id,
|
||||
report_id, status, response_json,
|
||||
creator, create_time, updater, update_time, deleted)
|
||||
SELECT tenant_id, user_id, 'SUBMIT_SESSION', idempotency_key, request_hash, session_id,
|
||||
report_id, status, response_json,
|
||||
creator, create_time, updater, update_time, deleted
|
||||
FROM education_submit_idempotency
|
||||
ON CONFLICT (tenant_id, user_id, operation, idempotency_key) DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,51 @@
|
||||
ALTER TABLE education_idempotency
|
||||
ADD COLUMN IF NOT EXISTS claim_token VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS claim_started_at TIMESTAMP;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
claim_token_type TEXT;
|
||||
claim_token_length INTEGER;
|
||||
claim_started_at_type TEXT;
|
||||
BEGIN
|
||||
SELECT data_type, character_maximum_length INTO claim_token_type, claim_token_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_idempotency'
|
||||
AND column_name = 'claim_token';
|
||||
IF claim_token_type <> 'character varying' OR claim_token_length < 36 THEN
|
||||
RAISE EXCEPTION 'education_idempotency.claim_token must be VARCHAR(36+), found %(%)',
|
||||
claim_token_type, claim_token_length;
|
||||
END IF;
|
||||
|
||||
SELECT data_type INTO claim_started_at_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'education_idempotency'
|
||||
AND column_name = 'claim_started_at';
|
||||
IF claim_started_at_type <> 'timestamp without time zone' THEN
|
||||
RAISE EXCEPTION 'education_idempotency.claim_started_at must be timestamp without time zone, found %',
|
||||
claim_started_at_type;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN education_idempotency.claim_token IS '当前幂等处理租约令牌;完成或非交卷记录为空';
|
||||
COMMENT ON COLUMN education_idempotency.claim_started_at IS '当前幂等处理租约开始时间;用于进程崩溃后的超时接管';
|
||||
|
||||
UPDATE education_idempotency
|
||||
SET status = 'PROCESSING',
|
||||
claim_token = NULL,
|
||||
claim_started_at = create_time
|
||||
WHERE operation = 'SUBMIT_SESSION'
|
||||
AND status = 'ACCEPTED'
|
||||
AND (report_id IS NULL OR response_json IS NULL OR BTRIM(response_json) = '');
|
||||
|
||||
UPDATE education_idempotency
|
||||
SET status = 'COMPLETED',
|
||||
claim_token = NULL,
|
||||
claim_started_at = NULL
|
||||
WHERE operation = 'SUBMIT_SESSION'
|
||||
AND status = 'ACCEPTED'
|
||||
AND report_id IS NOT NULL
|
||||
AND response_json IS NOT NULL
|
||||
AND BTRIM(response_json) <> '';
|
||||
Reference in New Issue
Block a user