-- ============================================= -- 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 '父节点 ID(NULL=根节点)'; 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;