Zsh 脚本 + VS Code 任务:NestJS + Vue3 一键部署到 1Panel

7 阅读9分钟

预览图

image.png

image.png

介绍

每次改完代码都要手动部署,真的挺折磨人的。

打包、传文件、登服务器、解压、重启服务... 步骤多不说,还容易手滑传错文件。

后来干脆写了个 Zsh 脚本,配合 VS Code 的任务系统,现在部署变得特别简单:

  • Command + Shift + P 呼出命令面板
  • 选个环境(dev/prod),选个目标(后端/前端/全部)
  • 回车,等着就行 脚本会自动完成打包、调用 1Panel API 上传、启停服务这一系列操作。还支持演练模式,先跑一遍看看要干啥,心里有个底。

这套方案最爽的是 简单配置就能用。

下面聊聊具体是怎么实现的,希望能给同样有部署烦恼的同学一些参考。


文件结构

image.png

代码

scripts/.onepanel-api-key

# 将这一行替换成你的 1Panel API 密钥
your-1panel-api-key

deploy-youlai-nest-1panel.dev.conf

# 1Panel 部署配置 - 开发/测试环境
# 复制此文件为 deploy-youlai-nest-1panel.dev.conf 并修改配置

# ==================== 1Panel 连接配置 ====================
ONEPANEL_BASE_URL="http://your-server-ip:port"    # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="./.onepanel-api-key"       # API 密钥文件路径(相对 scripts 目录)

# ==================== 部署目标 ====================
DEPLOY_TARGET="${DEPLOY_TARGET:-all}"             # 部署目标: nest | admin | lab | all

# ==================== 后端 Nest 配置 ====================
NEST_ENABLED=1                                    # 是否部署后端: 1=是, 0=否
NEST_PROJECT_DIR="/path/to/youlai-nest-master"    # 后端项目本地目录
NEST_TARGET_DIR="/opt/1panel/www/sites/your-site" # 后端部署目标目录(服务器上)
NEST_RUNTIME_NAME="your-runtime-name"             # 1Panel 运行环境名称

# ==================== 前端 Admin 配置 ====================
ADMIN_ENABLED=1                                   # 是否部署管理后台: 1=是, 0=否
ADMIN_PROJECT_DIR="/path/to/vue3-element-admin-master"  # 管理后台项目本地目录
ADMIN_TARGET_DIR="/opt/1panel/www/sites/your-admin-site" # 管理后台部署目标目录
ADMIN_DIST_PATH="dist"                            # 管理后台打包输出目录

# ==================== 前端 Lab 配置 ====================
LAB_ENABLED=1                                     # 是否部署用户端: 1=是, 0=否
LAB_PROJECT_DIR="/path/to/lab-store-web"          # 用户端项目本地目录
LAB_TARGET_DIR="/opt/1panel/www/sites/your-lab-site"    # 用户端部署目标目录
LAB_DIST_PATH="dist"                              # 用户端打包输出目录

# ==================== 部署选项 ====================
DRY_RUN="${DRY_RUN:-0}"                           # 演练模式: 1=只显示不执行, 0=真正执行
START_AFTER_DEPLOY="${START_AFTER_DEPLOY:-1}"     # 部署后启动服务: 1=启动, 0=不启动

# ==================== 压缩包保存选项 ====================
KEEP_ARCHIVES="${KEEP_ARCHIVES:-0}"               # 是否保留压缩包: 1=保留, 0=删除
ARCHIVE_DIR="${ARCHIVE_DIR:-./dist-archives}"     # 压缩包保存目录(相对项目根目录)

deploy-youlai-nest-1panel.prod.conf

一样的只是文件名称区别

deploy-youlai-nest-1panel.sh

#!/usr/bin/env zsh

