From b8643e085f4472e58fd67ed42dbbd81dd1efce95 Mon Sep 17 00:00:00 2001 From: danaisuiyuan Date: Sun, 10 May 2026 09:13:29 +0800 Subject: [PATCH] feat: add syj variant automated deploy script One-shot release pipeline for syj-shop variant: admin build -> rsync to 8.140.50.89 -> remote .env switch + cache clear + Swoole reload -> healthcheck with auto-rollback on failure. Includes operations manual under docs/project-syj/. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/project-syj/deploy.md | 126 +++++++++++++++++ pro_v3.5.1/deploy/release-syj.sh | 211 ++++++++++++++++++++++++++++ pro_v3.5.1/deploy/rsync-exclude.txt | 26 ++++ pro_v3.5.1/deploy/syj.conf | 27 ++++ 4 files changed, 390 insertions(+) create mode 100644 docs/project-syj/deploy.md create mode 100755 pro_v3.5.1/deploy/release-syj.sh create mode 100644 pro_v3.5.1/deploy/rsync-exclude.txt create mode 100644 pro_v3.5.1/deploy/syj.conf diff --git a/docs/project-syj/deploy.md b/docs/project-syj/deploy.md new file mode 100644 index 00000000..8bed4572 --- /dev/null +++ b/docs/project-syj/deploy.md @@ -0,0 +1,126 @@ +# syj 商城变体发布手册 + +一键脚本:`pro_v3.5.1/deploy/release-syj.sh` +覆盖范围:后端 PHP + admin 前端(dist)+ `.env` 切换 + Swoole 平滑 reload + 健康检查 + 失败自动回滚。 +**不覆盖**:`view/uniapp_v2/`(HBuilderX 手动)、`view/uniapp/`(本次不发布)、composer 依赖、数据库迁移。 + +--- + +## 1. 首次配置 + +### 1.1 本机 +- 安装 `rsync`、`ssh`、Node 16+(admin 构建) +- 配置 SSH 免密:`ssh-copy-id -p 22 root@8.140.50.89` +- 验证:`ssh root@8.140.50.89 "ls /www/wwwroot/syj.fsgx.cn"` +- 脚本参数集中在 `pro_v3.5.1/deploy/syj.conf`,按实际环境修改 + +### 1.2 服务器(首次部署后) +- 路径:`/www/wwwroot/syj.fsgx.cn/`(CRMEB 项目根,对应仓库的 `pro_v3.5.1/`) +- PHP 8.0 + Swoole 4.x 已装好;`config/swoole.php` 建议显式设置 `pid_file => runtime/swoole/swoole.pid` +- Swoole 进程通过 systemd 或 screen 长驻。systemd 单元示例: + + ```ini + # /etc/systemd/system/syj-swoole.service + [Unit] + Description=syj-shop Swoole HTTP/WS + After=network.target mysql.service redis.service + + [Service] + Type=simple + WorkingDirectory=/www/wwwroot/syj.fsgx.cn + ExecStart=/usr/bin/php think swoole + ExecReload=/bin/kill -USR1 $MAINPID + Restart=on-failure + RestartSec=2 + User=www + Group=www + + [Install] + WantedBy=multi-user.target + ``` + + `systemctl daemon-reload && systemctl enable --now syj-swoole` + +- Nginx 反代关键片段(端口 443 → Swoole 20199): + + ```nginx + location / { + proxy_pass http://127.0.0.1:20199; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + location /admin/ { + alias /www/wwwroot/syj.fsgx.cn/view/admin/dist/; + try_files $uri $uri/ /admin/index.html; + } + ``` + +### 1.3 `.env-syj` 字段含义 +- `[DATABASE]` HOSTNAME=8.140.50.89 → 与 web 同机的 MySQL +- `[REDIS]` HOSTNAME=8.140.50.89 → 同机 Redis +- `APP_DEBUG = true` ⚠️ **生产建议改为 false**(脚本不强制改,避免吞掉用户配置) + +--- + +## 2. 日常发布 + +```bash +cd /Users/mac/scott2026/huangjingfen/pro_v3.5.1 +bash deploy/release-syj.sh +``` + +阶段日志说明: +1. `Pre-flight checks` — 校验分支、`.env-syj` 存在、远端目录可达 +2. `Build admin` — `view/admin` 下 `npm run build`,产物 `view/admin/dist/` +3. `Release tag: 20260510-093015-79436c01` — 时间戳 + git short SHA +4. `rsync ->` — 推送代码(带 `--backup-dir` 把被覆盖的旧文件备份到远端 `/www/wwwroot/syj.fsgx.cn-bak//`) +5. `Remote: switch .env, php think clear, Swoole reload` — 远端拷贝 `.env-syj` → `.env`、清缓存、`kill -USR1 master` +6. `Healthcheck` — 串行 curl `https://syj.fsgx.cn/api/version` 与 `/admin/`,每个 URL 重试 5 次 +7. 成功:写 `deploy/.last-release` 并清理超出 `KEEP_BACKUPS=10` 的旧备份 + +可选标志: +- `--dry-run` 只打印 rsync 改动,不动远端 +- `--skip-build` 复用已有 `view/admin/dist/` +- `--rollback ` 回滚到指定备份(见下) + +--- + +## 3. 回滚 + +### 3.1 自动回滚 +健康检查失败时脚本自动执行:用 `` 备份覆盖远端代码 → 清缓存 → reload → 再次健康检查。仍失败则脚本退出 2,备份目录保留供人工排查。 + +### 3.2 手动回滚 +查看远端备份: + +```bash +ssh root@8.140.50.89 'ls -1t /www/wwwroot/syj.fsgx.cn-bak | head -10' +``` + +执行: + +```bash +bash deploy/release-syj.sh --rollback 20260510-093015-79436c01 +``` + +`.env` 不在回滚范围内(`rsync --exclude='.env'`),避免误覆盖运行中的环境配置。 + +--- + +## 4. 故障排查 + +| 现象 | 排查 | +|------|------| +| admin 502 | Swoole 是否在跑:`systemctl status syj-swoole` 或 `ps aux \| grep "think swoole"` | +| `/api/version` 不是 200 | 看远端 `runtime/log/<日期>/cli.log`、Swoole 是否成功 reload | +| reload 后代码未生效 | 检查 `runtime/swoole/swoole.pid` 是否过期;删除后让 Swoole 重启重生 | +| `.env` 错配(连不上 DB) | `cat /www/wwwroot/syj.fsgx.cn/.env` 对比 `.env-syj` | +| 健康检查超时 | 调大 `HEALTH_INTERVAL` / `HEALTH_RETRY`(在 `syj.conf`) | +| `public/uploads/` 被清掉 | `rsync-exclude.txt` 有 `public/uploads/`,不会被同步;若已被覆盖,从 `` 备份恢复 | +| 分支不对脚本仍要继续 | 输入 `y` 二次确认;建议先 `git checkout syj-bypass-auth` | + +远端 Swoole 日志:`/www/wwwroot/syj.fsgx.cn/runtime/swoole/swoole.log` +本地最近一次发布的 tag:`pro_v3.5.1/deploy/.last-release` diff --git a/pro_v3.5.1/deploy/release-syj.sh b/pro_v3.5.1/deploy/release-syj.sh new file mode 100755 index 00000000..2040f622 --- /dev/null +++ b/pro_v3.5.1/deploy/release-syj.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# syj 商城变体一键发布脚本 +# 用法: +# bash deploy/release-syj.sh # 正常发布 +# bash deploy/release-syj.sh --dry-run # 干跑(不动远端) +# bash deploy/release-syj.sh --rollback # 手动回滚到指定备份 +# bash deploy/release-syj.sh --skip-build # 跳过 admin 构建(复用 dist/) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONF_FILE="$SCRIPT_DIR/syj.conf" +EXCLUDE_FILE="$SCRIPT_DIR/rsync-exclude.txt" +LAST_RELEASE_FILE="$SCRIPT_DIR/.last-release" + +[ -f "$CONF_FILE" ] || { echo "ERROR: $CONF_FILE not found"; exit 1; } +[ -f "$EXCLUDE_FILE" ] || { echo "ERROR: $EXCLUDE_FILE not found"; exit 1; } +# shellcheck disable=SC1090 +source "$CONF_FILE" + +DRY_RUN=0 +SKIP_BUILD=0 +ROLLBACK_TAG="" + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --skip-build) SKIP_BUILD=1; shift ;; + --rollback) ROLLBACK_TAG="${2:-}"; shift 2 ;; + -h|--help) sed -n '2,8p' "$0"; exit 0 ;; + *) echo "Unknown arg: $1"; exit 2 ;; + esac +done + +_ts() { date +%H:%M:%S; } +log() { printf "\033[1;36m[%s]\033[0m %s\n" "$(_ts)" "$*"; } +warn() { printf "\033[1;33m[%s] WARN\033[0m %s\n" "$(_ts)" "$*"; } +err() { printf "\033[1;31m[%s] ERR\033[0m %s\n" "$(_ts)" "$*" >&2; } + +ssh_run() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" "$@"; } +ssh_pipe() { ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" bash -se; } + +check_health() { + local url code i + for url in "${HEALTH_URLS[@]}"; do + for ((i=1; i<=HEALTH_RETRY; i++)); do + code=$(curl -ksS -o /dev/null -w "%{http_code}" --max-time 10 "$url" || echo 000) + if [ "$code" = "200" ]; then + log " ✓ $url -> 200" + break + fi + if [ "$i" -eq "$HEALTH_RETRY" ]; then + err " ✗ $url -> $code (after $HEALTH_RETRY tries)" + return 1 + fi + sleep "$HEALTH_INTERVAL" + done + done + return 0 +} + +remote_clear_and_reload() { + REMOTE_ROOT="$REMOTE_ROOT" ENV_SOURCE_FILE="$ENV_SOURCE_FILE" \ + ssh_pipe <<'EOF' +set -euo pipefail +cd "$REMOTE_ROOT" +if [ ! -f "$ENV_SOURCE_FILE" ]; then + echo "ERR: $ENV_SOURCE_FILE missing on remote" >&2 + exit 1 +fi +cp -p "$ENV_SOURCE_FILE" .env +php think clear +PIDFILE=runtime/swoole/swoole.pid +if [ -f "$PIDFILE" ]; then + PID=$(cat "$PIDFILE") + if kill -0 "$PID" 2>/dev/null; then + kill -USR1 "$PID" + echo "Swoole reload via pidfile (master=$PID)" + else + echo "WARN: stale pidfile, falling back to ps" + PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}') + [ -n "$PID" ] && kill -USR1 "$PID" && echo "Swoole reload via ps (master=$PID)" + fi +else + PID=$(ps -eo pid,ppid,command | awk '/php think swoole/ && $2==1 {print $1; exit}') + if [ -n "$PID" ]; then + kill -USR1 "$PID" + echo "Swoole reload via ps (master=$PID)" + else + echo "WARN: Swoole master not found; nothing reloaded" + fi +fi +EOF +} + +prune_old_backups() { + REMOTE_BAK="$REMOTE_BAK" KEEP_BACKUPS="$KEEP_BACKUPS" ssh_pipe <<'EOF' +set -euo pipefail +mkdir -p "$REMOTE_BAK" +cd "$REMOTE_BAK" +ls -1t 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r d; do + [ -n "$d" ] && rm -rf -- "$d" && echo "Pruned $REMOTE_BAK/$d" +done +EOF +} + +# ---- ROLLBACK MODE ---- +if [ -n "$ROLLBACK_TAG" ]; then + log "ROLLBACK to $ROLLBACK_TAG" + REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$ROLLBACK_TAG" \ + ssh_pipe <<'EOF' +set -euo pipefail +SRC="$REMOTE_BAK/$TAG" +[ -d "$SRC" ] || { echo "Backup $SRC not found"; exit 1; } +rsync -a --exclude='.env' "$SRC/" "$REMOTE_ROOT/" +echo "Rolled back files from $SRC" +EOF + remote_clear_and_reload + sleep "$HEALTH_INTERVAL" + check_health || { err "Healthcheck failed after rollback"; exit 1; } + log "Rollback OK" + exit 0 +fi + +# ---- PRE-FLIGHT ---- +log "Pre-flight checks" +cd "$PROJECT_ROOT" + +CURRENT_BRANCH=$(git -C "$PROJECT_ROOT/.." rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?") +if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then + warn "Current branch is '$CURRENT_BRANCH', expected '$EXPECTED_BRANCH'" + if [ "$DRY_RUN" -ne 1 ]; then + read -r -p "Continue anyway? [y/N] " ans + [[ "$ans" =~ ^[Yy]$ ]] || { err "Aborted"; exit 1; } + fi +fi + +[ -f "$PROJECT_ROOT/$ENV_SOURCE_FILE" ] || { err "$ENV_SOURCE_FILE missing locally"; exit 1; } +grep -q "syj.fsgx.cn" "$PROJECT_ROOT/view/admin/.env.production" || warn "view/admin/.env.production not pointing to syj.fsgx.cn" + +ssh_run "test -d '$REMOTE_ROOT'" || { err "Remote root $REMOTE_ROOT does not exist"; exit 1; } + +# ---- BUILD ADMIN ---- +if [ "$SKIP_BUILD" -eq 1 ]; then + log "Skip admin build (--skip-build)" +else + log "Build admin" + ( + cd "$PROJECT_ROOT/view/admin" + if [ ! -d node_modules ]; then + npm ci --silent + fi + npm run build + ) +fi +[ -d "$PROJECT_ROOT/view/admin/dist" ] || { err "view/admin/dist missing; build failed?"; exit 1; } + +# ---- VERSION ---- +TS=$(date +%Y%m%d-%H%M%S) +SHA=$(git -C "$PROJECT_ROOT/.." rev-parse --short HEAD 2>/dev/null || echo "nogit") +TAG="${TS}-${SHA}" +log "Release tag: $TAG" + +# ---- RSYNC ---- +RSYNC_OPTS=(-az --delete --human-readable + --exclude-from="$EXCLUDE_FILE" + --backup --backup-dir="$REMOTE_BAK/$TAG" + -e "ssh -p $SSH_PORT") +[ "$DRY_RUN" -eq 1 ] && RSYNC_OPTS+=(--dry-run --itemize-changes) + +log "rsync -> ${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT (backup: $REMOTE_BAK/$TAG)" +ssh_run "mkdir -p '$REMOTE_BAK/$TAG'" +rsync "${RSYNC_OPTS[@]}" "$PROJECT_ROOT/" "${SSH_USER}@${SSH_HOST}:$REMOTE_ROOT/" + +if [ "$DRY_RUN" -eq 1 ]; then + log "Dry-run done. No remote changes applied." + exit 0 +fi + +# ---- REMOTE: env switch + clear + reload ---- +log "Remote: switch .env, php think clear, Swoole reload" +remote_clear_and_reload + +# ---- HEALTHCHECK ---- +sleep "$HEALTH_INTERVAL" +log "Healthcheck" +if check_health; then + echo "$TAG" > "$LAST_RELEASE_FILE" + log "Released $TAG ✓" + prune_old_backups || warn "Prune step failed (non-fatal)" + exit 0 +fi + +# ---- AUTO ROLLBACK ---- +err "Healthcheck failed. Auto-rolling back from $REMOTE_BAK/$TAG" +REMOTE_ROOT="$REMOTE_ROOT" REMOTE_BAK="$REMOTE_BAK" TAG="$TAG" \ +ssh_pipe <<'EOF' +set -euo pipefail +rsync -a --exclude='.env' "$REMOTE_BAK/$TAG/" "$REMOTE_ROOT/" +EOF +remote_clear_and_reload +sleep "$HEALTH_INTERVAL" +if check_health; then + err "Rolled back to previous state. Release $TAG aborted." + exit 1 +else + err "Rollback healthcheck STILL failing. Manual intervention required." + err "Backup at: ${SSH_USER}@${SSH_HOST}:$REMOTE_BAK/$TAG" + exit 2 +fi diff --git a/pro_v3.5.1/deploy/rsync-exclude.txt b/pro_v3.5.1/deploy/rsync-exclude.txt new file mode 100644 index 00000000..bbaf3a9a --- /dev/null +++ b/pro_v3.5.1/deploy/rsync-exclude.txt @@ -0,0 +1,26 @@ +.git/ +.idea/ +.vscode/ +.DS_Store +runtime/ +backup/ +tests/ +node_modules/ +public/uploads/ +.env +.env-fsgx +.env-huangjinfen +view/uniapp/ +view/uniapp_v2/ +view/admin/node_modules/ +view/admin/src/ +view/admin/public/ +view/admin/.env* +view/admin/package*.json +view/admin/babel.config.js +view/admin/vue.config.js +view/admin/jest.config.js +view/admin/alias.config.js +view/admin/tests/ +view/admin/dist/.DS_Store +deploy/.last-release diff --git a/pro_v3.5.1/deploy/syj.conf b/pro_v3.5.1/deploy/syj.conf new file mode 100644 index 00000000..b71a4476 --- /dev/null +++ b/pro_v3.5.1/deploy/syj.conf @@ -0,0 +1,27 @@ +# syj 商城变体发布参数(被 release-syj.sh source) +# 修改后立即生效,不需要重新构建 + +SSH_USER=root +SSH_HOST=8.140.50.89 +SSH_PORT=22 +REMOTE_ROOT=/www/wwwroot/syj.fsgx.cn +REMOTE_BAK=/www/wwwroot/syj.fsgx.cn-bak + +# 健康检查 URL,任一非 200 即触发回滚 +HEALTH_URLS=( + "https://syj.fsgx.cn/api/version" + "https://syj.fsgx.cn/admin/" +) + +# 备份保留数量(远端 REMOTE_BAK 下保留最近 N 份) +KEEP_BACKUPS=10 + +# 期望分支;不一致时脚本会要求二次确认 +EXPECTED_BRANCH=syj-bypass-auth + +# 远端 .env 取自该文件(位于 pro_v3.5.1/ 根) +ENV_SOURCE_FILE=.env-syj + +# 健康检查重试 +HEALTH_RETRY=5 +HEALTH_INTERVAL=3