gen_deploy_sql.sh 12.8 KB
#!/usr/bin/env bash
# ============================================================================
#  parking-cloud · 部署包生成器
#
#  产出两个文件(都落在 sql/ 下,与 00/01/02 同目录,编号接续):
#
#    03_legacy_schema.sql    679 张存量表的**表结构**(无数据,约 1.3 MB)
#    04_legacy_sample.sql    679 张存量表的**抽样数据**(少量,可选,见 §PII)
#
#  和 00/01/02 的分工:
#    00_create_database.sql   建库
#    01_system_schema.sql     13 张 system_* 表结构(P1 手写,权威)
#    02_system_seed.sql       13 张 system_* 表种子(admin/admin123 等)
#    03_legacy_schema.sql     ← 本脚本产出:679 张存量表结构
#    04_legacy_sample.sql     ← 本脚本产出:679 张存量表抽样数据
#  所以 03/04 都**排除 system_***,绝不与 01/02 重叠。
#
#  ⚠️ 为什么要生成本文件,而不是直接用 10_dump_legacy.sh 的产物:
#     10_dump_legacy.sh 面向**本地镜像**(生产全量/抽样导入本机),
#     它的 dump 里带着本地 MySQL 的默认排序规则;本脚本面向
#     **拿到别的服务器上执行**,必须与目标服务端版本无关。差异见 §normalize。
#
#  用法:
#    bash sql/tools/gen_deploy_sql.sh              # 只出 03
#    bash sql/tools/gen_deploy_sql.sh --with-data  # 03 + 04
#    DEPLOY_DB_HOST=127.0.0.1 DEPLOY_DB_USER=root bash sql/tools/gen_deploy_sql.sh
#
#  退出码:0 = 生成且自检全过;非 0 = 有硬约束不满足(绝不产出坏文件)
# ============================================================================
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
SQL_DIR="$(cd "$HERE/.." && pwd)"

DEPLOY_DB_HOST="${DEPLOY_DB_HOST:-127.0.0.1}"
DEPLOY_DB_PORT="${DEPLOY_DB_PORT:-3306}"
DEPLOY_DB_USER="${DEPLOY_DB_USER:-root}"
DEPLOY_DB="${DEPLOY_DB:-parking_cloud}"
# 密码走 MYSQL_PWD(所有 MySQL 官方客户端都认),**不上 argv**——否则会出现在 ps 里
[[ -n "${DEPLOY_DB_PASSWORD:-}" ]] && export MYSQL_PWD="$DEPLOY_DB_PASSWORD"

MYSQL_BIN="${MYSQL_BIN:-$(command -v mysql || echo /opt/homebrew/opt/mysql-client/bin/mysql)}"
DUMP_BIN="${DUMP_BIN:-$(command -v mysqldump || echo /opt/homebrew/opt/mysql-client/bin/mysqldump)}"

OUT_SCHEMA="$SQL_DIR/03_legacy_schema.sql"
OUT_DATA="$SQL_DIR/04_legacy_sample.sql"
WITH_DATA=0
[[ "${1:-}" == "--with-data" ]] && WITH_DATA=1