# 用法:
# 1. 复制 scripts/deploy-youlai-nest-1panel.dev.conf.example 为 scripts/deploy-youlai-nest-1panel.dev.conf
# 2. 按需修改配置项
# 3. 复制 scripts/.onepanel-api-key.example 为 scripts/.onepanel-api-key,并填入 API 密钥
# 4. 执行:
#    DEPLOY_ENV=dev ./scripts/deploy-youlai-nest-1panel.sh    # 开发/测试环境
#    DEPLOY_ENV=prod ./scripts/deploy-youlai-nest-1panel.sh  # 正式环境
#
# 可选:
#    DEPLOY_ENV=dev DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh    # 仅部署后端 (dev)
#    DEPLOY_ENV=prod DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh  # 仅部署后端 (prod)
#    DEPLOY_ENV=dev DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh  # 仅部署管理后台 (dev)
#    DEPLOY_ENV=prod DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh # 仅部署管理后台 (prod)
#    DEPLOY_ENV=dev DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh    # 仅部署用户端 (dev)
#    DEPLOY_ENV=prod DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh   # 仅部署用户端 (prod)
#    DEPLOY_ENV=dev DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh            # 演练模式 (dev)
#    DEPLOY_ENV=prod DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh           # 演练模式 (prod)

# 启用严格模式:遇到错误立即退出、未定义变量报错、管道错误检测
set -euo pipefail

# 获取脚本所在目录的绝对路径
SCRIPT_DIR="${0:A:h}"
# 获取项目根目录(脚本目录的父目录)
REPO_ROOT="${SCRIPT_DIR:h}"

# 部署环境,默认 dev,可通过环境变量覆盖
DEPLOY_ENV="${DEPLOY_ENV:-dev}"
# 配置文件路径,根据环境自动选择
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/deploy-youlai-nest-1panel.${DEPLOY_ENV}.conf}"
# 配置文件所在目录
CONFIG_DIR="${CONFIG_FILE:A:h}"

# 如果配置文件存在,则加载配置
if [[ -f "$CONFIG_FILE" ]]; then
  source "$CONFIG_FILE"
fi

# ==================== 1Panel 连接配置 ====================
ONEPANEL_BASE_URL="${ONEPANEL_BASE_URL:-}"              # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="${ONEPANEL_API_KEY_FILE:-$SCRIPT_DIR/.onepanel-api-key}"  # API 密钥文件路径
ONEPANEL_API_KEY="${ONEPANEL_API_KEY:-}"                # API 密钥(从文件读取)

# ==================== 部署选项 ====================
DRY_RUN="${DRY_RUN:-0}"                                 # 演练模式:1=只显示不执行,0=真正执行
START_AFTER_DEPLOY="${START_AFTER_DEPLOY:-1}"           # 部署后是否启动服务:1=启动,0=不启动
DEPLOY_TARGET="${DEPLOY_TARGET:-all}"                   # 部署目标:nest|admin|lab|all

# ==================== 后端 Nest 项目配置 ====================
NEST_ENABLED="${NEST_ENABLED:-0}"                       # 是否启用后端部署
NEST_PROJECT_DIR="${NEST_PROJECT_DIR:-}"                # 后端项目本地目录
NEST_TARGET_DIR="${NEST_TARGET_DIR:-}"                  # 后端部署目标目录(服务器上)
NEST_RUNTIME_NAME="${NEST_RUNTIME_NAME:-}"              # 1Panel 运行环境名称

# ==================== 前端 Admin 项目配置 ====================
ADMIN_ENABLED="${ADMIN_ENABLED:-0}"                     # 是否启用管理后台部署
ADMIN_PROJECT_DIR="${ADMIN_PROJECT_DIR:-}"              # 管理后台项目本地目录
ADMIN_TARGET_DIR="${ADMIN_TARGET_DIR:-}"                # 管理后台部署目标目录
ADMIN_DIST_PATH="${ADMIN_DIST_PATH:-dist}"              # 管理后台打包输出目录

# ==================== 前端 Lab 项目配置 ====================
LAB_ENABLED="${LAB_ENABLED:-0}"                         # 是否启用用户端部署
LAB_PROJECT_DIR="${LAB_PROJECT_DIR:-}"                  # 用户端项目本地目录
LAB_TARGET_DIR="${LAB_TARGET_DIR:-}"                    # 用户端部署目标目录
LAB_DIST_PATH="${LAB_DIST_PATH:-dist}"                  # 用户端打包输出目录

