#!/bin/bash # ============================================================ # 充电项目一键部署脚本 (2核8G 服务器) # 用法: ./deploy.sh [admin|miniapp|all] [--skip-frontend] [--no-stop] # --no-stop 构建前不停止服务(构建期间服务继续运行,但启动新进程前需手动停旧进程) # --skip-frontend 跳过前端构建(仅后端代码变更时使用) # 调试: bash -x ./deploy.sh # ============================================================ APP_DIR="/data/app" CODE_DIR="/data/code/charge-java" BACKUP_DIR="/data/backup" LOG_DIR="/data/logs" HISTORY_FILE="/data/history" CERT_DIR="/data/pem" # 构建工具路径(对齐 Jenkins 环境) NPM="${NPM:-/usr/local/bin/npm}" MVN="${MVN:-mvn}" # 将 npm 全局 bin 加入 PATH(确保 uni 等全局包可被找到) NPM_PREFIX=$($NPM config get prefix 2>/dev/null || echo "/usr/local") export PATH="$NPM_PREFIX/bin:$PATH" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "[$(date '+%H:%M:%S')] $1"; } info() { log "${GREEN}INFO${NC} $1"; } warn() { log "${YELLOW}WARN${NC} $1"; } error() { log "${RED}ERROR${NC} $1"; } mkdir -p "$LOG_DIR" "$BACKUP_DIR" # ============================================================ # 资源限制配置 (2核8G 服务器) # ============================================================ # ---- 构建阶段 ---- NODE_MAX_MEM="${NODE_MAX_MEM:-2048}" # Node 最大 old-space MB MAVEN_XMX="${MAVEN_XMX:-1024}" # Maven JVM 最大堆 MB MAVEN_XMS="${MAVEN_XMS:-256}" # Maven JVM 初始堆 MB MAVEN_THREADS="${MAVEN_THREADS:-2}" # Maven 编译线程数 # ---- 运行时 JVM (admin 模块: 管理后台 + 定时任务 + POI Excel) ---- ADMIN_XMX="${ADMIN_XMX:-1024}" # 最大堆 MB ADMIN_XMS="${ADMIN_XMS:-256}" # 初始堆 MB ADMIN_META="${ADMIN_META:-256}" # 元空间上限 MB ADMIN_CODE_CACHE="${ADMIN_CODE_CACHE:-128}" # JIT 代码缓存 MB ADMIN_XSS="${ADMIN_XSS:-256}" # 线程栈 KB # ---- 运行时 JVM (miniapp 模块: 小程序API) ---- MINIAPP_XMX="${MINIAPP_XMX:-768}" MINIAPP_XMS="${MINIAPP_XMS:-256}" MINIAPP_META="${MINIAPP_META:-256}" MINIAPP_CODE_CACHE="${MINIAPP_CODE_CACHE:-128}" MINIAPP_XSS="${MINIAPP_XSS:-256}" # 低优先级包装器(避免构建抢占服务 CPU) low_prio() { nice -n 19 ionice -c 2 -n 7 "$@" } # ---- 参数解析 ---- TARGET="${1:-all}" SKIP_FRONTEND=false NO_STOP=false for arg in "$@"; do [ "$arg" = "--skip-frontend" ] && SKIP_FRONTEND=true [ "$arg" = "--no-stop" ] && NO_STOP=true done info "开始部署 充电项目 (目标=$TARGET, 零停机=$NO_STOP)" info "Maven:$MVN | NPM:$NPM | Node:${NODE_MAX_MEM}M | Admin-JVM:${ADMIN_XMX}M | Miniapp-JVM:${MINIAPP_XMX}M" # ---- 拉取代码 ---- info "拉取最新代码..." cd "$CODE_DIR" || { error "无法进入 $CODE_DIR"; exit 1; } low_prio git pull origin master || { error "git pull 失败"; exit 1; } # ============================================================ # 阶段 1: 停止服务 (释放内存给构建) # ============================================================ stop_service() { local module=$1 local pid=$(ps -ef | grep java | grep "$module" | grep -v grep | awk '{print $2}') if [ -z "$pid" ]; then info "$module 未在运行" return 0 fi for p in $pid; do info "终止 $module (pid=$p)..." kill "$p" 2>/dev/null for i in $(seq 1 10); do if ! kill -0 "$p" 2>/dev/null; then info "进程 $p 已退出" break fi sleep 1 done if kill -0 "$p" 2>/dev/null; then warn "强制终止 $p" kill -9 "$p" 2>/dev/null sleep 1 fi done } if [ "$NO_STOP" = false ]; then case "$TARGET" in admin) stop_service "admin-0.0.1-SNAPSHOT" ;; miniapp) stop_service "miniapp-0.0.1-SNAPSHOT" ;; all) stop_service "admin-0.0.1-SNAPSHOT" stop_service "miniapp-0.0.1-SNAPSHOT" ;; esac sleep 3 fi # ============================================================ # 阶段 2: 安装公共模块到本地仓库 (对齐 Jenkins mvn install 流程) # ============================================================ info "安装公共模块到本地仓库..." ( cd "$CODE_DIR" low_prio $MVN install -DskipTests -Dmaven.test.skip=true ) || { error "Maven install 失败"; exit 1; } # ============================================================ # 阶段 3: 前端构建 # ============================================================ if [ "$SKIP_FRONTEND" = false ] && [ "$TARGET" != "miniapp" ]; then # ---- 管理后台前端 (admin-web) ---- info "构建前端 admin-web ..." # 清理旧前端产物,避免残留文件打入 jar rm -rf "$CODE_DIR/admin/src/main/resources/static/" mkdir -p "$CODE_DIR/admin/src/main/resources/static/" ( cd "$CODE_DIR/admin-web" || exit 1 export NODE_OPTIONS="--max-old-space-size=$NODE_MAX_MEM" $NPM install low_prio $NPM run build ) || { error "admin-web 构建失败"; exit 1; } info "admin-web 构建完成" # ---- 小程序前端 (charge-front, uni-app) ---- info "构建前端 charge-front ..." ( # uni-app CLI 依赖,首次部署需全局安装 if ! command -v uni >/dev/null 2>&1; then info "安装 @dcloudio/uvm ..." $NPM install -g @dcloudio/uvm fi cd "$CODE_DIR/charge-front" || exit 1 export NODE_OPTIONS="--max-old-space-size=$NODE_MAX_MEM" $NPM install low_prio $NPM run build ) || { error "charge-front 构建失败"; exit 1; } info "charge-front 构建完成" fi # ============================================================ # 阶段 4: Maven 打包 # ============================================================ export MAVEN_OPTS="-Xmx${MAVEN_XMX}m -Xms${MAVEN_XMS}m -XX:+UseG1GC" if [ "$TARGET" = "miniapp" ]; then info "Maven 打包 (仅 miniapp)..." ( cd "$CODE_DIR/miniapp" low_prio $MVN clean package -DskipTests -Dmaven.test.skip=true ) || { error "Maven 打包失败"; exit 1; } elif [ "$TARGET" = "admin" ]; then info "Maven 打包 (仅 admin)..." ( cd "$CODE_DIR/admin" low_prio $MVN clean package -DskipTests -Dmaven.test.skip=true ) || { error "Maven 打包失败"; exit 1; } else info "Maven 打包 (全量, 线程=${MAVEN_THREADS})..." ( cd "$CODE_DIR" low_prio $MVN clean package -DskipTests \ -T "${MAVEN_THREADS}" \ -Dmaven.test.skip=true \ -Dmaven.compiler.fork=false ) || { error "Maven 打包失败"; exit 1; } fi sleep 2 # ============================================================ # 阶段 5: 备份 & 部署 & 启动 # ============================================================ backup_and_copy() { local module=$1 local jar_src="$CODE_DIR/$module/target/${module}-0.0.1-SNAPSHOT.jar" local jar_dst="$APP_DIR/${module}-0.0.1-SNAPSHOT.jar" if [ ! -f "$jar_src" ]; then error "未找到 jar 文件: $jar_src" exit 1 fi if [ -f "$jar_dst" ]; then cp "$jar_dst" "$BACKUP_DIR/${module}-0.0.1-SNAPSHOT.jar.$(date +%Y%m%d_%H%M%S)" info "已备份 $module (保留最近 5 个)" ls -t "$BACKUP_DIR/${module}"-*.jar 2>/dev/null | tail -n +6 | xargs -r rm -f fi cp "$jar_src" "$jar_dst" info "$module jar 已复制到 $APP_DIR" } # 构建模块专用的 JVM 参数 build_jvm_opts() { local module=$1 if [ "$module" = "admin" ]; then echo "-Xmx${ADMIN_XMX}m -Xms${ADMIN_XMS}m -Xss${ADMIN_XSS}k \ -XX:MaxMetaspaceSize=${ADMIN_META}m -XX:ReservedCodeCacheSize=${ADMIN_CODE_CACHE}m \ -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/ \ -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom" else echo "-Xmx${MINIAPP_XMX}m -Xms${MINIAPP_XMS}m -Xss${MINIAPP_XSS}k \ -XX:MaxMetaspaceSize=${MINIAPP_META}m -XX:ReservedCodeCacheSize=${MINIAPP_CODE_CACHE}m \ -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/ \ -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom" fi } start_service() { local module=$1 local jar_file="${module}-0.0.1-SNAPSHOT.jar" local start_log="$LOG_DIR/${module}.log" local jvm_opts=$(build_jvm_opts "$module") info "启动 $module (JVM: $(echo $jvm_opts | tr -s ' '))..." # admin 模块需要指定微信支付证书路径 local cert_arg="" if [ "$module" = "admin" ]; then cert_arg="-Dwechat.payment.certPath=${CERT_DIR}/apiclient_cert.pem -Dwechat.payment.keyPath=${CERT_DIR}/apiclient_key.pem" fi cd "$APP_DIR" nohup java $jvm_opts -jar -Dspring.profiles.active=prod $cert_arg "$jar_file" > "$start_log" 2>&1 & sleep 5 local new_pid=$(ps -ef | grep java | grep "$module" | grep -v grep | awk '{print $2}') if [ -n "$new_pid" ]; then info "$module 启动成功 (pid=$new_pid)" echo "$(date) $module restarted" >> "$HISTORY_FILE" else error "$module 可能启动失败,查看日志:" echo "--- $start_log ---" tail -30 "$start_log" echo "--- end ---" exit 1 fi } case "$TARGET" in admin) backup_and_copy "admin" start_service "admin" ;; miniapp) backup_and_copy "miniapp" start_service "miniapp" ;; all) backup_and_copy "admin" backup_and_copy "miniapp" start_service "miniapp" start_service "admin" ;; *) error "未知模块: $TARGET (可选: admin | miniapp | all)" exit 1 ;; esac echo "========================================" info "充电项目 部署完成" echo "========================================"