#!/usr/bin/env bash # SHZ 后端测试环境部署脚本 # # 默认部署方式: # ./buildAndStart-test.sh # # 常用命令: # ./buildAndStart-test.sh status # ./buildAndStart-test.sh stop # ./buildAndStart-test.sh logs # # deploy/start 成功后会自动跟踪后端日志;按 Ctrl+C 退出日志跟踪,不会停止后端进程。 # # 可通过环境变量覆盖默认配置,例如: # PROFILE=test MAVEN_BIN=/opt/apache-maven-3.9.9/bin/mvn ./buildAndStart-test.sh set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" # 目录和应用配置。默认使用脚本所在目录,适配当前服务器的 # /data/code/shz-backend;BASE_PATH 仍可用于部署到其他目录。 BASE_PATH="${BASE_PATH:-$SCRIPT_DIR}" JAR_PATH="${JAR_PATH:-$BASE_PATH/ruoyi-admin/target/ruoyi-admin.jar}" LOG_PATH="${LOG_PATH:-$BASE_PATH/logs}" LOG_FILE="${LOG_FILE:-$LOG_PATH/backend.log}" PID_FILE="${PID_FILE:-$LOG_PATH/backend.pid}" LOCK_FILE="${LOCK_FILE:-$LOG_PATH/deploy.lock}" PROFILE="${PROFILE:-test}" GIT_REMOTE="${GIT_REMOTE:-origin}" GIT_BRANCH="${GIT_BRANCH:-main}" UPDATE_CODE="${UPDATE_CODE:-true}" MAVEN_THREADS="${MAVEN_THREADS:-4}" STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-120}" HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:9091/}" # 测试 profile 默认使用这些本机目录。目录不存在时由脚本创建。 RUOYI_PROFILE_DIR="${RUOYI_PROFILE_DIR:-/home/ruoyi/uploadPath}" FILE_UPLOAD_DIR="${FILE_UPLOAD_DIR:-/opt/service/file/data/file}" JVM_MEMORY="${JVM_MEMORY:--Xms2048M -Xmx2048M -XX:MaxDirectMemorySize=2048M}" JVM_DEBUG="${JVM_DEBUG:-}" JAVA_BIN="${JAVA_BIN:-}" MAVEN_BIN="${MAVEN_BIN:-}" __green() { printf '\033[1;32m%s\033[0m\n' "$1" } __red() { printf '\033[1;31m%s\033[0m\n' "$1" >&2 } __yellow() { printf '\033[1;33m%s\033[0m\n' "$1" } is_true() { case "${1:-}" in 1|true|TRUE|yes|YES|y|Y) return 0 ;; *) return 1 ;; esac } fail() { __red "$1" exit 1 } require_command() { command -v "$1" >/dev/null 2>&1 || fail "缺少必要命令: $1" } resolve_java() { if [[ -n "$JAVA_BIN" ]]; then [[ -x "$JAVA_BIN" ]] || fail "JAVA_BIN 不可执行: $JAVA_BIN" return fi if command -v java >/dev/null 2>&1; then JAVA_BIN="$(command -v java)" elif [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then JAVA_BIN="$JAVA_HOME/bin/java" else fail "未找到 Java。请安装 JDK 8+,或设置 JAVA_BIN/JAVA_HOME。" fi } resolve_maven() { if [[ -n "$MAVEN_BIN" ]]; then [[ -x "$MAVEN_BIN" ]] || fail "MAVEN_BIN 不可执行: $MAVEN_BIN" return fi if command -v mvn >/dev/null 2>&1; then MAVEN_BIN="$(command -v mvn)" return fi # 兼容服务器上未加入 PATH 的 Maven 安装。 local candidate for candidate in \ "$BASE_PATH/mvnw" \ "${MAVEN_HOME:-}/bin/mvn" \ /usr/local/apache-maven/bin/mvn \ /opt/apache-maven/bin/mvn \ /opt/apache-maven-3.9.9/bin/mvn \ /root/apache-maven/bin/mvn \ /root/apache-maven-3.9.9/bin/mvn; do if [[ -n "$candidate" && -x "$candidate" ]]; then MAVEN_BIN="$candidate" return fi done fail "未找到 Maven。请先安装 Maven 3.9+,或执行时设置 MAVEN_BIN=/实际路径/bin/mvn。" } prepare_environment() { [[ -d "$BASE_PATH" ]] || fail "项目目录不存在: $BASE_PATH" [[ -f "$BASE_PATH/pom.xml" ]] || fail "不是有效的 Maven 项目目录(缺少 pom.xml): $BASE_PATH" [[ -f "$BASE_PATH/ruoyi-admin/src/main/resources/application-${PROFILE}.yml" ]] \ || fail "未找到 profile 配置文件: $BASE_PATH/ruoyi-admin/src/main/resources/application-${PROFILE}.yml" mkdir -p "$LOG_PATH" "$RUOYI_PROFILE_DIR" "$FILE_UPLOAD_DIR" } find_app_pids() { local pid main_class pid_from_file cmdline declare -A seen=() # 优先读取本脚本记录的 PID,并确认该 PID 仍然是目标 JAR。 if [[ -s "$PID_FILE" ]]; then pid_from_file="$(<"$PID_FILE")" if [[ "$pid_from_file" =~ ^[0-9]+$ && -r "/proc/$pid_from_file/cmdline" ]]; then cmdline="$(tr '\0' ' ' < "/proc/$pid_from_file/cmdline")" if [[ "$cmdline" == *"-jar $JAR_PATH"* ]]; then printf '%s\n' "$pid_from_file" seen["$pid_from_file"]=1 fi fi fi # 兼容脚本升级前启动的实例。 if command -v jps >/dev/null 2>&1; then while read -r pid main_class; do if [[ "$main_class" == "$JAR_PATH" && -z "${seen[$pid]+x}" ]]; then printf '%s\n' "$pid" seen["$pid"]=1 fi done < <(jps -l 2>/dev/null || true) fi } pid_is_running() { local pid="$1" [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null } stop_app() { local pid second local -a pids=() mapfile -t pids < <(find_app_pids) if [[ "${#pids[@]}" -eq 0 ]]; then rm -f "$PID_FILE" __green "未找到运行中的测试环境进程" return 0 fi __green "停止进程: ${pids[*]}" for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null || true done for second in $(seq 1 30); do local remaining=() for pid in "${pids[@]}"; do pid_is_running "$pid" && remaining+=("$pid") done if [[ "${#remaining[@]}" -eq 0 ]]; then rm -f "$PID_FILE" __green "旧进程已停止" return 0 fi sleep 1 done __yellow "进程未在 30 秒内退出,执行强制终止: ${remaining[*]}" for pid in "${remaining[@]}"; do kill -9 "$pid" 2>/dev/null || true done rm -f "$PID_FILE" } update_code() { if ! is_true "$UPDATE_CODE"; then __yellow "跳过 Git 更新(UPDATE_CODE=$UPDATE_CODE)" return fi [[ -d "$BASE_PATH/.git" ]] || fail "项目不是 Git 仓库,无法更新代码: $BASE_PATH" __green "更新代码: $GIT_REMOTE/$GIT_BRANCH" git -C "$BASE_PATH" fetch "$GIT_REMOTE" "$GIT_BRANCH" git -C "$BASE_PATH" reset --hard "$GIT_REMOTE/$GIT_BRANCH" } build_app() { resolve_maven __green "开始构建(Maven: $MAVEN_BIN)" ( cd "$BASE_PATH" "$MAVEN_BIN" -B -T "$MAVEN_THREADS" clean install -DskipTests ) || fail "Maven 构建失败" [[ -s "$JAR_PATH" ]] || fail "构建完成但 JAR 文件不存在: $JAR_PATH" __green "构建完成: $JAR_PATH" } start_app() { local pid second local -a jvm_options=() read -r -a jvm_options <<< "$JVM_MEMORY $JVM_DEBUG" stop_app : > "$LOG_FILE" __green "启动测试环境(profile=$PROFILE)" ( cd "$BASE_PATH" nohup "$JAVA_BIN" "${jvm_options[@]}" -jar "$JAR_PATH" \ "--spring.profiles.active=$PROFILE" >> "$LOG_FILE" 2>&1 < /dev/null & echo "$!" > "$PID_FILE" ) pid="$(<"$PID_FILE")" for second in $(seq 1 "$STARTUP_TIMEOUT"); do if ! pid_is_running "$pid"; then __red "应用启动失败,进程已退出。最近日志:" tail -n 80 "$LOG_FILE" >&2 || true rm -f "$PID_FILE" return 1 fi if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then __green "应用启动成功: PID=$pid,健康检查通过($HEALTH_URL)" return 0 fi sleep 1 done __red "应用进程仍在运行,但健康检查超时(${STARTUP_TIMEOUT}s): $HEALTH_URL" __red "最近日志:" tail -n 80 "$LOG_FILE" >&2 || true return 1 } deploy() { prepare_environment exec 9>"$LOCK_FILE" flock -n 9 || fail "已有另一个部署任务正在执行: $LOCK_FILE" update_code build_app start_app __green "部署完成。日志文件: $LOG_FILE" logs } status() { prepare_environment local -a pids=() mapfile -t pids < <(find_app_pids) if [[ "${#pids[@]}" -eq 0 ]]; then __yellow "测试环境未运行" return 1 fi __green "测试环境运行中: PID=${pids[*]}" if command -v curl >/dev/null 2>&1 && curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then __green "健康检查通过: $HEALTH_URL" else __yellow "健康检查未通过: $HEALTH_URL" fi } logs() { prepare_environment touch "$LOG_FILE" tail -f "$LOG_FILE" } main() { # 非交互式部署也加载系统 PATH;不存在时不影响运行。 # 某些服务器的 /etc/profile 会引用未定义变量,不能在 nounset 下直接 source。 if [[ -r /etc/profile ]]; then set +u source /etc/profile || true set -u fi resolve_java case "${1:-deploy}" in deploy|start) deploy ;; stop) prepare_environment stop_app ;; status) status ;; logs|log) logs ;; *) __red "用法: $0 [deploy|start|stop|status|logs]" exit 2 ;; esac } main "$@"