# ==================== 压缩包保存配置 ====================
KEEP_ARCHIVES="${KEEP_ARCHIVES:-0}"                     # 是否保留压缩包:1=保留,0=删除
ARCHIVE_DIR="${ARCHIVE_DIR:-$REPO_ROOT/dist-archives}"  # 压缩包保存目录

# ==================== 临时文件和状态变量 ====================
TMP_DIR="$(mktemp -d)"                                  # 创建临时目录用于存放压缩包
RUNTIME_ID=""                                           # 1Panel 运行环境 ID
RUNTIME_STOPPED=0                                       # 标记运行环境是否已停止

# 清理函数:脚本退出时自动执行
cleanup() {
  local exit_code=$?
  # 如果脚本异常退出且已停止运行环境,则尝试恢复启动
  if [[ "$exit_code" -ne 0 && "$RUNTIME_STOPPED" == "1" && "$START_AFTER_DEPLOY" == "1" && -n "$RUNTIME_ID" ]]; then
    print -u2 -- "脚本异常退出,尝试恢复启动运行环境 ${NEST_RUNTIME_NAME} ..."
    if runtime_operate "up" >/dev/null 2>&1; then
      print -u2 -- "已尝试重新启动 ${NEST_RUNTIME_NAME}。"
    else
      print -u2 -- "自动恢复启动失败,请在 1Panel 中手动检查 ${NEST_RUNTIME_NAME}。"
    fi
  fi
  # 如果不保留压缩包,删除临时目录
  if [[ "$KEEP_ARCHIVES" != "1" ]]; then
    rm -rf "$TMP_DIR"
  fi
  exit "$exit_code"
}
# 注册清理函数,在脚本退出时自动调用
trap cleanup EXIT

# 检查依赖命令是否存在
require_cmd() {
  local cmd="$1"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    print -u2 -- "缺少依赖命令: $cmd"
    exit 1
  fi
}

# 检查必需的命令
require_cmd curl    # HTTP 请求
require_cmd jq      # JSON 处理
require_cmd pnpm    # 包管理器
require_cmd tar     # 压缩解压
require_cmd date    # 日期时间
require_cmd awk     # 文本处理

