Files
2025-11-28 14:51:25 +08:00

642 lines
19 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================
# LJWX 数据库迁移管理脚本
# ============================================
# 功能:
# - 导出宿主数据库作为基线
# - 应用版本化迁移脚本
# - 迁移历史跟踪
# - 支持回滚
# ============================================
set -e
# 加载配置
if [ -f .env ]; then
source .env
else
echo "❌ 错误: .env 文件不存在"
echo "💡 请先复制: cp .env.unified .env"
exit 1
fi
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 配置
MIGRATION_DIR="${DB_MIGRATION_DIR:-./docker/mysql/migrations}"
BACKUP_DIR="./backups/mysql"
CONTAINER_NAME="ljwx-mysql"
# 创建必要目录
mkdir -p "$MIGRATION_DIR/rollback"
mkdir -p "$BACKUP_DIR"
# ============================================
# 工具函数
# ============================================
print_header() {
echo -e "\n${BLUE}========================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}========================================${NC}\n"
}
print_success() {
echo -e "${GREEN}✅ $1${NC}"
}
print_error() {
echo -e "${RED}❌ $1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_info() {
echo -e "${BLUE}ℹ️ $1${NC}"
}
# 检查MySQL连接(宿主机)
check_host_mysql() {
if mysql -h ${HOST_MYSQL_HOST} -P ${HOST_MYSQL_PORT} -u${HOST_MYSQL_USER} -p${HOST_MYSQL_PASSWORD} -e "SELECT 1" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# 检查MySQL连接(容器)
check_container_mysql() {
if docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} -e "SELECT 1" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# 创建迁移历史表
create_migration_table() {
local target=$1 # host 或 container
local sql="CREATE TABLE IF NOT EXISTS \`schema_migrations\` (
\`version\` VARCHAR(50) NOT NULL PRIMARY KEY,
\`description\` VARCHAR(255),
\`type\` VARCHAR(20) DEFAULT 'migration',
\`script\` VARCHAR(255),
\`checksum\` VARCHAR(64),
\`installed_by\` VARCHAR(100) DEFAULT 'db-migrate.sh',
\`installed_on\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
\`execution_time\` INT,
\`success\` BOOLEAN DEFAULT TRUE,
INDEX idx_installed_on (installed_on),
INDEX idx_success (success)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库迁移历史记录';"
if [ "$target" = "host" ]; then
mysql -h ${HOST_MYSQL_HOST} -P ${HOST_MYSQL_PORT} \
-u${HOST_MYSQL_USER} -p${HOST_MYSQL_PASSWORD} \
${HOST_MYSQL_DATABASE} -e "$sql" 2>/dev/null
else
docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} -e "$sql" 2>/dev/null
fi
}
# 获取已应用的迁移版本
get_applied_migrations() {
local target=$1
if [ "$target" = "host" ]; then
mysql -h ${HOST_MYSQL_HOST} -P ${HOST_MYSQL_PORT} \
-u${HOST_MYSQL_USER} -p${HOST_MYSQL_PASSWORD} \
${HOST_MYSQL_DATABASE} \
-Nse "SELECT version FROM schema_migrations WHERE success = TRUE ORDER BY version" 2>/dev/null || echo ""
else
docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} \
-Nse "SELECT version FROM schema_migrations WHERE success = TRUE ORDER BY version" 2>/dev/null || echo ""
fi
}
# 计算文件MD5
calc_checksum() {
local file=$1
if [ "$(uname)" = "Darwin" ]; then
md5 -q "$file"
else
md5sum "$file" | awk '{print $1}'
fi
}
# ============================================
# 主要功能
# ============================================
# 导出宿主数据库作为基线
export_baseline() {
print_header "导出宿主数据库作为基线"
if ! check_host_mysql; then
print_error "无法连接到宿主数据库"
exit 1
fi
local version=${1:-${LJWX_VERSION}}
local timestamp=$(date +%Y%m%d_%H%M%S)
local baseline_file="${MIGRATION_DIR}/V${version}__baseline.sql"
local backup_file="${BACKUP_DIR}/baseline_${version}_${timestamp}.sql"
print_info "正在导出数据库: ${HOST_MYSQL_DATABASE}"
print_info "目标版本: V${version}"
# 导出完整数据库
mysqldump -h ${HOST_MYSQL_HOST} -P ${HOST_MYSQL_PORT} \
-u${HOST_MYSQL_USER} -p${HOST_MYSQL_PASSWORD} \
--single-transaction \
--quick \
--lock-tables=false \
--routines \
--triggers \
--events \
--complete-insert \
--add-drop-table \
--databases ${HOST_MYSQL_DATABASE} \
2>/dev/null > "$baseline_file"
# 备份一份
cp "$baseline_file" "$backup_file"
# 添加文件头
local header="-- ============================================
-- 迁移版本: V${version}
-- 创建时间: $(date +'%Y-%m-%d %H:%M:%S')
-- 描述: 数据库基线 - 从宿主机导出
-- 数据库: ${HOST_MYSQL_DATABASE}
-- 表数量: $(mysql -h ${HOST_MYSQL_HOST} -P ${HOST_MYSQL_PORT} -u${HOST_MYSQL_USER} -p${HOST_MYSQL_PASSWORD} ${HOST_MYSQL_DATABASE} -Nse "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${HOST_MYSQL_DATABASE}'" 2>/dev/null)
-- ============================================
"
echo "$header" | cat - "$baseline_file" > temp && mv temp "$baseline_file"
local file_size=$(du -h "$baseline_file" | awk '{print $1}')
print_success "基线导出完成"
print_info "基线文件: $baseline_file ($file_size)"
print_info "备份文件: $backup_file"
# 更新data.sql(用于Docker镜像构建)
cp "$baseline_file" "./docker/mysql/data.sql"
print_success "已更新 docker/mysql/data.sql"
}
# 创建新的迁移脚本模板
create_migration() {
local version=${1:-${LJWX_VERSION}}
local description=$2
if [ -z "$description" ]; then
print_error "请提供迁移描述"
echo "用法: $0 create <版本号> <描述>"
echo "示例: $0 create 2.0.4 add_health_features"
exit 1
fi
print_header "创建迁移脚本"
local migration_file="${MIGRATION_DIR}/V${version}__${description}.sql"
local rollback_file="${MIGRATION_DIR}/rollback/R${version}__rollback.sql"
if [ -f "$migration_file" ]; then
print_error "迁移文件已存在: $migration_file"
exit 1
fi
# 创建迁移脚本模板
cat > "$migration_file" <<EOF
-- ============================================
-- 迁移版本: V${version}
-- 创建时间: $(date +'%Y-%m-%d %H:%M:%S')
-- 描述: ${description}
-- 依赖版本: (请填写依赖的前一个版本)
-- ============================================
-- 开始事务
START TRANSACTION;
-- ============================================
-- 在这里编写迁移SQL
-- ============================================
-- 示例1: 创建新表
CREATE TABLE IF NOT EXISTS \`t_example_new_table\` (
\`id\` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
\`name\` varchar(100) NOT NULL COMMENT '名称',
\`created_at\` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (\`id\`),
KEY \`idx_name\` (\`name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='示例新表';
-- 示例2: 添加字段(幂等性)
SET @col_exists = 0;
SELECT COUNT(*) INTO @col_exists
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 't_user'
AND column_name = 'new_field';
SET @sql = IF(@col_exists = 0,
'ALTER TABLE t_user ADD COLUMN new_field VARCHAR(100) COMMENT ''新字段''',
'SELECT ''Column new_field already exists'' AS message');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 示例3: 创建索引(幂等性)
SET @index_exists = 0;
SELECT COUNT(*) INTO @index_exists
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 't_user'
AND index_name = 'idx_new_field';
SET @sql = IF(@index_exists = 0,
'ALTER TABLE t_user ADD INDEX idx_new_field (new_field)',
'SELECT ''Index idx_new_field already exists'' AS message');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================
-- 验证迁移结果
-- ============================================
-- 在这里添加验证SQL,确保迁移正确执行
-- 提交事务
COMMIT;
-- 记录迁移成功
-- (此记录由迁移系统自动添加)
EOF
# 创建回滚脚本模板
cat > "$rollback_file" <<EOF
-- ============================================
-- 回滚版本: R${version}
-- 创建时间: $(date +'%Y-%m-%d %H:%M:%S')
-- 描述: 回滚 ${description}
-- 目标版本: (请填写回滚后的版本)
-- ============================================
-- 开始事务
START TRANSACTION;
-- ============================================
-- 在这里编写回滚SQL(与迁移脚本相反的操作)
-- ============================================
-- 示例: 删除添加的表
DROP TABLE IF EXISTS \`t_example_new_table\`;
-- 示例: 删除添加的字段
ALTER TABLE t_user DROP COLUMN IF EXISTS new_field;
-- 示例: 删除添加的索引
ALTER TABLE t_user DROP INDEX IF EXISTS idx_new_field;
-- ============================================
-- 验证回滚结果
-- ============================================
-- 提交事务
COMMIT;
-- 删除迁移记录
DELETE FROM schema_migrations WHERE version = '${version}';
EOF
print_success "迁移脚本已创建: $migration_file"
print_success "回滚脚本已创建: $rollback_file"
print_info "请编辑脚本文件,完成实际的迁移SQL"
}
# 应用迁移
apply_migration() {
local version=$1
local force=${2:-false}
if [ -z "$version" ]; then
print_error "请指定要应用的版本"
echo "用法: $0 apply <版本号> [--force]"
exit 1
fi
print_header "应用迁移: V${version}"
# 查找迁移文件
local migration_file=$(find "$MIGRATION_DIR" -maxdepth 1 -name "V${version}__*.sql" | head -1)
if [ -z "$migration_file" ]; then
print_error "找不到迁移文件: V${version}__*.sql"
exit 1
fi
print_info "迁移文件: $migration_file"
# 检查容器是否运行
if ! docker ps | grep -q $CONTAINER_NAME; then
print_error "MySQL容器未运行: $CONTAINER_NAME"
print_info "请先启动容器: docker-compose -f docker-compose.prod.yml up -d ljwx-mysql"
exit 1
fi
# 创建迁移历史表
create_migration_table "container"
# 检查是否已应用
local applied=$(docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} -Nse \
"SELECT COUNT(*) FROM schema_migrations WHERE version='${version}' AND success=TRUE" 2>/dev/null || echo "0")
if [ "$applied" -gt 0 ] && [ "$force" != "--force" ]; then
print_warning "迁移 V${version} 已经应用过"
print_info "如需重新应用,使用: $0 apply ${version} --force"
return 0
fi
# 备份当前数据库
local backup_file="${BACKUP_DIR}/before_${version}_$(date +%Y%m%d_%H%M%S).sql"
print_info "备份当前数据库..."
docker exec $CONTAINER_NAME mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
--single-transaction --routines --triggers --events \
${MYSQL_DATABASE} > "$backup_file" 2>/dev/null
print_success "备份完成: $backup_file"
# 应用迁移
print_info "应用迁移..."
local start_time=$(date +%s)
if docker exec -i $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} < "$migration_file" 2>&1 | tee /tmp/migration.log; then
local end_time=$(date +%s)
local execution_time=$((end_time - start_time))
# 记录迁移历史
local checksum=$(calc_checksum "$migration_file")
local script_name=$(basename "$migration_file")
local description=$(echo "$script_name" | sed 's/V[0-9.]*__//g' | sed 's/.sql//g')
docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} -e "INSERT INTO schema_migrations
(version, description, script, checksum, execution_time, success)
VALUES ('${version}', '${description}', '${script_name}', '${checksum}', ${execution_time}, TRUE)
ON DUPLICATE KEY UPDATE
execution_time = ${execution_time},
success = TRUE,
installed_on = CURRENT_TIMESTAMP" 2>/dev/null
print_success "迁移应用成功 (耗时: ${execution_time}秒)"
else
print_error "迁移应用失败"
print_info "错误日志: /tmp/migration.log"
print_info "备份文件: $backup_file"
print_warning "建议回滚到备份"
exit 1
fi
}
# 应用所有待执行迁移
apply_all() {
print_header "应用所有待执行迁移"
# 创建迁移历史表
create_migration_table "container"
# 获取已应用的版本
local applied_versions=$(get_applied_migrations "container")
# 获取所有迁移文件(按版本排序)
local migration_files=$(find "$MIGRATION_DIR" -maxdepth 1 -name "V*.sql" -type f | sort -V)
local pending_count=0
local applied_count=0
for file in $migration_files; do
local filename=$(basename "$file")
local version=$(echo "$filename" | sed 's/V\([0-9.]*\)__.*/\1/')
if echo "$applied_versions" | grep -q "^${version}$"; then
print_info "跳过已应用: V${version}"
((applied_count++))
else
print_info "待应用: V${version}"
apply_migration "$version"
((pending_count++))
fi
done
print_success "迁移应用完成"
print_info "已应用: ${applied_count} 个"
print_info "新应用: ${pending_count} 个"
}
# 查看迁移状态
show_status() {
print_header "迁移状态"
if ! docker ps | grep -q $CONTAINER_NAME; then
print_warning "MySQL容器未运行"
# 显示可用的迁移文件
print_info "可用的迁移脚本:"
find "$MIGRATION_DIR" -maxdepth 1 -name "V*.sql" -type f | sort -V | while read file; do
local filename=$(basename "$file")
local version=$(echo "$filename" | sed 's/V\([0-9.]*\)__.*/\1/')
local size=$(du -h "$file" | awk '{print $1}')
echo " - V${version} ($size)"
done
return 0
fi
# 创建迁移历史表
create_migration_table "container"
# 获取已应用的迁移
print_info "已应用的迁移:"
docker exec $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} -e \
"SELECT version, description, installed_on, execution_time, success
FROM schema_migrations
ORDER BY installed_on DESC
LIMIT 10" 2>/dev/null
echo ""
# 待应用的迁移
local applied_versions=$(get_applied_migrations "container")
print_info "待应用的迁移:"
local has_pending=false
find "$MIGRATION_DIR" -maxdepth 1 -name "V*.sql" -type f | sort -V | while read file; do
local filename=$(basename "$file")
local version=$(echo "$filename" | sed 's/V\([0-9.]*\)__.*/\1/')
if ! echo "$applied_versions" | grep -q "^${version}$"; then
echo " - V${version}"
has_pending=true
fi
done
if [ "$has_pending" != "true" ]; then
echo " (无待应用迁移)"
fi
}
# 回滚迁移
rollback_migration() {
local version=$1
if [ -z "$version" ]; then
print_error "请指定要回滚的版本"
echo "用法: $0 rollback <版本号>"
exit 1
fi
print_header "回滚迁移: V${version}"
# 查找回滚文件
local rollback_file="${MIGRATION_DIR}/rollback/R${version}__rollback.sql"
if [ ! -f "$rollback_file" ]; then
print_error "找不到回滚文件: $rollback_file"
exit 1
fi
print_warning "警告: 即将回滚迁移 V${version}"
print_info "回滚文件: $rollback_file"
read -p "确认继续? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
print_info "已取消回滚操作"
exit 0
fi
# 备份
local backup_file="${BACKUP_DIR}/before_rollback_${version}_$(date +%Y%m%d_%H%M%S).sql"
print_info "备份当前数据库..."
docker exec $CONTAINER_NAME mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
--single-transaction --routines --triggers --events \
${MYSQL_DATABASE} > "$backup_file" 2>/dev/null
print_success "备份完成: $backup_file"
# 执行回滚
print_info "执行回滚..."
if docker exec -i $CONTAINER_NAME mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
${MYSQL_DATABASE} < "$rollback_file" 2>&1; then
print_success "回滚成功"
else
print_error "回滚失败"
exit 1
fi
}
# ============================================
# 主程序
# ============================================
show_help() {
cat <<EOF
LJWX 数据库迁移管理工具
用法:
$0 <命令> [参数]
命令:
export [版本号] 导出宿主数据库作为基线
版本号默认为 .env 中的 LJWX_VERSION
示例: $0 export 2.0.3
create <版本号> <描述> 创建新的迁移脚本模板
示例: $0 create 2.0.4 add_health_features
apply <版本号> [--force] 应用指定版本的迁移
--force 强制重新应用
示例: $0 apply 2.0.3
apply-all 应用所有待执行的迁移
示例: $0 apply-all
status 查看迁移状态
示例: $0 status
rollback <版本号> 回滚到指定版本
示例: $0 rollback 2.0.2
help 显示此帮助信息
配置:
配置文件: .env
迁移目录: ${MIGRATION_DIR}
备份目录: ${BACKUP_DIR}
示例工作流:
1. 导出当前宿主数据库作为基线
$ $0 export 2.0.3
2. 创建新的迁移脚本
$ $0 create 2.0.4 add_new_features
3. 编辑迁移脚本(手动)
$ vim ${MIGRATION_DIR}/V2.0.4__add_new_features.sql
4. 测试应用迁移
$ $0 apply 2.0.4
5. 查看迁移状态
$ $0 status
6. 如有问题,执行回滚
$ $0 rollback 2.0.4
更多信息请查看: ${MIGRATION_DIR}/README.md
EOF
}
# 主命令分发
case "${1:-help}" in
export)
export_baseline "$2"
;;
create)
create_migration "$2" "$3"
;;
apply)
apply_migration "$2" "$3"
;;
apply-all)
apply_all
;;
status)
show_status
;;
rollback)
rollback_migration "$2"
;;
help|--help|-h)
show_help
;;
*)
print_error "未知命令: $1"
show_help
exit 1
;;
esac