# SmartClean 自动化打包部署方案(无 Docker 版) ## 一、目标 基于现有 build.sh 构建能力,实现 **一键构建 → 远程备份 → 上传部署 → 服务重启 → 健康检查 → 失败回滚 → 飞书通知** 全流程自动化。不依赖 Docker,直接操作远程服务器。 ## 二、前提条件 | 条件 | 说明 | |------|------| | SSH 免密登录 | 本机 → 182 服务器配好 SSH Key,免输密码 | | 服务器目录约定 | 统一部署路径,脚本按约定操作 | | 现有构建脚本 | 复用已有的 build.sh | ### SSH 免密配置(一次性) ```bash # 生成密钥(如已有可跳过) ssh-keygen -t ed25519 # 将公钥复制到服务器 ssh-copy-id root@192.168.1.182 # 验证 ssh root@192.168.1.182 "echo ok" ``` ## 三、整体架构 ``` 本机 (Mac) 远程服务器 (192.168.1.182) ┌──────────────────────┐ ┌──────────────────────────────┐ │ │ │ │ │ 源码 → build.sh 构建 │ │ /opt/smartclean/ │ │ │ SCP 上传 │ ├── web/ │ │ 产物: │ ──────────────→ │ │ └── ROOT.war │ │ - ROOT.war │ │ ├── task/ │ │ - task.jar │ SSH 远程执行 │ │ └── task.jar │ │ - dist/ │ ──────────────→ │ ├── front/ │ │ │ │ │ └── dist/ │ │ deploy.sh 控制全流程 │ │ ├── backups/ │ │ │ │ │ ├── 20260415-153000/ │ │ │ │ │ └── 20260415-140000/ │ │ │ │ └── scripts/ │ │ │ │ ├── restart-web.sh │ │ │ │ ├── restart-task.sh │ │ │ │ └── restart-front.sh │ └──────────────────────┘ └──────────────────────────────┘ ``` ## 四、服务器目录规划 ``` /opt/smartclean/ # 部署根目录 ├── web/ │ ├── tomcat/ # Tomcat 安装目录 │ │ └── webapps/ │ │ └── ROOT.war # Web 服务 WAR 包 │ └── logs/ ├── task/ │ ├── task.jar # Task 服务 JAR 包 │ ├── task.pid # 进程 PID 文件 │ └── logs/ ├── front/ │ └── dist/ # 前端静态文件(Nginx 指向此目录) ├── backups/ # 版本备份(自动保留最近 5 个) │ ├── 20260415-153000/ │ │ ├── ROOT.war │ │ ├── task.jar │ │ └── dist/ │ └── 20260415-140000/ │ └── ... └── scripts/ # 服务器端管理脚本 ├── restart-web.sh ├── restart-task.sh └── restart-front.sh ``` ## 五、新增文件结构 ``` smartclean/ ├── build.sh # 已有,构建脚本 └── deploy/ ├── deploy.sh # 一键部署主脚本(本机执行) ├── rollback.sh # 一键回滚脚本(本机执行) ├── config.sh # 部署配置(服务器地址、路径等) ├── .env # 敏感信息(密码等,不入 git) └── remote/ # 服务器端脚本(首次部署时自动上传) ├── setup.sh # 服务器初始化(创建目录结构,一次性) ├── restart-web.sh # 重启 Web 服务(Tomcat) ├── restart-task.sh # 重启 Task 服务(JAR) └── restart-front.sh # 重新加载前端(Nginx reload) ``` ## 六、配置文件 ### 6.1 部署配置 ```bash # deploy/config.sh # ===== 服务器配置 ===== DEPLOY_HOST="192.168.1.182" DEPLOY_USER="root" DEPLOY_BASE="/opt/smartclean" # ===== 服务器目录 ===== REMOTE_WEB_DIR="$DEPLOY_BASE/web" REMOTE_TASK_DIR="$DEPLOY_BASE/task" REMOTE_FRONT_DIR="$DEPLOY_BASE/front" REMOTE_BACKUP_DIR="$DEPLOY_BASE/backups" REMOTE_SCRIPTS_DIR="$DEPLOY_BASE/scripts" # ===== Tomcat 配置 ===== TOMCAT_HOME="/opt/smartclean/web/tomcat" TOMCAT_WEBAPPS="$TOMCAT_HOME/webapps" # ===== Nginx 配置 ===== NGINX_HTML="/opt/smartclean/front/dist" # ===== Task 服务配置 ===== TASK_JAR_NAME="xiaoqu-intellectual-task-0.0.1-SNAPSHOT.jar" TASK_PROFILE="prod" TASK_JVM_OPTS="-Xms256m -Xmx512m" # ===== 本地构建产物路径 ===== LOCAL_WAR="backend/xiaoqu-intellectual-web/target/ROOT.war" LOCAL_TASK_JAR="backend/xiaoqu-intellectual-task/target/$TASK_JAR_NAME" LOCAL_FRONT_DIST="frontend/witcleansystem/dist" # ===== 备份保留数量 ===== MAX_BACKUPS=5 # ===== 健康检查 ===== HEALTHCHECK_URL="http://$DEPLOY_HOST:8095/dropDown/districtTree" HEALTHCHECK_RETRIES=20 HEALTHCHECK_INTERVAL=5 # ===== 飞书通知 ===== FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/5703e8cc-6998-46a6-af9d-8c5102cc8c1e" ``` ## 七、服务器端脚本 ### 7.1 服务器初始化脚本(一次性执行) ```bash # deploy/remote/setup.sh #!/bin/bash # 在服务器上创建标准目录结构,仅需执行一次 DEPLOY_BASE="/opt/smartclean" mkdir -p "$DEPLOY_BASE"/{web/logs,task/logs,front,backups,scripts} echo "目录结构创建完成:" find "$DEPLOY_BASE" -maxdepth 2 -type d ``` ### 7.2 重启 Web 服务 ```bash # deploy/remote/restart-web.sh #!/bin/bash # 重启 Tomcat(部署 ROOT.war) TOMCAT_HOME="/opt/smartclean/web/tomcat" echo "[INFO] 停止 Tomcat..." "$TOMCAT_HOME/bin/shutdown.sh" 2>/dev/null sleep 3 # 确保进程已停 TOMCAT_PID=$(ps -ef | grep "catalina" | grep -v grep | awk '{print $2}') if [ -n "$TOMCAT_PID" ]; then echo "[WARN] Tomcat 未正常关闭,强制终止 PID=$TOMCAT_PID" kill -9 $TOMCAT_PID sleep 1 fi # 清理旧的解压目录,保留 WAR rm -rf "$TOMCAT_HOME/webapps/ROOT" rm -rf "$TOMCAT_HOME/work/Catalina" echo "[INFO] 启动 Tomcat..." "$TOMCAT_HOME/bin/startup.sh" echo "[INFO] Tomcat 已启动" ``` ### 7.3 重启 Task 服务 ```bash # deploy/remote/restart-task.sh #!/bin/bash # 重启 Task 服务(Spring Boot JAR) TASK_DIR="/opt/smartclean/task" JAR_FILE="$TASK_DIR/task.jar" PID_FILE="$TASK_DIR/task.pid" LOG_FILE="$TASK_DIR/logs/task.log" PROFILE="${1:-prod}" JVM_OPTS="${2:--Xms256m -Xmx512m}" # 停止旧进程 if [ -f "$PID_FILE" ]; then OLD_PID=$(cat "$PID_FILE") if kill -0 "$OLD_PID" 2>/dev/null; then echo "[INFO] 停止旧进程 PID=$OLD_PID" kill "$OLD_PID" sleep 3 # 强制终止 if kill -0 "$OLD_PID" 2>/dev/null; then kill -9 "$OLD_PID" fi fi rm -f "$PID_FILE" fi # 启动新进程 echo "[INFO] 启动 Task 服务 (profile=$PROFILE)..." nohup java $JVM_OPTS \ -jar "$JAR_FILE" \ --spring.profiles.active=$PROFILE \ > "$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" echo "[INFO] Task 服务已启动, PID=$(cat "$PID_FILE")" ``` ### 7.4 重新加载前端 ```bash # deploy/remote/restart-front.sh #!/bin/bash # 重新加载 Nginx(前端静态文件已更新) echo "[INFO] 测试 Nginx 配置..." nginx -t 2>&1 if [ $? -ne 0 ]; then echo "[ERROR] Nginx 配置有误" exit 1 fi echo "[INFO] 重新加载 Nginx..." nginx -s reload echo "[INFO] Nginx 已重新加载" ``` ## 八、一键部署主脚本 ```bash # deploy/deploy.sh #!/bin/bash # # SmartClean 一键部署脚本(无 Docker 版) # # 用法: # ./deploy.sh # 构建并部署所有服务 # ./deploy.sh web # 仅构建部署 Web 服务 # ./deploy.sh task # 仅构建部署 Task 服务 # ./deploy.sh front # 仅构建部署前端 # ./deploy.sh front-test # 构建测试环境前端并部署 # ./deploy.sh backend # 构建部署后端(web + task) # ./deploy.sh rollback # 回滚到上一版本 # ./deploy.sh setup # 首次初始化服务器目录 set -e DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(dirname "$DEPLOY_DIR")" source "$DEPLOY_DIR/config.sh" # ===== 版本号 ===== GIT_HASH=$(cd "$ROOT_DIR" && git rev-parse --short HEAD) TIMESTAMP=$(date +%Y%m%d-%H%M%S) VERSION="${TIMESTAMP}-${GIT_HASH}" BRANCH=$(cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD) COMMIT=$(cd "$ROOT_DIR" && git log -1 --format='%h %s') RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } SSH_CMD="ssh $DEPLOY_USER@$DEPLOY_HOST" SCP_CMD="scp" # ===== 飞书通知 ===== notify_feishu() { local title="$1" content="$2" color="$3" curl -s -X POST "$FEISHU_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"msg_type\": \"interactive\", \"card\": { \"header\": {\"title\":{\"tag\":\"plain_text\",\"content\":\"$title\"},\"template\":\"$color\"}, \"elements\": [{\"tag\":\"markdown\",\"content\":\"$content\"}] } }" > /dev/null 2>&1 } # ===== 检查 SSH 连接 ===== check_ssh() { log_info "检查 SSH 连接..." if ! $SSH_CMD "echo ok" > /dev/null 2>&1; then log_error "无法连接到 $DEPLOY_USER@$DEPLOY_HOST" log_error "请先配置 SSH 免密登录: ssh-copy-id $DEPLOY_USER@$DEPLOY_HOST" exit 1 fi log_info "SSH 连接正常" } # ===== 首次初始化服务器 ===== setup_server() { log_info "初始化服务器目录结构..." $SCP_CMD "$DEPLOY_DIR/remote/setup.sh" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/setup.sh" $SSH_CMD "bash /tmp/setup.sh" log_info "上传服务器端管理脚本..." $SCP_CMD "$DEPLOY_DIR/remote/restart-web.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" $SCP_CMD "$DEPLOY_DIR/remote/restart-task.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" $SCP_CMD "$DEPLOY_DIR/remote/restart-front.sh" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_SCRIPTS_DIR/" $SSH_CMD "chmod +x $REMOTE_SCRIPTS_DIR/*.sh" log_info "服务器初始化完成" } # ===== 本地构建 ===== build_local() { local target="$1" log_info "开始本地构建 (目标: $target)..." cd "$ROOT_DIR" bash build.sh "$target" # 验证产物存在 case "$target" in web|backend|all) if [ ! -f "$ROOT_DIR/$LOCAL_WAR" ]; then log_error "构建产物不存在: $LOCAL_WAR" exit 1 fi ;; esac case "$target" in task|backend|all) if [ ! -f "$ROOT_DIR/$LOCAL_TASK_JAR" ]; then log_error "构建产物不存在: $LOCAL_TASK_JAR" exit 1 fi ;; esac case "$target" in front|front-test|all) if [ ! -d "$ROOT_DIR/$LOCAL_FRONT_DIST" ]; then log_error "构建产物不存在: $LOCAL_FRONT_DIST" exit 1 fi ;; esac log_info "本地构建完成" } # ===== 远程备份 ===== backup_remote() { local target="$1" log_info "备份服务器当前版本 ($VERSION)..." $SSH_CMD bash </dev/null && echo "已备份 ROOT.war" || echo "ROOT.war 不存在,跳过" ;; esac case "$target" in task|backend|all) cp "$REMOTE_TASK_DIR/task.jar" "\$BACKUP/" 2>/dev/null && echo "已备份 task.jar" || echo "task.jar 不存在,跳过" ;; esac case "$target" in front|front-test|all) if [ -d "$REMOTE_FRONT_DIR/dist" ]; then cp -r "$REMOTE_FRONT_DIR/dist" "\$BACKUP/" echo "已备份 front/dist" fi ;; esac # 清理过期备份,保留最近 $MAX_BACKUPS 个 cd "$REMOTE_BACKUP_DIR" ls -dt */ 2>/dev/null | tail -n +\$(($MAX_BACKUPS + 1)) | xargs rm -rf 2>/dev/null echo "当前备份列表:" ls -dt */ 2>/dev/null | head -5 EOF log_info "远程备份完成" } # ===== 上传产物 ===== upload_artifacts() { local target="$1" case "$target" in web|backend|all) log_info "上传 ROOT.war..." $SCP_CMD "$ROOT_DIR/$LOCAL_WAR" "$DEPLOY_USER@$DEPLOY_HOST:$TOMCAT_WEBAPPS/ROOT.war" ;; esac case "$target" in task|backend|all) log_info "上传 task.jar..." $SCP_CMD "$ROOT_DIR/$LOCAL_TASK_JAR" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_TASK_DIR/task.jar" ;; esac case "$target" in front|front-test|all) log_info "上传前端文件..." # 先清空远程 dist,再上传 $SSH_CMD "rm -rf $REMOTE_FRONT_DIR/dist" $SCP_CMD -r "$ROOT_DIR/$LOCAL_FRONT_DIST" "$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_FRONT_DIR/dist" ;; esac log_info "产物上传完成" } # ===== 远程重启服务 ===== restart_services() { local target="$1" case "$target" in web|backend|all) log_info "重启 Web 服务..." $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-web.sh" ;; esac case "$target" in task|backend|all) log_info "重启 Task 服务..." $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-task.sh $TASK_PROFILE '$TASK_JVM_OPTS'" ;; esac case "$target" in front|front-test|all) log_info "重新加载前端..." $SSH_CMD "bash $REMOTE_SCRIPTS_DIR/restart-front.sh" ;; esac } # ===== 健康检查 ===== healthcheck() { local target="$1" # Web 服务检查 if [ "$target" = "all" ] || [ "$target" = "web" ] || [ "$target" = "backend" ]; then log_info "健康检查 Web 服务 ($HEALTHCHECK_URL)..." for i in $(seq 1 $HEALTHCHECK_RETRIES); do if curl -sf "$HEALTHCHECK_URL" > /dev/null 2>&1; then log_info "✅ Web 服务健康 (第${i}次检查)" return 0 fi echo -n "." sleep $HEALTHCHECK_INTERVAL done log_error "❌ Web 服务健康检查失败 (${HEALTHCHECK_RETRIES}次重试后)" return 1 fi # 前端检查 if [ "$target" = "front" ] || [ "$target" = "front-test" ]; then log_info "健康检查前端..." if curl -sf "http://$DEPLOY_HOST/" > /dev/null 2>&1; then log_info "✅ 前端服务健康" return 0 else log_error "❌ 前端服务健康检查失败" return 1 fi fi return 0 } # ===== 回滚 ===== rollback() { log_info "查找最近的备份..." # 获取最近的备份目录名 LATEST_BACKUP=$($SSH_CMD "ls -dt $REMOTE_BACKUP_DIR/*/ 2>/dev/null | head -1 | xargs basename 2>/dev/null") if [ -z "$LATEST_BACKUP" ]; then log_error "没有可回滚的备份" exit 1 fi log_warn "回滚到版本: $LATEST_BACKUP" $SSH_CMD bash <