# 如果密钥文件路径是相对路径,转换为绝对路径
if [[ "$ONEPANEL_API_KEY_FILE" != /* ]]; then
  ONEPANEL_API_KEY_FILE="${CONFIG_DIR}/${ONEPANEL_API_KEY_FILE}"
fi

# 验证必要配置
if [[ -z "$ONEPANEL_BASE_URL" ]]; then
  print -u2 -- "请在配置文件中设置 ONEPANEL_BASE_URL: $CONFIG_FILE"
  exit 1
fi

# 读取 API 密钥
if [[ -z "$ONEPANEL_API_KEY" ]]; then
  if [[ ! -f "$ONEPANEL_API_KEY_FILE" ]]; then
    print -u2 -- "未找到 API 密钥文件: $ONEPANEL_API_KEY_FILE"
    exit 1
  fi
  # 读取密钥文件内容,去除换行符
  ONEPANEL_API_KEY="$(tr -d '\r\n' < "$ONEPANEL_API_KEY_FILE")"
fi

if [[ -z "$ONEPANEL_API_KEY" ]]; then
  print -u2 -- "API 密钥为空,请检查: $ONEPANEL_API_KEY_FILE"
  exit 1
fi

# ==================== 1Panel API 工具函数 ====================

# MD5 加密函数(跨平台兼容 macOS 和 Linux)
md5_hex() {
  local raw="$1"
  if command -v md5 >/dev/null 2>&1; then
    md5 -q -s "$raw"
  else
    printf '%s' "$raw" | md5sum | awk '{print $1}'
  fi
}

# 生成 1Panel API 请求令牌
# 格式:md5("1panel" + API_KEY + 时间戳)
make_panel_token() {
  local ts="$1"
  md5_hex "1panel${ONEPANEL_API_KEY}${ts}"
}

# 发送 JSON 请求到 1Panel API
# 参数:method(GET/POST), endpoint, payload(JSON字符串)
panel_request_json() {
  local method="$1"
  local endpoint="$2"
  local payload="${3:-}"
  local ts token url response code

  # 生成时间戳和令牌
  ts="$(date +%s)"
  token="$(make_panel_token "$ts")"
  url="${ONEPANEL_BASE_URL%/}/api/v2/${endpoint}"

  # 发送请求
  if [[ "$method" == "GET" ]]; then
    response="$(
      curl --compressed -sS -X GET "$url" \
        -H "1Panel-Token: $token" \
        -H "1Panel-Timestamp: $ts"
    )"
  else
    response="$(
      curl --compressed -sS -X "$method" "$url" \
        -H "1Panel-Token: $token" \
        -H "1Panel-Timestamp: $ts" \
        -H "Content-Type: application/json" \
        --data "$payload"
    )"
  fi

  # 验证响应是否为 JSON
  if ! jq empty >/dev/null 2>&1 <<<"$response"; then
    print -u2 -- "1Panel API 返回了非 JSON 响应:"
    print -u2 -- "$response"
    return 1
  fi

  # 检查响应状态码
  code="$(jq -r '.code // empty' <<<"$response")"
  if [[ "$code" != "200" && "$code" != "0" ]]; then
    print -u2 -- "1Panel API 调用失败: ${endpoint}"
    print -u2 -- "$(jq -r '.message // "unknown error"' <<<"$response")"
    return 1
  fi

  print -r -- "$response"
}

# 上传文件到 1Panel
# 参数:remote_dir(远程目录), local_file(本地文件路径)
panel_upload_file() {
  local remote_dir="$1"
  local local_file="$2"
  local ts token url response code

  ts="$(date +%s)"
  token="$(make_panel_token "$ts")"
  url="${ONEPANEL_BASE_URL%/}/api/v2/files/upload"

  # 使用 multipart/form-data 上传文件
  response="$(
    curl --compressed -sS -X POST "$url" \
      -H "1Panel-Token: $token" \
      -H "1Panel-Timestamp: $ts" \
      -F "path=${remote_dir}" \
      -F "overwrite=true" \
      -F "file=@${local_file}"
  )"

  if ! jq empty >/dev/null 2>&1 <<<"$response"; then
    print -u2 -- "1Panel 上传接口返回了非 JSON 响应:"
    print -u2 -- "$response"
    return 1
  fi

  code="$(jq -r '.code // empty' <<<"$response")"
  if [[ "$code" != "200" && "$code" != "0" ]]; then
    print -u2 -- "文件上传失败: $local_file"
    print -u2 -- "$(jq -r '.message // "unknown error"' <<<"$response")"
    return 1
  fi
}

# 检查远程路径是否存在
panel_check_exists() {
  local target_path="$1"
  local payload response
  payload="$(jq -nc --arg path "$target_path" '{path:$path,withInit:false}')"
  response="$(panel_request_json POST "files/check" "$payload")"
  [[ "$(jq -r '.data' <<<"$response")" == "true" ]]
}

# 删除远程路径(文件或目录)
# 参数:target_path, is_dir(true/false)
panel_delete_path() {
  local target_path="$1"
  local is_dir="$2"
  local payload

  payload="$(jq -nc \
    --arg path "$target_path" \
    --argjson isDir "$is_dir" \
    '{path:$path,isDir:$isDir,forceDelete:true}')"

  panel_request_json POST "files/del" "$payload" >/dev/null
}

# 解压远程压缩包
# 参数:archive_path(压缩包路径), dst_dir(目标目录)
panel_decompress_archive() {
  local archive_path="$1"
  local dst_dir="$2"
  local payload

  payload="$(jq -nc \
    --arg path "$archive_path" \
    --arg dst "$dst_dir" \
    --arg type "tar.gz" \
    '{path:$path,dst:$dst,type:$type,secret:""}')"

  panel_request_json POST "files/decompress" "$payload" >/dev/null
}

# 修改远程文件/目录的所有者和组
# 参数:target_path, user, group, sub(true/false是否递归)
panel_chown_path() {
  local target_path="$1"
  local user="$2"
  local group="$3"
  local sub="${4:-true}"
  local payload

  payload="$(jq -nc \
    --arg path "$target_path" \
    --arg user "$user" \
    --arg group "$group" \
    --argjson sub "$sub" \
    '{path:$path,user:$user,group:$group,sub:$sub}')"

  panel_request_json POST "files/owner" "$payload" >/dev/null
}

# 查找运行环境 ID
find_runtime_id() {
  local payload response
  # 搜索运行环境列表
  payload="$(jq -nc \
    --arg name "$NEST_RUNTIME_NAME" \
    '{page:1,pageSize:100,name:$name,status:"",type:""}')"

  response="$(panel_request_json POST "runtimes/search" "$payload")"
  # 从响应中提取匹配的运行环境 ID
  jq -r \
    --arg name "$NEST_RUNTIME_NAME" \
    '.data.items[]? | select(.name == $name) | .id' <<<"$response" | head -n 1
}

# 操作运行环境(启动/停止)
# 参数:action(up/down)
runtime_operate() {
  local action="$1"
  local payload

  payload="$(jq -nc --arg operate "$action" --argjson id "$RUNTIME_ID" '{operate:$operate,ID:$id}')"
  panel_request_json POST "runtimes/operate" "$payload" >/dev/null
}

# ==================== 部署辅助函数 ====================

# 验证项目目录是否存在
validate_project_dir() {
  local project_dir="$1"
  local name="$2"
  if [[ ! -d "$project_dir" ]]; then
    print -u2 -- "${name} 项目目录不存在: $project_dir"
    exit 1
  fi
}

# 验证远程目标目录是否存在
validate_remote_target() {
  local target_path="$1"
  local name="$2"
  if ! panel_check_exists "$target_path"; then
    print -u2 -- "${name} 远端目标目录不存在: $target_path"
    exit 1
  fi
}

# 构建项目(执行 pnpm build)
build_project() {
  local project_dir="$1"
  local name="$2"

  print -- "开始打包 ${name} ..."
  (
    cd "$project_dir"
    pnpm build
  )
}

# 部署前端项目
# 参数:project_dir, target_dir, dist_path, name
deploy_frontend() {
  local project_dir="$1"
  local target_dir="$2"
  local dist_path="$3"
  local name="$4"
  local full_dist_path="${project_dir}/${dist_path}"

  # 检查 dist 目录是否存在
  if [[ ! -d "$full_dist_path" ]]; then
    print -u2 -- "${name} dist 目录不存在: $full_dist_path"
    return 1
  fi

  # 创建压缩包保存目录
  if [[ "$KEEP_ARCHIVES" == "1" && ! -d "$ARCHIVE_DIR" ]]; then
    mkdir -p "$ARCHIVE_DIR"
  fi

  # 生成压缩包文件名(带时间戳)
  local archive_name="${name}-dist-$(date +%Y%m%d%H%M%S).tar.gz"
  # 根据 KEEP_ARCHIVES 决定保存位置
  if [[ "$KEEP_ARCHIVES" == "1" ]]; then
    local archive_local_path="$ARCHIVE_DIR/$archive_name"
  else
    local archive_local_path="$TMP_DIR/$archive_name"
  fi
  local archive_remote_path="${target_dir%/}/$archive_name"

  print -- "压缩 ${name} dist 目录内容..."
  # 打包 dist 目录内的内容(不包含 dist 目录本身)
  tar -C "$full_dist_path" -czf "$archive_local_path" .

  # 如果保留压缩包,打印保存位置
  if [[ "$KEEP_ARCHIVES" == "1" ]]; then
    print -- "压缩包已保存: $archive_local_path"
  fi

  # 演练模式:只显示不执行
  if [[ "$DRY_RUN" == "1" ]]; then
    print -- "[DRY RUN] 上传 ${archive_local_path} -> ${target_dir}/"
    print -- "[DRY RUN] 解压 ${archive_remote_path} -> ${target_dir}"
    return 0
  fi

  # 上传压缩包
  print -- "上传 ${name} 压缩包..."
  panel_upload_file "$target_dir" "$archive_local_path"

  # 修改压缩包权限为 1panel:1panel
  print -- "修改 ${name} 压缩包权限..."
  panel_chown_path "$archive_remote_path" "1panel" "1panel" false

  # 解压压缩包
  print -- "解压 ${name} 压缩包到 ${target_dir}..."
  panel_decompress_archive "$archive_remote_path" "$target_dir"

  # 修改解压后的文件权限为 1panel:1panel(递归)
  print -- "修改 ${name} 文件权限..."
  panel_chown_path "$target_dir" "1panel" "1panel" true

  # 清理远程压缩包
  if panel_check_exists "$archive_remote_path"; then
    print -- "清理 ${name} 压缩包..."
    panel_delete_path "$archive_remote_path" false
  fi

  print -- "${name} 部署完成"
}

# ==================== 主部署函数 ====================

# 部署后端 Nest 项目
deploy_nest() {
  # 检查是否启用后端部署
  if [[ "$NEST_ENABLED" != "1" ]]; then
    print -- "跳过后端部署 (NEST_ENABLED=0)"
    return 0
  fi

  # 检查部署目标是否匹配
  if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "nest" ]]; then
    print -- "跳过后端部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
    return 0
  fi

  # 验证目录
  validate_project_dir "$NEST_PROJECT_DIR" "后端 Nest"
  validate_remote_target "$NEST_TARGET_DIR" "后端"

  # 构建项目
  build_project "$NEST_PROJECT_DIR" "后端 Nest"

  # 创建压缩包保存目录
  if [[ "$KEEP_ARCHIVES" == "1" && ! -d "$ARCHIVE_DIR" ]]; then
    mkdir -p "$ARCHIVE_DIR"
  fi

  # 准备压缩包
  local archive_name="nest-dist-$(date +%Y%m%d%H%M%S).tar.gz"
  # 根据 KEEP_ARCHIVES 决定保存位置
  if [[ "$KEEP_ARCHIVES" == "1" ]]; then
    local archive_local_path="$ARCHIVE_DIR/$archive_name"
  else
    local archive_local_path="$TMP_DIR/$archive_name"
  fi
  local archive_remote_path="${NEST_TARGET_DIR%/}/$archive_name"

  print -- "压缩后端 dist 目录..."
  # 后端打包包含 dist 目录本身
  tar -C "$NEST_PROJECT_DIR" -czf "$archive_local_path" dist

  # 如果保留压缩包,打印保存位置
  if [[ "$KEEP_ARCHIVES" == "1" ]]; then
    print -- "压缩包已保存: $archive_local_path"
  fi

  # 演练模式
  if [[ "$DRY_RUN" == "1" ]]; then
    print -- "[DRY RUN] 停止运行环境 ${NEST_RUNTIME_NAME}"
    print -- "[DRY RUN] 上传 ${archive_local_path} -> ${NEST_TARGET_DIR}/"
    print -- "[DRY RUN] 解压 ${archive_remote_path} -> ${NEST_TARGET_DIR}"
    print -- "[DRY RUN] 启动运行环境 ${NEST_RUNTIME_NAME}"
    return 0
  fi

  # 查找运行环境 ID
  RUNTIME_ID="$(find_runtime_id)"
  if [[ -z "$RUNTIME_ID" ]]; then
    print -u2 -- "未找到运行环境: $NEST_RUNTIME_NAME"
    exit 1
  fi

  # 停止运行环境
  print -- "停止运行环境 ${NEST_RUNTIME_NAME} ..."
  runtime_operate "down"
  RUNTIME_STOPPED=1

  # 上传压缩包
  print -- "上传后端压缩包..."
  panel_upload_file "$NEST_TARGET_DIR" "$archive_local_path"

  # 修改压缩包权限为 1panel:1panel
  print -- "修改后端压缩包权限..."
  panel_chown_path "$archive_remote_path" "1panel" "1panel" false

  # 删除旧 dist 目录
  local remote_dist_path="${NEST_TARGET_DIR%/}/dist"
  if panel_check_exists "$remote_dist_path"; then
    print -- "删除远端旧 dist 目录..."
    panel_delete_path "$remote_dist_path" true
  fi

  # 解压新压缩包
  print -- "解压后端压缩包..."
  panel_decompress_archive "$archive_remote_path" "$NEST_TARGET_DIR"

  # 修改解压后的文件权限为 1panel:1panel(递归)
  print -- "修改后端文件权限..."
  panel_chown_path "$remote_dist_path" "1panel" "1panel" true

  # 清理压缩包
  if panel_check_exists "$archive_remote_path"; then
    print -- "清理后端压缩包..."
    panel_delete_path "$archive_remote_path" false
  fi

  # 启动运行环境
  print -- "启动运行环境 ${NEST_RUNTIME_NAME} ..."
  runtime_operate "up"
  RUNTIME_STOPPED=0

  print -- "后端部署完成"
}

# 部署管理后台 Admin
deploy_admin() {
  if [[ "$ADMIN_ENABLED" != "1" ]]; then
    print -- "跳过管理后台部署 (ADMIN_ENABLED=0)"
    return 0
  fi

  if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "admin" ]]; then
    print -- "跳过管理后台部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
    return 0
  fi

  validate_project_dir "$ADMIN_PROJECT_DIR" "管理后台 Admin"
  validate_remote_target "$ADMIN_TARGET_DIR" "管理后台"

  build_project "$ADMIN_PROJECT_DIR" "管理后台 Admin"
  deploy_frontend "$ADMIN_PROJECT_DIR" "$ADMIN_TARGET_DIR" "$ADMIN_DIST_PATH" "管理后台"
}

# 部署用户端 Lab
deploy_lab() {
  if [[ "$LAB_ENABLED" != "1" ]]; then
    print -- "跳过用户端部署 (LAB_ENABLED=0)"
    return 0
  fi

  if [[ "$DEPLOY_TARGET" != "all" && "$DEPLOY_TARGET" != "lab" ]]; then
    print -- "跳过用户端部署 (DEPLOY_TARGET=$DEPLOY_TARGET)"
    return 0
  fi

  validate_project_dir "$LAB_PROJECT_DIR" "用户端 Lab"
  validate_remote_target "$LAB_TARGET_DIR" "用户端"

  build_project "$LAB_PROJECT_DIR" "用户端 Lab"
  deploy_frontend "$LAB_PROJECT_DIR" "$LAB_TARGET_DIR" "$LAB_DIST_PATH" "用户端"
}

# 主函数
main() {
  print -- "=========================================="
  print -- "1Panel 自动化部署脚本"
  print -- "=========================================="
  print -- "部署环境: $DEPLOY_ENV (dev | prod)"
  print -- "部署目标: $DEPLOY_TARGET (nest | admin | lab | all)"
  print -- "面板地址: $ONEPANEL_BASE_URL"
  print -- "=========================================="

  # 按顺序部署:后端 -> 管理后台 -> 用户端
  deploy_nest
  deploy_admin
  deploy_lab

  print -- "=========================================="
  print -- "全部部署完成!"
  print -- "=========================================="
}

# 执行主函数
main "$@"


1Panel 自动化部署说明

本文档说明 deploy-youlai-nest-1panel.sh 的使用方式。

支持的项目

项目说明部署方式
后端 Nestyoulai-nest-master停止服务 → 打包 dist 目录 → 上传 → 解压 → 启动服务
管理后台 Adminvue3-element-admin-master打包 dist 内容 → 上传 → 解压
用户端 Lablab-store-web打包 dist 内容 → 上传 → 解压

目录说明

配置文件

通过 DEPLOY_ENV 选择配置文件:

  • DEPLOY_ENV=dev 对应 deploy-youlai-nest-1panel.dev.conf
  • DEPLOY_ENV=prod 对应 deploy-youlai-nest-1panel.prod.conf

配置文件支持以下字段:

ONEPANEL_BASE_URL="http://your-server-ip:port"    # 1Panel 面板地址
ONEPANEL_API_KEY_FILE="./.onepanel-api-key"       # API 密钥文件路径

DEPLOY_TARGET="all"                               # 部署目标: nest | admin | lab | all

NEST_ENABLED=1                                    # 是否部署后端
NEST_PROJECT_DIR="/path/to/youlai-nest-master"    # 后端项目目录
NEST_TARGET_DIR="/opt/1panel/www/sites/your-site" # 后端目标目录
NEST_RUNTIME_NAME="your-runtime-name"             # 1Panel 运行环境名称

ADMIN_ENABLED=1                                   # 是否部署管理后台
ADMIN_PROJECT_DIR="/path/to/vue3-element-admin-master"  # 管理后台项目目录
ADMIN_TARGET_DIR="/opt/1panel/www/sites/your-admin"     # 管理后台目标目录
ADMIN_DIST_PATH="dist"                            # 管理后台 dist 目录

LAB_ENABLED=1                                     # 是否部署用户端
LAB_PROJECT_DIR="/path/to/lab-store-web"          # 用户端项目目录
LAB_TARGET_DIR="/opt/1panel/www/sites/your-lab"   # 用户端目标目录
LAB_DIST_PATH="dist"                              # 用户端 dist 目录

DRY_RUN=0                                         # 演练模式: 1=只显示不执行
START_AFTER_DEPLOY=1                              # 部署后启动服务: 1=启动
KEEP_ARCHIVES=0                                   # 保留压缩包: 1=保留, 0=删除
ARCHIVE_DIR="./dist-archives"                     # 压缩包保存目录

部署命令

部署全部

# dev 环境
DEPLOY_ENV=dev ./scripts/deploy-youlai-nest-1panel.sh

# prod 环境
DEPLOY_ENV=prod ./scripts/deploy-youlai-nest-1panel.sh

仅部署后端

# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh

# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=nest ./scripts/deploy-youlai-nest-1panel.sh

仅部署管理后台

# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh

# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=admin ./scripts/deploy-youlai-nest-1panel.sh

仅部署用户端

# dev 环境
DEPLOY_ENV=dev DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh

# prod 环境
DEPLOY_ENV=prod DEPLOY_TARGET=lab ./scripts/deploy-youlai-nest-1panel.sh

常用选项

演练模式(不真正停服务和上传)

# dev 环境
DEPLOY_ENV=dev DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh

# prod 环境
DEPLOY_ENV=prod DRY_RUN=1 ./scripts/deploy-youlai-nest-1panel.sh

部署后不自动启动运行环境

DEPLOY_ENV=dev START_AFTER_DEPLOY=0 ./scripts/deploy-youlai-nest-1panel.sh

保留压缩包到本地

# 临时启用(仅本次部署)
DEPLOY_ENV=dev KEEP_ARCHIVES=1 ./scripts/deploy-youlai-nest-1panel.sh

# 或在配置文件中设置永久启用
# KEEP_ARCHIVES=1

压缩包将保存在项目根目录的 dist-archives/ 文件夹中。

注意事项

  • 前端打包 dist 目录内容,解压后直接覆盖目标目录
  • 后端打包 dist 目录本身,包含 dist 子目录
  • 后端部署会停止/启动 1Panel 运行环境,前端部署无服务操作
  • 脚本异常退出时,会自动尝试恢复启动后端运行环境
  • 面板地址和目标目录都通过配置文件维护
  • 配置文件包含敏感信息,请勿提交到 Git