由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - serve-proxy:API_TARGET 生效、SPA 404 语义、流式静态文件、hop-by-hop/超时/断连/穿越防护 - migrate.sh:密码不进 argv/不泄漏 pm2、原子锁防重入、DML-only 事务说明、就绪诊断 - deploy.sh/CI:保护 .env、总是 npm ci(迁移依赖 devDeps)、健康检查、并发锁 - ecosystem/oxlint/.env.example:优雅停机、React 版本对齐、TRUST_PROXY 说明 Reviewed-by: OCR (open-codereview.ai)
163 lines
7.0 KiB
Bash
Executable File
163 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 恭学教育 — 数据迁移脚本(仅首次运行)
|
||
# 用法:
|
||
# 1. 确认 MySQL 已启动:docker compose up -d mysql
|
||
# 2. ./migrate.sh /path/to/dump.sql.gz
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
# 数据库名:旧库(导入源)与新库(应用目标)。
|
||
# 注意:migrate-legacy.sql 内部仍固定写入 gongxue(未改 SQL 文件),
|
||
# 若修改 TARGET_DB 需同步调整该 SQL 文件,保持两者一致。
|
||
LEGACY_DB="dorm_billing"
|
||
TARGET_DB="gongxue"
|
||
|
||
DUMP="${1:-}"
|
||
if [ -z "$DUMP" ]; then
|
||
echo "用法: ./migrate.sh /path/to/dump.sql.gz"
|
||
echo "示例: ./migrate.sh ~/Downloads/dorm_billing_2026-07-06_01-30-02_mysql_data.sql.gz"
|
||
exit 1
|
||
fi
|
||
# 相对路径转绝对路径:必须在 cd 之前转换,否则 cd 到脚本目录后相对路径会失效
|
||
if [[ "$DUMP" != /* ]]; then
|
||
DUMP="$(pwd)/$DUMP"
|
||
fi
|
||
# 校验 dump 文件确实存在,避免 gunzip 报出难以理解的错误
|
||
if [ ! -f "$DUMP" ]; then
|
||
echo "错误: dump 文件不存在: $DUMP" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 先切到脚本目录:docker compose exec 与 pm2 startOrReload 都按调用者 cwd 解析,
|
||
# 不 cd 的话从其他目录调用会找不到 compose 文件 / ecosystem.config.cjs。
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# 关键文件存在性预检,避免 set -euo pipefail 下出现难懂的错误
|
||
if [ ! -f "$SCRIPT_DIR/migrate-legacy.sql" ]; then
|
||
echo "错误: 缺少 $SCRIPT_DIR/migrate-legacy.sql" >&2
|
||
exit 1
|
||
fi
|
||
if [ ! -f "$SCRIPT_DIR/ecosystem.config.cjs" ]; then
|
||
echo "错误: 缺少 $SCRIPT_DIR/ecosystem.config.cjs" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# .migrate.done 仅作为「已完成」提示:脚本成功结束时 touch;
|
||
# 开头若已存在则提示并退出(真正的防重入由下面的原子锁保证)。
|
||
if [ -f "$SCRIPT_DIR/.migrate.done" ]; then
|
||
echo "检测到 $SCRIPT_DIR/.migrate.done,迁移已完成。如需重新迁移,请先删除该标记文件。" >&2
|
||
# “已完成”是正常提示,不是错误,返回 0
|
||
exit 0
|
||
fi
|
||
|
||
# 防重入:mkdir 原子锁。mkdir 创建目录是原子的,同一路径已存在即失败,
|
||
# 失败即代表已有实例正在运行。macOS 不自带 flock(1)(flock 是 Linux 专有命令),
|
||
# 所以不用 flock,改用目录锁;trap EXIT 保证成功/失败(含 ERR/INT/TERM 后的退出)都 rmdir 释放。
|
||
if ! mkdir "$SCRIPT_DIR/.migrate.lock" 2>/dev/null; then
|
||
echo "检测到 $SCRIPT_DIR/.migrate.lock,已有迁移实例在运行。若确认无实例,请手动删除该锁目录。" >&2
|
||
exit 1
|
||
fi
|
||
release_lock() {
|
||
rmdir "$SCRIPT_DIR/.migrate.lock" 2>/dev/null || true
|
||
}
|
||
trap release_lock EXIT
|
||
|
||
# 密码只通过 MYSQL_PWD 环境变量传递,绝不拼进命令行(避免出现在 ps aux / 日志)
|
||
if [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then
|
||
echo "错误: 请先设置环境变量 MYSQL_ROOT_PASSWORD(不要使用硬编码兜底密码)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# .env 缺失时 sed 会非零退出,在 set -euo pipefail 下会直接终止脚本;
|
||
# 用 || true 兜底,缺失时回退到默认 ${LEGACY_DB}(现有逻辑保持不变)。
|
||
# 用子 shell 限定 MYSQL_PWD 作用域:
|
||
# - 不拼进命令行(避免出现在 ps aux / cmdline)
|
||
# - 不全局 export(避免泄漏给 pm2 启动的后端进程)
|
||
run_mysql() {
|
||
(
|
||
export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"
|
||
docker compose exec -T -e MYSQL_PWD mysql mysql -u root "$@"
|
||
)
|
||
}
|
||
|
||
APP_STARTED=0
|
||
# 失败时确保应用停止,避免留下半启动状态
|
||
cleanup_on_error() {
|
||
if [ "$APP_STARTED" = "1" ]; then
|
||
echo "检测到失败,停止 gongxue-backend..." >&2
|
||
pm2 stop gongxue-backend 2>/dev/null || true
|
||
fi
|
||
}
|
||
trap cleanup_on_error ERR INT TERM
|
||
|
||
echo "=== 1/4 导入旧 dump ==="
|
||
# 全新 MySQL 可能还没有 ${LEGACY_DB} schema,直接 mysql "${LEGACY_DB}" 会失败;
|
||
# run_mysql 是连 mysql 不带库名,先确保库存在再导入。
|
||
run_mysql -e "CREATE DATABASE IF NOT EXISTS ${LEGACY_DB}; CREATE DATABASE IF NOT EXISTS ${TARGET_DB}"
|
||
gunzip -c "$DUMP" | run_mysql "$LEGACY_DB"
|
||
IMPORTED_COUNT="$(run_mysql -N -e "SELECT COUNT(*) FROM ${LEGACY_DB}.students" 2>/dev/null || true)"
|
||
if [ -z "${IMPORTED_COUNT}" ] || ! [[ "${IMPORTED_COUNT}" =~ ^[0-9]+$ ]]; then
|
||
echo "警告: 无法读取 ${LEGACY_DB}.students 计数(dump 可能不含该表或查询失败)" >&2
|
||
IMPORTED_COUNT="未知"
|
||
fi
|
||
echo "旧库导入完成: ${IMPORTED_COUNT}"
|
||
|
||
echo "=== 2/4 启动应用(创建新表结构) ==="
|
||
# 短暂启动让 TypeORM synchronize 创建表,然后停掉。
|
||
# --update-env 确保 DB_SYNCHRONIZE=true 真正传给已在运行的 pm2 进程(startOrReload 默认不刷新环境变量)。
|
||
APP_STARTED=1
|
||
DB_SYNCHRONIZE=true pm2 startOrReload ecosystem.config.cjs --only gongxue-backend --update-env
|
||
|
||
wait_for_tables() {
|
||
local tries=40
|
||
# migrate-legacy.sql 内部仍固定写入 gongxue(未改 SQL 文件),默认值必须与 TARGET_DB 一致;
|
||
# 就绪检查只校验 ${TARGET_DB},不回退到 DB_DATABASE。
|
||
echo "等待应用创建表结构(${TARGET_DB})..."
|
||
local i table_count err_file
|
||
err_file="$SCRIPT_DIR/.migrate.err"
|
||
: > "$err_file"
|
||
for i in $(seq 1 "$tries"); do
|
||
# 读入变量判断,避免 grep -q 触发 SIGPIPE;stderr 先收集,超时时输出便于诊断(如密码错误/容器未启动)
|
||
table_count="$(run_mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${TARGET_DB}' AND table_name IN ('users','tenants','operation_logs','students','rooms')" 2>>"$err_file" || true)"
|
||
if [ "$table_count" = "5" ]; then
|
||
echo "表结构就绪(${TARGET_DB}: users/tenants/operation_logs/students/rooms)"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
echo "错误: 等待应用创建表结构超时(${TARGET_DB})" >&2
|
||
if [ -s "$err_file" ]; then
|
||
echo "--- 期间 MySQL 错误(可能原因:密码错误/容器未启动/连接失败)---" >&2
|
||
cat "$err_file" >&2
|
||
fi
|
||
return 1
|
||
}
|
||
wait_for_tables
|
||
pm2 stop gongxue-backend 2>/dev/null || true
|
||
APP_STARTED=0
|
||
|
||
echo "=== 3/4 执行数据迁移 ==="
|
||
# 包进事务,中途失败时自动回滚(连接断开未提交即回滚)。
|
||
# 注意:事务只对 DML 生效;migrate-legacy.sql 目前全部是 INSERT(无 DDL),
|
||
# 若将来加入 CREATE/ALTER/DROP 等 DDL,MySQL 会隐式提交,该回滚保证不再成立,需拆分 DDL/DML。
|
||
{
|
||
echo 'START TRANSACTION;'
|
||
cat "$SCRIPT_DIR/migrate-legacy.sql"
|
||
# 用 printf 保证 COMMIT 前有换行,避免粘到 SQL 最后一行
|
||
printf '\nCOMMIT;\n'
|
||
} | run_mysql
|
||
|
||
# 数据已提交:立即写「已完成」标记,之后重启失败也不应重跑导入(重跑会重复导入/重复 INSERT)
|
||
touch "$SCRIPT_DIR/.migrate.done"
|
||
|
||
echo "=== 4/4 重启应用 ==="
|
||
# --update-env 确保 .env / 环境变量变更(如 DB_SYNCHRONIZE=false)对已在运行的进程生效
|
||
pm2 startOrReload ecosystem.config.cjs --update-env
|
||
pm2 save
|
||
|
||
|
||
echo ""
|
||
echo "迁移完成!运行以下验证(密码通过 MYSQL_PWD 传入,不会出现在进程列表):"
|
||
echo " export MYSQL_PWD=\"\$MYSQL_ROOT_PASSWORD\"; docker compose exec -T -e MYSQL_PWD mysql mysql -u root "${TARGET_DB}" -e 'SELECT COUNT(*) FROM students'"
|