部署脚本

This commit is contained in:
wkc
2026-04-28 17:27:24 +08:00
parent 592c58534a
commit cf91be838f
12 changed files with 2868 additions and 1 deletions

212
deploy/deploy-release-prod.sh Executable file
View File

@@ -0,0 +1,212 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BACKEND_DIR="${SCRIPT_DIR}/backend"
FRONTEND_DIR="${SCRIPT_DIR}/frontend"
START_SCRIPT="${SCRIPT_DIR}/start-java-backend-prod.sh"
BACKUP_ROOT="${SCRIPT_DIR}/backups"
WORK_ROOT="${SCRIPT_DIR}/.deploy-work"
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}"
RELEASE_ZIP="${1:-}"
RELEASE_JAR=""
RELEASE_DIST_ZIP=""
FRONTEND_SOURCE_DIR=""
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'
用法: ./deploy-release-prod.sh [上线压缩包路径]
目录要求:
deploy-release-prod.sh
start-java-backend-prod.sh
backend/
frontend/
ccdi_YYYYMMDD.zip
说明:
未传上线压缩包路径时,脚本会自动使用当前脚本目录下唯一的 .zip 文件。
上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。
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_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_layout() {
if [ ! -d "${BACKEND_DIR}" ]; then
log_error "未找到后端目录: ${BACKEND_DIR}"
exit 1
fi
if [ ! -d "${FRONTEND_DIR}" ]; then
log_error "未找到前端目录: ${FRONTEND_DIR}"
exit 1
fi
if [ ! -f "${START_SCRIPT}" ]; then
log_error "未找到后端启动脚本: ${START_SCRIPT}"
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 "${BACKEND_DIR}" "${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/ruoyi-admin.jar"
RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip"
if [ ! -f "${RELEASE_JAR}" ]; then
log_error "上线压缩包根层缺少 ruoyi-admin.jar"
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="${BACKEND_DIR}/ruoyi-admin.jar"
deploying_jar="${BACKEND_DIR}/.ruoyi-admin.jar.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}"
}
main() {
case "${1:-}" in
-h|--help|help)
usage
exit 0
;;
esac
if [ "$#" -gt 1 ]; then
usage
exit 1
fi
require_command "unzip"
require_command "find"
resolve_release_zip
assert_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 "开始重启后端并输出日志"
bash "${START_SCRIPT}" restart
}
main "$@"

310
deploy/start-java-backend-prod.sh Executable file
View File

@@ -0,0 +1,310 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[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}" != /* ]]; then
APP_HOME="${SCRIPT_DIR}/${APP_HOME}"
fi
JAR_PATH="${APP_HOME}/${JAR_NAME}"
RELATIVE_JAR_PATH=""
if [[ "${APP_HOME}" == "${SCRIPT_DIR}/"* ]]; then
RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}"
fi
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"
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'
用法: ./start-java-backend-prod.sh [start|stop|restart|status|logs]
默认动作:
start 先关闭旧后端进程,再启动生产后端 Jar启动成功后持续输出控制台日志
常用配置:
配置统一写在脚本顶部“生产配置区”,包括 BACKEND_JAVA_HOME、APP_HOME、SPRING_PROFILES_ACTIVE、JAVA_OPTS。
示例:
./start-java-backend-prod.sh restart
EOF
}
ensure_command() {
local command_name="$1"
if ! command -v "${command_name}" >/dev/null 2>&1; then
log_error "缺少命令: ${command_name}"
exit 1
fi
}
resolve_java_cmd() {
local 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
export JAVA_HOME="${configured_java_home}"
JAVA_CMD="${JAVA_HOME}/bin/java"
else
ensure_command "java"
JAVA_CMD="java"
fi
log_info "使用 Java 命令: ${JAVA_CMD}"
}
get_process_table() {
local process_table
if ! process_table="$(ps -ef 2>/dev/null)"; then
log_error "执行 ps -ef 失败,无法扫描旧进程"
return 1
fi
printf '%s\n' "${process_table}"
}
is_managed_pid() {
local pid="$1"
if [[ -z "${pid}" ]] || ! kill -0 "${pid}" 2>/dev/null; then
return 1
fi
local process_table
if ! process_table="$(get_process_table)"; then
return 1
fi
local line
while IFS= read -r line; do
set -- ${line}
if [[ "${2:-}" == "${pid}" ]] && is_backend_process_line "${line}"; then
return 0
fi
done <<<"${process_table}"
return 1
}
is_backend_process_line() {
local line="$1"
[[ "${line}" != *"<defunct>"* ]] || return 1
[[ "${line}" == *" -jar ${JAR_PATH}"* ]] && return 0
[[ -n "${RELATIVE_JAR_PATH}" && "${line}" == *" -jar ${RELATIVE_JAR_PATH}"* ]]
}
collect_pids() {
local all_pids=""
local pid
local process_table
if ! process_table="$(get_process_table)"; then
return 1
fi
if [[ -f "${PID_FILE}" ]]; then
pid="$(cat "${PID_FILE}" 2>/dev/null || true)"
if is_managed_pid "${pid}"; then
all_pids="${all_pids} ${pid}"
fi
fi
local line
while IFS= read -r line; do
set -- ${line}
pid="${2:-}"
if [[ "${pid}" =~ ^[0-9]+$ ]] && is_backend_process_line "${line}"; then
all_pids="${all_pids} ${pid}"
fi
done <<<"${process_table}"
local unique_pids=""
for pid in ${all_pids}; do
case " ${unique_pids} " in
*" ${pid} "*) ;;
*) unique_pids="${unique_pids} ${pid}" ;;
esac
done
xargs <<<"${unique_pids}" 2>/dev/null || true
}
start_backend() {
resolve_java_cmd
if [[ ! -f "${JAR_PATH}" ]]; then
log_error "未找到后端 Jar: ${JAR_PATH}"
exit 1
fi
local running_pids
running_pids="$(collect_pids)"
if [[ -n "${running_pids}" ]]; then
log_error "检测到后端已在运行: ${running_pids}"
exit 1
fi
mkdir -p "${LOG_DIR}"
printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}"
local 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
local starter_pid
starter_pid="$(cat "${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() {
local pids
pids="$(collect_pids)"
if [[ -z "${pids}" ]]; then
log_info "未发现运行中的后端进程"
rm -f "${PID_FILE}"
return 0
fi
log_info "准备停止后端进程: ${pids}"
local pid
for pid in ${pids}; do
kill -TERM "${pid}" 2>/dev/null || true
done
local elapsed=0
local 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="$(xargs <<<"${remaining_pids}" 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 "后端停止完成"
}
status_backend() {
local pids
pids="$(collect_pids)"
if [[ -n "${pids}" ]]; then
log_info "后端正在运行,进程: ${pids}"
return 0
fi
log_info "后端未运行"
}
follow_logs() {
mkdir -p "${LOG_DIR}"
touch "${CONSOLE_LOG}"
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程"
tail -n 200 -F "${CONSOLE_LOG}"
}
start_action() {
stop_backend
start_backend
follow_logs
}
main() {
local action="${1:-start}"
case "${action}" in
start)
start_action
;;
stop)
stop_backend
;;
restart)
start_action
;;
status)
status_backend
;;
logs)
follow_logs
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"