die() { echo "✘ $*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }

# ---------------------------------------------------------------------------
# 存量表清单:库里除了 13 张 system_* 之外的全部
# 顺序固定(ORDER BY TABLE_NAME)→ 产出**可复现**(重跑 md5 一致)
# ---------------------------------------------------------------------------
list_tables() {
  "$MYSQL_BIN" -h"$DEPLOY_DB_HOST" -P"$DEPLOY_DB_PORT" -u"$DEPLOY_DB_USER" \
    -N -B -e "SELECT TABLE_NAME FROM information_schema.TABLES
              WHERE TABLE_SCHEMA='$DEPLOY_DB' AND TABLE_TYPE='BASE TABLE'
                AND TABLE_NAME NOT LIKE 'system\\_%'
              ORDER BY TABLE_NAME;"
}

TABLES=()
while IFS= read -r t; do [[ -n "$t" ]] && TABLES+=("$t"); done < <(list_tables)
[[ ${#TABLES[@]} -gt 0 ]] || die "从 $DEPLOY_DB 里一张存量表都没查到——库名或连接参数不对?"

for t in "${TABLES[@]}"; do
  [[ "$t" == system_* ]] && die "system_* 漏进了存量表清单($t)——排除规则失效,停"
done

SYSTEM_CNT=$("$MYSQL_BIN" -h"$DEPLOY_DB_HOST" -P"$DEPLOY_DB_PORT" -u"$DEPLOY_DB_USER" \
  -N -B -e "SELECT COUNT(*) FROM information_schema.TABLES
            WHERE TABLE_SCHEMA='$DEPLOY_DB' AND TABLE_NAME LIKE 'system\\_%';")
TOTAL_CNT=$("$MYSQL_BIN" -h"$DEPLOY_DB_HOST" -P"$DEPLOY_DB_PORT" -u"$DEPLOY_DB_USER" \
  -N -B -e "SELECT COUNT(*) FROM information_schema.TABLES
            WHERE TABLE_SCHEMA='$DEPLOY_DB' AND TABLE_TYPE='BASE TABLE';")

say "存量表 ${#TABLES[@]} 张 / system_* ${SYSTEM_CNT} 张 / 库内合计 ${TOTAL_CNT} 张"
[[ $(( ${#TABLES[@]} + SYSTEM_CNT )) -eq "$TOTAL_CNT" ]] \
  || die "679+13 对不上库内总数(${#TABLES[@]}+$SYSTEM_CNT != $TOTAL_CNT)"

# ---------------------------------------------------------------------------
# 统一 dump 参数
#   --set-gtid-purged=OFF  不往输出里写 GTID(目标库多半没开 GTID,写了反而报错)
#   --no-tablespaces       不给普通账号添 PROCESS 权限要求
#   --skip-comments/date   去掉 "Dump completed on ..." 之类**每次都变**的行
#   ⚠️ 若换用 8.x 客户端去 dump 5.7 服务端,还需加 --column-statistics=0
# ---------------------------------------------------------------------------
dump_base() {
  "$DUMP_BIN" -h"$DEPLOY_DB_HOST" -P"$DEPLOY_DB_PORT" -u"$DEPLOY_DB_USER" \
    --skip-comments --skip-dump-date --no-tablespaces --set-gtid-purged=OFF \
    --single-transaction --default-character-set=utf8mb4 "$@"
}

# ---------------------------------------------------------------------------
# normalize:把「本地镜像的产物」修成「与目标服务端版本无关的 DDL」
#
#  ① COLLATE=utf8mb4_0900_ai_ci → utf8mb4_general_ci
#     **这是本脚本存在的首要理由。** 0900_* 是 MySQL 8.0 专有排序规则,
#     生产库是 5.7.28,拿到 5.7 上执行会直接 `Unknown collation` 失败。
#     它怎么来的:本地镜像库建表时没写 COLLATE,落到**本地 MySQL 26.7 的
#     utf8mb4 默认**(= 0900_ai_ci)。源库 5.7 的真身是 utf8_general_ci,
#     所以这不是"改",是**改回去**。实测命中 697 处。
#     为什么是**替换**而不是**删除** COLLATE:删了就退回目标服务端默认值,
#     于是同一份脚本在 5.7 和 8.0 上建出两种排序规则——文件就不再可复现了。
#     写死 general_ci(= 00_create_database.sql 定的目标形态)才是确定的。
#     ⚠️ `utf8mb4_bin`(294 处)**必须原样保留**:它对应源库那 30 张 utf8_bin 表,
#        是 10_dump_legacy.sh 特意映射过来的,换成 general_ci 会静默改变
#        `parking_lot.pl_no` 这类字段的**等值比较语义**。
#  ② 抹掉 AUTO_INCREMENT=N(仅表选项行)
#     它随本地抽样数据量变化(实测 585 处),留着则重跑产出不一致。
#     抹掉是安全的:04 的数据带显式主键,InnoDB 插入后会把计数器顶到 max(id)+1。
# ---------------------------------------------------------------------------
normalize() {
  sed -E \
    -e 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' \
    -e '/^\) ENGINE/ s/ AUTO_INCREMENT=[0-9]+//'
}

# ---------------------------------------------------------------------------
# 生成 03:表结构
# ---------------------------------------------------------------------------
gen_schema() {
  local tmp; tmp="$(mktemp)"
  {
    cat <<'HDR'
-- ============================================================================
--  parking-cloud · 存量表结构(679 张)
--  ⚠️ 本文件由 sql/tools/gen_deploy_sql.sh 生成,**不要手改**——改了下次重跑就没了。
--
--  执行前提:先跑 00_create_database.sql 建好 parking_cloud 库
--  执行:    mysql -uroot parking_cloud < 03_legacy_schema.sql
--
--  内容:zt_sys / zt_park / zt_pay / zt_order / zt_bill / zt_charge 六库合并后的
--        全部存量表**结构**,不含数据(数据见 04_legacy_sample.sql,可选)。
--        **不含 system_* 13 张表**——那些在 01_system_schema.sql。
--
--  幂等:每条都是 DROP TABLE IF EXISTS + CREATE TABLE,可重复执行
--        (⚠️ 重跑会清掉这些表里的数据,对着有数据的库执行前先想清楚)
--
--  兼容:MySQL 5.7 与 8.0 都能建(不含 8.0 专有排序规则,不含 8.0 专有语法)。
--        外键 0 个、触发器/视图/存储过程/事件 均 0 个,5 张分区表的分区定义原样保留。
-- ============================================================================

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

HDR
    dump_base --no-data "$DEPLOY_DB" "${TABLES[@]}" | normalize
    cat <<'FTR'

SET FOREIGN_KEY_CHECKS = 1;
FTR
  } > "$tmp"

  # ---- 自检:任何一条不成立就不落盘(宁可不出文件,不出坏文件)----
  local n
  n=$(grep -c '^CREATE TABLE' "$tmp" || true)
  [[ "$n" -eq "${#TABLES[@]}" ]] || die "03 产出 $n 张表,期望 ${#TABLES[@]} 张"
  n=$(grep -c '^CREATE TABLE `system_' "$tmp" || true)
  [[ "$n" -eq 0 ]] || die "03 里混进了 $n 张 system_* 表(会和 01 撞车)"
  n=$(grep -c '0900' "$tmp" || true)
  [[ "$n" -eq 0 ]] || die "03 里还剩 $n 处 0900_* 排序规则,5.7 上会 Unknown collation"
  n=$(grep -c 'AUTO_INCREMENT=' "$tmp" || true)
  [[ "$n" -eq 0 ]] || die "03 里还剩 $n 处 AUTO_INCREMENT=N(产出不可复现)"
  n=$(grep -cE 'mb4mb4|mb4mb3|utf8mb3|utf8_general_ci|utf8_bin|utf8_unicode_ci|latin1|CHARSET=utf8([^m]|$)' "$tmp" || true)
  [[ "$n" -eq 0 ]] || die "03 里还剩 $n 处未归一的字符集/排序规则声明"
  n=$(grep -c 'PARTITION BY' "$tmp" || true)
  [[ "$n" -eq 5 ]] || die "03 里分区表是 $n 张,期望 5 张(分区定义不能丢)"
  mv "$tmp" "$OUT_SCHEMA"
  say "✔ 03_legacy_schema.sql  ${#TABLES[@]} 张表结构,$(wc -c < "$OUT_SCHEMA" | tr -d ' ') 字节"
}

# ---------------------------------------------------------------------------
# 生成 04:抽样数据
#
#  ⚠️ PII 警告:这里面是**真实生产库抽出来的行**——含客户手机号、车牌、姓名。
#     · 不随仓库提交(.gitignore 已排除本文件)
#     · 只该放到开发/测试机,**不要往生产或对外环境灌**
#     · 目标库有 system_* 种子(02)但**存量表是空的**也完全能跑起来——
#       04 是"让列表页有东西可看"用的,不是启动必需
#     · 21 张密钥类表(struct_only_tables.txt)在本地镜像里本来就是空的,
#       所以 dump 出来自然 0 行,不会泄漏密钥
# ---------------------------------------------------------------------------
gen_data() {
  local tmp; tmp="$(mktemp)"
  {
    cat <<HDR
-- ============================================================================
--  parking-cloud · 存量表**抽样数据**(可选,非启动必需)
--  ⚠️ 本文件由 sql/tools/gen_deploy_sql.sh --with-data 生成,**不要手改**。
--  ⚠️ 内含真实生产数据的抽样(手机号 / 车牌 / 姓名),**不要提交、不要上生产**。
--
--  执行前提:先跑 00 → 01 → 02 → 03
--  执行:    mysql -uroot parking_cloud < 04_legacy_sample.sql
--
--  本库抽样上限 500 行/表(生成时的本地镜像口径)。表间**无外键**(实测 0 个),
--  所以导入顺序无所谓;但也意味着**没有一致性保证**——仅够"页面有数据"。
-- ============================================================================

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

HDR
    # --no-create-info:只要 INSERT,结构已由 03 建好
    # --extended-insert(默认开):一张表一条 INSERT,文件小、导入快
    dump_base --no-create-info --no-create-db --skip-triggers "$DEPLOY_DB" "${TABLES[@]}" | normalize
    cat <<'FTR'

SET FOREIGN_KEY_CHECKS = 1;
FTR
  } > "$tmp"

  local rows
  # 行数用"插入了多少行"直接数:INSERT 语句里的 ( 计数过于脆弱,改数 VALUES 组
  # mysqldump 的 --extended-insert 形如 INSERT INTO `t` VALUES (..),(..),(..);
  # 用 INSERT 条数与总字节作为自检口径更稳(行数用库侧再核一次)
  rows=$(grep -c '^INSERT INTO' "$tmp" || true)
  [[ "$rows" -gt 0 ]] || die "04 里一条 INSERT 都没有——抽样数据没导进本地镜像?"
  local bad
  bad=$(grep -cE '^INSERT INTO `system_' "$tmp" || true)
  [[ "$bad" -eq 0 ]] || die "04 里混进了 $bad 条 system_* 插入(会和 02 的种子撞主键)"
  mv "$tmp" "$OUT_DATA"
  say "✔ 04_legacy_sample.sql  涉及 $rows 张表,$(wc -c < "$OUT_DATA" | tr -d ' ') 字节"
}

gen_schema
[[ "$WITH_DATA" == "1" ]] && gen_data

# ---------------------------------------------------------------------------
# 复现性自检:schema 连生成两次必须字节一致
# ---------------------------------------------------------------------------
if [[ "${DEPLOY_SKIP_REPRO:-0}" != "1" ]]; then
  a=$(md5 -q "$OUT_SCHEMA" 2>/dev/null || md5sum "$OUT_SCHEMA" | cut -d' ' -f1)
  gen_schema >/dev/null 2>&1 || true
  b=$(md5 -q "$OUT_SCHEMA" 2>/dev/null || md5sum "$OUT_SCHEMA" | cut -d' ' -f1)
  # ⚠️ 变量一律写成 ${a}:macOS 自带 bash 3.2 下 `$a)` 会把全角括号
  #    当成变量名的一部分,报 "a): unbound variable"(实测踩中)
  [[ "$a" == "$b" ]] || die "03 两次生成 md5 不一致(${a} vs ${b})——产出不可复现"
  say "✔ 复现性:两次生成 md5 一致(${a})"
fi

say ""
say "部署顺序: 00_create_database → 01_system_schema → 02_system_seed → 03_legacy_schema [→ 04_legacy_sample]"
say "详细步骤见 sql/DEPLOY.md"