#!/bin/sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) # ==================== 生产配置区:按服务器实际路径修改 ==================== # JDK 安装目录。留空时使用服务器已有 JAVA_HOME;仍为空时使用 PATH 中的 java。 BACKEND_JAVA_HOME="" # 后端 Jar 所在目录。生产目录结构为:脚本在外层,Jar 位于 backend/ruoyi-admin.jar。 APP_HOME="${SCRIPT_DIR}/backend" # 后端 Jar 文件名。 JAR_NAME="ruoyi-admin.jar" # Spring Profile。 SPRING_PROFILES_ACTIVE="uat" # JVM 参数。 JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" # 额外应用启动参数,例如:--server.port=8080 APP_ARGS="" # 停止进程等待秒数。 STOP_WAIT_SECONDS=30 # ==================== 以下为脚本逻辑,一般不需要修改 ==================== if [ "${APP_HOME#/}" = "${APP_HOME}" ]; then APP_HOME="${SCRIPT_DIR}/${APP_HOME}" fi APP_HOME="${APP_HOME%/}" JAR_PATH="${APP_HOME}/${JAR_NAME}" FRONTEND_DIR="${SCRIPT_DIR}/frontend" LOG_DIR="${APP_HOME}/logs" CONSOLE_LOG="${LOG_DIR}/backend-console.log" PID_FILE="${LOG_DIR}/backend-java.pid" APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}" JAVA_CMD="java" RELATIVE_JAR_PATH="" TIMESTAMP=$(date '+%Y%m%d%H%M%S') BACKUP_ROOT="${SCRIPT_DIR}/backups" BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}" WORK_ROOT="${SCRIPT_DIR}/.deploy-work" WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}" RELEASE_ZIP="" RELEASE_JAR="" RELEASE_DIST_ZIP="" FRONTEND_SOURCE_DIR="" case "${APP_HOME}" in "${SCRIPT_DIR}"/*) RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}" ;; esac timestamp() { date '+%Y-%m-%d %H:%M:%S' } log_info() { printf '[%s] %s\n' "$(timestamp)" "$1" } log_error() { printf '[%s] %s\n' "$(timestamp)" "$1" >&2 } usage() { cat <<'EOF' 用法: ./ccdi_function.sh [deploy|restart|stop] [上线压缩包路径] 命令: deploy 备份 backend/ 与 frontend/,部署上线包,然后重启后端并持续输出日志 restart 停止旧后端进程,启动 backend/ruoyi-admin.jar,并持续输出日志 stop 停止当前脚本目录对应的 backend/ruoyi-admin.jar 进程 上线包要求: 未传上线压缩包路径时,脚本会自动使用当前脚本目录下唯一的 .zip 文件。 上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。 dist.zip 解压后必须包含 dist/index.html。 生产目录示例: ccdi_function.sh backend/ frontend/ ccdi_YYYYMMDD.zip 说明: restart 和 deploy 启动成功后会持续输出 backend/logs/backend-console.log。 按 Ctrl+C 仅退出日志查看,不会停止已启动的后端进程。 EOF } require_command() { if ! command -v "$1" >/dev/null 2>&1; then log_error "缺少命令: $1" exit 1 fi } resolve_path() { input_path="$1" case "${input_path}" in /*) printf '%s\n' "${input_path}" ;; *) input_dir=$(dirname "${input_path}") input_base=$(basename "${input_path}") printf '%s/%s\n' "$(CDPATH= cd -- "${input_dir}" && pwd)" "${input_base}" ;; esac } resolve_java_cmd() { configured_java_home="${BACKEND_JAVA_HOME}" if [ -z "${configured_java_home}" ]; then configured_java_home="${JAVA_HOME:-}" fi if [ -n "${configured_java_home}" ]; then configured_java_home="${configured_java_home%/}" if [ ! -x "${configured_java_home}/bin/java" ]; then log_error "配置的 JAVA_HOME 无效,未找到可执行文件: ${configured_java_home}/bin/java" exit 1 fi JAVA_HOME="${configured_java_home}" export JAVA_HOME JAVA_CMD="${JAVA_HOME}/bin/java" else require_command "java" JAVA_CMD="java" fi log_info "使用 Java 命令: ${JAVA_CMD}" } get_process_table() { if ! ps -ef 2>/dev/null; then log_error "执行 ps -ef 失败,无法扫描旧进程" return 1 fi } is_backend_process_line() { process_line="$1" case "${process_line}" in *""*) return 1 ;; *" -jar ${JAR_PATH}"*) return 0 ;; *"${APP_MARKER}"*) return 0 ;; esac if [ -n "${RELATIVE_JAR_PATH}" ]; then case "${process_line}" in *" -jar ${RELATIVE_JAR_PATH}"*) return 0 ;; esac fi return 1 } is_managed_pid() { check_pid="$1" if [ -z "${check_pid}" ] || ! kill -0 "${check_pid}" 2>/dev/null; then return 1 fi if ! process_table=$(get_process_table); then return 1 fi while IFS= read -r process_line; do set -- ${process_line} line_pid="${2:-}" if [ "${line_pid}" = "${check_pid}" ] && is_backend_process_line "${process_line}"; then return 0 fi done </dev/null || true) if is_managed_pid "${pid_file_value}"; then all_pids="${all_pids} ${pid_file_value}" fi fi while IFS= read -r process_line; do set -- ${process_line} scan_pid="${2:-}" case "${scan_pid}" in ''|*[!0-9]*) continue ;; esac if is_backend_process_line "${process_line}"; then all_pids="${all_pids} ${scan_pid}" fi done </dev/null || true } start_backend() { resolve_java_cmd if [ ! -f "${JAR_PATH}" ]; then log_error "未找到后端 Jar: ${JAR_PATH}" exit 1 fi if ! running_pids=$(collect_pids); then log_error "扫描后端进程失败" exit 1 fi if [ -n "${running_pids}" ]; then log_error "检测到后端已在运行: ${running_pids}" exit 1 fi mkdir -p "${LOG_DIR}" printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}" profile_arg="" if [ -n "${SPRING_PROFILES_ACTIVE}" ]; then profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}" fi log_info "开始启动后端 Jar: ${JAR_PATH}" nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 & echo $! >"${PID_FILE}" sleep 3 starter_pid=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true) if [ -z "${starter_pid}" ] || ! kill -0 "${starter_pid}" 2>/dev/null; then log_error "启动命令未保持运行,请检查日志: ${CONSOLE_LOG}" exit 1 fi log_info "后端启动完成,PID: ${starter_pid}" } stop_backend() { if ! pids=$(collect_pids); then log_error "扫描后端进程失败" exit 1 fi if [ -z "${pids}" ]; then log_info "未发现运行中的后端进程" rm -f "${PID_FILE}" return 0 fi log_info "准备停止后端进程: ${pids}" for pid in ${pids}; do kill -TERM "${pid}" 2>/dev/null || true done elapsed=0 remaining_pids="${pids}" while [ -n "${remaining_pids}" ] && [ "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]; do sleep 1 elapsed=$((elapsed + 1)) remaining_pids="" for pid in ${pids}; do if kill -0 "${pid}" 2>/dev/null; then remaining_pids="${remaining_pids} ${pid}" fi done remaining_pids=$(printf '%s\n' "${remaining_pids}" | xargs 2>/dev/null || true) done if [ -n "${remaining_pids}" ]; then log_info "仍有进程未退出,执行强制停止: ${remaining_pids}" for pid in ${remaining_pids}; do kill -KILL "${pid}" 2>/dev/null || true done fi rm -f "${PID_FILE}" log_info "后端停止完成" } follow_logs() { require_command "tail" mkdir -p "${LOG_DIR}" touch "${CONSOLE_LOG}" log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程" tail -n 200 -F "${CONSOLE_LOG}" } restart_action() { stop_backend start_backend follow_logs } resolve_release_zip() { if [ -n "${RELEASE_ZIP}" ]; then RELEASE_ZIP=$(resolve_path "${RELEASE_ZIP}") else mkdir -p "${WORK_ROOT}" candidate_file="${WORK_ROOT}/zip-candidates-${TIMESTAMP}.txt" find "${SCRIPT_DIR}" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip' | sort >"${candidate_file}" candidate_count=$(wc -l <"${candidate_file}" | tr -d ' ') if [ "${candidate_count}" -eq 0 ]; then log_error "未在脚本目录找到上线压缩包,请传入压缩包路径" exit 1 fi if [ "${candidate_count}" -gt 1 ]; then log_error "脚本目录存在多个上线压缩包,请显式传入压缩包路径" cat "${candidate_file}" >&2 exit 1 fi RELEASE_ZIP=$(sed -n '1p' "${candidate_file}") rm -f "${candidate_file}" fi if [ ! -f "${RELEASE_ZIP}" ]; then log_error "上线压缩包不存在: ${RELEASE_ZIP}" exit 1 fi } assert_deploy_layout() { if [ ! -d "${APP_HOME}" ]; then log_error "未找到后端目录: ${APP_HOME}" exit 1 fi if [ ! -d "${FRONTEND_DIR}" ]; then log_error "未找到前端目录: ${FRONTEND_DIR}" exit 1 fi } backup_dir() { source_dir="$1" target_dir="$2" mkdir -p "${target_dir}" if [ -n "$(find "${source_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then cp -a "${source_dir}/." "${target_dir}/" log_info "已备份 ${source_dir} 到 ${target_dir}" else log_info "目录为空,已创建空备份目录: ${target_dir}" fi } backup_current_files() { mkdir -p "${BACKUP_DIR}" backup_dir "${APP_HOME}" "${BACKUP_DIR}/backend" backup_dir "${FRONTEND_DIR}" "${BACKUP_DIR}/frontend" } extract_release_package() { mkdir -p "${WORK_DIR}/release" "${WORK_DIR}/frontend" log_info "开始解压上线压缩包: ${RELEASE_ZIP}" unzip -q "${RELEASE_ZIP}" -d "${WORK_DIR}/release" RELEASE_JAR="${WORK_DIR}/release/${JAR_NAME}" RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip" if [ ! -f "${RELEASE_JAR}" ]; then log_error "上线压缩包根层缺少 ${JAR_NAME}" exit 1 fi if [ ! -f "${RELEASE_DIST_ZIP}" ]; then log_error "上线压缩包根层缺少 dist.zip" exit 1 fi unzip -q "${RELEASE_DIST_ZIP}" -d "${WORK_DIR}/frontend" FRONTEND_SOURCE_DIR="${WORK_DIR}/frontend/dist" if [ ! -f "${FRONTEND_SOURCE_DIR}/index.html" ]; then log_error "dist.zip 解压后未找到 dist/index.html" exit 1 fi } deploy_backend() { target_jar="${APP_HOME}/${JAR_NAME}" deploying_jar="${APP_HOME}/.${JAR_NAME}.deploying" cp "${RELEASE_JAR}" "${deploying_jar}" mv "${deploying_jar}" "${target_jar}" log_info "后端 Jar 已部署: ${target_jar}" } deploy_frontend() { find "${FRONTEND_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + cp -a "${FRONTEND_SOURCE_DIR}/." "${FRONTEND_DIR}/" log_info "前端文件已部署到: ${FRONTEND_DIR}" } cleanup_work_dir() { rm -rf "${WORK_DIR}" } deploy_action() { RELEASE_ZIP="${1:-}" require_command "find" require_command "unzip" resolve_release_zip assert_deploy_layout backup_current_files trap cleanup_work_dir 0 extract_release_package deploy_backend deploy_frontend cleanup_work_dir trap - 0 log_info "部署完成,备份目录: ${BACKUP_DIR}" log_info "开始重启后端并输出日志" restart_action } main() { action="${1:-help}" case "${action}" in deploy) shift if [ "$#" -gt 1 ]; then usage exit 1 fi deploy_action "${1:-}" ;; restart) shift if [ "$#" -ne 0 ]; then usage exit 1 fi restart_action ;; stop) shift if [ "$#" -ne 0 ]; then usage exit 1 fi stop_backend ;; -h|--help|help) usage ;; *) usage exit 1 ;; esac } main "$@"