fix(education): align Flyway delivery review
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 题库目录表回滚 DDL
|
||||
-- Ticket #20 rollback
|
||||
-- =============================================
|
||||
-- WARNING: This is an explicit data-destruction script.
|
||||
-- It is NOT part of application version rollback.
|
||||
-- Application version rollback preserves these tables and their data;
|
||||
-- this script is only used when permanently removing the catalog module.
|
||||
-- =============================================
|
||||
|
||||
DROP TABLE IF EXISTS `education_question_collection_question`;
|
||||
DROP TABLE IF EXISTS `education_practice_blueprint`;
|
||||
DROP TABLE IF EXISTS `education_question`;
|
||||
DROP TABLE IF EXISTS `education_question_collection`;
|
||||
DROP TABLE IF EXISTS `education_content_node`;
|
||||
DROP TABLE IF EXISTS `education_content_entry`;
|
||||
DROP TABLE IF EXISTS `education_category`;
|
||||
DROP TABLE IF EXISTS `education_subject`;
|
||||
DROP TABLE IF EXISTS `education_major`;
|
||||
DROP TABLE IF EXISTS `education_school`;
|
||||
DROP TABLE IF EXISTS `education_region`;
|
||||
@@ -1,349 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 题库目录原生表 DDL
|
||||
-- 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_schema = 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 NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统地区 ID',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '地区名称',
|
||||
`code` VARCHAR(32) DEFAULT NULL COMMENT '地区编码',
|
||||
`short_name` VARCHAR(50) DEFAULT NULL COMMENT '地区简称',
|
||||
`full_name` VARCHAR(200) DEFAULT NULL COMMENT '地区全称',
|
||||
`icon` VARCHAR(500) DEFAULT NULL COMMENT '地区图标 URL',
|
||||
`pinyin` VARCHAR(200) DEFAULT NULL COMMENT '地区拼音',
|
||||
`is_hot` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否热门地区',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_active_order` (`tenant_id`, `is_active`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-地区';
|
||||
|
||||
-- =============================================
|
||||
-- 2. 院校表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_school` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统院校 ID',
|
||||
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
|
||||
`module_id` BIGINT DEFAULT NULL COMMENT '地区模块 ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '院校名称',
|
||||
`professional_exam_date` VARCHAR(200) DEFAULT NULL COMMENT '专业考试日期说明',
|
||||
`metadata` JSON DEFAULT NULL COMMENT '院校扩展数据',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_region` (`tenant_id`, `region_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-院校';
|
||||
|
||||
-- =============================================
|
||||
-- 3. 专业表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_major` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统专业 ID',
|
||||
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
|
||||
`school_id` BIGINT DEFAULT NULL COMMENT '院校 ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '专业名称',
|
||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '专业说明',
|
||||
`study_tips` VARCHAR(500) DEFAULT NULL COMMENT '学习建议',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_school` (`tenant_id`, `school_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-专业';
|
||||
|
||||
-- =============================================
|
||||
-- 4. 科目表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_subject` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
|
||||
`school_id` BIGINT DEFAULT NULL COMMENT '院校 ID',
|
||||
`major_id` BIGINT DEFAULT NULL COMMENT '专业 ID',
|
||||
`module_id` BIGINT DEFAULT NULL COMMENT '地区模块 ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '科目名称',
|
||||
`type` VARCHAR(50) DEFAULT NULL COMMENT '科目类型',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_major` (`tenant_id`, `major_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-科目';
|
||||
|
||||
-- =============================================
|
||||
-- 5. 题目分类表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_category` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`subject_id` BIGINT DEFAULT NULL COMMENT '科目 ID',
|
||||
`legacy_node_id` VARCHAR(64) DEFAULT NULL COMMENT '旧导航节点 ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '分类名称',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_subject` (`tenant_id`, `subject_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题目分类';
|
||||
|
||||
-- =============================================
|
||||
-- 6. 内容入口表
|
||||
-- =============================================
|
||||
-- Indexes:
|
||||
-- uk_tenant_entry_key — per-tenant uniqueness for entry key
|
||||
CREATE TABLE `education_content_entry` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`legacy_id` VARCHAR(64) DEFAULT NULL COMMENT '旧系统内容入口 ID',
|
||||
`region_id` BIGINT DEFAULT NULL COMMENT '地区 ID',
|
||||
`entry_key` VARCHAR(100) NOT NULL COMMENT '内容入口唯一键',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '内容入口名称',
|
||||
`entry_type` VARCHAR(50) NOT NULL COMMENT '内容入口类型',
|
||||
`icon` VARCHAR(500) DEFAULT NULL COMMENT '图标',
|
||||
`route` VARCHAR(200) DEFAULT NULL COMMENT '前端路由',
|
||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '入口说明',
|
||||
`visibility` VARCHAR(50) NOT NULL DEFAULT 'PUBLIC' COMMENT '可见性',
|
||||
`access_rules` JSON DEFAULT NULL COMMENT '访问规则 JSON',
|
||||
`layout_config` JSON DEFAULT NULL COMMENT '布局配置 JSON',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
UNIQUE KEY `uk_tenant_entry_key` (`tenant_id`, `entry_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-内容入口';
|
||||
|
||||
-- =============================================
|
||||
-- 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 NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`entry_id` BIGINT NOT NULL COMMENT '内容入口 ID',
|
||||
`parent_id` BIGINT DEFAULT NULL COMMENT '父节点 ID(NULL=根节点)',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '节点名称',
|
||||
`title` VARCHAR(200) DEFAULT NULL COMMENT '节点标题',
|
||||
`node_type` VARCHAR(50) NOT NULL COMMENT '节点类型',
|
||||
`marker_type` VARCHAR(50) DEFAULT NULL COMMENT '节点标记类型',
|
||||
`depth` INT NOT NULL DEFAULT 0 COMMENT '树深度',
|
||||
`is_leaf` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否叶子节点',
|
||||
`is_selectable` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否可被选择',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_entry_parent` (`entry_id`, `parent_id`),
|
||||
KEY `idx_tenant_entry` (`tenant_id`, `entry_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-内容节点';
|
||||
|
||||
-- =============================================
|
||||
-- 8. 题集表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_question_collection` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`entry_id` BIGINT DEFAULT NULL COMMENT '内容入口 ID',
|
||||
`node_id` BIGINT DEFAULT NULL COMMENT '内容节点 ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '题集名称',
|
||||
`title` VARCHAR(200) DEFAULT NULL COMMENT '题集标题',
|
||||
`collection_type` VARCHAR(50) NOT NULL COMMENT '题集类型',
|
||||
`question_count` INT NOT NULL DEFAULT 0 COMMENT '题目数量',
|
||||
`duration_minutes` INT DEFAULT NULL COMMENT '建议作答时长(分钟)',
|
||||
`access_rules` JSON DEFAULT NULL COMMENT '访问规则 JSON',
|
||||
`is_active` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_entry` (`tenant_id`, `entry_id`),
|
||||
KEY `idx_tenant_node` (`tenant_id`, `node_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题集';
|
||||
|
||||
-- =============================================
|
||||
-- 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 JSON: 存储 label, content, isCorrect, order — 完整选项数据。
|
||||
-- Service 层在学生端返回前剥离 isCorrect。
|
||||
CREATE TABLE `education_question` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`content_version` INT NOT NULL DEFAULT 1 COMMENT '内容版本号(递增,保证历史报告稳定性)',
|
||||
`stem` TEXT NOT NULL COMMENT '题干',
|
||||
`type` VARCHAR(32) NOT NULL COMMENT '题型',
|
||||
`type_label` VARCHAR(50) DEFAULT NULL COMMENT '题型显示名称',
|
||||
`difficulty` VARCHAR(32) DEFAULT NULL COMMENT '难度',
|
||||
`question_content` JSON DEFAULT NULL COMMENT '题干结构化内容(兼容复杂题型)',
|
||||
`options` JSON NOT NULL COMMENT '选项 JSON [{label, content, isCorrect, order}]',
|
||||
`correct_answer` VARCHAR(500) DEFAULT NULL COMMENT '正确答案(敏感字段,学生端不可返回)',
|
||||
`explanation` TEXT DEFAULT NULL COMMENT '答案解析(敏感字段,学生端不可返回)',
|
||||
`analysis` TEXT DEFAULT NULL COMMENT '深度解析(敏感字段)',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED' COMMENT '题目状态:PUBLISHED-已发布, HIDDEN-已隐藏, DRAFT-草稿, INACTIVE-停用',
|
||||
`is_published` BIT(1) NOT NULL DEFAULT b'1' COMMENT '是否已发布',
|
||||
`collection_id` BIGINT DEFAULT NULL COMMENT '所属题集 ID',
|
||||
`subject_id` BIGINT DEFAULT NULL COMMENT '所属科目 ID',
|
||||
`node_id` BIGINT DEFAULT NULL COMMENT '所属内容节点 ID',
|
||||
`tags` JSON DEFAULT NULL COMMENT '标签 JSON 数组',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '显示排序值',
|
||||
`metadata` JSON DEFAULT NULL COMMENT '扩展元数据',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_published` (`tenant_id`, `is_published`, `status`),
|
||||
KEY `idx_tenant_collection` (`tenant_id`, `collection_id`),
|
||||
KEY `idx_tenant_subject` (`tenant_id`, `subject_id`),
|
||||
KEY `idx_tenant_node` (`tenant_id`, `node_id`),
|
||||
KEY `idx_tenant_type_diff` (`tenant_id`, `type`, `difficulty`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题目';
|
||||
|
||||
-- =============================================
|
||||
-- 10. 练习蓝图表
|
||||
-- =============================================
|
||||
CREATE TABLE `education_practice_blueprint` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`tenant_id` BIGINT NOT NULL COMMENT '租户编号',
|
||||
`mode` VARCHAR(50) NOT NULL COMMENT '练习模式',
|
||||
`entry_id` BIGINT DEFAULT NULL COMMENT '内容入口 ID',
|
||||
`node_id` BIGINT DEFAULT NULL COMMENT '内容节点 ID',
|
||||
`collection_id` BIGINT DEFAULT NULL COMMENT '题集 ID',
|
||||
`question_limit` INT DEFAULT NULL COMMENT '题目数量限制',
|
||||
`duration_minutes` INT DEFAULT NULL COMMENT '建议作答时长(分钟)',
|
||||
`eligible_count` INT NOT NULL DEFAULT 0 COMMENT '符合条件的题目数',
|
||||
`total_count` INT NOT NULL DEFAULT 0 COMMENT '题库总数',
|
||||
`available_types` JSON DEFAULT NULL COMMENT '可用题型列表 JSON',
|
||||
`available_difficulties` JSON DEFAULT NULL COMMENT '可用难度列表 JSON',
|
||||
`min_questions` INT NOT NULL DEFAULT 1 COMMENT '最少题目数',
|
||||
`max_questions` INT NOT NULL DEFAULT 200 COMMENT '最多题目数',
|
||||
`suggested_count` INT NOT NULL DEFAULT 20 COMMENT '建议题目数',
|
||||
`creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updater` VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant` (`tenant_id`),
|
||||
KEY `idx_tenant_collection` (`tenant_id`, `collection_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-练习蓝图';
|
||||
|
||||
-- =============================================
|
||||
-- 11. 题集-题目关联表(多对多)
|
||||
-- =============================================
|
||||
CREATE TABLE `education_question_collection_question` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`collection_id` BIGINT NOT NULL COMMENT '题集 ID',
|
||||
`question_id` BIGINT NOT NULL COMMENT '题目 ID',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序值',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_collection_question` (`collection_id`, `question_id`),
|
||||
KEY `idx_question` (`question_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教育-题集题目关联';
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify all tables exist:
|
||||
-- SELECT TABLE_NAME FROM information_schema.tables
|
||||
-- WHERE table_schema = DATABASE()
|
||||
-- AND TABLE_NAME LIKE 'education_%'
|
||||
-- ORDER BY TABLE_NAME;
|
||||
-- Verify indexes (sample):
|
||||
-- SHOW INDEX FROM education_question WHERE Key_name IN ('idx_tenant_published', 'idx_tenant_collection');
|
||||
-- SHOW INDEX FROM education_content_node WHERE Key_name = 'idx_entry_parent';
|
||||
-- Verify no orphan data (should be 0 after fresh migration for catalog tables):
|
||||
-- SELECT 'region', 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;
|
||||
@@ -1,174 +0,0 @@
|
||||
-- =============================================
|
||||
-- Member 模块建表 — 从 PostgreSQL 测试 SQL 转换为 MySQL
|
||||
-- 用于本地开发环境补全 member 表
|
||||
-- =============================================
|
||||
|
||||
-- 1. 会员配置表
|
||||
CREATE TABLE IF NOT EXISTS member_config (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
group_id bigint NULL DEFAULT NULL COMMENT '用户分组编号',
|
||||
level_id bigint NULL DEFAULT NULL COMMENT '等级编号',
|
||||
tenant_id bigint NOT NULL DEFAULT 0,
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员配置表';
|
||||
|
||||
-- 2. 会员标签表
|
||||
CREATE TABLE IF NOT EXISTS member_tag (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
name varchar(50) NOT NULL COMMENT '标签名称',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员标签';
|
||||
|
||||
-- 3. 用户分组表
|
||||
CREATE TABLE IF NOT EXISTS member_group (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
name varchar(50) NOT NULL COMMENT '分组名称',
|
||||
remark varchar(255) DEFAULT '' COMMENT '备注',
|
||||
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员分组';
|
||||
|
||||
-- 4. 会员等级表(完整版,覆盖之前创建的简化版)
|
||||
CREATE TABLE IF NOT EXISTS member_level (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
name varchar(50) NOT NULL COMMENT '等级名称',
|
||||
experience int NOT NULL DEFAULT 0 COMMENT '所需经验',
|
||||
level int NOT NULL DEFAULT 1 COMMENT '等级',
|
||||
discount_percent int NOT NULL DEFAULT 100 COMMENT '折扣百分比',
|
||||
icon varchar(255) DEFAULT '' COMMENT '图标',
|
||||
background_url varchar(255) DEFAULT '' COMMENT '背景图',
|
||||
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级';
|
||||
|
||||
-- 5. 会员等级记录表
|
||||
CREATE TABLE IF NOT EXISTS member_level_record (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint NOT NULL COMMENT '用户编号',
|
||||
level_id bigint NOT NULL COMMENT '等级编号',
|
||||
level_name varchar(50) DEFAULT '' COMMENT '等级名称',
|
||||
experience int NOT NULL DEFAULT 0 COMMENT '当前经验',
|
||||
remark varchar(255) DEFAULT '' COMMENT '备注',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级记录';
|
||||
|
||||
-- 6. 会员经验记录表
|
||||
CREATE TABLE IF NOT EXISTS member_experience_record (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint NOT NULL COMMENT '用户编号',
|
||||
biz_type varchar(50) NOT NULL COMMENT '业务类型',
|
||||
biz_id varchar(100) NOT NULL COMMENT '业务编号',
|
||||
title varchar(100) NOT NULL COMMENT '标题',
|
||||
experience int NOT NULL COMMENT '经验',
|
||||
total_experience int NOT NULL DEFAULT 0 COMMENT '累计经验',
|
||||
description varchar(255) DEFAULT '' COMMENT '描述',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员经验记录';
|
||||
|
||||
-- 7. 会员积分记录表
|
||||
CREATE TABLE IF NOT EXISTS member_point_record (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint NOT NULL COMMENT '用户编号',
|
||||
biz_type varchar(50) NOT NULL COMMENT '业务类型',
|
||||
biz_id varchar(100) NOT NULL COMMENT '业务编号',
|
||||
title varchar(100) NOT NULL COMMENT '标题',
|
||||
point int NOT NULL COMMENT '积分',
|
||||
total_point int NOT NULL DEFAULT 0 COMMENT '累计积分',
|
||||
description varchar(255) DEFAULT '' COMMENT '描述',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员积分记录';
|
||||
|
||||
-- 8. 会员签到配置表
|
||||
CREATE TABLE IF NOT EXISTS member_sign_in_config (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
day int NOT NULL COMMENT '签到天数',
|
||||
point int NOT NULL DEFAULT 0 COMMENT '奖励积分',
|
||||
experience int NOT NULL DEFAULT 0 COMMENT '奖励经验',
|
||||
status tinyint NOT NULL DEFAULT 0 COMMENT '状态',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员签到配置';
|
||||
|
||||
-- 9. 会员签到记录表
|
||||
CREATE TABLE IF NOT EXISTS member_sign_in_record (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint NOT NULL COMMENT '用户编号',
|
||||
day int NOT NULL COMMENT '签到天数',
|
||||
point int NOT NULL DEFAULT 0 COMMENT '获得积分',
|
||||
experience int NOT NULL DEFAULT 0 COMMENT '获得经验',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
tenant_id bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员签到记录';
|
||||
|
||||
-- 10. 会员地址表
|
||||
CREATE TABLE IF NOT EXISTS member_address (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint NOT NULL COMMENT '用户编号',
|
||||
name varchar(20) NOT NULL COMMENT '收件人姓名',
|
||||
mobile varchar(20) NOT NULL COMMENT '手机号',
|
||||
area_id bigint NOT NULL COMMENT '地区编号',
|
||||
detail_address varchar(250) NOT NULL COMMENT '详细地址',
|
||||
default_status bit(1) NOT NULL DEFAULT b'0' COMMENT '是否默认',
|
||||
creator varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updater varchar(64) DEFAULT '' COMMENT '更新者',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员地址';
|
||||
|
||||
-- =============================================
|
||||
-- 种子数据
|
||||
-- =============================================
|
||||
INSERT IGNORE INTO member_level (id, name, level, experience, status, tenant_id) VALUES (1, '普通会员', 1, 0, 0, 1);
|
||||
@@ -1,8 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块种子数据回滚 (PostgreSQL)
|
||||
-- =============================================
|
||||
|
||||
DELETE FROM system_menu
|
||||
WHERE id = 6801 AND permission = 'education:capability' AND parent_id = 6800;
|
||||
DELETE FROM system_menu
|
||||
WHERE id = 6800 AND path = '/education' AND name = '教育管理';
|
||||
@@ -1,7 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 DDL (PostgreSQL)
|
||||
-- 当前为应用外壳阶段,无业务表;后续票据在此追加 CREATE TABLE 语句。
|
||||
-- =============================================
|
||||
-- 占位:education 模块当前无业务表
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS education;
|
||||
@@ -1,15 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块种子数据 (PostgreSQL)
|
||||
-- 菜单 ID 范围:6800-6899
|
||||
-- 权限标识前缀:education:
|
||||
-- 可重复执行;角色授权由管理员按租户完成
|
||||
-- =============================================
|
||||
|
||||
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
|
||||
SELECT 6800, '教育管理', '', 1, 50, 0, '/education', 'ep:school', NULL, NULL, 0, true, true, true, 'admin', NOW(), 'admin', NOW(), false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 6800);
|
||||
|
||||
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
|
||||
SELECT 6801, '能力查询', 'education:capability', 3, 1, 6800, '', '', '', NULL, 0, true, true, true, 'admin', NOW(), 'admin', NOW(), false
|
||||
WHERE EXISTS (SELECT 1 FROM system_menu WHERE id = 6800 AND path = '/education' AND deleted = false)
|
||||
AND NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 6801);
|
||||
@@ -1 +0,0 @@
|
||||
-- Ticket #3 creates no database records, so rollback is intentionally a no-op.
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Ticket #3 adds no administrator permission.
|
||||
-- Tenant resolution is @PermitAll and current education context only requires an authenticated Member session.
|
||||
-- Therefore no system_menu rows are required for this vertical slice.
|
||||
@@ -1,32 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 练习会话与题目快照回滚
|
||||
-- Ticket #6 / Migration 002
|
||||
-- =============================================
|
||||
--
|
||||
-- WARNING: This file contains NO executable SQL.
|
||||
-- Destructive rollback (DROP TABLE) requires manual operator verification.
|
||||
--
|
||||
-- Manual rollback procedure (operator must execute):
|
||||
-- 1. Verify no other tables depend on these tables:
|
||||
-- SELECT tc.table_name, kcu.column_name, ccu.table_name AS referenced_table
|
||||
-- FROM information_schema.table_constraints tc
|
||||
-- JOIN information_schema.key_column_usage kcu
|
||||
-- ON tc.constraint_name = kcu.constraint_name
|
||||
-- JOIN information_schema.constraint_column_usage ccu
|
||||
-- ON tc.constraint_name = ccu.constraint_name
|
||||
-- WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
-- AND ccu.table_name IN ('education_practice_session', 'education_practice_question');
|
||||
-- Result MUST be empty before proceeding.
|
||||
--
|
||||
-- 2. Verify the tables contain only data from this migration:
|
||||
-- SELECT COUNT(*) AS session_count FROM education_practice_session;
|
||||
-- SELECT COUNT(*) AS question_count FROM education_practice_question;
|
||||
-- Operator must confirm these counts are acceptable to destroy.
|
||||
--
|
||||
-- 3. After verification, execute:
|
||||
-- DROP TABLE IF EXISTS education_practice_question;
|
||||
-- DROP TABLE IF EXISTS education_practice_session;
|
||||
--
|
||||
-- DO NOT uncomment or execute the lines below without operator verification.
|
||||
-- -- DROP TABLE IF EXISTS education_practice_question;
|
||||
-- -- DROP TABLE IF EXISTS education_practice_session;
|
||||
@@ -1,105 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 练习会话与题目快照 DDL
|
||||
-- Ticket #6: 练习会话创建、题目快照、恢复与状态机
|
||||
-- Migration: 002
|
||||
-- Prerequisites: 000-education-schema.sql (database creation)
|
||||
-- 001-education-tenant-seed.sql (tenant seed data)
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- This migration MUST fail if either table already exists (no IF NOT EXISTS).
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_schema = current_database()
|
||||
-- AND table_name IN ('education_practice_session', 'education_practice_question');
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
|
||||
-- =============================================
|
||||
-- 练习会话表
|
||||
-- =============================================
|
||||
-- Indexes:
|
||||
-- uk_tenant_client_session — per-tenant uniqueness for clientSessionId idempotency.
|
||||
-- Used by: selectByTenantAndClientSessionId (idempotent create check),
|
||||
-- INSERT ... ON CONFLICT ... for concurrent-create race resolution.
|
||||
-- idx_tenant_user_status — covers getCurrentSession (latest ACTIVE by tenant+user)
|
||||
-- and ownership queries. Column order: (tenant_id, user_id, status) so the
|
||||
-- index supports both filtering by tenant+user and tenant+user+status.
|
||||
-- Lock impact: INSERT acquires a row-level lock on the unique index entry;
|
||||
-- concurrent inserts with same (tenant_id, client_session_id) serialize naturally.
|
||||
-- No additional table-level locks required.
|
||||
CREATE TABLE education_practice_session (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 会话主键 */,
|
||||
tenant_id BIGINT NOT NULL /* 租户编号 */,
|
||||
user_id BIGINT NOT NULL /* Member 用户编号 */,
|
||||
client_session_id VARCHAR(36) NOT NULL /* 客户端生成的会话标识(UUID),用于幂等创建 */,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
|
||||
/* 会话状态:ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消 */,
|
||||
question_count INT NOT NULL DEFAULT 0 /* 题目总数 */,
|
||||
collection_id VARCHAR(64) DEFAULT NULL /* 源题集 ID */,
|
||||
node_id VARCHAR(64) DEFAULT NULL /* 源目录节点 ID */,
|
||||
type VARCHAR(32) DEFAULT NULL /* 筛选题型 */,
|
||||
difficulty VARCHAR(32) DEFAULT NULL /* 筛选难度 */,
|
||||
version 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_practice_session IS '教育-练习会话';
|
||||
|
||||
CREATE UNIQUE INDEX uk_tenant_client_session ON education_practice_session (tenant_id, client_session_id);
|
||||
CREATE INDEX idx_tenant_user_status ON education_practice_session (tenant_id, user_id, status);
|
||||
|
||||
-- =============================================
|
||||
-- 练习会话题目快照表
|
||||
-- =============================================
|
||||
-- Indexes:
|
||||
-- uk_session_sequence — per-session uniqueness for question sequence numbers.
|
||||
-- Used by: insertBatch to ensure no duplicate sequences within a session.
|
||||
-- idx_session_id — covers selectBySessionIdOrderBySequence (load all questions
|
||||
-- for a session, ordered by sequence). Also used by cascade delete lookups.
|
||||
-- Lock impact: INSERT acquires gap locks within session_id range on uk_session_sequence;
|
||||
-- concurrent inserts into different sessions are independent.
|
||||
-- Options column: JSONB data type stores only label, content, order — never isCorrect.
|
||||
-- Application layer (optionsToSafeJson) strips correctness before storage.
|
||||
CREATE TABLE education_practice_question (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
|
||||
tenant_id BIGINT NOT NULL /* 租户编号 */,
|
||||
session_id BIGINT NOT NULL /* 会话 ID */,
|
||||
sequence INT NOT NULL /* 题目序号(1-based,服务端固定) */,
|
||||
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 快照时的题目内容版本 */,
|
||||
stem TEXT NOT NULL /* 题干快照 */,
|
||||
type VARCHAR(32) NOT NULL /* 题型快照 */,
|
||||
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
|
||||
options JSONB NOT NULL /* 选项快照 JSON(不含 isCorrect) */,
|
||||
correct_answer TEXT DEFAULT NULL /* 正确答案快照 */,
|
||||
explanation TEXT DEFAULT NULL /* 解析快照 */,
|
||||
selected_answer TEXT DEFAULT NULL /* 学生已选答案 */,
|
||||
is_answered BOOLEAN NOT NULL DEFAULT false /* 是否已作答 */,
|
||||
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 '教育-练习会话题目快照';
|
||||
|
||||
CREATE UNIQUE INDEX uk_session_sequence ON education_practice_question (session_id, sequence);
|
||||
CREATE INDEX idx_session_id ON education_practice_question (session_id);
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify tables exist with correct structure:
|
||||
-- \d education_practice_session
|
||||
-- \d education_practice_question
|
||||
-- Verify unique keys are enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_session' AND indexname = 'uk_tenant_client_session';
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_question' AND indexname = 'uk_session_sequence';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_practice_session;
|
||||
-- SELECT COUNT(*) FROM education_practice_question;
|
||||
@@ -1,35 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 答案保存幂等性回滚 (PostgreSQL)
|
||||
-- Ticket #7 / Migration 003
|
||||
-- =============================================
|
||||
--
|
||||
-- WARNING: This file contains NO executable SQL.
|
||||
-- Destructive rollback requires manual operator verification.
|
||||
--
|
||||
-- Manual rollback procedure (operator must execute):
|
||||
-- 1. Verify no other tables depend on education_answer_idempotency:
|
||||
-- SELECT tc.table_name, kcu.column_name, ccu.table_name AS referenced_table
|
||||
-- FROM information_schema.table_constraints AS tc
|
||||
-- JOIN information_schema.key_column_usage AS kcu
|
||||
-- ON tc.constraint_name = kcu.constraint_name
|
||||
-- JOIN information_schema.constraint_column_usage AS ccu
|
||||
-- ON ccu.constraint_name = tc.constraint_name
|
||||
-- WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
-- AND ccu.table_name = 'education_answer_idempotency'
|
||||
-- AND tc.table_catalog = current_database();
|
||||
-- Result MUST be empty before proceeding.
|
||||
--
|
||||
-- 2. Verify the table contains only data from this migration:
|
||||
-- SELECT COUNT(*) AS idempotency_count FROM education_answer_idempotency;
|
||||
-- Operator must confirm this count is acceptable to destroy.
|
||||
--
|
||||
-- 3. Verify no application code depends on client_sequence column:
|
||||
-- Search codebase for 'clientSequence' / 'client_sequence' references.
|
||||
--
|
||||
-- 4. After verification, execute:
|
||||
-- DROP TABLE IF EXISTS education_answer_idempotency;
|
||||
-- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
|
||||
--
|
||||
-- DO NOT uncomment or execute the lines below without operator verification.
|
||||
-- -- DROP TABLE IF EXISTS education_answer_idempotency;
|
||||
-- -- ALTER TABLE education_practice_question DROP COLUMN client_sequence;
|
||||
@@ -1,113 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 答案保存幂等性 DDL (PostgreSQL)
|
||||
-- Ticket #7: 答案命令幂等、乐观锁并发控制、答案恢复
|
||||
-- Migration: 003
|
||||
-- Prerequisites: 002-education-practice-session.sql (session + question snapshots)
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name = 'education_answer_idempotency';
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
--
|
||||
-- Verify prerequisite tables exist:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name IN ('education_practice_session', 'education_practice_question');
|
||||
-- Result MUST be 2.
|
||||
|
||||
-- =============================================
|
||||
-- 答案命令幂等表
|
||||
-- =============================================
|
||||
-- Purpose: Provide durable idempotency for answer save commands.
|
||||
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
|
||||
-- Same key + different request_hash → conflict.
|
||||
-- Concurrent same-key inserts are resolved by unique constraint race handling.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_answer_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
|
||||
-- INSERT during answer save. DuplicateKeyException catch for concurrent-create race resolution.
|
||||
-- idx_tenant_session — covers lookup by session for audit/debug.
|
||||
--
|
||||
-- response_json: Stores the serialized answer response for replay after network timeout/retry.
|
||||
-- request_hash: SHA-256 of canonical payload (sorted JSON fields) for content-based dedup.
|
||||
CREATE TABLE education_answer_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 DEFAULT 'SUBMIT_ANSWER',
|
||||
idempotency_key VARCHAR(64) NOT NULL,
|
||||
request_hash VARCHAR(64) NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_id VARCHAR(64) NOT NULL,
|
||||
selected_answer TEXT DEFAULT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||||
response_json TEXT NOT NULL,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_answer_idempotency IS '教育-答案命令幂等记录';
|
||||
COMMENT ON COLUMN education_answer_idempotency.id IS '主键';
|
||||
COMMENT ON COLUMN education_answer_idempotency.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_answer_idempotency.user_id IS '答题用户编号';
|
||||
COMMENT ON COLUMN education_answer_idempotency.operation IS '操作类型:SUBMIT_ANSWER';
|
||||
COMMENT ON COLUMN education_answer_idempotency.idempotency_key IS '客户端幂等键(UUID)';
|
||||
COMMENT ON COLUMN education_answer_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
|
||||
COMMENT ON COLUMN education_answer_idempotency.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_answer_idempotency.question_id IS '题目 ID';
|
||||
COMMENT ON COLUMN education_answer_idempotency.selected_answer IS '学生已选答案';
|
||||
COMMENT ON COLUMN education_answer_idempotency.status IS '状态:ACCEPTED-已接受, CONFLICT-冲突';
|
||||
COMMENT ON COLUMN education_answer_idempotency.response_json IS '首次成功响应 JSON(用于重试重放)';
|
||||
COMMENT ON COLUMN education_answer_idempotency.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_answer_idempotency.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_answer_idempotency.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_answer_idempotency.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_answer_idempotency.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_answer_idempotency ON education_answer_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||||
CREATE INDEX idx_tenant_session ON education_answer_idempotency (tenant_id, session_id);
|
||||
|
||||
-- =============================================
|
||||
-- PracticeQuestionDO: add client_sequence column
|
||||
-- =============================================
|
||||
-- Purpose: Track the last accepted client command sequence per question.
|
||||
-- Rejects stale clientSequence: only sequences strictly greater than the
|
||||
-- stored value are accepted (monotonic forward progression).
|
||||
-- NULL means no answer has been accepted yet.
|
||||
ALTER TABLE education_practice_question
|
||||
ADD COLUMN client_sequence INT DEFAULT NULL;
|
||||
CREATE INDEX idx_client_sequence ON education_practice_question (client_sequence);
|
||||
|
||||
-- =============================================
|
||||
-- PracticeSessionDO: add last_client_sequence column
|
||||
-- =============================================
|
||||
-- Purpose: Session-wide monotonic counter for client commands.
|
||||
-- Rejects stale clientSequence across questions (not just per-question).
|
||||
-- CAS incrementVersion now updates this column alongside version.
|
||||
-- NULL means no answer has been accepted yet for this session.
|
||||
ALTER TABLE education_practice_session
|
||||
ADD COLUMN last_client_sequence INT DEFAULT NULL;
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new table exists:
|
||||
-- \d education_answer_idempotency
|
||||
-- Verify unique key is enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_answer_idempotency' AND indexname = 'uk_answer_idempotency';
|
||||
-- Verify column added to question table:
|
||||
-- SELECT column_name, data_type, column_default
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name = 'education_practice_question'
|
||||
-- AND column_name = 'client_sequence';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_answer_idempotency;
|
||||
@@ -1,50 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — Ticket #8 迁移回滚 (PostgreSQL)
|
||||
-- 004-education-submit-report-rollback.sql
|
||||
-- =============================================
|
||||
--
|
||||
-- WARNING: This file contains NO executable SQL.
|
||||
-- Destructive rollback requires manual operator verification.
|
||||
--
|
||||
-- Manual rollback procedure (operator must execute):
|
||||
-- 1. Verify no other tables depend on these tables:
|
||||
-- SELECT
|
||||
-- tc.table_name,
|
||||
-- kcu.column_name,
|
||||
-- ccu.table_name AS referenced_table_name
|
||||
-- FROM information_schema.table_constraints AS tc
|
||||
-- JOIN information_schema.key_column_usage AS kcu
|
||||
-- ON tc.constraint_name = kcu.constraint_name
|
||||
-- JOIN information_schema.constraint_column_usage AS ccu
|
||||
-- ON ccu.constraint_name = tc.constraint_name
|
||||
-- WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
-- AND ccu.table_name IN ('education_submit_idempotency',
|
||||
-- 'education_practice_report', 'education_practice_report_detail')
|
||||
-- AND ccu.table_catalog = current_database();
|
||||
-- Result MUST be empty before proceeding.
|
||||
--
|
||||
-- 2. Verify columns are not referenced by application code:
|
||||
-- Search codebase for 'correct_answer' / 'explanation' references in
|
||||
-- education_practice_question to confirm no other consumers.
|
||||
--
|
||||
-- 3. Verify the tables contain only data from this migration:
|
||||
-- SELECT COUNT(*) AS idempotency_count FROM education_submit_idempotency;
|
||||
-- SELECT COUNT(*) AS report_count FROM education_practice_report;
|
||||
-- SELECT COUNT(*) AS detail_count FROM education_practice_report_detail;
|
||||
-- Operator must confirm these counts are acceptable to destroy.
|
||||
--
|
||||
-- 4. After verification, execute:
|
||||
-- DROP TABLE IF EXISTS education_practice_report_detail;
|
||||
-- DROP TABLE IF EXISTS education_practice_report;
|
||||
-- DROP TABLE IF EXISTS education_submit_idempotency;
|
||||
-- ALTER TABLE education_practice_question
|
||||
-- DROP COLUMN correct_answer,
|
||||
-- DROP COLUMN explanation;
|
||||
--
|
||||
-- DO NOT uncomment or execute the lines below without operator verification.
|
||||
-- -- DROP TABLE IF EXISTS education_practice_report_detail;
|
||||
-- -- DROP TABLE IF EXISTS education_practice_report;
|
||||
-- -- DROP TABLE IF EXISTS education_submit_idempotency;
|
||||
-- -- ALTER TABLE education_practice_question
|
||||
-- -- DROP COLUMN correct_answer,
|
||||
-- -- DROP COLUMN explanation;
|
||||
@@ -1,220 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 交卷提交与成绩报告 DDL (PostgreSQL)
|
||||
-- Ticket #8: 交卷 CAS、保护性答案快照、评分与报告
|
||||
-- Migration: 004
|
||||
-- Prerequisites: 003-education-answer-idempotency.sql
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name IN ('education_submit_idempotency',
|
||||
-- 'education_practice_report',
|
||||
-- 'education_practice_report_detail');
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
--
|
||||
-- Verify prerequisite tables exist:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name IN ('education_practice_session',
|
||||
-- 'education_practice_question',
|
||||
-- 'education_answer_idempotency');
|
||||
-- Result MUST be 3.
|
||||
|
||||
-- =============================================
|
||||
-- PracticeQuestionDO: add protected answer snapshot columns
|
||||
-- =============================================
|
||||
-- Purpose: At session creation, snapshot correct_answer and explanation
|
||||
-- from the full CatalogQuestionDTO. These fields are NEVER exposed
|
||||
-- before submission (enforced by SafeQuestionRespVO allow-list and
|
||||
-- PracticeQuestionRespVO which does not include them).
|
||||
ALTER TABLE education_practice_question
|
||||
ADD COLUMN correct_answer TEXT DEFAULT NULL,
|
||||
ADD COLUMN explanation TEXT DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN education_practice_question.correct_answer IS '正确答案快照(不可在交卷前暴露)';
|
||||
COMMENT ON COLUMN education_practice_question.explanation IS '解析快照(不可在交卷前暴露)';
|
||||
|
||||
-- =============================================
|
||||
-- 交卷幂等表
|
||||
-- =============================================
|
||||
-- Purpose: Provide durable idempotency for submit-session commands.
|
||||
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original report.
|
||||
-- Same key + different request_hash → conflict.
|
||||
-- Concurrent same-key inserts resolved by unique constraint race handling.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_submit_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
|
||||
-- INSERT during submit. DuplicateKeyException catch for concurrent-create race resolution.
|
||||
-- idx_submit_session — covers lookup by session for audit/debug.
|
||||
CREATE TABLE education_submit_idempotency (
|
||||
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL DEFAULT 'SUBMIT_SESSION',
|
||||
idempotency_key VARCHAR(64) NOT NULL,
|
||||
request_hash VARCHAR(64) NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
report_id BIGINT DEFAULT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||||
response_json TEXT NOT NULL,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_submit_idempotency IS '教育-交卷幂等记录';
|
||||
COMMENT ON COLUMN education_submit_idempotency.id IS '主键';
|
||||
COMMENT ON COLUMN education_submit_idempotency.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_submit_idempotency.user_id IS '交卷用户编号';
|
||||
COMMENT ON COLUMN education_submit_idempotency.operation IS '操作类型:SUBMIT_SESSION';
|
||||
COMMENT ON COLUMN education_submit_idempotency.idempotency_key IS '客户端幂等键(UUID)';
|
||||
COMMENT ON COLUMN education_submit_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
|
||||
COMMENT ON COLUMN education_submit_idempotency.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_submit_idempotency.report_id IS '关联的报告 ID(成功时有值)';
|
||||
COMMENT ON COLUMN education_submit_idempotency.status IS '状态:ACCEPTED-已接受, CONFLICT-冲突';
|
||||
COMMENT ON COLUMN education_submit_idempotency.response_json IS '首次成功响应 JSON(用于重试重放)';
|
||||
COMMENT ON COLUMN education_submit_idempotency.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_submit_idempotency.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_submit_idempotency.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_submit_idempotency.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_submit_idempotency.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_submit_idempotency ON education_submit_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||||
CREATE INDEX idx_submit_session ON education_submit_idempotency (tenant_id, session_id);
|
||||
|
||||
-- =============================================
|
||||
-- 练习报告表(会话级)
|
||||
-- =============================================
|
||||
-- Purpose: Store the computed scoring result for a submitted session.
|
||||
-- One report per session. Immutable after creation.
|
||||
-- Question snapshots (stem, selectedAnswer, correctAnswer, explanation)
|
||||
-- are stored in report_details so source question edits don't affect history.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_report_session — one report per session (unique).
|
||||
-- idx_report_tenant_user — covers paginated history queries for current tenant+user.
|
||||
-- idx_report_create_time — covers time-sorted listing.
|
||||
CREATE TABLE education_practice_report (
|
||||
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_count INT NOT NULL,
|
||||
answered_count INT NOT NULL DEFAULT 0,
|
||||
unanswered_count INT NOT NULL DEFAULT 0,
|
||||
correct_count INT NOT NULL DEFAULT 0,
|
||||
incorrect_count INT NOT NULL DEFAULT 0,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED',
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_practice_report IS '教育-练习报告';
|
||||
COMMENT ON COLUMN education_practice_report.id IS '主键';
|
||||
COMMENT ON COLUMN education_practice_report.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_practice_report.user_id IS '用户编号';
|
||||
COMMENT ON COLUMN education_practice_report.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_practice_report.question_count IS '题目总数';
|
||||
COMMENT ON COLUMN education_practice_report.answered_count IS '已答题数';
|
||||
COMMENT ON COLUMN education_practice_report.unanswered_count IS '未答题数';
|
||||
COMMENT ON COLUMN education_practice_report.correct_count IS '正确题数';
|
||||
COMMENT ON COLUMN education_practice_report.incorrect_count IS '错误题数';
|
||||
COMMENT ON COLUMN education_practice_report.score IS '得分(整数,满分 100 为基准)';
|
||||
COMMENT ON COLUMN education_practice_report.status IS '报告状态:SUBMITTED';
|
||||
COMMENT ON COLUMN education_practice_report.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_practice_report.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_practice_report.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_practice_report.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_practice_report.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_report_session ON education_practice_report (session_id);
|
||||
CREATE INDEX idx_report_tenant_user ON education_practice_report (tenant_id, user_id);
|
||||
CREATE INDEX idx_report_create_time ON education_practice_report (create_time);
|
||||
|
||||
-- =============================================
|
||||
-- 练习报告明细表(逐题结果)
|
||||
-- =============================================
|
||||
-- Purpose: Store per-question scoring results at submission time.
|
||||
-- Includes snapshot of stem, selectedAnswer, correctAnswer, and explanation
|
||||
-- so that history is stable even if source questions are later edited.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_report_sequence — per-report uniqueness for question sequence.
|
||||
-- idx_detail_session — covers lookup by session for report assembly.
|
||||
CREATE TABLE education_practice_report_detail (
|
||||
id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
report_id BIGINT NOT NULL,
|
||||
session_id BIGINT NOT NULL,
|
||||
question_id VARCHAR(64) NOT NULL,
|
||||
sequence INT NOT NULL,
|
||||
stem TEXT NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
difficulty VARCHAR(32) DEFAULT NULL,
|
||||
selected_answer TEXT DEFAULT NULL,
|
||||
correct_answer TEXT DEFAULT NULL,
|
||||
is_correct BOOLEAN NOT NULL DEFAULT false,
|
||||
explanation TEXT DEFAULT NULL,
|
||||
creator VARCHAR(64) DEFAULT '',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) DEFAULT '',
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_practice_report_detail IS '教育-练习报告明细';
|
||||
COMMENT ON COLUMN education_practice_report_detail.id IS '主键';
|
||||
COMMENT ON COLUMN education_practice_report_detail.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_practice_report_detail.user_id IS '用户编号';
|
||||
COMMENT ON COLUMN education_practice_report_detail.report_id IS '报告 ID';
|
||||
COMMENT ON COLUMN education_practice_report_detail.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_practice_report_detail.question_id IS '原始题目 ID';
|
||||
COMMENT ON COLUMN education_practice_report_detail.sequence IS '题目序号(1-based)';
|
||||
COMMENT ON COLUMN education_practice_report_detail.stem IS '题干快照';
|
||||
COMMENT ON COLUMN education_practice_report_detail.type IS '题型快照';
|
||||
COMMENT ON COLUMN education_practice_report_detail.difficulty IS '难度快照';
|
||||
COMMENT ON COLUMN education_practice_report_detail.selected_answer IS '学生已选答案';
|
||||
COMMENT ON COLUMN education_practice_report_detail.correct_answer IS '正确答案快照';
|
||||
COMMENT ON COLUMN education_practice_report_detail.is_correct IS '是否正确';
|
||||
COMMENT ON COLUMN education_practice_report_detail.explanation IS '解析快照';
|
||||
COMMENT ON COLUMN education_practice_report_detail.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_practice_report_detail.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_practice_report_detail.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_practice_report_detail.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_practice_report_detail.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_report_sequence ON education_practice_report_detail (report_id, sequence);
|
||||
CREATE INDEX idx_detail_session ON education_practice_report_detail (session_id);
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new tables exist:
|
||||
-- \d education_submit_idempotency
|
||||
-- \d education_practice_report
|
||||
-- \d education_practice_report_detail
|
||||
-- Verify columns added to question table:
|
||||
-- SELECT column_name, data_type, column_default
|
||||
-- FROM information_schema.columns
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name = 'education_practice_question'
|
||||
-- AND column_name IN ('correct_answer', 'explanation');
|
||||
-- Verify unique keys are enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_submit_idempotency' AND indexname = 'uk_submit_idempotency';
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report' AND indexname = 'uk_report_session';
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_report_detail' AND indexname = 'uk_report_sequence';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_submit_idempotency;
|
||||
-- SELECT COUNT(*) FROM education_practice_report;
|
||||
-- SELECT COUNT(*) FROM education_practice_report_detail;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 错题本 DDL Rollback
|
||||
-- Migration: 005
|
||||
-- =============================================
|
||||
-- IMPORTANT: This is a documentation-only rollback.
|
||||
-- No DROP/ALTER/DELETE statements are executed. The wrong_question
|
||||
-- table is provenance-safe: it only accumulates data and mastering
|
||||
-- is a status flag. Dropping these tables would lose student error
|
||||
-- history with no recovery path.
|
||||
--
|
||||
-- What this migration created:
|
||||
-- - education_wrong_question (new table)
|
||||
-- - education_wrong_question_idempotency (new table)
|
||||
-- - education_practice_report_detail.options (new column)
|
||||
-- - education_practice_session.review_fingerprint (new column)
|
||||
--
|
||||
-- Manual rollback requires:
|
||||
-- 1. Verified database backup before rollback
|
||||
-- 2. Operator approval (DBA sign-off)
|
||||
-- 3. Provenance of all wrong-question records preserved (exported)
|
||||
-- 4. Soft-delete via deleted = true before any hard drop
|
||||
--
|
||||
-- These tables are NOT deleted by this script. Wrong history is
|
||||
-- retained; if deletion is required by external policy, consult
|
||||
-- the DBA for a verified rollback procedure.
|
||||
@@ -1,156 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 错题本 DDL
|
||||
-- Ticket #9: 错题自动收集、复习练习创建
|
||||
-- Migration: 005
|
||||
-- Prerequisites: 004-education-submit-report.sql (report + detail tables)
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_schema = current_database()
|
||||
-- AND table_name IN ('education_wrong_question',
|
||||
-- 'education_wrong_question_idempotency');
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
--
|
||||
-- Verify prerequisite tables exist:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_schema = current_database()
|
||||
-- AND table_name IN ('education_practice_report',
|
||||
-- 'education_practice_report_detail');
|
||||
-- Result MUST be 2.
|
||||
|
||||
-- =============================================
|
||||
-- PracticeReportDetailDO: add content_version + options snapshot columns
|
||||
-- =============================================
|
||||
-- Purpose: At submit time, snapshot question content version and options
|
||||
-- (without isCorrect) so wrong-question book and review sessions have
|
||||
-- stable display data. The options are already stripped of isCorrect
|
||||
-- by the submit flow.
|
||||
ALTER TABLE education_practice_report_detail
|
||||
ADD COLUMN content_version VARCHAR(64) NOT NULL DEFAULT '' /* 题目内容版本快照 */,
|
||||
ADD COLUMN options TEXT DEFAULT NULL /* 选项快照 JSON(不含 isCorrect) */;
|
||||
|
||||
-- =============================================
|
||||
-- PracticeSessionDO: add review fingerprint column
|
||||
-- =============================================
|
||||
-- Purpose: Persist the canonical fingerprint of wrong-question IDs used
|
||||
-- to create a review session. On idempotent replay, the fingerprint
|
||||
-- is compared: same tenant+clientSessionId+sameUser+sortedIDs match
|
||||
-- returns the existing session; different ID set returns
|
||||
-- SESSION_IDEMPOTENCY_MISMATCH.
|
||||
ALTER TABLE education_practice_session
|
||||
ADD COLUMN review_fingerprint VARCHAR(64) DEFAULT NULL /* 复习会话题目指纹(SHA-256 of sorted unique wrongQuestionIds) */;
|
||||
|
||||
-- 错题表
|
||||
-- =============================================
|
||||
-- Purpose: Persistent wrong-question book per student.
|
||||
-- Each (tenant, user, question) is a unique entry.
|
||||
-- Repeated wrong answers on the SAME question increment wrong_count
|
||||
-- and update last_wrong_time. The idempotency guard table ensures
|
||||
-- each (tenant, user, question, report) can upsert at most once.
|
||||
--
|
||||
-- master_status values: 'PENDING' (default) | 'MASTERED'
|
||||
-- Marking mastered retains the full history and count; it does NOT
|
||||
-- delete or archive the record. Students can optionally un-master.
|
||||
--
|
||||
-- Snapshot fields (stem, type, difficulty, options, content_version):
|
||||
-- populated from the latest report detail that touched this question.
|
||||
-- These are for listing/detail display without joining report details.
|
||||
--
|
||||
-- latest_correct_answer, latest_explanation:
|
||||
-- also from the latest report detail; available for detail display
|
||||
-- post-submit (not exposed in review session creation pre-submit).
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_tenant_user_question — per-tenant, per-user, per-question uniqueness.
|
||||
-- INSERT ... ON CONFLICT ... DO UPDATE is the primary write path.
|
||||
-- idx_tenant_user_status — covers filtered list queries (page with status filter).
|
||||
-- idx_tenant_user_last_wrong — covers time-sorted listing.
|
||||
CREATE TABLE 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 /* 原始题目 ID */,
|
||||
-- snapshot fields for listing / detail (from latest report detail)
|
||||
stem TEXT NOT NULL /* 题干快照(最新) */,
|
||||
type VARCHAR(32) NOT NULL /* 题型快照 */,
|
||||
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
|
||||
options JSONB NOT NULL /* 选项快照 JSON(不含 isCorrect) */,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 题目内容版本 */,
|
||||
latest_correct_answer TEXT DEFAULT NULL /* 正确答案快照(最新,供详情展示) */,
|
||||
latest_explanation TEXT DEFAULT NULL /* 解析快照(最新,供详情展示) */,
|
||||
-- timing & count
|
||||
first_wrong_time TIMESTAMP NOT NULL /* 首次错误时间 */,
|
||||
last_wrong_time TIMESTAMP NOT NULL /* 最近错误时间 */,
|
||||
wrong_count INT NOT NULL DEFAULT 1 /* 累计错误次数 */,
|
||||
-- mastery
|
||||
master_status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
|
||||
/* 掌握状态:PENDING-待掌握, MASTERED-已掌握 */,
|
||||
mastered_time TIMESTAMP DEFAULT NULL /* 标记掌握时间 */,
|
||||
-- provenance
|
||||
last_report_id BIGINT DEFAULT NULL /* 最近关联的报告 ID */,
|
||||
last_session_id BIGINT DEFAULT NULL /* 最近关联的会话 ID */,
|
||||
-- audit
|
||||
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 '教育-错题本';
|
||||
|
||||
CREATE UNIQUE INDEX uk_tenant_user_question ON education_wrong_question (tenant_id, user_id, question_id);
|
||||
CREATE INDEX idx_tenant_user_status ON education_wrong_question (tenant_id, user_id, master_status);
|
||||
CREATE INDEX idx_tenant_user_last_wrong ON education_wrong_question (tenant_id, user_id, last_wrong_time);
|
||||
|
||||
-- =============================================
|
||||
-- 错题流水幂等表
|
||||
-- =============================================
|
||||
-- Purpose: Ensure each (tenant, user, question, report) upserts the
|
||||
-- wrong-question book exactly once. The submitSession transaction
|
||||
-- INSERT ... ON CONFLICT DO NOTHING into this table BEFORE the
|
||||
-- wrong question upsert; a duplicate means this report already
|
||||
-- contributed to the count. This guards against:
|
||||
-- - Replayed submit (idempotent resubmit) double-counting
|
||||
-- - Concurrent submit races where both threads evaluate the
|
||||
-- same report details
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_tenant_user_question_report — per (tenant, user, question, report) uniqueness.
|
||||
-- INSERT ... ON CONFLICT DO NOTHING provides the idempotency guard
|
||||
-- BEFORE upserting. wrong_question_id is filled after upsert for
|
||||
-- audit purposes.
|
||||
-- idx_report — fast lookup by report for audit/debug.
|
||||
CREATE TABLE 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 DEFAULT NULL /* 错题记录 ID(upsert 后填充) */,
|
||||
report_id BIGINT NOT NULL /* 报告 ID */,
|
||||
question_id VARCHAR(64) NOT NULL /* 题目 ID */,
|
||||
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 uk_tenant_user_question_report ON education_wrong_question_idempotency (tenant_id, user_id, question_id, report_id);
|
||||
CREATE INDEX idx_report ON education_wrong_question_idempotency (report_id);
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new tables exist:
|
||||
-- \d education_wrong_question
|
||||
-- \d education_wrong_question_idempotency
|
||||
-- Verify unique keys are enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question' AND indexname = 'uk_tenant_user_question';
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_wrong_question_idempotency' AND indexname = 'uk_tenant_user_question_report';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_wrong_question;
|
||||
-- SELECT COUNT(*) FROM education_wrong_question_idempotency;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 收藏夹 DDL Rollback (PostgreSQL)
|
||||
-- Migration: 007
|
||||
-- =============================================
|
||||
-- IMPORTANT: This is a documentation-only rollback.
|
||||
-- No DROP/ALTER/DELETE statements are executed. The favorite
|
||||
-- table is provenance-safe: it only accumulates user preference
|
||||
-- data. Dropping this table would lose student favorites with
|
||||
-- no recovery path.
|
||||
--
|
||||
-- What this migration created:
|
||||
-- - education_favorite (new table)
|
||||
-- - idx_tenant_user (index)
|
||||
-- - idx_tenant_user_target_type (index)
|
||||
-- - uk_tenant_user_target (unique constraint)
|
||||
--
|
||||
-- Manual rollback requires:
|
||||
-- 1. Verified database backup before rollback
|
||||
-- 2. Operator approval (DBA sign-off)
|
||||
-- 3. Provenance of all favorite records preserved (exported)
|
||||
-- 4. Soft-delete via deleted = true before any hard drop
|
||||
--
|
||||
-- These tables are NOT deleted by this script. Favorite history is
|
||||
-- retained; if deletion is required by external policy, consult
|
||||
-- the DBA for a verified rollback procedure.
|
||||
@@ -1,95 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 收藏夹 DDL (PostgreSQL)
|
||||
-- Ticket #10: 学生收藏题目
|
||||
-- Migration: 007
|
||||
-- Prerequisites: 000-education-schema.sql (base tables)
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name = 'education_favorite';
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
|
||||
-- =============================================
|
||||
-- 收藏表
|
||||
-- =============================================
|
||||
-- Purpose: Student favorites for questions with safe snapshots.
|
||||
-- Each (tenant, user, target_type, target_id) is a unique entry.
|
||||
-- Logical deletion: setting deleted=true marks as unfavorited.
|
||||
-- Re-adding after deletion reactivates the row via ON CONFLICT ... DO UPDATE.
|
||||
--
|
||||
-- target_type values: 'QUESTION' (extensible enum)
|
||||
--
|
||||
-- Snapshot fields (stem, type, difficulty, options, content_version):
|
||||
-- populated at creation time from the visible question's safe fields.
|
||||
-- These snapshots preserve the question state as it appeared when favorited,
|
||||
-- and remain stable even if the source question later changes or becomes unavailable.
|
||||
--
|
||||
-- available flag:
|
||||
-- FALSE when the source question becomes hidden/unpublished after being
|
||||
-- favorited. Existing favorites with available=FALSE remain listable but
|
||||
-- display an "unavailable" indicator. New favorites cannot be created for
|
||||
-- unavailable resources.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_tenant_user_target — per (tenant, user, target_type, target_id) uniqueness.
|
||||
-- INSERT ... ON CONFLICT DO UPDATE is the primary reactivation path.
|
||||
-- idx_tenant_user — covers listing queries filtered by current tenant+user.
|
||||
-- idx_tenant_user_target_type — covers target-type-filtered listing.
|
||||
CREATE TABLE 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,
|
||||
-- safe snapshot fields
|
||||
stem TEXT DEFAULT NULL,
|
||||
type VARCHAR(32) DEFAULT NULL,
|
||||
difficulty VARCHAR(32) DEFAULT NULL,
|
||||
options JSONB DEFAULT NULL,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
-- availability
|
||||
available BOOLEAN NOT NULL DEFAULT true,
|
||||
-- audit
|
||||
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 uk_tenant_user_target UNIQUE (tenant_id, user_id, target_type, target_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE education_favorite IS '教育-收藏夹';
|
||||
COMMENT ON COLUMN education_favorite.id IS '主键';
|
||||
COMMENT ON COLUMN education_favorite.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_favorite.user_id IS '学生用户编号';
|
||||
COMMENT ON COLUMN education_favorite.target_type IS '目标类型:QUESTION';
|
||||
COMMENT ON COLUMN education_favorite.target_id IS '目标 ID(题目 ID)';
|
||||
COMMENT ON COLUMN education_favorite.stem IS '题干快照';
|
||||
COMMENT ON COLUMN education_favorite.type IS '题型快照';
|
||||
COMMENT ON COLUMN education_favorite.difficulty IS '难度快照';
|
||||
COMMENT ON COLUMN education_favorite.options IS '选项快照 JSON(不含 isCorrect)';
|
||||
COMMENT ON COLUMN education_favorite.content_version IS '题目内容版本';
|
||||
COMMENT ON COLUMN education_favorite.available IS '源资源是否可用';
|
||||
COMMENT ON COLUMN education_favorite.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_favorite.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_favorite.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_favorite.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_favorite.deleted IS '是否删除';
|
||||
|
||||
CREATE INDEX idx_tenant_user ON education_favorite (tenant_id, user_id);
|
||||
CREATE INDEX idx_tenant_user_target_type ON education_favorite (tenant_id, user_id, target_type);
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new table exists:
|
||||
-- \d education_favorite
|
||||
-- Verify unique constraint is enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_favorite' AND indexname = 'uk_tenant_user_target';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_favorite;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 题库目录表回滚 (PostgreSQL)
|
||||
-- Migration: 008 rollback
|
||||
-- Drops all 11 tables created by 008-education-catalog.sql
|
||||
-- =============================================
|
||||
|
||||
DROP TABLE IF EXISTS education_question_collection_question;
|
||||
DROP TABLE IF EXISTS education_practice_blueprint;
|
||||
DROP TABLE IF EXISTS education_question;
|
||||
DROP TABLE IF EXISTS education_question_collection;
|
||||
DROP TABLE IF EXISTS education_content_node;
|
||||
DROP TABLE IF EXISTS education_content_entry;
|
||||
DROP TABLE IF EXISTS education_category;
|
||||
DROP TABLE IF EXISTS education_subject;
|
||||
DROP TABLE IF EXISTS education_major;
|
||||
DROP TABLE IF EXISTS education_school;
|
||||
DROP TABLE IF EXISTS education_region;
|
||||
@@ -1,530 +0,0 @@
|
||||
-- =============================================
|
||||
-- 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;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 统一幂等表 回滚
|
||||
-- Migration: 009 回滚
|
||||
-- =============================================
|
||||
|
||||
DROP TABLE IF EXISTS education_idempotency;
|
||||
|
||||
-- Recreate the original split tables from 003 + 004 migrations if needed:
|
||||
-- (restore from backup or re-run 003 and 004 DDL scripts)
|
||||
|
||||
-- Remove review_fingerprint column
|
||||
ALTER TABLE education_practice_session
|
||||
DROP COLUMN IF EXISTS review_fingerprint;
|
||||
@@ -1,103 +0,0 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 统一幂等表 DDL (PostgreSQL)
|
||||
-- Ticket: 合并 Answer + Submit 幂等表为统一 education_idempotency
|
||||
-- Migration: 009
|
||||
-- Prerequisites: 002-education-practice-session.sql
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- Verify table does not already exist:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_catalog = current_database()
|
||||
-- AND table_name = 'education_idempotency';
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
--
|
||||
-- NOTE: This table replaces education_answer_idempotency and education_submit_idempotency.
|
||||
-- Those tables should be dropped after this migration is verified:
|
||||
-- DROP TABLE IF EXISTS education_answer_idempotency;
|
||||
-- DROP TABLE IF EXISTS education_submit_idempotency;
|
||||
|
||||
-- =============================================
|
||||
-- 统一幂等表
|
||||
-- =============================================
|
||||
-- Purpose: Unified idempotency store for all education write operations.
|
||||
-- operation field distinguishes: SUBMIT_ANSWER vs SUBMIT_SESSION.
|
||||
-- Same (tenant, user, operation, idempotency_key) + same request_hash → replay original response.
|
||||
-- Same key + different request_hash → conflict.
|
||||
-- PostgreSQL ON CONFLICT DO NOTHING resolves concurrent same-key inserts.
|
||||
--
|
||||
-- Indexes:
|
||||
-- uk_idempotency — per-tenant, per-actor, per-operation uniqueness for idempotency key.
|
||||
-- Used by: IdempotencyStoreMapper.insertIgnore() ON CONFLICT resolution.
|
||||
-- idx_tenant_session — covers lookup by session for audit/debug.
|
||||
--
|
||||
-- Fields specific to SUBMIT_ANSWER: question_id, selected_answer (nullable for SUBMIT_SESSION).
|
||||
-- Fields specific to SUBMIT_SESSION: report_id (nullable for SUBMIT_ANSWER).
|
||||
-- response_json: stores serialized response for replay after timeout/retry.
|
||||
-- request_hash: SHA-256 of canonical payload for content-based dedup.
|
||||
CREATE TABLE 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) DEFAULT NULL,
|
||||
selected_answer TEXT DEFAULT NULL,
|
||||
report_id BIGINT DEFAULT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACCEPTED',
|
||||
response_json TEXT DEFAULT NULL,
|
||||
business_payload 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_idempotency IS '教育-统一幂等记录(合并 answer + submit)';
|
||||
COMMENT ON COLUMN education_idempotency.id IS '主键';
|
||||
COMMENT ON COLUMN education_idempotency.tenant_id IS '租户编号';
|
||||
COMMENT ON COLUMN education_idempotency.user_id IS '用户编号';
|
||||
COMMENT ON COLUMN education_idempotency.operation IS '操作类型:SUBMIT_ANSWER-答题, SUBMIT_SESSION-交卷';
|
||||
COMMENT ON COLUMN education_idempotency.idempotency_key IS '客户端幂等键(UUID)';
|
||||
COMMENT ON COLUMN education_idempotency.request_hash IS '请求载荷 SHA-256 哈希';
|
||||
COMMENT ON COLUMN education_idempotency.session_id IS '会话 ID';
|
||||
COMMENT ON COLUMN education_idempotency.question_id IS '题目 ID(SUBMIT_ANSWER)';
|
||||
COMMENT ON COLUMN education_idempotency.selected_answer IS '学生已选答案(SUBMIT_ANSWER)';
|
||||
COMMENT ON COLUMN education_idempotency.report_id IS '关联报告 ID(SUBMIT_SESSION)';
|
||||
COMMENT ON COLUMN education_idempotency.status IS '状态:ACCEPTED-已接受, CONFLICT-冲突';
|
||||
COMMENT ON COLUMN education_idempotency.response_json IS '首次成功响应 JSON(用于重试重放)';
|
||||
COMMENT ON COLUMN education_idempotency.business_payload IS '业务扩展载荷(JSONB)';
|
||||
COMMENT ON COLUMN education_idempotency.creator IS '创建者';
|
||||
COMMENT ON COLUMN education_idempotency.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN education_idempotency.updater IS '更新者';
|
||||
COMMENT ON COLUMN education_idempotency.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN education_idempotency.deleted IS '是否删除';
|
||||
|
||||
CREATE UNIQUE INDEX uk_idempotency ON education_idempotency (tenant_id, user_id, operation, idempotency_key);
|
||||
CREATE INDEX idx_idempotency_tenant_session ON education_idempotency (tenant_id, session_id);
|
||||
|
||||
-- =============================================
|
||||
-- PracticeSessionDO: add review_fingerprint column
|
||||
-- =============================================
|
||||
-- Purpose: Store canonical fingerprint for wrong-question review session idempotency.
|
||||
-- SHA-256 of comma-joined sorted wrong_question IDs for deterministic comparison.
|
||||
ALTER TABLE education_practice_session
|
||||
ADD COLUMN review_fingerprint VARCHAR(64) DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN education_practice_session.review_fingerprint IS '错题复习指纹(SHA-256),用于幂等创建';
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify new table exists:
|
||||
-- \d education_idempotency
|
||||
-- Verify unique index:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_idempotency';
|
||||
-- Verify column added:
|
||||
-- SELECT column_name, data_type FROM information_schema.columns
|
||||
-- WHERE table_name = 'education_practice_session' AND column_name = 'review_fingerprint';
|
||||
Reference in New Issue
Block a user