fix(education): align Flyway delivery review

This commit is contained in:
2026-07-30 14:10:52 +08:00
parent 2d97858680
commit 191811b643
59 changed files with 19 additions and 23736 deletions

View File

@@ -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`;

View File

@@ -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 '父节点 IDNULL=根节点)',
`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;

View File

@@ -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);

View File

@@ -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 = '教育管理';

View File

@@ -1,7 +0,0 @@
-- =============================================
-- Education 模块 DDL (PostgreSQL)
-- 当前为应用外壳阶段,无业务表;后续票据在此追加 CREATE TABLE 语句。
-- =============================================
-- 占位education 模块当前无业务表
CREATE SCHEMA IF NOT EXISTS education;

View File

@@ -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);

View File

@@ -1 +0,0 @@
-- Ticket #3 creates no database records, so rollback is intentionally a no-op.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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 /* 错题记录 IDupsert 后填充) */,
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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 '父节点 IDNULL=根节点)';
COMMENT ON COLUMN education_content_node.name IS '节点名称';
COMMENT ON COLUMN education_content_node.title IS '节点标题';
COMMENT ON COLUMN education_content_node.node_type IS '节点类型';
COMMENT ON COLUMN education_content_node.marker_type IS '节点标记类型';
COMMENT ON COLUMN education_content_node.depth IS '树深度';
COMMENT ON COLUMN education_content_node.is_leaf IS '是否叶子节点';
COMMENT ON COLUMN education_content_node.is_selectable IS '是否可被选择';
COMMENT ON COLUMN education_content_node.is_active IS '是否启用';
COMMENT ON COLUMN education_content_node.sort_order IS '显示排序值';
COMMENT ON COLUMN education_content_node.metadata IS '扩展元数据';
COMMENT ON COLUMN education_content_node.creator IS '创建者';
COMMENT ON COLUMN education_content_node.create_time IS '创建时间';
COMMENT ON COLUMN education_content_node.updater IS '更新者';
COMMENT ON COLUMN education_content_node.update_time IS '更新时间';
COMMENT ON COLUMN education_content_node.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_content_node (tenant_id);
CREATE INDEX idx_entry_parent ON education_content_node (entry_id, parent_id);
CREATE INDEX idx_tenant_entry ON education_content_node (tenant_id, entry_id);
-- =============================================
-- 8. 题集表
-- =============================================
CREATE TABLE education_question_collection (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
name VARCHAR(200) NOT NULL,
title VARCHAR(200) DEFAULT NULL,
collection_type VARCHAR(50) NOT NULL,
question_count INT NOT NULL DEFAULT 0,
duration_minutes INT DEFAULT NULL,
access_rules JSONB DEFAULT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_question_collection IS '教育-题集';
COMMENT ON COLUMN education_question_collection.id IS '主键';
COMMENT ON COLUMN education_question_collection.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question_collection.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_question_collection.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_question_collection.name IS '题集名称';
COMMENT ON COLUMN education_question_collection.title IS '题集标题';
COMMENT ON COLUMN education_question_collection.collection_type IS '题集类型';
COMMENT ON COLUMN education_question_collection.question_count IS '题目数量';
COMMENT ON COLUMN education_question_collection.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_question_collection.access_rules IS '访问规则 JSON';
COMMENT ON COLUMN education_question_collection.is_active IS '是否启用';
COMMENT ON COLUMN education_question_collection.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question_collection.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question_collection.creator IS '创建者';
COMMENT ON COLUMN education_question_collection.create_time IS '创建时间';
COMMENT ON COLUMN education_question_collection.updater IS '更新者';
COMMENT ON COLUMN education_question_collection.update_time IS '更新时间';
COMMENT ON COLUMN education_question_collection.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question_collection (tenant_id);
CREATE INDEX idx_tenant_entry ON education_question_collection (tenant_id, entry_id);
CREATE INDEX idx_tenant_node ON education_question_collection (tenant_id, node_id);
-- =============================================
-- 9. 题目表(核心表)
-- =============================================
-- Indexes:
-- idx_tenant_published — 查询已发布题目(主查询路径)
-- idx_tenant_collection — 按题集筛选
-- idx_tenant_subject — 按科目筛选
-- idx_tenant_node — 按内容节点筛选
-- idx_tenant_type_diff — 按题型和难度筛选
-- Security note: correct_answer 和 explanation 列存在但需在 service 层过滤;
-- 学生端 DTO 绝不得包含这两列。
-- Options JSONB: 存储 label, content, isCorrect, order — 完整选项数据。
-- Service 层在学生端返回前剥离 isCorrect。
CREATE TABLE education_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
content_version INT NOT NULL DEFAULT 1,
stem TEXT NOT NULL,
type VARCHAR(32) NOT NULL,
type_label VARCHAR(50) DEFAULT NULL,
difficulty VARCHAR(32) DEFAULT NULL,
question_content JSONB DEFAULT NULL,
options JSONB NOT NULL,
correct_answer VARCHAR(500) DEFAULT NULL,
explanation TEXT DEFAULT NULL,
analysis TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED',
is_published BOOLEAN NOT NULL DEFAULT true,
collection_id BIGINT DEFAULT NULL,
subject_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
tags JSONB DEFAULT NULL,
sort_order INT NOT NULL DEFAULT 0,
metadata JSONB DEFAULT NULL,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_question IS '教育-题目';
COMMENT ON COLUMN education_question.id IS '主键';
COMMENT ON COLUMN education_question.tenant_id IS '租户编号';
COMMENT ON COLUMN education_question.content_version IS '内容版本号(递增,保证历史报告稳定性)';
COMMENT ON COLUMN education_question.stem IS '题干';
COMMENT ON COLUMN education_question.type IS '题型';
COMMENT ON COLUMN education_question.type_label IS '题型显示名称';
COMMENT ON COLUMN education_question.difficulty IS '难度';
COMMENT ON COLUMN education_question.question_content IS '题干结构化内容(兼容复杂题型)';
COMMENT ON COLUMN education_question.options IS '选项 JSONB [{label, content, isCorrect, order}]';
COMMENT ON COLUMN education_question.correct_answer IS '正确答案(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.explanation IS '答案解析(敏感字段,学生端不可返回)';
COMMENT ON COLUMN education_question.analysis IS '深度解析(敏感字段)';
COMMENT ON COLUMN education_question.status IS '题目状态PUBLISHED-已发布, HIDDEN-已隐藏, DRAFT-草稿, INACTIVE-停用';
COMMENT ON COLUMN education_question.is_published IS '是否已发布';
COMMENT ON COLUMN education_question.collection_id IS '所属题集 ID';
COMMENT ON COLUMN education_question.subject_id IS '所属科目 ID';
COMMENT ON COLUMN education_question.node_id IS '所属内容节点 ID';
COMMENT ON COLUMN education_question.tags IS '标签 JSONB 数组';
COMMENT ON COLUMN education_question.sort_order IS '显示排序值';
COMMENT ON COLUMN education_question.metadata IS '扩展元数据';
COMMENT ON COLUMN education_question.creator IS '创建者';
COMMENT ON COLUMN education_question.create_time IS '创建时间';
COMMENT ON COLUMN education_question.updater IS '更新者';
COMMENT ON COLUMN education_question.update_time IS '更新时间';
COMMENT ON COLUMN education_question.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_question (tenant_id);
CREATE INDEX idx_tenant_published ON education_question (tenant_id, is_published, status);
CREATE INDEX idx_tenant_collection ON education_question (tenant_id, collection_id);
CREATE INDEX idx_tenant_subject ON education_question (tenant_id, subject_id);
CREATE INDEX idx_tenant_node ON education_question (tenant_id, node_id);
CREATE INDEX idx_tenant_type_diff ON education_question (tenant_id, type, difficulty);
-- =============================================
-- 10. 练习蓝图表
-- =============================================
CREATE TABLE education_practice_blueprint (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL,
mode VARCHAR(50) NOT NULL,
entry_id BIGINT DEFAULT NULL,
node_id BIGINT DEFAULT NULL,
collection_id BIGINT DEFAULT NULL,
question_limit INT DEFAULT NULL,
duration_minutes INT DEFAULT NULL,
eligible_count INT NOT NULL DEFAULT 0,
total_count INT NOT NULL DEFAULT 0,
available_types JSONB DEFAULT NULL,
available_difficulties JSONB DEFAULT NULL,
min_questions INT NOT NULL DEFAULT 1,
max_questions INT NOT NULL DEFAULT 200,
suggested_count INT NOT NULL DEFAULT 20,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON TABLE education_practice_blueprint IS '教育-练习蓝图';
COMMENT ON COLUMN education_practice_blueprint.id IS '主键';
COMMENT ON COLUMN education_practice_blueprint.tenant_id IS '租户编号';
COMMENT ON COLUMN education_practice_blueprint.mode IS '练习模式';
COMMENT ON COLUMN education_practice_blueprint.entry_id IS '内容入口 ID';
COMMENT ON COLUMN education_practice_blueprint.node_id IS '内容节点 ID';
COMMENT ON COLUMN education_practice_blueprint.collection_id IS '题集 ID';
COMMENT ON COLUMN education_practice_blueprint.question_limit IS '题目数量限制';
COMMENT ON COLUMN education_practice_blueprint.duration_minutes IS '建议作答时长(分钟)';
COMMENT ON COLUMN education_practice_blueprint.eligible_count IS '符合条件的题目数';
COMMENT ON COLUMN education_practice_blueprint.total_count IS '题库总数';
COMMENT ON COLUMN education_practice_blueprint.available_types IS '可用题型列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.available_difficulties IS '可用难度列表 JSONB';
COMMENT ON COLUMN education_practice_blueprint.min_questions IS '最少题目数';
COMMENT ON COLUMN education_practice_blueprint.max_questions IS '最多题目数';
COMMENT ON COLUMN education_practice_blueprint.suggested_count IS '建议题目数';
COMMENT ON COLUMN education_practice_blueprint.creator IS '创建者';
COMMENT ON COLUMN education_practice_blueprint.create_time IS '创建时间';
COMMENT ON COLUMN education_practice_blueprint.updater IS '更新者';
COMMENT ON COLUMN education_practice_blueprint.update_time IS '更新时间';
COMMENT ON COLUMN education_practice_blueprint.deleted IS '是否删除';
CREATE INDEX idx_tenant ON education_practice_blueprint (tenant_id);
CREATE INDEX idx_tenant_collection ON education_practice_blueprint (tenant_id, collection_id);
-- =============================================
-- 11. 题集-题目关联表(多对多)
-- =============================================
CREATE TABLE education_question_collection_question (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collection_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
sort_order INT NOT NULL DEFAULT 0
);
COMMENT ON TABLE education_question_collection_question IS '教育-题集题目关联';
COMMENT ON COLUMN education_question_collection_question.id IS '主键';
COMMENT ON COLUMN education_question_collection_question.collection_id IS '题集 ID';
COMMENT ON COLUMN education_question_collection_question.question_id IS '题目 ID';
COMMENT ON COLUMN education_question_collection_question.sort_order IS '排序值';
CREATE UNIQUE INDEX uk_collection_question ON education_question_collection_question (collection_id, question_id);
CREATE INDEX idx_question ON education_question_collection_question (question_id);
-- =============================================
-- Post-migration verification queries
-- =============================================
-- Verify all tables exist:
-- SELECT table_name FROM information_schema.tables
-- WHERE table_catalog = current_database()
-- AND table_name LIKE 'education_%'
-- ORDER BY table_name;
-- Verify indexes (sample):
-- -- PostgreSQL equivalent of SHOW INDEX:
-- SELECT * FROM pg_indexes WHERE tablename = 'education_question' AND indexname IN ('idx_tenant_published', 'idx_tenant_collection');
-- SELECT * FROM pg_indexes WHERE tablename = 'education_content_node' AND indexname = 'idx_entry_parent';
-- Verify no orphan data (should be 0 after fresh migration for catalog tables):
-- SELECT 'region' AS tbl, COUNT(*) FROM education_region
-- UNION ALL SELECT 'school', COUNT(*) FROM education_school
-- UNION ALL SELECT 'major', COUNT(*) FROM education_major
-- UNION ALL SELECT 'subject', COUNT(*) FROM education_subject
-- UNION ALL SELECT 'category', COUNT(*) FROM education_category
-- UNION ALL SELECT 'content_entry', COUNT(*) FROM education_content_entry
-- UNION ALL SELECT 'content_node', COUNT(*) FROM education_content_node
-- UNION ALL SELECT 'question_collection', COUNT(*) FROM education_question_collection
-- UNION ALL SELECT 'question', COUNT(*) FROM education_question
-- UNION ALL SELECT 'practice_blueprint', COUNT(*) FROM education_practice_blueprint
-- UNION ALL SELECT 'collection_question', COUNT(*) FROM education_question_collection_question;

View File

@@ -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;

View File

@@ -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 '题目 IDSUBMIT_ANSWER';
COMMENT ON COLUMN education_idempotency.selected_answer IS '学生已选答案SUBMIT_ANSWER';
COMMENT ON COLUMN education_idempotency.report_id IS '关联报告 IDSUBMIT_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';