#!/usr/bin/env bash # 恢复演练 —— 把最近一份备份恢复到「临时库」并核对行数(⑨:备份不演练 = 没有备份) # # 演练绝不直接覆盖生产库;临时库名 payroll_drill_<时间戳>,核对完由 # 调用方决定是否删除(本脚本默认不 DROP,安全优先)。 # # 用法:在项目根目录执行 bash scripts/restore_drill.sh # 产物:临时库 + 终端行数对比报告;--drop-exec 参数在核对通过后删除临时库。 set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" if [[ -f docker/.env ]]; then # shellcheck disable=SC1091 source docker/.env else echo "ERROR: docker/.env 不存在" >&2 exit 1 fi : "${MYSQL_DATABASE:?docker/.env 缺 MYSQL_DATABASE}" : "${MYSQL_ROOT_PASSWORD:?docker/.env 缺 MYSQL_ROOT_PASSWORD}" CONTAINER="ys-mysql-dev" if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then echo "ERROR: 容器 $CONTAINER 未运行" >&2 exit 1 fi BACKUP_DIR="$ROOT/db/backups" LATEST="$(ls -t "$BACKUP_DIR"/payroll_*.sql.gz 2>/dev/null | head -1)" if [[ -z "$LATEST" ]]; then echo "ERROR: 未找到备份文件(先 bash scripts/backup_full.sh)" >&2 exit 1 fi DRILL_DB="payroll_drill_$(date +%Y%m%d_%H%M%S)" # 0. --drop-exec:演练前先清掉**全部**历史遗留 drill 库(含上一轮「保留供 # 核对」的那份——否则每演练一轮净增一份整库拷贝,磁盘被无人认领的 # drill 库吃满),演练完成后本轮临时库也随之删除 if [[ "${1:-}" == "--drop-exec" ]]; then OLD_DBS=$(docker exec "$CONTAINER" sh -c \ "mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" -N -B -e \"SHOW DATABASES LIKE 'payroll_drill_%';\"" 2>/dev/null || true) for OLD in $OLD_DBS; do docker exec "$CONTAINER" sh -c \ "mysqladmin -uroot -p\"\$MYSQL_ROOT_PASSWORD\" -f drop \"$OLD\"" echo "已清理历史演练库 $OLD" done fi echo "演练恢复:$(basename "$LATEST") → 临时库 $DRILL_DB" # 1. 创建临时库(mysqladmin create 无反引号转义问题) docker exec "$CONTAINER" sh -c \ "mysqladmin -uroot -p\"\$MYSQL_ROOT_PASSWORD\" create \"$DRILL_DB\"" # 2. 解压导入 gunzip -c "$LATEST" | docker exec -i "$CONTAINER" sh -c \ "mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" \"$DRILL_DB\"" # 3. 行数核对:**全部业务表**(新增表必须同步补入本清单——备份漏表时 # 演练要能发现;alembic_version 不列,行数恒 1 无核对价值) TABLES=( adjustment_orders approval_orders approval_steps business_items business_roles business_secondary_allocations business_status_logs businesses customer_rating_grades customers login_logs monthly_settlements operation_logs ownership_change_logs param_change_batches param_definitions param_values performance_ledgers permissions personnel personnel_plans personnel_regions products receipts role_permissions roles settlement_items shipment_items shipments system_settings user_roles users ) echo "" echo "======== 行数对比(源库 vs 演练库)========" printf "%-24s %12s %12s %6s\n" "表名" "源库" "演练库" "结果" ALL_OK=1 for T in "${TABLES[@]}"; do SRC=$(docker exec "$CONTAINER" sh -c \ "mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" -N -B -e \"SELECT COUNT(*) FROM $MYSQL_DATABASE.$T;\" 2>/dev/null" || echo "?") DST=$(docker exec "$CONTAINER" sh -c \ "mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" -N -B -e \"SELECT COUNT(*) FROM $DRILL_DB.$T;\" 2>/dev/null" || echo "?") if [[ "$SRC" == "?" || "$DST" == "?" ]]; then # 查询失败(表缺失/权限/客户端故障)不可与「数据一致」混淆:? # 与 ? 相等会误报可恢复——备份真的漏表时演练必须红 MARK="✗" ALL_OK=0 elif [[ "$SRC" == "$DST" ]]; then MARK="✓" else MARK="✗" ALL_OK=0 fi printf "%-24s %12s %12s %6s\n" "$T" "$SRC" "$DST" "$MARK" done echo "==========================================" if [[ "$ALL_OK" == "1" ]]; then echo "" echo "结果:全部表行数一致,备份可恢复。" if [[ "${1:-}" == "--drop-exec" ]]; then docker exec "$CONTAINER" sh -c \ "mysqladmin -uroot -p\"\$MYSQL_ROOT_PASSWORD\" -f drop \"$DRILL_DB\"" echo "本轮临时库 $DRILL_DB 已删除(历史遗留亦在演练前清理)。" else echo "临时库 $DRILL_DB 保留(核对用);确认无误后重跑本脚本加 --drop-exec," echo "会先清掉包括它在内的全部演练库再演练,本轮完成后同样即删。" fi exit 0 else echo "" echo "结果:存在不一致,请检查备份文件与数据库结构。" >&2 echo "临时库 $DRILL_DB 保留供排查。" >&2 exit 1 fi