feat(education): complete Flyway migration and atomic submit

This commit is contained in:
2026-07-30 12:06:55 +08:00
parent ce02f8acb4
commit 79a5799502
228 changed files with 32551 additions and 1376 deletions

View File

@@ -0,0 +1,530 @@
-- =============================================
-- Education 模块 — 题库目录原生表 DDL (PostgreSQL)
-- Ticket #20: 原生实现题库目录浏览,替换 Scalar adapter
-- Migration: 008
-- Prerequisites: 000-education-schema.sql (database creation)
-- 001-education-tenant-seed.sql (tenant seed data)
-- =============================================
-- =============================================
-- Preconditions
-- =============================================
-- Verify none of these tables already exist:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name IN (
-- 'education_region', 'education_school', 'education_major',
-- 'education_subject', 'education_category',
-- 'education_content_entry', 'education_content_node',
-- 'education_question_collection', 'education_question',
-- 'education_practice_blueprint', 'education_question_collection_question'
-- );
-- Result MUST be 0 before executing this migration.
-- =============================================
-- 1. 地区表
-- =============================================
-- Indexes:
-- idx_tenant — tenant-scoped queries (tenant-aware interceptor default)
-- idx_tenant_active_order — active region listing ordered by display order
CREATE TABLE education_region (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
name VARCHAR(100) NOT NULL,
code VARCHAR(32) DEFAULT NULL,
short_name VARCHAR(50) DEFAULT NULL,
full_name VARCHAR(200) DEFAULT NULL,
icon VARCHAR(500) DEFAULT NULL,
pinyin VARCHAR(200) DEFAULT NULL,
is_hot BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_region IS '教育-地区';
COMMENT ON COLUMN education_region.id IS '主键';
COMMENT ON COLUMN education_region.tenant_id IS '租户编号';
COMMENT ON COLUMN education_region.legacy_id IS '旧系统地区 ID';
COMMENT ON COLUMN education_region.name IS '地区名称';
COMMENT ON COLUMN education_region.code IS '地区编码';
COMMENT ON COLUMN education_region.short_name IS '地区简称';
COMMENT ON COLUMN education_region.full_name IS '地区全称';
COMMENT ON COLUMN education_region.icon IS '地区图标 URL';
COMMENT ON COLUMN education_region.pinyin IS '地区拼音';
COMMENT ON COLUMN education_region.is_hot IS '是否热门地区';
COMMENT ON COLUMN education_region.is_active IS '是否启用';
COMMENT ON COLUMN education_region.sort_order IS '显示排序值';
COMMENT ON COLUMN education_region.creator IS '创建者';
COMMENT ON COLUMN education_region.create_time IS '创建时间';
COMMENT ON COLUMN education_region.updater IS '更新者';
COMMENT ON COLUMN education_region.update_time IS '更新时间';
COMMENT ON COLUMN education_region.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_region (tenant_id);
CREATE INDEX idx_tenant_active_order ON education_region (tenant_id, is_active, sort_order);
-- =============================================
-- 2. 院校表
-- =============================================
CREATE TABLE education_school (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
module_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
professional_exam_date VARCHAR(200) DEFAULT NULL,
metadata JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_school IS '教育-院校';
COMMENT ON COLUMN education_school.id IS '主键';
COMMENT ON COLUMN education_school.tenant_id IS '租户编号';
COMMENT ON COLUMN education_school.legacy_id IS '旧系统院校 ID';
COMMENT ON COLUMN education_school.region_id IS '地区 ID';
COMMENT ON COLUMN education_school.module_id IS '地区模块 ID';
COMMENT ON COLUMN education_school.name IS '院校名称';
COMMENT ON COLUMN education_school.professional_exam_date IS '专业考试日期说明';
COMMENT ON COLUMN education_school.metadata IS '院校扩展数据';
COMMENT ON COLUMN education_school.is_active IS '是否启用';
COMMENT ON COLUMN education_school.sort_order IS '显示排序值';
COMMENT ON COLUMN education_school.creator IS '创建者';
COMMENT ON COLUMN education_school.create_time IS '创建时间';
COMMENT ON COLUMN education_school.updater IS '更新者';
COMMENT ON COLUMN education_school.update_time IS '更新时间';
COMMENT ON COLUMN education_school.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_school (tenant_id);
CREATE INDEX idx_tenant_region ON education_school (tenant_id, region_id);
-- =============================================
-- 3. 专业表
-- =============================================
CREATE TABLE education_major (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
school_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
description VARCHAR(500) DEFAULT NULL,
study_tips VARCHAR(500) DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_major IS '教育-专业';
COMMENT ON COLUMN education_major.id IS '主键';
COMMENT ON COLUMN education_major.tenant_id IS '租户编号';
COMMENT ON COLUMN education_major.legacy_id IS '旧系统专业 ID';
COMMENT ON COLUMN education_major.region_id IS '地区 ID';
COMMENT ON COLUMN education_major.school_id IS '院校 ID';
COMMENT ON COLUMN education_major.name IS '专业名称';
COMMENT ON COLUMN education_major.description IS '专业说明';
COMMENT ON COLUMN education_major.study_tips IS '学习建议';
COMMENT ON COLUMN education_major.is_active IS '是否启用';
COMMENT ON COLUMN education_major.sort_order IS '显示排序值';
COMMENT ON COLUMN education_major.creator IS '创建者';
COMMENT ON COLUMN education_major.create_time IS '创建时间';
COMMENT ON COLUMN education_major.updater IS '更新者';
COMMENT ON COLUMN education_major.update_time IS '更新时间';
COMMENT ON COLUMN education_major.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_major (tenant_id);
CREATE INDEX idx_tenant_school ON education_major (tenant_id, school_id);
-- =============================================
-- 4. 科目表
-- =============================================
CREATE TABLE education_subject (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
region_id BIGINT DEFAULT NULL,
school_id BIGINT DEFAULT NULL,
major_id BIGINT DEFAULT NULL,
module_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
type VARCHAR(50) DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_subject IS '教育-科目';
COMMENT ON COLUMN education_subject.id IS '主键';
COMMENT ON COLUMN education_subject.tenant_id IS '租户编号';
COMMENT ON COLUMN education_subject.region_id IS '地区 ID';
COMMENT ON COLUMN education_subject.school_id IS '院校 ID';
COMMENT ON COLUMN education_subject.major_id IS '专业 ID';
COMMENT ON COLUMN education_subject.module_id IS '地区模块 ID';
COMMENT ON COLUMN education_subject.name IS '科目名称';
COMMENT ON COLUMN education_subject.type IS '科目类型';
COMMENT ON COLUMN education_subject.is_active IS '是否启用';
COMMENT ON COLUMN education_subject.sort_order IS '显示排序值';
COMMENT ON COLUMN education_subject.creator IS '创建者';
COMMENT ON COLUMN education_subject.create_time IS '创建时间';
COMMENT ON COLUMN education_subject.updater IS '更新者';
COMMENT ON COLUMN education_subject.update_time IS '更新时间';
COMMENT ON COLUMN education_subject.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_subject (tenant_id);
CREATE INDEX idx_tenant_major ON education_subject (tenant_id, major_id);
-- =============================================
-- 5. 题目分类表
-- =============================================
CREATE TABLE education_category (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
subject_id BIGINT DEFAULT NULL,
legacy_node_id VARCHAR(64) DEFAULT NULL,
name VARCHAR(200) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_category IS '教育-题目分类';
COMMENT ON COLUMN education_category.id IS '主键';
COMMENT ON COLUMN education_category.tenant_id IS '租户编号';
COMMENT ON COLUMN education_category.subject_id IS '科目 ID';
COMMENT ON COLUMN education_category.legacy_node_id IS '旧导航节点 ID';
COMMENT ON COLUMN education_category.name IS '分类名称';
COMMENT ON COLUMN education_category.is_active IS '是否启用';
COMMENT ON COLUMN education_category.sort_order IS '显示排序值';
COMMENT ON COLUMN education_category.creator IS '创建者';
COMMENT ON COLUMN education_category.create_time IS '创建时间';
COMMENT ON COLUMN education_category.updater IS '更新者';
COMMENT ON COLUMN education_category.update_time IS '更新时间';
COMMENT ON COLUMN education_category.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_category (tenant_id);
CREATE INDEX idx_tenant_subject ON education_category (tenant_id, subject_id);
-- =============================================
-- 6. 内容入口表
-- =============================================
-- Indexes:
-- uk_tenant_entry_key — per-tenant uniqueness for entry key
CREATE TABLE education_content_entry (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
legacy_id VARCHAR(64) DEFAULT NULL,
region_id BIGINT DEFAULT NULL,
entry_key VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
entry_type VARCHAR(50) NOT NULL,
icon VARCHAR(500) DEFAULT NULL,
route VARCHAR(200) DEFAULT NULL,
description VARCHAR(500) DEFAULT NULL,
visibility VARCHAR(50) NOT NULL DEFAULT 'PUBLIC',
access_rules JSONB DEFAULT NULL,
layout_config JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT 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
);
COMMENT ON TABLE education_content_entry IS '教育-内容入口';
COMMENT ON COLUMN education_content_entry.id IS '主键';
COMMENT ON COLUMN education_content_entry.tenant_id IS '租户编号';
COMMENT ON COLUMN education_content_entry.legacy_id IS '旧系统内容入口 ID';
COMMENT ON COLUMN education_content_entry.region_id IS '地区 ID';
COMMENT ON COLUMN education_content_entry.entry_key IS '内容入口唯一键';
COMMENT ON COLUMN education_content_entry.name IS '内容入口名称';
COMMENT ON COLUMN education_content_entry.entry_type IS '内容入口类型';
COMMENT ON COLUMN education_content_entry.icon IS '图标';
COMMENT ON COLUMN education_content_entry.route IS '前端路由';
COMMENT ON COLUMN education_content_entry.description IS '入口说明';
COMMENT ON COLUMN education_content_entry.visibility IS '可见性';
COMMENT ON COLUMN education_content_entry.access_rules IS '访问规则 JSON';
COMMENT ON COLUMN education_content_entry.layout_config IS '布局配置 JSON';
COMMENT ON COLUMN education_content_entry.is_active IS '是否启用';
COMMENT ON COLUMN education_content_entry.sort_order IS '显示排序值';
COMMENT ON COLUMN education_content_entry.creator IS '创建者';
COMMENT ON COLUMN education_content_entry.create_time IS '创建时间';
COMMENT ON COLUMN education_content_entry.updater IS '更新者';
COMMENT ON COLUMN education_content_entry.update_time IS '更新时间';
COMMENT ON COLUMN education_content_entry.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_content_entry (tenant_id);
CREATE UNIQUE INDEX uk_tenant_entry_key ON education_content_entry (tenant_id, entry_key);
-- =============================================
-- 7. 内容节点表(树形结构)
-- =============================================
-- Indexes:
-- idx_entry_parent — supports tree navigation: find children of a parent within an entry
-- idx_tenant_entry — tenant-scoped queries by entry
CREATE TABLE education_content_node (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
entry_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
title VARCHAR(200) DEFAULT NULL,
node_type VARCHAR(50) NOT NULL,
marker_type VARCHAR(50) DEFAULT NULL,
depth INT NOT NULL DEFAULT 0,
is_leaf BOOLEAN NOT NULL DEFAULT false,
is_selectable BOOLEAN NOT NULL DEFAULT true,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB 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_content_node IS '教育-内容节点';
COMMENT ON COLUMN education_content_node.id IS '主键';
COMMENT ON COLUMN education_content_node.tenant_id IS '租户编号';
COMMENT ON COLUMN education_content_node.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_content_node.parent_id IS '父节点 IDNULL=根节点)';
COMMENT ON COLUMN education_content_node.name IS '节点名称';
COMMENT ON COLUMN education_content_node.title IS '节点标题';
COMMENT ON COLUMN education_content_node.node_type IS '节点类型';
COMMENT ON COLUMN education_content_node.marker_type IS '节点标记类型';
COMMENT ON COLUMN education_content_node.depth IS '树深度';
COMMENT ON COLUMN education_content_node.is_leaf IS '是否叶子节点';
COMMENT ON COLUMN education_content_node.is_selectable IS '是否可被选择';
COMMENT ON COLUMN education_content_node.is_active IS '是否启用';
COMMENT ON COLUMN education_content_node.sort_order IS '显示排序值';
COMMENT ON COLUMN education_content_node.metadata IS '扩展元数据';
COMMENT ON COLUMN education_content_node.creator IS '创建者';
COMMENT ON COLUMN education_content_node.create_time IS '创建时间';
COMMENT ON COLUMN education_content_node.updater IS '更新者';
COMMENT ON COLUMN education_content_node.update_time IS '更新时间';
COMMENT ON COLUMN education_content_node.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_content_node (tenant_id);
CREATE INDEX idx_entry_parent ON education_content_node (entry_id, parent_id);
CREATE INDEX idx_tenant_entry ON education_content_node (tenant_id, entry_id);
-- =============================================
-- 8. 题集表
-- =============================================
CREATE TABLE education_question_collection (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
title VARCHAR(200) DEFAULT NULL,
collection_type VARCHAR(50) NOT NULL,
question_count INT NOT NULL DEFAULT 0,
duration_minutes INT DEFAULT NULL,
access_rules JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB 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_question_collection IS '教育-题集';
COMMENT ON COLUMN education_question_collection.id IS '主键';
COMMENT ON COLUMN education_question_collection.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question_collection.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_question_collection.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_question_collection.name IS '题集名称';
COMMENT ON COLUMN education_question_collection.title IS '题集标题';
COMMENT ON COLUMN education_question_collection.collection_type IS '题集类型';
COMMENT ON COLUMN education_question_collection.question_count IS '题目数量';
COMMENT ON COLUMN education_question_collection.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_question_collection.access_rules IS '访问规则 JSON';
COMMENT ON COLUMN education_question_collection.is_active IS '是否启用';
COMMENT ON COLUMN education_question_collection.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question_collection.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question_collection.creator IS '创建者';
COMMENT ON COLUMN education_question_collection.create_time IS '创建时间';
COMMENT ON COLUMN education_question_collection.updater IS '更新者';
COMMENT ON COLUMN education_question_collection.update_time IS '更新时间';
COMMENT ON COLUMN education_question_collection.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question_collection (tenant_id);
CREATE INDEX idx_tenant_entry ON education_question_collection (tenant_id, entry_id);
CREATE INDEX idx_tenant_node ON education_question_collection (tenant_id, node_id);
-- =============================================
-- 9. 题目表(核心表)
-- =============================================
-- Indexes:
-- idx_tenant_published — 查询已发布题目(主查询路径)
-- idx_tenant_collection — 按题集筛选
-- idx_tenant_subject — 按科目筛选
-- idx_tenant_node — 按内容节点筛选
-- idx_tenant_type_diff — 按题型和难度筛选
-- Security note: correct_answer 和 explanation 列存在但需在 service 层过滤;
-- 学生端 DTO 绝不得包含这两列。
-- Options JSONB: 存储 label, content, isCorrect, order — 完整选项数据。
-- Service 层在学生端返回前剥离 isCorrect。
CREATE TABLE education_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
content_version INT NOT NULL DEFAULT 1,
stem TEXT NOT NULL,
type VARCHAR(32) NOT NULL,
type_label VARCHAR(50) DEFAULT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
question_content JSONB DEFAULT NULL,
options JSONB NOT NULL,
correct_answer VARCHAR(500) DEFAULT NULL,
explanation TEXT DEFAULT NULL,
analysis TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED',
is_published BOOLEAN NOT NULL DEFAULT true,
collection_id BIGINT DEFAULT NULL,
subject_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
tags JSONB DEFAULT NULL,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB 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_question IS '教育-题目';
COMMENT ON COLUMN education_question.id IS '主键';
COMMENT ON COLUMN education_question.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question.content_version IS '内容版本号(递增,保证历史报告稳定性)';
COMMENT ON COLUMN education_question.stem IS '题干';
COMMENT ON COLUMN education_question.type IS '题型';
COMMENT ON COLUMN education_question.type_label IS '题型显示名称';
COMMENT ON COLUMN education_question.difficulty IS '难度';
COMMENT ON COLUMN education_question.question_content IS '题干结构化内容(兼容复杂题型)';
COMMENT ON COLUMN education_question.options IS '选项 JSONB [{label, content, isCorrect, order}]';
COMMENT ON COLUMN education_question.correct_answer IS '正确答案(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.explanation IS '答案解析(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.analysis IS '深度解析(敏感字段)';
COMMENT ON COLUMN education_question.status IS '题目状态PUBLISHED-已发布, HIDDEN-已隐藏, DRAFT-草稿, INACTIVE-停用';
COMMENT ON COLUMN education_question.is_published IS '是否已发布';
COMMENT ON COLUMN education_question.collection_id IS '所属题集 ID';
COMMENT ON COLUMN education_question.subject_id IS '所属科目 ID';
COMMENT ON COLUMN education_question.node_id IS '所属内容节点 ID';
COMMENT ON COLUMN education_question.tags IS '标签 JSONB 数组';
COMMENT ON COLUMN education_question.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question.creator IS '创建者';
COMMENT ON COLUMN education_question.create_time IS '创建时间';
COMMENT ON COLUMN education_question.updater IS '更新者';
COMMENT ON COLUMN education_question.update_time IS '更新时间';
COMMENT ON COLUMN education_question.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question (tenant_id);
CREATE INDEX idx_tenant_published ON education_question (tenant_id, is_published, status);
CREATE INDEX idx_tenant_collection ON education_question (tenant_id, collection_id);
CREATE INDEX idx_tenant_subject ON education_question (tenant_id, subject_id);
CREATE INDEX idx_tenant_node ON education_question (tenant_id, node_id);
CREATE INDEX idx_tenant_type_diff ON education_question (tenant_id, type, difficulty);
-- =============================================
-- 10. 练习蓝图表
-- =============================================
CREATE TABLE education_practice_blueprint (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
mode VARCHAR(50) NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
collection_id BIGINT DEFAULT NULL,
question_limit INT DEFAULT NULL,
duration_minutes INT DEFAULT NULL,
eligible_count INT NOT NULL DEFAULT 0,
total_count INT NOT NULL DEFAULT 0,
available_types JSONB DEFAULT NULL,
available_difficulties JSONB DEFAULT NULL,
min_questions INT NOT NULL DEFAULT 1,
max_questions INT NOT NULL DEFAULT 200,
suggested_count INT NOT NULL DEFAULT 20,
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_blueprint IS '教育-练习蓝图';
COMMENT ON COLUMN education_practice_blueprint.id IS '主键';
COMMENT ON COLUMN education_practice_blueprint.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_blueprint.mode IS '练习模式';
COMMENT ON COLUMN education_practice_blueprint.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_practice_blueprint.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_practice_blueprint.collection_id IS '题集 ID';
COMMENT ON COLUMN education_practice_blueprint.question_limit IS '题目数量限制';
COMMENT ON COLUMN education_practice_blueprint.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_practice_blueprint.eligible_count IS '符合条件的题目数';
COMMENT ON COLUMN education_practice_blueprint.total_count IS '题库总数';
COMMENT ON COLUMN education_practice_blueprint.available_types IS '可用题型列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.available_difficulties IS '可用难度列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.min_questions IS '最少题目数';
COMMENT ON COLUMN education_practice_blueprint.max_questions IS '最多题目数';
COMMENT ON COLUMN education_practice_blueprint.suggested_count IS '建议题目数';
COMMENT ON COLUMN education_practice_blueprint.creator IS '创建者';
COMMENT ON COLUMN education_practice_blueprint.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_blueprint.updater IS '更新者';
COMMENT ON COLUMN education_practice_blueprint.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_blueprint.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_practice_blueprint (tenant_id);
CREATE INDEX idx_tenant_collection ON education_practice_blueprint (tenant_id, collection_id);
-- =============================================
-- 11. 题集-题目关联表(多对多)
-- =============================================
CREATE TABLE education_question_collection_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collection_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
sort_order INT NOT NULL DEFAULT 0
);
COMMENT ON TABLE education_question_collection_question IS '教育-题集题目关联';
COMMENT ON COLUMN education_question_collection_question.id IS '主键';
COMMENT ON COLUMN education_question_collection_question.collection_id IS '题集 ID';
COMMENT ON COLUMN education_question_collection_question.question_id IS '题目 ID';
COMMENT ON COLUMN education_question_collection_question.sort_order IS '排序值';
CREATE UNIQUE INDEX uk_collection_question ON education_question_collection_question (collection_id, question_id);
CREATE INDEX idx_question ON education_question_collection_question (question_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify all tables exist:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name LIKE 'education_%'
-- ORDER BY table_name;
-- Verify indexes (sample):
-- -- PostgreSQL equivalent of SHOW INDEX:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_question' AND indexname IN ('idx_tenant_published', 'idx_tenant_collection');
-- SELECT * FROM pg_indexes WHERE tablename = 'education_content_node' AND indexname = 'idx_entry_parent';
-- Verify no orphan data (should be 0 after fresh migration for catalog tables):
-- SELECT 'region' AS tbl, COUNT(*) FROM education_region
-- UNION ALL SELECT 'school', COUNT(*) FROM education_school
-- UNION ALL SELECT 'major', COUNT(*) FROM education_major
-- UNION ALL SELECT 'subject', COUNT(*) FROM education_subject
-- UNION ALL SELECT 'category', COUNT(*) FROM education_category
-- UNION ALL SELECT 'content_entry', COUNT(*) FROM education_content_entry
-- UNION ALL SELECT 'content_node', COUNT(*) FROM education_content_node
-- UNION ALL SELECT 'question_collection', COUNT(*) FROM education_question_collection
-- UNION ALL SELECT 'question', COUNT(*) FROM education_question
-- UNION ALL SELECT 'practice_blueprint', COUNT(*) FROM education_practice_blueprint
-- UNION ALL SELECT 'collection_question', COUNT(*) FROM education_question_collection_question;