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

预览图

介绍

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

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

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

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

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

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


文件结构

代码

scripts/.onepanel-api-key

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

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

ini 复制代码
# 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

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 的使用方式。

支持的项目

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

目录说明

配置文件

通过 DEPLOY_ENV 选择配置文件:

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

配置文件支持以下字段:

bash 复制代码
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"                     # 压缩包保存目录

部署命令

部署全部

bash 复制代码
# dev 环境
DEPLOY_ENV=dev ./scripts/deploy-youlai-nest-1panel.sh

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

仅部署后端

bash 复制代码
# 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

仅部署管理后台

bash 复制代码
# 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

仅部署用户端

bash 复制代码
# 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

常用选项

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

bash 复制代码
# 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

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

bash 复制代码
DEPLOY_ENV=dev START_AFTER_DEPLOY=0 ./scripts/deploy-youlai-nest-1panel.sh

保留压缩包到本地

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

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

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

注意事项

  • 前端打包 dist 目录内容,解压后直接覆盖目标目录
  • 后端打包 dist 目录本身,包含 dist 子目录
  • 后端部署会停止/启动 1Panel 运行环境,前端部署无服务操作
  • 脚本异常退出时,会自动尝试恢复启动后端运行环境
  • 面板地址和目标目录都通过配置文件维护
  • 配置文件包含敏感信息,请勿提交到 Git
相关推荐
Fox爱分享8 小时前
字节二面:10亿数据毫秒级查手机尾号后4位,答不出“异构索引”直接挂?
java·后端·面试
折哥的程序人生 · 物流技术专研8 小时前
《Java面试85题图解版(二)》进阶深化上篇:并发编程 + JVM
java·开发语言·后端·面试
Mahir088 小时前
MySQL 数据一致性的基石:三大日志( redo log/undo log/binlog)与两阶段提交(Prepare 阶段和Commit 阶段)深度解密
数据库·后端·mysql·面试
L0CK8 小时前
Redis 内存淘汰策略
后端
zhengzizhe9 小时前
ReBAC 与 Google Zanzibar:权限系统的未来
后端·架构
用户8356290780519 小时前
使用 Python 自动创建 Excel 折线图
后端·python
梅兮昂9 小时前
Cloudflare Tunnel 实践教程
后端
张文君9 小时前
上古世纪服务端编译安装AAEmu docker编译安装
运维·docker·容器
倒流时光三十年9 小时前
PostgreSQL VACUUM 清理机制详解
后端
雾岛心情9 小时前
小铭邮件管理工具箱的界面(公司版)
运维·服务器·工具·o365·小铭邮件工具箱(公司版)