#!/usr/bin/env bash # run-prod-cleanup.sh — 经 SSH 隧道连接生产库,执行测试数据清理 # # 用法: # bash deploy/run-prod-cleanup.sh # dry-run # bash deploy/run-prod-cleanup.sh --apply --yes # 写库(默认带 --force 处理已打款测试核销) # bash deploy/run-prod-cleanup.sh --inventory # 仅盘点 # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$SCRIPT_DIR" LOCAL_CONTAINER="dukang-v1-mysql" TUNNEL_PORT=6020 DOCKER_HOST_ALIAS="host.docker.internal" CLIENT_HOST="127.0.0.1" APPLY=0 ASSUME_YES=0 FORCE=1 INVENTORY=0 SCRIPT_NAME="" EXTRA_ARGS=() for arg in "$@"; do case "$arg" in --apply) APPLY=1 ;; --yes|-y) ASSUME_YES=1 ;; --no-force) FORCE=0 ;; --force) FORCE=1 ;; --inventory) INVENTORY=1 ;; --script=*) SCRIPT_NAME="${arg#*=}" ;; --help|-h) sed -n '3,12p' "$0" exit 0 ;; *) EXTRA_ARGS+=("$arg") ;; esac done if [[ -f deploy.env ]]; then # shellcheck disable=SC1091 source deploy.env fi DEPLOY_HOST="${DEPLOY_HOST:?请在 deploy.env 配置 DEPLOY_HOST}" DEPLOY_USER="${DEPLOY_USER:-root}" DEPLOY_PORT="${DEPLOY_PORT:-22}" SSH=(ssh -o StrictHostKeyChecking=accept-new -p "$DEPLOY_PORT") if [[ -n "${DEPLOY_SSH_KEY:-}" ]]; then SSH+=(-i "$DEPLOY_SSH_KEY"); fi TARGET="$DEPLOY_USER@$DEPLOY_HOST" if ! docker ps --format '{{.Names}}' | grep -qx "$LOCAL_CONTAINER"; then echo "本地容器 $LOCAL_CONTAINER 未运行。" >&2 exit 1 fi PARSED="$("${SSH[@]}" "$TARGET" bash -s <<'NODEEOF' cat > /tmp/_parse_db.js <<'JSEOF' const fs=require("fs"); const l=fs.readFileSync("/opt/dukang/server/dukang-api/.env.production","utf8").match(/^DATABASE_URL=(.*)$/m)[1].trim(); let u=l; if((u[0]==='"'&&u[u.length-1]==='"')||(u[0]==="'"&&u[u.length-1]==="'"))u=u.slice(1,-1); const U=new URL(u); const b=s=>Buffer.from(s).toString("base64"); const path=U.pathname.replace(/^\//,"").split("?")[0]; process.stdout.write([b(decodeURIComponent(U.username)),b(U.hostname),U.port||"3306",b(path),b(decodeURIComponent(U.password))].join("|")+"\n"); JSEOF node /tmp/_parse_db.js; rm -f /tmp/_parse_db.js NODEEOF )" IFS='|' read -r _U _H _P _DB _PASS <<< "$PARSED" REMOTE_USER="$(echo "$_U" | base64 -d)" REMOTE_HOST="$(echo "$_H" | base64 -d)" REMOTE_PORT="$_P" REMOTE_DB="$(echo "$_DB" | base64 -d)" REMOTE_PASS="$(echo "$_PASS" | base64 -d)" CTL="/tmp/run-prod-cleanup-$$.sock" echo "==> 建立 SSH 隧道 $TARGET -L $TUNNEL_PORT:$REMOTE_HOST:$REMOTE_PORT" "${SSH[@]}" -M -S "$CTL" -f -N -L "$TUNNEL_PORT:$REMOTE_HOST:$REMOTE_PORT" "$TARGET" cleanup_tunnel() { "${SSH[@]}" -S "$CTL" -O exit "$TARGET" 2>/dev/null || true } trap cleanup_tunnel EXIT ready=0 for _ in 1 2 3 4 5 6 7 8 9 10; do if docker exec -e "MYSQL_PWD=$REMOTE_PASS" "$LOCAL_CONTAINER" \ mysqladmin -h "$DOCKER_HOST_ALIAS" -P "$TUNNEL_PORT" -u "$REMOTE_USER" ping >/dev/null 2>&1; then ready=1; break fi sleep 1 done if [[ $ready -ne 1 ]]; then echo "隧道未就绪,中止。" >&2 exit 1 fi ENC_PASS="$(node -e "console.log(encodeURIComponent(process.argv[1]))" "$REMOTE_PASS")" export DATABASE_URL="mysql://${REMOTE_USER}:${ENC_PASS}@${CLIENT_HOST}:${TUNNEL_PORT}/${REMOTE_DB}?charset=utf8mb4" export FORCE_DATABASE_URL=1 cd "$REPO_ROOT/server/dukang-api" CMD=(pnpm) if [[ -n "$SCRIPT_NAME" ]]; then CMD+=(ts-node --transpile-only "scripts/${SCRIPT_NAME}.ts") [[ $FORCE -eq 1 ]] && CMD+=(--force) [[ $APPLY -eq 1 ]] && CMD+=(--apply) [[ $ASSUME_YES -eq 1 ]] && CMD+=(--yes) [[ ${#EXTRA_ARGS[@]} -gt 0 ]] && CMD+=("${EXTRA_ARGS[@]}") elif [[ $INVENTORY -eq 1 ]]; then CMD+=(inventory:test-data -- "${EXTRA_ARGS[@]}") else CMD+=(cleanup:test-data --) [[ $FORCE -eq 1 ]] && CMD+=(--force) [[ $APPLY -eq 1 ]] && CMD+=(--apply) [[ $ASSUME_YES -eq 1 ]] && CMD+=(--yes) CMD+=("${EXTRA_ARGS[@]}") fi echo "==> 执行: DATABASE_URL=*** ${CMD[*]}" "${CMD[@]}"