#!/bin/bash ################################################################################ # 玩玩云 (WanWanYun) - 一键部署/卸载/更新脚本 # 项目地址: https://gitee.com/yu-yon/vue-driven-cloud-storage # 版本: v1.2.0 ################################################################################ set -e # 检查运行模式 MODE="install" if [[ "$1" == "--uninstall" ]] || [[ "$1" == "-u" ]] || [[ "$1" == "uninstall" ]]; then MODE="uninstall" elif [[ "$1" == "--update" ]] || [[ "$1" == "--upgrade" ]] || [[ "$1" == "update" ]]; then MODE="update" elif [[ "$1" == "--repair" ]] || [[ "$1" == "--fix" ]] || [[ "$1" == "repair" ]]; then MODE="repair" elif [[ "$1" == "--ssl" ]] || [[ "$1" == "--cert" ]] || [[ "$1" == "ssl" ]]; then MODE="ssl" fi # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' NC='\033[0m' # No Color # 全局变量 PROJECT_NAME="wanwanyun" PROJECT_DIR="/var/www/${PROJECT_NAME}" REPO_URL="https://gitee.com/yu-yon/vue-driven-cloud-storage.git" NODE_VERSION="20" ADMIN_USERNAME="" ADMIN_PASSWORD="" DOMAIN="" USE_DOMAIN=false SSL_METHOD="" HTTP_PORT="80" HTTPS_PORT="443" BACKEND_PORT="40001" ################################################################################ # 工具函数 ################################################################################ print_banner() { clear echo -e "${CYAN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🌩️ 玩玩云 一键部署脚本 ║" echo "║ ║" echo "║ Cloud Storage Platform ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" } print_step() { echo -e "\n${BLUE}▶ $1${NC}" } print_success() { echo -e "${GREEN}✓ $1${NC}" } print_error() { echo -e "${RED}✗ $1${NC}" } print_warning() { echo -e "${YELLOW}⚠ $1${NC}" } print_info() { echo -e "${CYAN}ℹ $1${NC}" } # 检测操作系统 detect_os() { if [[ -f /etc/os-release ]]; then . /etc/os-release OS=$ID OS_VERSION=$VERSION_ID OS_NAME=$NAME else print_error "无法检测操作系统" exit 1 fi # 统一操作系统标识和包管理器检测 case $OS in ubuntu) PKG_MANAGER="apt" ;; debian) PKG_MANAGER="apt" ;; centos) if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi ;; rhel|redhat) OS="rhel" if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi ;; rocky|rockylinux) OS="rocky" PKG_MANAGER="dnf" ;; almalinux|alma) OS="almalinux" PKG_MANAGER="dnf" ;; fedora) PKG_MANAGER="dnf" ;; opensuse|opensuse-leap|opensuse-tumbleweed) OS="opensuse" PKG_MANAGER="zypper" ;; *) # 自动检测包管理器作为后备方案 print_warning "未识别的操作系统: $OS,尝试自动检测包管理器" if command -v apt-get &> /dev/null; then PKG_MANAGER="apt" print_info "检测到APT包管理器" elif command -v dnf &> /dev/null; then PKG_MANAGER="dnf" print_info "检测到DNF包管理器" elif command -v yum &> /dev/null; then PKG_MANAGER="yum" print_info "检测到YUM包管理器" elif command -v zypper &> /dev/null; then PKG_MANAGER="zypper" print_info "检测到Zypper包管理器" else print_error "无法检测到支持的包管理器" exit 1 fi ;; esac } # 检测系统架构 detect_arch() { ARCH=$(uname -m) case $ARCH in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; *) print_error "不支持的系统架构: $ARCH" exit 1 ;; esac } # 检测root权限 check_root() { if [[ $EUID -ne 0 ]]; then print_error "此脚本需要root权限运行" print_info "请使用: sudo bash install.sh" exit 1 fi } ################################################################################ # 环境检测 ################################################################################ system_check() { print_step "正在检测系统环境..." # 检测操作系统 detect_os print_success "操作系统: $OS $OS_VERSION" # 检测架构 detect_arch print_success "系统架构: $ARCH" # 检测内存 TOTAL_MEM=$(free -m | awk '/^Mem:/{print $2}') if [[ $TOTAL_MEM -lt 512 ]]; then print_warning "内存不足512MB,可能影响性能" else print_success "可用内存: ${TOTAL_MEM}MB" fi # 检测磁盘空间 DISK_AVAIL=$(df -m / | awk 'NR==2 {print $4}') if [[ $DISK_AVAIL -lt 2048 ]]; then print_warning "磁盘空间不足2GB,可能影响运行" else print_success "可用磁盘: ${DISK_AVAIL}MB" fi # 检测网络 if ping -c 1 gitee.com &> /dev/null; then print_success "网络连接正常" else print_error "无法连接到网络" exit 1 fi # 检测公网IP PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知") print_info "公网IP: $PUBLIC_IP" echo "" } ################################################################################ # 软件源配置 ################################################################################ choose_mirror() { print_step "选择软件包安装源" echo "" echo "请选择软件源:" echo -e "${GREEN}[1]${NC} 官方源 (国外服务器推荐)" echo -e "${GREEN}[2]${NC} 阿里云镜像源 (国内服务器推荐,速度更快)" echo "" while true; do read -p "请输入选项 [1-2]: " mirror_choice < /dev/tty case $mirror_choice in 1) print_info "使用官方源" USE_ALIYUN_MIRROR=false break ;; 2) print_info "使用阿里云镜像源" USE_ALIYUN_MIRROR=true configure_aliyun_mirror break ;; *) print_error "无效选项,请重新选择" ;; esac done echo "" } configure_aliyun_mirror() { print_step "配置阿里云镜像源..." case $OS in ubuntu) # 备份原有源 if [[ ! -f /etc/apt/sources.list.bak ]]; then cp /etc/apt/sources.list /etc/apt/sources.list.bak fi # 配置Ubuntu阿里云源 cat > /etc/apt/sources.list << EOF deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-updates main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-backports main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs)-security main restricted universe multiverse EOF print_success "阿里云源配置完成" ;; debian) # 备份原有源 if [[ ! -f /etc/apt/sources.list.bak ]]; then cp /etc/apt/sources.list /etc/apt/sources.list.bak fi # 配置Debian阿里云源 cat > /etc/apt/sources.list << EOF deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs) main contrib non-free non-free-firmware deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-updates main contrib non-free non-free-firmware deb http://mirrors.aliyun.com/debian/ $(lsb_release -cs)-backports main contrib non-free non-free-firmware deb http://mirrors.aliyun.com/debian-security $(lsb_release -cs)-security main contrib non-free non-free-firmware EOF print_success "阿里云源配置完成" ;; centos) # 备份并配置CentOS阿里云源 if [[ -f /etc/yum.repos.d/CentOS-Base.repo ]]; then if [[ ! -f /etc/yum.repos.d/CentOS-Base.repo.bak ]]; then cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak fi fi CENTOS_VERSION="${OS_VERSION%%.*}" if [[ "$CENTOS_VERSION" == "7" ]]; then curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo elif [[ "$CENTOS_VERSION" == "8" ]]; then curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo sed -i 's/mirrors.cloud.aliyuncs.com/mirrors.aliyun.com/g' /etc/yum.repos.d/CentOS-Base.repo fi yum clean all yum makecache print_success "阿里云源配置完成" ;; rhel) # RHEL使用EPEL和阿里云镜像 print_info "配置RHEL阿里云镜像源..." yum install -y epel-release print_success "阿里云源配置完成" ;; rocky) # 备份并配置Rocky Linux阿里云源 if [[ -d /etc/yum.repos.d ]]; then mkdir -p /etc/yum.repos.d/backup cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true fi sed -e 's|^mirrorlist=|#mirrorlist=|g' \ -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \ -i.bak /etc/yum.repos.d/rocky*.repo dnf clean all dnf makecache print_success "阿里云源配置完成" ;; almalinux) # 备份并配置AlmaLinux阿里云源 if [[ -d /etc/yum.repos.d ]]; then mkdir -p /etc/yum.repos.d/backup cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true fi sed -e 's|^mirrorlist=|#mirrorlist=|g' \ -e 's|^# baseurl=https://repo.almalinux.org|baseurl=https://mirrors.aliyun.com|g' \ -i.bak /etc/yum.repos.d/almalinux*.repo dnf clean all dnf makecache print_success "阿里云源配置完成" ;; fedora) # 备份并配置Fedora阿里云源 if [[ -d /etc/yum.repos.d ]]; then mkdir -p /etc/yum.repos.d/backup cp /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup/ 2>/dev/null || true fi sed -e 's|^metalink=|#metalink=|g' \ -e 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://mirrors.aliyun.com/fedora|g' \ -i.bak /etc/yum.repos.d/fedora*.repo /etc/yum.repos.d/fedora-updates*.repo dnf clean all dnf makecache print_success "阿里云源配置完成" ;; opensuse) # 配置openSUSE阿里云源 print_info "配置openSUSE阿里云镜像源..." zypper mr -da zypper ar -fcg https://mirrors.aliyun.com/opensuse/distribution/leap/\$releasever/repo/oss/ aliyun-oss zypper ar -fcg https://mirrors.aliyun.com/opensuse/distribution/leap/\$releasever/repo/non-oss/ aliyun-non-oss zypper ar -fcg https://mirrors.aliyun.com/opensuse/update/leap/\$releasever/oss/ aliyun-update-oss zypper ar -fcg https://mirrors.aliyun.com/opensuse/update/leap/\$releasever/non-oss/ aliyun-update-non-oss zypper ref print_success "阿里云源配置完成" ;; *) print_warning "当前系统($OS)暂不支持阿里云镜像源自动配置,使用官方源" ;; esac } ################################################################################ check_cpp_compiler() { print_step "检查C++编译器版本..." # 检查g++是否已安装 if ! command -v g++ &> /dev/null; then print_warning "g++未安装,将在依赖安装时自动安装" return fi # 获取g++版本号 GXX_VERSION=$(g++ --version | head -n1 | grep -oP '\d+\.\d+\.\d+' | head -1 | cut -d'.' -f1) if [[ -z "$GXX_VERSION" ]]; then print_warning "无法检测g++版本,跳过版本检查" return fi print_info "当前g++版本: $GXX_VERSION.x" # better-sqlite3 v11+ 需要C++20支持(g++ 10+) if [[ $GXX_VERSION -lt 10 ]]; then print_warning "g++版本过低(需要10+以支持C++20),正在升级..." echo "" case $PKG_MANAGER in apt) # Ubuntu/Debian: 使用toolchain PPA print_info "添加Ubuntu Toolchain PPA..." add-apt-repository ppa:ubuntu-toolchain-r/test -y || { print_error "添加PPA失败" return 1 } apt-get update print_info "安装g++-11..." apt-get install -y g++-11 gcc-11 || { print_error "g++-11安装失败" return 1 } # 设置为默认编译器 update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 100 update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 # 验证 NEW_VERSION=$(g++ --version | head -n1 | grep -oP '\d+\.\d+\.\d+' | head -1 | cut -d'.' -f1) print_success "g++已升级到版本: $NEW_VERSION.x" ;; yum|dnf) # CentOS/RHEL: 使用devtoolset或gcc-toolset if [[ "$OS" == "centos" ]] && [[ "$OS_VERSION" == "7" ]]; then # CentOS 7 使用devtoolset-11 print_info "安装devtoolset-11..." yum install -y centos-release-scl yum install -y devtoolset-11-gcc devtoolset-11-gcc-c++ # 启用devtoolset-11 echo "source /opt/rh/devtoolset-11/enable" >> /etc/profile source /opt/rh/devtoolset-11/enable print_success "devtoolset-11安装完成" else # CentOS 8+ 使用gcc-toolset-11 print_info "安装gcc-toolset-11..." $PKG_MANAGER install -y gcc-toolset-11-gcc gcc-toolset-11-gcc-c++ # 启用gcc-toolset-11 echo "source /opt/rh/gcc-toolset-11/enable" >> /etc/profile source /opt/rh/gcc-toolset-11/enable print_success "gcc-toolset-11安装完成" fi ;; zypper) # OpenSUSE print_info "升级g++..." zypper install -y gcc11-c++ || { print_error "g++升级失败" return 1 } # 设置为默认 update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 100 update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 print_success "g++已升级" ;; *) print_warning "不支持的包管理器,无法自动升级g++" print_warning "请手动升级g++到10或更高版本" return 1 ;; esac echo "" else print_success "g++版本满足要求(10+)" echo "" fi } # 安装依赖环境 ################################################################################ install_dependencies() { print_step "正在安装依赖环境..." case $PKG_MANAGER in apt) apt-get update apt-get install -y curl wget git unzip lsb-release build-essential python3 install_nodejs_apt install_nginx_apt ;; yum) yum install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3 install_nodejs_yum install_nginx_yum ;; dnf) dnf install -y curl wget git unzip redhat-lsb-core gcc-c++ make python3 install_nodejs_dnf install_nginx_dnf ;; zypper) zypper install -y curl wget git unzip lsb-release gcc-c++ make python3 install_nodejs_zypper install_nginx_zypper ;; *) print_error "不支持的包管理器: $PKG_MANAGER" exit 1 ;; esac install_pm2 print_success "依赖环境安装完成" # 检查并升级C++编译器(如果需要) check_cpp_compiler echo "" } install_nodejs_apt() { if command -v node &> /dev/null; then NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ $NODE_VER -ge $NODE_VERSION ]]; then print_success "Node.js 已安装: $(node -v)" return fi fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - apt-get install -y nodejs print_success "Node.js 安装完成: $(node -v)" } install_nodejs_yum() { if command -v node &> /dev/null; then NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ $NODE_VER -ge $NODE_VERSION ]]; then print_success "Node.js 已安装: $(node -v)" return fi fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash - yum install -y nodejs print_success "Node.js 安装完成: $(node -v)" } install_nginx_apt() { if command -v nginx &> /dev/null; then print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)" return fi print_info "正在安装 Nginx..." apt-get install -y nginx systemctl enable nginx print_success "Nginx 安装完成" } install_nginx_yum() { if command -v nginx &> /dev/null; then print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)" return fi print_info "正在安装 Nginx..." yum install -y nginx systemctl enable nginx print_success "Nginx 安装完成" } install_nodejs_dnf() { if command -v node &> /dev/null; then NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ $NODE_VER -ge $NODE_VERSION ]]; then print_success "Node.js 已安装: $(node -v)" return fi fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash - dnf install -y nodejs print_success "Node.js 安装完成: $(node -v)" } install_nginx_dnf() { if command -v nginx &> /dev/null; then print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)" return fi print_info "正在安装 Nginx..." dnf install -y nginx systemctl enable nginx print_success "Nginx 安装完成" } install_nodejs_zypper() { if command -v node &> /dev/null; then NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ $NODE_VER -ge $NODE_VERSION ]]; then print_success "Node.js 已安装: $(node -v)" return fi fi print_info "正在安装 Node.js ${NODE_VERSION}.x..." # openSUSE使用官方仓库的Node.js zypper install -y nodejs${NODE_VERSION} print_success "Node.js 安装完成: $(node -v)" } install_nginx_zypper() { if command -v nginx &> /dev/null; then print_success "Nginx 已安装: $(nginx -v 2>&1 | cut -d'/' -f2)" return fi print_info "正在安装 Nginx..." zypper install -y nginx systemctl enable nginx print_success "Nginx 安装完成" } install_pm2() { if command -v pm2 &> /dev/null; then print_success "PM2 已安装: $(pm2 -v)" return fi print_info "正在安装 PM2..." npm install -g pm2 pm2 startup print_success "PM2 安装完成" } ################################################################################ # 智能端口检测和配置 ################################################################################ # 检查端口是否可用(保留用于兼容性) check_port_available() { local port=$1 if command -v netstat &> /dev/null; then if netstat -tuln | grep -q ":${port} "; then return 1 # 端口被占用 fi elif command -v ss &> /dev/null; then if ss -tuln | grep -q ":${port} "; then return 1 # 端口被占用 fi fi return 0 # 端口可用 } # 智能检测端口状态和占用进程 check_port_status() { local port=$1 # 1. 检查端口是否被监听 if command -v netstat &> /dev/null; then if ! netstat -tuln 2>/dev/null | grep -q ":${port} "; then echo "available" return 0 fi elif command -v ss &> /dev/null; then if ! ss -tuln 2>/dev/null | grep -q ":${port} "; then echo "available" return 0 fi else echo "available" return 0 fi # 2. 端口被占用,检查是什么进程 local process="" if command -v netstat &> /dev/null; then process=$(netstat -tulnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f2 | head -1) fi if [[ -z "$process" ]] && command -v ss &> /dev/null; then # 使用sed替代grep -oP以提高兼容性 process=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*users:(("\([^"]*\)".*/\1/p' | head -1) fi # 3. 根据进程返回状态(始终返回0以避免set -e导致脚本退出) if [[ -z "$process" ]]; then # 无法获取进程名(可能权限不足) echo "occupied" elif [[ "$process" == "nginx" ]] || [[ "$process" =~ ^nginx: ]]; then # Nginx占用 echo "nginx" elif [[ "$process" == "apache2" ]] || [[ "$process" == "httpd" ]] || [[ "$process" =~ apache ]]; then # Apache占用 echo "apache" else # 其他进程 echo "other:$process" fi # 始终返回0,避免set -e导致脚本退出 return 0 } # 改进的端口配置函数 configure_ports() { print_step "智能端口配置" echo "" # 全局标志:是否共用Nginx端口 SHARE_NGINX=false # ========== 检测80端口 ========== port_80_status=$(check_port_status 80) case $port_80_status in "available") print_success "80 端口可用" HTTP_PORT=80 ;; "nginx") print_info "检测到 Nginx 已占用 80 端口" echo "" echo "🎯 好消息:可以通过虚拟主机配置与现有Nginx共用此端口!" echo "" echo "请选择部署方式:" echo "" echo -e "${GREEN}[1]${NC} 共用80端口(推荐)" echo " ✅ 需要配置不同的域名" echo " ✅ 访问: http://your-domain.com" echo " ✅ 不需要端口号" echo "" echo -e "${GREEN}[2]${NC} 使用其他HTTP端口" echo " ℹ️ 独立端口" echo " ℹ️ 访问: http://your-domain.com:8080" echo "" while true; do read -p "请选择 [1-2]: " choice < /dev/tty if [[ "$choice" == "1" ]]; then HTTP_PORT=80 SHARE_NGINX=true print_success "将与现有Nginx共用80端口(虚拟主机模式)" print_info "提示: 请确保使用不同的域名区分站点" break elif [[ "$choice" == "2" ]]; then # 选择其他端口的逻辑 while true; do read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty custom_port=${custom_port:-8080} if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_port; then print_error "端口 $custom_port 已被占用,请选择其他端口" continue fi HTTP_PORT=$custom_port print_success "将使用 HTTP 端口: $HTTP_PORT" break done break else print_error "无效选项,请重新选择" fi done ;; "apache") print_warning "检测到 Apache 已占用 80 端口" echo "" echo "⚠️ Apache和Nginx不能同时监听同一端口" echo "" echo "请选择解决方案:" echo "" echo -e "${GREEN}[1]${NC} 停止Apache,改用Nginx" echo " ⚠️ 需要迁移Apache配置" echo "" echo -e "${GREEN}[2]${NC} 使用其他HTTP端口(推荐)" echo " ✅ 不影响现有Apache服务" echo "" while true; do read -p "请选择 [1-2]: " choice < /dev/tty if [[ "$choice" == "1" ]]; then print_info "正在停止Apache..." systemctl stop apache2 2>/dev/null || systemctl stop httpd 2>/dev/null || true systemctl disable apache2 2>/dev/null || systemctl disable httpd 2>/dev/null || true HTTP_PORT=80 print_success "Apache已停止,将使用80端口" break elif [[ "$choice" == "2" ]]; then # 选择其他端口 while true; do read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty custom_port=${custom_port:-8080} if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_port; then print_error "端口 $custom_port 已被占用,请选择其他端口" continue fi HTTP_PORT=$custom_port print_success "将使用 HTTP 端口: $HTTP_PORT" break done break else print_error "无效选项,请重新选择" fi done ;; "occupied"|other:*) process=${port_80_status#other:} if [[ "$port_80_status" == "occupied" ]]; then print_warning "80 端口已被占用(无法识别进程)" else print_warning "80 端口被进程 ${process} 占用" fi echo "" echo "请选择其他HTTP端口" while true; do read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty custom_port=${custom_port:-8080} if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_port; then print_error "端口 $custom_port 已被占用,请选择其他端口" continue fi HTTP_PORT=$custom_port print_success "将使用 HTTP 端口: $HTTP_PORT" break done ;; esac echo "" # ========== 检测443端口(仅在使用HTTPS时需要)========== if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then port_443_status=$(check_port_status 443) case $port_443_status in "available") print_success "443 端口可用" HTTPS_PORT=443 ;; "nginx") print_info "检测到 Nginx 已占用 443 端口" echo "" if [[ "$SHARE_NGINX" == "true" ]]; then # 如果HTTP端口也是共用的,默认继续共用 echo "🎯 将继续与现有Nginx共用443端口(虚拟主机模式)" HTTPS_PORT=443 print_success "将与现有Nginx共用443端口" else echo "请选择部署方式:" echo "" echo -e "${GREEN}[1]${NC} 共用443端口" echo " ✅ 需要配置不同的域名" echo "" echo -e "${GREEN}[2]${NC} 使用其他HTTPS端口" echo " ℹ️ 独立端口(如8443)" echo "" while true; do read -p "请选择 [1-2]: " choice < /dev/tty if [[ "$choice" == "1" ]]; then HTTPS_PORT=443 SHARE_NGINX=true print_success "将与现有Nginx共用443端口" break elif [[ "$choice" == "2" ]]; then # 选择其他端口 while true; do read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty custom_https_port=${custom_https_port:-8443} if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_https_port; then print_error "端口 $custom_https_port 已被占用,请选择其他端口" continue fi HTTPS_PORT=$custom_https_port print_success "将使用 HTTPS 端口: $HTTPS_PORT" break done break else print_error "无效选项,请重新选择" fi done fi ;; "apache"|"occupied"|other:*) # Apache或其他进程占用443,需要换端口 if [[ "$port_443_status" == "apache" ]]; then print_warning "检测到 Apache 已占用 443 端口" elif [[ "$port_443_status" == "occupied" ]]; then print_warning "443 端口已被占用" else process=${port_443_status#other:} print_warning "443 端口被进程 ${process} 占用" fi echo "" while true; do read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty custom_https_port=${custom_https_port:-8443} if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_https_port; then print_error "端口 $custom_https_port 已被占用,请选择其他端口" continue fi HTTPS_PORT=$custom_https_port print_success "将使用 HTTPS 端口: $HTTPS_PORT" break done ;; esac echo "" fi # ========== 检测后端端口 ========== if ! check_port_available 40001; then print_warning "检测到 40001 端口已被占用" echo "" while true; do read -p "请输入后端服务端口 [建议: 40002]: " custom_backend_port < /dev/tty custom_backend_port=${custom_backend_port:-40002} if [[ ! "$custom_backend_port" =~ ^[0-9]+$ ]] || [[ $custom_backend_port -lt 1024 ]] || [[ $custom_backend_port -gt 65535 ]]; then print_error "端口范围: 1024-65535" continue fi if ! check_port_available $custom_backend_port; then print_error "端口 $custom_backend_port 已被占用,请选择其他端口" continue fi BACKEND_PORT=$custom_backend_port print_success "将使用后端端口: $BACKEND_PORT" break done else print_success "40001 端口可用" fi echo "" print_info "端口配置摘要:" echo " - HTTP端口: $HTTP_PORT" if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then echo " - HTTPS端口: $HTTPS_PORT" fi echo " - 后端端口: $BACKEND_PORT" if [[ "$SHARE_NGINX" == "true" ]]; then echo " - 模式: 虚拟主机共用端口 ✅" fi echo "" } ################################################################################ # 访问模式选择 ################################################################################ choose_access_mode() { print_step "选择访问模式" echo "" echo "请选择访问模式:" echo -e "${GREEN}[1]${NC} 域名模式 (推荐,支持HTTPS)" echo -e "${GREEN}[2]${NC} IP模式 (仅HTTP,适合测试)" echo "" while true; do read -p "请输入选项 [1-2]: " mode_choice < /dev/tty case $mode_choice in 1) USE_DOMAIN=true configure_domain break ;; 2) USE_DOMAIN=false PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "未知") print_info "将使用 IP 模式访问: http://${PUBLIC_IP}" echo "" break ;; *) print_error "无效选项,请重新选择" ;; esac done } configure_domain() { echo "" while true; do read -p "请输入您的域名 (例如: wwy.example.com): " DOMAIN < /dev/tty if [[ -z "$DOMAIN" ]]; then print_error "域名不能为空" continue fi # 验证域名格式 if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then print_error "域名格式不正确" continue fi # 验证域名解析 print_info "正在验证域名解析..." DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n1) PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com) if [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then print_success "域名已正确解析到当前服务器IP" break else print_warning "域名未解析到当前服务器IP" print_info "域名解析IP: $DOMAIN_IP" print_info "当前服务器IP: $PUBLIC_IP" read -p "是否继续? (y/n): " continue_choice < /dev/tty if [[ "$continue_choice" == "y" || "$continue_choice" == "Y" ]]; then break fi fi done choose_ssl_method } ################################################################################ # SSL证书配置 ################################################################################ # 配置acme.sh自动续期 setup_acme_auto_renew() { echo "" print_step "配置SSL证书自动续期..." # acme.sh安装时会自动创建cron任务,这里验证并确保其正常工作 # 1. 检查cron服务是否运行 if systemctl is-active --quiet cron 2>/dev/null || systemctl is-active --quiet crond 2>/dev/null; then print_success "Cron服务运行正常" else print_warning "Cron服务未运行,正在启动..." systemctl start cron 2>/dev/null || systemctl start crond 2>/dev/null || true systemctl enable cron 2>/dev/null || systemctl enable crond 2>/dev/null || true fi # 2. 检查acme.sh cron任务 if crontab -l 2>/dev/null | grep -q "acme.sh.*--cron"; then print_success "acme.sh自动续期任务已配置" else print_warning "未检测到acme.sh cron任务,正在添加..." # acme.sh会自动安装cron,这里手动触发一次 ~/.acme.sh/acme.sh --install-cronjob 2>/dev/null || true fi # 3. 显示续期信息 echo "" print_info "SSL证书自动续期已配置:" echo " - 检查频率: 每天自动检查" echo " - 续期时机: 证书到期前30天自动续期" echo " - 续期后操作: 自动重载Nginx" echo "" # 显示下次续期时间 if [[ -f ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf ]]; then NEXT_RENEW=$(grep "Le_NextRenewTime=" ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf 2>/dev/null | cut -d'=' -f2) if [[ -n "$NEXT_RENEW" ]]; then RENEW_DATE=$(date -d "@${NEXT_RENEW}" "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || date -r ${NEXT_RENEW} "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || echo "未知") print_info "预计续期时间: ${RENEW_DATE}" fi fi # 4. 测试续期命令(不实际续期,只检查) print_info "验证续期配置..." if ~/.acme.sh/acme.sh --list 2>/dev/null | grep -q "$DOMAIN"; then print_success "证书续期配置验证通过" else print_warning "证书列表中未找到域名,续期可能需要手动配置" fi echo "" } choose_ssl_method() { echo "" print_step "选择SSL证书部署方式" echo "" echo -e "${YELLOW}【推荐方案】${NC}" echo -e "${GREEN}[1]${NC} acme.sh + Let's Encrypt" echo " - 纯Shell脚本,轻量级稳定" echo " - 自动续期,无需手动操作" echo "" echo -e "${YELLOW}【备选方案】${NC}" echo -e "${GREEN}[2]${NC} acme.sh + ZeroSSL" echo " - Let's Encrypt的免费替代品" echo -e "${GREEN}[3]${NC} acme.sh + Buypass" echo " - 挪威免费CA,有效期180天" echo "" echo -e "${YELLOW}【云服务商证书】${NC}" echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)" echo -e "${GREEN}[5]${NC} 腾讯云免费证书 (需提供SecretKey)" echo "" echo -e "${YELLOW}【其他选项】${NC}" echo -e "${GREEN}[6]${NC} 使用已有证书 (手动上传)" echo -e "${GREEN}[7]${NC} 暂不配置HTTPS (可后续配置)" echo "" while true; do read -p "请输入选项 [1-7]: " ssl_choice < /dev/tty case $ssl_choice in 1) SSL_METHOD="2" # acme.sh + Let's Encrypt break ;; 2) SSL_METHOD="3" # acme.sh + ZeroSSL break ;; 3) SSL_METHOD="5" # acme.sh + Buypass break ;; 4) SSL_METHOD="4" # 阿里云 break ;; 5) SSL_METHOD="6" # 腾讯云 break ;; 6) SSL_METHOD="7" # 手动上传 break ;; 7) SSL_METHOD="8" # 不配置HTTPS break ;; *) print_error "无效选项,请重新选择" ;; esac done echo "" } deploy_ssl() { if [[ "$USE_DOMAIN" != "true" ]]; then return 0 fi case $SSL_METHOD in 2) deploy_acme_letsencrypt || ssl_fallback "2" ;; 3) deploy_acme_zerossl || ssl_fallback "3" ;; 4) deploy_aliyun_ssl || ssl_fallback "4" ;; 5) deploy_acme_buypass || ssl_fallback "5" ;; 6) deploy_tencent_ssl || ssl_fallback "6" ;; 7) deploy_manual_ssl ;; 8) print_info "跳过HTTPS配置" return 0 ;; esac } ssl_fallback() { local failed_method=$1 # 接收失败的方案编号 print_error "SSL证书部署失败" echo "" print_warning "建议尝试备选方案:" echo "" # 动态显示可用选项(排除已失败的) local available_options=() # 方案2: acme.sh + Let's Encrypt if [[ "$failed_method" != "2" ]]; then echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt" available_options+=("2") fi # 方案3: acme.sh + ZeroSSL if [[ "$failed_method" != "3" ]]; then echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" available_options+=("3") fi # 方案5: acme.sh + Buypass if [[ "$failed_method" != "5" ]]; then echo -e "${GREEN}[5]${NC} acme.sh + Buypass" available_options+=("5") fi # 方案8: 不配置HTTPS echo -e "${GREEN}[8]${NC} 暂不配置HTTPS" available_options+=("8") echo "" echo -e "${YELLOW}提示: 方案 $failed_method 已失败,已从列表中移除${NC}" echo "" while true; do read -p "请选择备选方案: " retry_choice < /dev/tty # 检查输入是否在可用选项中 if [[ ! " ${available_options[@]} " =~ " ${retry_choice} " ]]; then print_error "无效选项或该方案已失败" continue fi case $retry_choice in 2) deploy_acme_letsencrypt && return 0 # 如果再次失败,继续调用fallback但排除方案2 ssl_fallback "2" return $? ;; 3) deploy_acme_zerossl && return 0 ssl_fallback "3" return $? ;; 5) deploy_acme_buypass && return 0 ssl_fallback "5" return $? ;; 8) print_info "跳过HTTPS配置" SSL_METHOD=8 return 0 ;; esac done } deploy_certbot() { print_step "使用 Certbot 部署SSL证书..." # 检查certbot是否已安装 if ! command -v certbot &> /dev/null; then print_info "正在安装 Certbot..." # 安装certbot case $PKG_MANAGER in apt) # Ubuntu/Debian: 优先使用snap(官方推荐,避免Python依赖冲突) if command -v snap &> /dev/null; then print_info "使用snap安装Certbot(官方推荐方式)..." snap install --classic certbot 2>/dev/null || true ln -sf /snap/bin/certbot /usr/bin/certbot 2>/dev/null || true # 验证snap安装是否成功 if /snap/bin/certbot --version &> /dev/null; then print_success "Certbot (snap版) 安装成功" else print_warning "snap安装失败,尝试apt安装..." # 修复urllib3依赖问题 apt-get remove -y python3-urllib3 2>/dev/null || true apt-get install -y certbot python3-certbot-nginx fi else print_info "snap不可用,使用apt安装..." # 修复urllib3依赖问题 apt-get remove -y python3-urllib3 2>/dev/null || true apt-get install -y certbot python3-certbot-nginx fi ;; yum) # 修复urllib3依赖问题 yum remove -y python3-urllib3 2>/dev/null || true yum install -y certbot python3-certbot-nginx ;; dnf) # 修复urllib3依赖问题 dnf remove -y python3-urllib3 2>/dev/null || true dnf install -y certbot python3-certbot-nginx ;; zypper) zypper install -y certbot python3-certbot-nginx ;; esac # 最终验证certbot是否可用 if ! command -v certbot &> /dev/null; then print_error "Certbot安装失败" return 1 fi else print_success "Certbot 已安装: $(certbot --version 2>&1 | head -1)" fi # 修复已安装certbot的urllib3依赖冲突 if ! certbot --version &> /dev/null; then print_warning "检测到Certbot依赖问题,正在修复..." case $PKG_MANAGER in apt) apt-get remove -y python3-urllib3 2>/dev/null || true apt-get install --reinstall -y certbot python3-certbot-nginx ;; yum|dnf) $PKG_MANAGER remove -y python3-urllib3 2>/dev/null || true $PKG_MANAGER reinstall -y certbot python3-certbot-nginx ;; esac # 再次验证 if ! certbot --version &> /dev/null; then print_error "Certbot依赖修复失败,建议尝试其他SSL方案" return 1 fi print_success "Certbot依赖已修复" fi # 申请证书(使用webroot模式,不自动修改Nginx配置) echo "" print_info "正在申请 Let's Encrypt 证书..." if certbot certonly --webroot -w "${PROJECT_DIR}/frontend" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then # 将证书复制到Nginx SSL目录 mkdir -p /etc/nginx/ssl ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt" ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key" # 配置自动续期 systemctl enable certbot.timer 2>/dev/null || true print_success "Certbot SSL证书申请成功" return 0 else # 检查证书是否已存在 if [[ -d "/etc/letsencrypt/live/${DOMAIN}" ]]; then print_warning "检测到证书已存在,使用已有证书" mkdir -p /etc/nginx/ssl ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt" ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key" print_success "已有证书已链接到Nginx目录" return 0 else print_error "Certbot SSL证书申请失败" echo "" print_warning "常见失败原因:" echo " 1. 域名未正确解析到此服务器" echo " 2. 防火墙阻止了80端口" echo " 3. Nginx未正确配置或未启动" echo " 4. Let's Encrypt速率限制" echo "" return 1 fi fi } deploy_acme_letsencrypt() { print_step "使用 acme.sh + Let's Encrypt 部署SSL证书..." # 安装acme.sh if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then echo "" print_info "正在安装 acme.sh..." # 如果目录存在但文件不存在,先清理 if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then print_warning "检测到不完整的安装,正在清理..." rm -rf ~/.acme.sh fi print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" # 使用官方安装方法:直接通过curl管道执行 print_info "正在下载并安装..." if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then install_result=$? print_info "安装脚本执行完成,退出码: $install_result" else install_result=$? print_error "安装脚本执行失败,退出码: $install_result" fi # 重新加载环境变量 source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true # 等待文件系统同步 print_info "等待安装完成..." sleep 3 # 验证安装是否真正成功 if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then print_success "acme.sh 安装成功" else print_error "acme.sh 安装失败" echo "" print_warning "诊断信息:" echo " - 安装命令退出码: $install_result" echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" echo " - HOME变量: $HOME" echo " - 当前用户: $(whoami)" echo "" if [[ -d ~/.acme.sh ]]; then print_info "~/.acme.sh 目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" echo "" fi print_info "尝试查找acme.sh安装位置..." find /root -name "acme.sh" -type f 2>/dev/null | head -5 || echo " 未找到" echo "" print_warning "可能的原因:" echo " 1. 网络连接问题或下载超时" echo " 2. GitHub访问受限(国内网络)" echo " 3. curl版本过低或不支持某些功能" echo "" print_warning "建议尝试其他SSL方案:" echo " 1. 返回选择 Certbot (推荐)" echo " 2. 或选择 [8] 暂不配置HTTPS" echo "" return 1 fi fi # 确认acme.sh可用 echo "" print_info "验证 acme.sh 安装..." # 等待文件系统同步 sleep 2 # 检查安装目录 if [[ ! -d ~/.acme.sh ]]; then print_error "安装目录不存在: ~/.acme.sh" return 1 fi # 检查主脚本文件 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" print_info "目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" return 1 fi # 检查脚本是否可执行 if [[ ! -x ~/.acme.sh/acme.sh ]]; then print_warning "脚本不可执行,正在添加执行权限..." chmod +x ~/.acme.sh/acme.sh fi # 测试脚本是否能运行 if ! ~/.acme.sh/acme.sh --version &> /dev/null; then print_error "acme.sh 无法运行" return 1 fi print_success "acme.sh 验证通过" # 申请证书 echo "" print_info "正在申请 Let's Encrypt 证书..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi # 使用webroot模式申请证书(更可靠) # 先尝试正常申请,如果证书已存在则使用--force强制更新 if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then print_warning "检测到证书已存在,使用已有证书" print_success "将直接安装现有证书" else print_error "证书申请失败" echo "" print_warning "常见失败原因:" echo " 1. 域名未正确解析到此服务器" echo " 2. Nginx未正确配置或未启动" echo " 3. 80端口被占用或防火墙阻止" echo " 4. 前端目录权限不足" echo "" return 1 fi fi # 安装证书 echo "" print_info "正在安装证书到Nginx..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi mkdir -p /etc/nginx/ssl # 确保nginx服务已启动(证书安装时需要reload) if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then print_warning "Nginx未运行,正在启动..." systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true sleep 2 fi # 先不带reload命令安装证书(避免nginx未启动导致失败) if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ --key-file /etc/nginx/ssl/${DOMAIN}.key \ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then print_success "证书文件已安装到: /etc/nginx/ssl/" # 手动reload nginx if systemctl is-active --quiet nginx 2>/dev/null; then systemctl reload nginx && print_success "Nginx配置已重载" elif pgrep -x nginx > /dev/null; then nginx -s reload && print_success "Nginx配置已重载" else print_warning "Nginx未运行,将在后续步骤启动" fi # 配置自动续期 setup_acme_auto_renew return 0 else print_error "证书安装失败" return 1 fi } deploy_acme_zerossl() { print_step "使用 acme.sh + ZeroSSL 部署SSL证书..." # 安装acme.sh(使用改进的安装逻辑) if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then echo "" print_info "正在安装 acme.sh..." # 如果目录存在但文件不存在,先清理 if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then print_warning "检测到不完整的安装,正在清理..." rm -rf ~/.acme.sh fi print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" # 使用官方安装方法:直接通过curl管道执行 print_info "正在下载并安装..." if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then install_result=$? print_info "安装脚本执行完成,退出码: $install_result" else install_result=$? print_error "安装脚本执行失败,退出码: $install_result" fi # 重新加载环境变量 source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true # 等待文件系统同步 print_info "等待安装完成..." sleep 3 # 验证安装 if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then print_success "acme.sh 安装成功" else print_error "acme.sh 安装失败" echo "" print_warning "诊断信息:" echo " - 安装命令退出码: $install_result" echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" echo "" if [[ -d ~/.acme.sh ]]; then print_info "~/.acme.sh 目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" echo "" fi return 1 fi fi # 确认acme.sh可用 echo "" print_info "验证 acme.sh 安装..." # 等待文件系统同步 sleep 2 # 检查安装目录 if [[ ! -d ~/.acme.sh ]]; then print_error "安装目录不存在: ~/.acme.sh" return 1 fi # 检查主脚本文件 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" print_info "目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" return 1 fi # 检查脚本是否可执行 if [[ ! -x ~/.acme.sh/acme.sh ]]; then print_warning "脚本不可执行,正在添加执行权限..." chmod +x ~/.acme.sh/acme.sh fi # 测试脚本是否能运行 if ! ~/.acme.sh/acme.sh --version &> /dev/null; then print_error "acme.sh 无法运行" return 1 fi print_success "acme.sh 验证通过" # 申请证书 echo "" print_info "正在申请 ZeroSSL 证书..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi # 使用webroot模式申请证书(更可靠) if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then print_warning "检测到证书已存在,使用已有证书" print_success "将直接安装现有证书" else print_error "证书申请失败" return 1 fi fi # 安装证书 echo "" print_info "正在安装证书到Nginx..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi mkdir -p /etc/nginx/ssl # 确保nginx服务已启动(证书安装时需要reload) if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then print_warning "Nginx未运行,正在启动..." systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true sleep 2 fi # 先不带reload命令安装证书(避免nginx未启动导致失败) if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ --key-file /etc/nginx/ssl/${DOMAIN}.key \ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then print_success "证书文件已安装到: /etc/nginx/ssl/" # 手动reload nginx if systemctl is-active --quiet nginx 2>/dev/null; then systemctl reload nginx && print_success "Nginx配置已重载" elif pgrep -x nginx > /dev/null; then nginx -s reload && print_success "Nginx配置已重载" else print_warning "Nginx未运行,将在后续步骤启动" fi # 配置自动续期 setup_acme_auto_renew return 0 else print_error "证书安装失败" return 1 fi } deploy_acme_buypass() { print_step "使用 acme.sh + Buypass 部署SSL证书..." # 安装acme.sh(使用改进的安装逻辑) if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then echo "" print_info "正在安装 acme.sh..." # 如果目录存在但文件不存在,先清理 if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then print_warning "检测到不完整的安装,正在清理..." rm -rf ~/.acme.sh fi print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)" # 使用官方安装方法:直接通过curl管道执行 print_info "正在下载并安装..." if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then install_result=$? print_info "安装脚本执行完成,退出码: $install_result" else install_result=$? print_error "安装脚本执行失败,退出码: $install_result" fi # 重新加载环境变量 source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true # 等待文件系统同步 print_info "等待安装完成..." sleep 3 # 验证安装 if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then print_success "acme.sh 安装成功" else print_error "acme.sh 安装失败" echo "" print_warning "诊断信息:" echo " - 安装命令退出码: $install_result" echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')" echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')" echo "" if [[ -d ~/.acme.sh ]]; then print_info "~/.acme.sh 目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录" echo "" fi return 1 fi fi # 确认acme.sh可用 echo "" print_info "验证 acme.sh 安装..." # 等待文件系统同步 sleep 2 # 检查安装目录 if [[ ! -d ~/.acme.sh ]]; then print_error "安装目录不存在: ~/.acme.sh" return 1 fi # 检查主脚本文件 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "主脚本文件不存在: ~/.acme.sh/acme.sh" print_info "目录内容:" ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录" return 1 fi # 检查脚本是否可执行 if [[ ! -x ~/.acme.sh/acme.sh ]]; then print_warning "脚本不可执行,正在添加执行权限..." chmod +x ~/.acme.sh/acme.sh fi # 测试脚本是否能运行 if ! ~/.acme.sh/acme.sh --version &> /dev/null; then print_error "acme.sh 无法运行" return 1 fi print_success "acme.sh 验证通过" # 申请证书 echo "" print_info "正在申请 Buypass 证书..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi # 使用webroot模式申请证书(更可靠) if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then print_success "证书申请成功" else # 检查是否是因为证书已存在 if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then print_warning "检测到证书已存在,使用已有证书" print_success "将直接安装现有证书" else print_error "证书申请失败" return 1 fi fi # 安装证书 echo "" print_info "正在安装证书到Nginx..." # 再次确认acme.sh存在 if [[ ! -f ~/.acme.sh/acme.sh ]]; then print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh" return 1 fi mkdir -p /etc/nginx/ssl # 确保nginx服务已启动(证书安装时需要reload) if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then print_warning "Nginx未运行,正在启动..." systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true sleep 2 fi # 先不带reload命令安装证书(避免nginx未启动导致失败) if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \ --key-file /etc/nginx/ssl/${DOMAIN}.key \ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then print_success "证书文件已安装到: /etc/nginx/ssl/" # 手动reload nginx if systemctl is-active --quiet nginx 2>/dev/null; then systemctl reload nginx && print_success "Nginx配置已重载" elif pgrep -x nginx > /dev/null; then nginx -s reload && print_success "Nginx配置已重载" else print_warning "Nginx未运行,将在后续步骤启动" fi # 配置自动续期 setup_acme_auto_renew return 0 else print_error "证书安装失败" return 1 fi } deploy_aliyun_ssl() { print_step "使用阿里云免费证书..." print_warning "此功能需要您提供阿里云AccessKey" echo "" read -p "阿里云AccessKey ID: " ALIYUN_ACCESS_KEY_ID < /dev/tty read -p "阿里云AccessKey Secret: " ALIYUN_ACCESS_KEY_SECRET < /dev/tty # 这里需要调用阿里云API申请证书 # 暂时返回失败,提示用户使用其他方案 print_error "阿里云证书申请功能开发中,请选择其他方案" return 1 } deploy_tencent_ssl() { print_step "使用腾讯云免费证书..." print_warning "此功能需要您提供腾讯云SecretKey" echo "" read -p "腾讯云SecretId: " TENCENT_SECRET_ID < /dev/tty read -p "腾讯云SecretKey: " TENCENT_SECRET_KEY < /dev/tty # 这里需要调用腾讯云API申请证书 # 暂时返回失败,提示用户使用其他方案 print_error "腾讯云证书申请功能开发中,请选择其他方案" return 1 } deploy_manual_ssl() { print_step "使用已有证书..." echo "" print_info "请将以下文件上传到服务器:" print_info "- 证书文件: /tmp/ssl_cert.crt" print_info "- 私钥文件: /tmp/ssl_key.key" echo "" read -p "上传完成后按回车继续..." < /dev/tty if [[ -f /tmp/ssl_cert.crt ]] && [[ -f /tmp/ssl_key.key ]]; then mkdir -p /etc/nginx/ssl cp /tmp/ssl_cert.crt /etc/nginx/ssl/${DOMAIN}.crt cp /tmp/ssl_key.key /etc/nginx/ssl/${DOMAIN}.key chmod 600 /etc/nginx/ssl/${DOMAIN}.key print_success "证书文件已复制" return 0 else print_error "证书文件未找到" return 1 fi } ################################################################################ # 项目部署 ################################################################################ create_project_directory() { print_step "创建项目目录..." if [[ -d "$PROJECT_DIR" ]]; then print_warning "项目目录已存在" read -p "是否删除并重新创建? (y/n): " recreate < /dev/tty if [[ "$recreate" == "y" || "$recreate" == "Y" ]]; then rm -rf "$PROJECT_DIR" else print_error "部署已取消" exit 1 fi fi mkdir -p "$PROJECT_DIR" print_success "项目目录已创建: $PROJECT_DIR" echo "" } download_project() { print_step "正在从Gitee下载项目..." cd /tmp if [[ -d "${PROJECT_NAME}" ]]; then rm -rf "${PROJECT_NAME}" fi git clone "$REPO_URL" "${PROJECT_NAME}" # 复制文件到项目目录 cp -r "/tmp/${PROJECT_NAME}"/* "$PROJECT_DIR/" # 清理临时文件 rm -rf "/tmp/${PROJECT_NAME}" print_success "项目下载完成" echo "" } configure_admin_account() { print_step "配置管理员账号" echo "" while true; do read -p "管理员用户名 [默认: admin]: " ADMIN_USERNAME < /dev/tty ADMIN_USERNAME=${ADMIN_USERNAME:-admin} if [[ ${#ADMIN_USERNAME} -lt 3 ]]; then print_error "用户名至少3个字符" continue fi break done while true; do read -s -p "管理员密码(至少6位): " ADMIN_PASSWORD < /dev/tty echo "" if [[ ${#ADMIN_PASSWORD} -lt 6 ]]; then print_error "密码至少6个字符" continue fi read -s -p "确认密码: " ADMIN_PASSWORD_CONFIRM < /dev/tty echo "" if [[ "$ADMIN_PASSWORD" != "$ADMIN_PASSWORD_CONFIRM" ]]; then print_error "两次密码不一致" continue fi break done print_success "管理员账号配置完成" echo "" } install_backend_dependencies() { print_step "安装后端依赖..." cd "${PROJECT_DIR}/backend" # 确保Python可用(node-gyp需要) if ! command -v python &> /dev/null; then if command -v python3 &> /dev/null; then # 创建python软链接指向python3 if [[ "$OS" == "ubuntu" ]] || [[ "$OS" == "debian" ]]; then ln -sf /usr/bin/python3 /usr/bin/python || true else # CentOS/RHEL/其他系统 alternatives --install /usr/bin/python python /usr/bin/python3 1 &> /dev/null || \ ln -sf /usr/bin/python3 /usr/bin/python || true fi print_info "已配置Python环境(python -> python3)" else print_warning "未找到Python,某些依赖可能需要手动处理" fi fi # 使用国内镜像加速 if [[ "$USE_ALIYUN_MIRROR" == "true" ]]; then npm config set registry https://registry.npmmirror.com fi print_info "正在安装依赖包(包含数据库native模块,可能需要几分钟)..." # 安装依赖,捕获错误 if PYTHON=python3 npm install --production; then print_success "后端依赖安装完成" else print_error "依赖安装失败" echo "" print_warning "可能的解决方案:" echo " 1. 检查网络连接" echo " 2. 手动执行: cd ${PROJECT_DIR}/backend && npm install --production" echo " 3. 查看详细错误日志: ~/.npm/_logs/" echo "" # 询问是否继续 read -p "是否忽略错误继续安装?(y/n): " continue_install < /dev/tty if [[ "$continue_install" != "y" ]] && [[ "$continue_install" != "Y" ]]; then exit 1 fi fi echo "" } create_env_file() { print_step "创建配置文件..." # 生成随机JWT密钥 JWT_SECRET=$(openssl rand -base64 32) # ========== CORS 安全配置自动生成 ========== # 根据部署模式自动配置 ALLOWED_ORIGINS 和 COOKIE_SECURE if [[ "$USE_DOMAIN" == "true" ]]; then # 域名模式 if [[ "$SSL_METHOD" == "8" || -z "$SSL_METHOD" ]]; then # HTTP 模式 PROTOCOL="http" COOKIE_SECURE_VALUE="false" PORT_VALUE=${HTTP_PORT:-80} ENFORCE_HTTPS_VALUE="false" else # HTTPS 模式 PROTOCOL="https" COOKIE_SECURE_VALUE="true" PORT_VALUE=${HTTPS_PORT:-443} ENFORCE_HTTPS_VALUE="true" fi # 生成 ALLOWED_ORIGINS (标准端口不需要显示端口号) if [[ "$PORT_VALUE" == "80" ]] || [[ "$PORT_VALUE" == "443" ]]; then ALLOWED_ORIGINS_VALUE="${PROTOCOL}://${DOMAIN}" else ALLOWED_ORIGINS_VALUE="${PROTOCOL}://${DOMAIN}:${PORT_VALUE}" fi print_info "CORS 配置: ${ALLOWED_ORIGINS_VALUE}" else # IP 模式(开发/测试环境) # 留空,后端默认允许所有来源(适合开发环境) ALLOWED_ORIGINS_VALUE="" COOKIE_SECURE_VALUE="false" ENFORCE_HTTPS_VALUE="false" print_warning "IP 模式下 CORS 将允许所有来源(仅适合开发环境)" print_info "生产环境建议使用域名模式" fi cat > "${PROJECT_DIR}/backend/.env" << EOF # 管理员账号 ADMIN_USERNAME=${ADMIN_USERNAME} ADMIN_PASSWORD=${ADMIN_PASSWORD} # JWT密钥 JWT_SECRET=${JWT_SECRET} # 数据库路径 DATABASE_PATH=./data/database.db # 存储目录 STORAGE_ROOT=./storage # 服务端口 PORT=${BACKEND_PORT} # 环境 NODE_ENV=production # 强制HTTPS(生产环境建议开启) ENFORCE_HTTPS=${ENFORCE_HTTPS_VALUE} # CORS 跨域配置 # 允许访问的前端域名(多个用逗号分隔) # 生产环境必须配置具体域名,开发环境可留空 ALLOWED_ORIGINS=${ALLOWED_ORIGINS_VALUE} # Cookie 安全配置 # HTTPS 环境必须设置为 true COOKIE_SECURE=${COOKIE_SECURE_VALUE} # 信任代理配置(重要安全配置) # 在 Nginx/CDN 后部署时必须配置,否则无法正确识别客户端 IP 和协议 # 配置选项: # - false: 不信任代理(直接暴露,默认值) # - 1: 信任前 1 跳代理(单层 Nginx,推荐) # - 2: 信任前 2 跳代理(CDN + Nginx) # - loopback: 仅信任本地回环地址 # 警告:不要设置为 true,这会信任所有代理,存在 IP/协议伪造风险! TRUST_PROXY=1 # 公开端口(nginx监听的端口,用于生成分享链接) # 如果使用标准端口(80/443)或未配置,分享链接将不包含端口号 PUBLIC_PORT=${HTTP_PORT} EOF print_success "配置文件创建完成" # 显示安全提示 if [[ -z "$ALLOWED_ORIGINS_VALUE" ]]; then echo "" print_warning "⚠️ 安全提示:" print_info " 当前配置允许所有域名访问(CORS: *)" print_info " 这仅适合开发环境,生产环境存在安全风险" print_info " 建议在生产环境使用域名模式部署" echo "" fi echo "" } create_data_directories() { print_step "创建数据目录..." mkdir -p "${PROJECT_DIR}/backend/data" mkdir -p "${PROJECT_DIR}/backend/storage" print_success "数据目录创建完成" echo "" } build_upload_tool() { print_step "下载上传工具..." cd "${PROJECT_DIR}/upload-tool" # 检查是否已存在可执行文件并验证大小 if [[ -f "dist/玩玩云上传工具.exe" ]]; then FILE_SIZE=$(stat -f%z "dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "dist/玩玩云上传工具.exe" 2>/dev/null || echo "0") if [[ $FILE_SIZE -gt 30000000 ]]; then FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 )) print_success "上传工具已存在(${FILE_SIZE_MB}MB),跳过下载" echo "" return 0 else print_warning "现有文件大小异常(${FILE_SIZE}字节),重新下载..." rm -f "dist/玩玩云上传工具.exe" fi fi # 创建dist目录 mkdir -p dist # 下载地址(Windows版本) TOOL_DOWNLOAD_URL="http://a.haory.top/e/e82/玩玩云上传工具.exe" TOOL_FILENAME="玩玩云上传工具.exe" print_info "正在下载上传工具(约43MB,可能需要1-2分钟)..." # 尝试下载(最多3次重试) DOWNLOAD_SUCCESS=false for attempt in 1 2 3; do print_info "尝试下载 ($attempt/3)..." if command -v wget &> /dev/null; then # wget: 超时300秒,重试3次 if wget --timeout=300 --tries=3 --no-check-certificate -q --show-progress -O "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then DOWNLOAD_SUCCESS=true break fi elif command -v curl &> /dev/null; then # curl: 连接超时60秒,总超时300秒 if curl --connect-timeout 60 --max-time 300 -L -# -o "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then DOWNLOAD_SUCCESS=true break fi else print_warning "未找到wget或curl,无法下载上传工具" print_info "用户仍可使用网页上传(本地存储)或SFTP客户端" echo "" return 0 fi # 如果不是最后一次尝试,等待后重试 if [[ $attempt -lt 3 ]]; then print_warning "下载失败,5秒后重试..." sleep 5 fi done # 验证下载结果 if [[ "$DOWNLOAD_SUCCESS" == "true" ]] && [[ -f "dist/${TOOL_FILENAME}" ]]; then FILE_SIZE=$(stat -f%z "dist/${TOOL_FILENAME}" 2>/dev/null || stat -c%s "dist/${TOOL_FILENAME}" 2>/dev/null || echo "0") FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 )) if [[ $FILE_SIZE -gt 30000000 ]]; then print_success "上传工具下载完成: ${FILE_SIZE_MB}MB" echo "" else print_error "下载的文件大小异常(${FILE_SIZE}字节),可能下载不完整" rm -f "dist/${TOOL_FILENAME}" print_warning "可手动下载: ${TOOL_DOWNLOAD_URL}" print_info "用户仍可使用网页上传(本地存储)或SFTP客户端" echo "" fi else print_error "上传工具下载失败(已重试3次)" print_warning "可能的原因:" echo " 1. 网络连接问题或下载速度过慢" echo " 2. CDN链接不可访问: ${TOOL_DOWNLOAD_URL}" echo " 3. 防火墙拦截HTTP连接" print_info "您可以稍后手动下载并放置到: ${PROJECT_DIR}/upload-tool/dist/" print_info "用户仍可使用网页上传(本地存储)或SFTP客户端" echo "" fi } ################################################################################ # Nginx配置 - 分步骤执行 ################################################################################ # 安全重启/重载Nginx(带回退) restart_nginx_safe() { print_info "尝试重启/重载 Nginx..." # 如果有systemd并存在nginx服务,优先使用(不强制重启,避免80/443冲突) if command -v systemctl &> /dev/null && systemctl list-unit-files | grep -q "^nginx.service"; then if systemctl is-active --quiet nginx; then if systemctl reload nginx 2>/dev/null; then print_success "已通过 systemctl reload 重载 Nginx" return 0 fi else if systemctl start nginx 2>/dev/null; then print_success "已通过 systemctl start 启动 Nginx" return 0 else print_warning "systemctl 启动失败,可能端口被占用或已有其他反代在跑" fi fi fi # 宝塔路径优先尝试 if [[ -x /www/server/nginx/sbin/nginx ]]; then if /www/server/nginx/sbin/nginx -t 2>/dev/null; then if /www/server/nginx/sbin/nginx -s reload 2>/dev/null; then print_success "已通过宝塔 nginx -s reload 重载" return 0 fi if /www/server/nginx/sbin/nginx 2>/dev/null; then print_success "已通过宝塔 nginx 启动" return 0 fi else print_error "宝塔 Nginx 配置测试失败" /www/server/nginx/sbin/nginx -t 2>&1 || true fi fi # 直接使用nginx命令 if command -v nginx &> /dev/null; then if ! nginx -t 2>/dev/null; then print_error "nginx -t 配置测试失败,请检查配置" nginx -t 2>&1 || true return 1 fi if nginx -s reload 2>/dev/null; then print_success "已使用 nginx -s reload 重载配置" return 0 fi # reload失败,尝试直接启动 if nginx 2>/dev/null; then print_success "已直接启动 Nginx" return 0 fi fi print_error "未能成功启动/重载 Nginx,请手动检查(端口占用/安装状态)" return 1 } # 确保已安装Nginx(更新/修复模式下可能未安装) ensure_nginx_installed() { if command -v nginx &> /dev/null || [[ -x /www/server/nginx/sbin/nginx ]]; then return 0 fi print_warning "未检测到 Nginx。若你已有其它反向代理占用80/443,可跳过安装并手动配置;如需本脚本自动配置,请先安装Nginx后再运行。" return 0 } # 步骤1: 先配置HTTP Nginx(为SSL证书验证做准备) configure_nginx_http_first() { print_step "配置基础HTTP Nginx(用于SSL证书验证)..." # 确保已安装Nginx ensure_nginx_installed || return 1 # 总是先配置HTTP模式 local server_name="${DOMAIN:-_}" # 检测Nginx配置目录结构并创建必要的目录 if [[ -d /www/server/nginx ]]; then # 宝塔面板 (BT Panel) NGINX_CONF_DIR="/www/server/panel/vhost/nginx" NGINX_ENABLED_DIR="" USE_SYMLINK=false IS_BT_PANEL=true # 确保目录存在 mkdir -p ${NGINX_CONF_DIR} print_info "检测到宝塔面板,使用宝塔Nginx配置目录" elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then # Debian/Ubuntu: 使用sites-available NGINX_CONF_DIR="/etc/nginx/sites-available" NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" USE_SYMLINK=true IS_BT_PANEL=false # 确保目录存在 mkdir -p ${NGINX_CONF_DIR} mkdir -p ${NGINX_ENABLED_DIR} else # CentOS/RHEL: 使用conf.d NGINX_CONF_DIR="/etc/nginx/conf.d" NGINX_ENABLED_DIR="" USE_SYMLINK=false IS_BT_PANEL=false # 确保目录存在 mkdir -p ${NGINX_CONF_DIR} fi cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF server { listen ${HTTP_PORT}; server_name ${server_name}; # 文件上传大小限制(10GB) client_max_body_size 10G; # ========== 安全响应头 ========== add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # 隐藏Nginx版本号 server_tokens off; # ========== 禁止访问隐藏文件 ========== location ~ /\\. { deny all; return 404; } # ========== 禁止访问敏感文件 ========== location ~ \\.(env|git|config|key|pem|crt|sql|bak|backup|old|log)$ { deny all; return 404; } # 前端静态文件 location / { root ${PROJECT_DIR}/frontend; index index.html; try_files \$uri \$uri/ /index.html; } # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; proxy_pass_header Set-Cookie; # 上传超时设置 proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 300s; } # 分享页面 location /s/ { proxy_pass http://localhost:${BACKEND_PORT}; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # 静态资源 location /libs { alias ${PROJECT_DIR}/frontend/libs; expires 30d; } # 上传工具下载 location /download-tool { alias ${PROJECT_DIR}/upload-tool/dist; } } EOF # 根据系统类型处理配置文件 if [[ "$USE_SYMLINK" == "true" ]]; then # Debian/Ubuntu: 创建软链接 ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf # 删除默认站点 rm -f ${NGINX_ENABLED_DIR}/default elif [[ "$IS_BT_PANEL" != "true" ]]; then # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 rm -f /etc/nginx/conf.d/default.conf fi # 宝塔面板:配置文件已自动包含,无需额外操作 # 测试nginx配置 if ! nginx -t; then print_error "Nginx配置测试失败" return 1 fi # 启动或重载Nginx if [[ "$IS_BT_PANEL" == "true" ]]; then # 宝塔面板:尝试多种方式 print_info "宝塔环境,尝试重载Nginx..." # 优先使用最可靠的方式: 直接使用nginx命令reload if [[ -f /www/server/nginx/sbin/nginx ]]; then # 先测试配置 if /www/server/nginx/sbin/nginx -t 2>/dev/null; then # 配置测试通过,尝试reload if /www/server/nginx/sbin/nginx -s reload 2>/dev/null; then print_success "已使用nginx -s reload重载配置" else # reload失败,尝试重启 print_warning "reload失败,尝试重启Nginx..." /www/server/nginx/sbin/nginx -s stop 2>/dev/null || true sleep 2 if /www/server/nginx/sbin/nginx 2>/dev/null; then print_success "Nginx已重新启动" else print_error "Nginx启动失败,请手动检查" # 不退出脚本,继续后续步骤 fi fi else print_error "Nginx配置测试失败" # 显示配置错误但不退出脚本 /www/server/nginx/sbin/nginx -t 2>&1 || true fi fi # 备用方式: 尝试systemctl(某些宝塔环境也支持) if systemctl is-active --quiet nginx 2>/dev/null; then systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置" || true fi else # 标准Nginx:重启(带回退) restart_nginx_safe || return 1 fi # 验证Nginx是否运行 sleep 2 if [[ "$IS_BT_PANEL" == "true" ]]; then # 宝塔:检查进程 if pgrep -x nginx > /dev/null; then print_success "Nginx运行正常" else print_error "Nginx未运行" print_warning "请在宝塔面板中手动启动Nginx,或运行:" print_warning "/www/server/nginx/sbin/nginx" return 1 fi else # 标准Nginx:使用systemctl检查 if command -v systemctl &> /dev/null && systemctl list-unit-files | grep -q "^nginx.service"; then if ! systemctl is-active --quiet nginx; then print_error "Nginx启动失败" return 1 fi elif ! pgrep -x nginx > /dev/null; then print_error "Nginx启动失败" return 1 fi fi print_success "基础HTTP Nginx配置完成" echo "" } # 步骤2: 根据SSL结果配置最终Nginx configure_nginx_final() { print_step "配置最终Nginx..." # 检查SSL是否成功部署 local ssl_deployed=false if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then # 检查SSL证书文件是否存在 if [[ -f /etc/nginx/ssl/${DOMAIN}.crt ]] && [[ -f /etc/nginx/ssl/${DOMAIN}.key ]]; then ssl_deployed=true print_info "检测到SSL证书,配置HTTPS..." else print_warning "SSL证书不存在,保持HTTP配置" fi fi # 根据SSL状态配置 if [[ "$ssl_deployed" == "true" ]]; then # 配置HTTPS configure_nginx_https else # 保持HTTP(已在第一步配置,这里只需确认) print_info "使用HTTP配置" fi # 测试nginx配置 if ! nginx -t; then print_error "Nginx配置测试失败" return 1 fi # 重载nginx - 兼容宝塔面板 if [[ "$IS_BT_PANEL" == "true" ]]; then # 宝塔面板:尝试多种方式 print_info "宝塔环境,重载Nginx配置..." # 优先使用最可靠的方式: 直接使用nginx命令reload if [[ -f /www/server/nginx/sbin/nginx ]]; then if /www/server/nginx/sbin/nginx -s reload 2>/dev/null; then print_success "已使用nginx -s reload重载配置" else # reload失败,尝试重启 print_warning "reload失败,尝试重启Nginx..." /www/server/nginx/sbin/nginx -s stop 2>/dev/null || true sleep 2 if /www/server/nginx/sbin/nginx 2>/dev/null; then print_success "已启动Nginx" else print_warning "Nginx启动失败,请手动检查" fi fi fi # 备用方式: 尝试systemctl if systemctl is-active --quiet nginx 2>/dev/null; then systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置" || true fi else # 标准Nginx:重载(带回退) restart_nginx_safe || return 1 fi print_success "Nginx最终配置完成" echo "" } configure_nginx() { print_step "配置Nginx..." if [[ "$USE_DOMAIN" == "true" ]]; then if [[ "$SSL_METHOD" == "8" ]]; then # HTTP配置 configure_nginx_http else # HTTPS配置 configure_nginx_https fi else # IP模式HTTP配置 configure_nginx_http fi # 测试nginx配置 nginx -t # 重启nginx restart_nginx_safe || return 1 print_success "Nginx配置完成" echo "" } configure_nginx_http() { local server_name="${DOMAIN:-_}" # 确保已安装Nginx ensure_nginx_installed || return 1 # 检测Nginx配置目录结构并创建必要的目录 if [[ -d /www/server/nginx ]]; then # 宝塔面板 NGINX_CONF_DIR="/www/server/panel/vhost/nginx" USE_SYMLINK=false IS_BT_PANEL=true mkdir -p ${NGINX_CONF_DIR} elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then # Debian/Ubuntu NGINX_CONF_DIR="/etc/nginx/sites-available" NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" USE_SYMLINK=true IS_BT_PANEL=false mkdir -p ${NGINX_CONF_DIR} mkdir -p ${NGINX_ENABLED_DIR} else # CentOS/RHEL NGINX_CONF_DIR="/etc/nginx/conf.d" USE_SYMLINK=false IS_BT_PANEL=false mkdir -p ${NGINX_CONF_DIR} fi cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF server { listen ${HTTP_PORT}; server_name ${server_name}; # 文件上传大小限制(10GB) client_max_body_size 10G; # ========== 安全响应头 ========== add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # 隐藏Nginx版本号 server_tokens off; # ========== 禁止访问隐藏文件 ========== location ~ /\\. { deny all; return 404; } # ========== 禁止访问敏感文件 ========== location ~ \\.(env|git|config|key|pem|crt|sql|bak|backup|old|log)$ { deny all; return 404; } # 前端静态文件 location / { root ${PROJECT_DIR}/frontend; index index.html; try_files \$uri \$uri/ /index.html; } # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; proxy_pass_header Set-Cookie; # 上传超时设置(大文件上传需要更长时间,设置为1小时) proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 300s; } # 分享页面(代理到后端处理重定向) location /s/ { proxy_pass http://localhost:${BACKEND_PORT}; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # 静态资源 location /libs { alias ${PROJECT_DIR}/frontend/libs; expires 30d; } # 上传工具下载 location /download-tool { alias ${PROJECT_DIR}/upload-tool/dist; } } EOF # 根据系统类型处理配置文件 if [[ "$USE_SYMLINK" == "true" ]]; then # Debian/Ubuntu: 创建软链接 ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf # 删除默认站点 rm -f ${NGINX_ENABLED_DIR}/default elif [[ "$IS_BT_PANEL" != "true" ]]; then # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 rm -f /etc/nginx/conf.d/default.conf fi } configure_nginx_https() { # 确保已安装Nginx ensure_nginx_installed || return 1 # 检测Nginx配置目录结构并创建必要的目录 if [[ -d /www/server/nginx ]]; then # 宝塔面板 NGINX_CONF_DIR="/www/server/panel/vhost/nginx" USE_SYMLINK=false IS_BT_PANEL=true mkdir -p ${NGINX_CONF_DIR} elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then # Debian/Ubuntu NGINX_CONF_DIR="/etc/nginx/sites-available" NGINX_ENABLED_DIR="/etc/nginx/sites-enabled" USE_SYMLINK=true IS_BT_PANEL=false mkdir -p ${NGINX_CONF_DIR} mkdir -p ${NGINX_ENABLED_DIR} else # CentOS/RHEL NGINX_CONF_DIR="/etc/nginx/conf.d" USE_SYMLINK=false IS_BT_PANEL=false mkdir -p ${NGINX_CONF_DIR} fi # 根据HTTPS端口生成正确的重定向URL if [[ "$HTTPS_PORT" == "443" ]]; then REDIRECT_URL="https://\$server_name\$request_uri" else REDIRECT_URL="https://\$server_name:${HTTPS_PORT}\$request_uri" fi cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF server { listen ${HTTP_PORT}; server_name ${DOMAIN}; return 301 ${REDIRECT_URL}; } server { listen ${HTTPS_PORT} ssl http2; server_name ${DOMAIN}; ssl_certificate /etc/nginx/ssl/${DOMAIN}.crt; ssl_certificate_key /etc/nginx/ssl/${DOMAIN}.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # 文件上传大小限制(10GB) client_max_body_size 10G; # ========== 安全响应头 ========== add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 隐藏Nginx版本号 server_tokens off; # ========== 禁止访问隐藏文件 ========== location ~ /\\. { deny all; return 404; } # ========== 禁止访问敏感文件 ========== location ~ \\.(env|git|config|key|pem|crt|sql|bak|backup|old|log)$ { deny all; return 404; } # 前端静态文件 location / { root ${PROJECT_DIR}/frontend; index index.html; try_files \$uri \$uri/ /index.html; } # 后端API location /api { proxy_pass http://localhost:${BACKEND_PORT}; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_cache_bypass \$http_upgrade; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # Cookie传递配置(验证码session需要) proxy_set_header Cookie \$http_cookie; proxy_pass_header Set-Cookie; # 上传超时设置(大文件上传需要更长时间,设置为1小时) proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 300s; } # 分享页面(代理到后端处理重定向) location /s/ { proxy_pass http://localhost:${BACKEND_PORT}; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # 静态资源 location /libs { alias ${PROJECT_DIR}/frontend/libs; expires 30d; } # 上传工具下载 location /download-tool { alias ${PROJECT_DIR}/upload-tool/dist; } } EOF # 根据系统类型处理配置文件 if [[ "$USE_SYMLINK" == "true" ]]; then # Debian/Ubuntu: 创建软链接 ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf # 删除默认站点 rm -f ${NGINX_ENABLED_DIR}/default elif [[ "$IS_BT_PANEL" != "true" ]]; then # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载 rm -f /etc/nginx/conf.d/default.conf fi } start_backend_service() { print_step "启动后端服务..." cd "${PROJECT_DIR}/backend" # 使用PM2启动 pm2 start server.js --name ${PROJECT_NAME}-backend pm2 save print_success "后端服务已启动" echo "" } ################################################################################ # 健康检查 ################################################################################ health_check() { print_step "正在进行健康检查..." sleep 3 # 检查后端服务 if pm2 status | grep -q "${PROJECT_NAME}-backend.*online"; then print_success "后端服务运行正常" else print_error "后端服务启动失败" print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend" return 1 fi # 检查端口 if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then print_success "后端端口监听正常 (${BACKEND_PORT})" else print_error "后端端口监听异常" return 1 fi # 检查Nginx if [[ -d /www/server/nginx ]]; then # 宝塔面板:检查进程 if pgrep -x nginx > /dev/null; then print_success "Nginx服务运行正常" else print_error "Nginx服务异常" return 1 fi else # 标准Nginx:使用systemctl检查 if systemctl is-active --quiet nginx; then print_success "Nginx服务运行正常" else print_error "Nginx服务异常" return 1 fi fi # 检查数据库 if [[ -f "${PROJECT_DIR}/backend/ftp-manager.db" ]]; then print_success "数据库初始化成功" else print_warning "数据库文件不存在" fi # 检查存储目录 if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then print_success "文件存储目录就绪" else print_warning "存储目录不存在" fi echo "" return 0 } ################################################################################ # 完成提示 ################################################################################ print_completion() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🎉 部署成功! ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" # 访问地址 if [[ "$USE_DOMAIN" == "true" ]]; then if [[ "$SSL_METHOD" == "8" ]]; then if [[ "$HTTP_PORT" == "80" ]]; then echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}" fi else if [[ "$HTTPS_PORT" == "443" ]]; then echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}" fi fi else PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP") if [[ "$HTTP_PORT" == "80" ]]; then echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}" else echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}" fi fi echo -e "${CYAN}管理员账号:${NC} ${ADMIN_USERNAME}" echo -e "${CYAN}管理员密码:${NC} ********" echo "" # 端口信息 if [[ "$HTTP_PORT" != "80" ]] || [[ "$HTTPS_PORT" != "443" ]] || [[ "$BACKEND_PORT" != "40001" ]]; then echo -e "${YELLOW}端口配置:${NC}" echo " HTTP端口: $HTTP_PORT" if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then echo " HTTPS端口: $HTTPS_PORT" fi echo " 后端端口: $BACKEND_PORT" echo "" fi # 常用命令 echo -e "${YELLOW}常用命令:${NC}" echo " 查看服务状态: pm2 status" echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend" echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend" echo " 停止服务: pm2 stop ${PROJECT_NAME}-backend" echo "" # 配置文件位置 echo -e "${YELLOW}配置文件位置:${NC}" echo " 后端配置: ${PROJECT_DIR}/backend/.env" echo " Nginx配置: /etc/nginx/sites-enabled/${PROJECT_NAME}.conf" echo " 数据库: ${PROJECT_DIR}/backend/data/database.db" echo " 文件存储: ${PROJECT_DIR}/backend/storage" echo "" # SSL续期提示 if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then echo -e "${YELLOW}SSL证书自动续期:${NC}" echo " - 方式: acme.sh cron任务" echo " - 频率: 每天自动检查" echo " - 时机: 证书到期前30天自动续期" echo " - 检查任务: crontab -l | grep acme" echo " - 查看证书: ~/.acme.sh/acme.sh --list" echo " - 手动续期: ~/.acme.sh/acme.sh --renew -d $DOMAIN --force" echo "" fi echo -e "${GREEN}祝您使用愉快!${NC}" echo "" } ################################################################################ # 卸载功能 ################################################################################ print_uninstall_banner() { clear echo -e "${RED}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ⚠️ 玩玩云 卸载模式 ║" echo "║ ║" echo "║ Uninstall Mode ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" } confirm_uninstall() { print_uninstall_banner echo -e "${YELLOW}" echo "本脚本将执行以下操作:" echo "" echo "【将要删除】" echo " ✓ PM2 进程: ${PROJECT_NAME}-backend" echo " ✓ 项目目录: ${PROJECT_DIR}" echo " ✓ Nginx 配置: /etc/nginx/sites-enabled/${PROJECT_NAME}.conf" echo " ✓ 数据库文件: ${PROJECT_DIR}/backend/data/" echo " ✓ 用户文件: ${PROJECT_DIR}/backend/storage/" echo "" echo "【将会保留】" echo " ✓ Node.js" echo " ✓ Nginx (程序本身)" echo " ✓ PM2 (程序本身)" echo " ✓ 编译工具 (build-essential等)" echo -e "${NC}" echo "" print_warning "此操作不可逆,所有数据将被永久删除!" echo "" read -p "确定要卸载吗? (yes/no): " confirm < /dev/tty if [[ "$confirm" != "yes" ]]; then print_info "已取消卸载" exit 0 fi echo "" read -p "请再次确认 (yes/no): " confirm2 < /dev/tty if [[ "$confirm2" != "yes" ]]; then print_info "已取消卸载" exit 0 fi echo "" } uninstall_backup_data() { print_step "备份用户数据..." if [[ ! -d "$PROJECT_DIR" ]]; then print_info "项目目录不存在,跳过备份" return fi BACKUP_DIR="/root/${PROJECT_NAME}-backup-$(date +%Y%m%d-%H%M%S)" echo "" read -p "是否备份用户数据? (y/n): " backup_choice < /dev/tty if [[ "$backup_choice" == "y" || "$backup_choice" == "Y" ]]; then mkdir -p "$BACKUP_DIR" # 备份数据库 if [[ -d "${PROJECT_DIR}/backend/data" ]]; then cp -r "${PROJECT_DIR}/backend/data" "$BACKUP_DIR/" print_success "数据库已备份" fi # 备份用户文件 if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then cp -r "${PROJECT_DIR}/backend/storage" "$BACKUP_DIR/" print_success "用户文件已备份" fi # 备份配置文件 if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then cp "${PROJECT_DIR}/backend/.env" "$BACKUP_DIR/" print_success "配置文件已备份" fi print_success "备份已保存到: $BACKUP_DIR" echo "" else print_warning "跳过备份,数据将被永久删除" echo "" fi } uninstall_stop_pm2() { print_step "停止PM2进程..." if command -v pm2 &> /dev/null; then if pm2 list | grep -q "${PROJECT_NAME}-backend"; then pm2 delete ${PROJECT_NAME}-backend pm2 save --force print_success "PM2进程已停止并删除" else print_info "PM2进程不存在,跳过" fi else print_info "PM2未安装,跳过" fi } uninstall_nginx_config() { print_step "删除Nginx配置..." local need_reload=false # 删除sites-enabled软链接 if [[ -L /etc/nginx/sites-enabled/${PROJECT_NAME}.conf ]]; then rm -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf print_success "删除 sites-enabled 配置" need_reload=true fi # 删除sites-available配置文件 if [[ -f /etc/nginx/sites-available/${PROJECT_NAME}.conf ]]; then rm -f /etc/nginx/sites-available/${PROJECT_NAME}.conf print_success "删除 sites-available 配置" fi # 测试并重载nginx if [[ "$need_reload" == true ]] && command -v nginx &> /dev/null; then if nginx -t &> /dev/null; then systemctl reload nginx print_success "Nginx配置已重载" else print_warning "Nginx配置测试失败,请手动检查" fi fi } uninstall_ssl_certificates() { print_step "清理SSL证书..." # 删除nginx SSL证书目录下的项目证书 local cert_removed=false if [[ -d /etc/nginx/ssl ]]; then # 使用find查找包含项目名或域名的证书 find /etc/nginx/ssl -name "*${PROJECT_NAME}*" -type f -delete 2>/dev/null && cert_removed=true fi if [[ "$cert_removed" == true ]]; then print_success "已删除Nginx SSL证书" else print_info "未发现Nginx SSL证书" fi # 提示acme.sh证书 if [[ -d ~/.acme.sh ]]; then if ~/.acme.sh/acme.sh --list 2>/dev/null | grep -q "Main_Domain"; then print_info "检测到acme.sh证书" print_warning "如需删除,请手动运行: ~/.acme.sh/acme.sh --remove -d " fi fi } uninstall_project_directory() { print_step "删除项目目录..." if [[ -d "$PROJECT_DIR" ]]; then # 统计大小 SIZE=$(du -sh "$PROJECT_DIR" 2>/dev/null | cut -f1 || echo "未知") print_info "项目目录大小: $SIZE" # 删除目录 rm -rf "$PROJECT_DIR" print_success "项目目录已删除: $PROJECT_DIR" else print_info "项目目录不存在: $PROJECT_DIR" fi } uninstall_temp_files() { print_step "清理临时文件..." local cleaned=false # 清理/tmp下的临时文件 if [[ -d "/tmp/${PROJECT_NAME}" ]]; then rm -rf "/tmp/${PROJECT_NAME}" print_success "删除临时目录" cleaned=true fi # 清理npm缓存 if command -v npm &> /dev/null; then npm cache clean --force &> /dev/null || true print_success "清理npm缓存" cleaned=true fi if [[ "$cleaned" == false ]]; then print_info "无需清理" fi } uninstall_check_residual() { print_step "检查残留文件..." local residual_found=false # 检查项目目录 if [[ -d "$PROJECT_DIR" ]]; then print_warning "残留: 项目目录 $PROJECT_DIR" residual_found=true fi # 检查Nginx配置 if [[ -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf ]] || [[ -f /etc/nginx/sites-available/${PROJECT_NAME}.conf ]]; then print_warning "残留: Nginx配置文件" residual_found=true fi # 检查PM2进程 if command -v pm2 &> /dev/null; then if pm2 list | grep -q "${PROJECT_NAME}"; then print_warning "残留: PM2进程" residual_found=true fi fi if [[ "$residual_found" == false ]]; then print_success "未发现残留文件" fi } print_uninstall_completion() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ✓ 卸载完成! ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" echo -e "${CYAN}已删除内容:${NC}" echo " ✓ PM2进程" echo " ✓ 项目目录" echo " ✓ Nginx配置" echo " ✓ 数据库和用户文件" echo "" echo -e "${CYAN}保留的环境:${NC}" echo " ✓ Node.js $(node -v 2>/dev/null || echo '(未安装)')" echo " ✓ Nginx $(nginx -v 2>&1 | cut -d'/' -f2 || echo '(未安装)')" echo " ✓ PM2 $(pm2 -v 2>/dev/null || echo '(未安装)')" echo " ✓ 编译工具 (build-essential 等)" echo "" if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then echo -e "${YELLOW}备份位置:${NC}" echo " $BACKUP_DIR" echo "" fi echo -e "${GREEN}感谢使用玩玩云!${NC}" echo "" } ################################################################################ # 更新功能 ################################################################################ print_update_banner() { clear echo -e "${BLUE}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🔄 玩玩云 更新模式 ║" echo "║ ║" echo "║ Update Mode ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" } confirm_update() { print_update_banner echo -e "${YELLOW}" echo "本脚本将执行以下操作:" echo "" echo "【将要更新】" echo " ✓ 从Gitee拉取最新代码" echo " ✓ 更新后端依赖(npm install)" echo " ✓ 重启后端服务" echo "" echo "【将会保留】" echo " ✓ 数据库文件(用户数据)" echo " ✓ 用户上传的文件" echo " ✓ .env 配置文件" echo " ✓ Nginx 配置" echo -e "${NC}" echo "" print_info "建议在更新前备份重要数据" echo "" read -p "确定要更新吗? (y/n): " confirm < /dev/tty if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then print_info "已取消更新" exit 0 fi echo "" } update_check_project() { print_step "检查项目是否已安装..." if [[ ! -d "$PROJECT_DIR" ]]; then print_error "项目未安装: $PROJECT_DIR" print_info "请先运行安装命令" exit 1 fi if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then print_error "项目目录不完整" exit 1 fi print_success "项目已安装: $PROJECT_DIR" } update_backup_important_files() { print_step "备份重要文件..." TEMP_BACKUP="/tmp/${PROJECT_NAME}-update-backup-$(date +%Y%m%d-%H%M%S)" mkdir -p "$TEMP_BACKUP" # 备份数据库 if [[ -d "${PROJECT_DIR}/backend/data" ]]; then cp -r "${PROJECT_DIR}/backend/data" "$TEMP_BACKUP/" print_success "数据库已备份" fi # 备份用户文件 if [[ -d "${PROJECT_DIR}/backend/storage" ]]; then cp -r "${PROJECT_DIR}/backend/storage" "$TEMP_BACKUP/" print_success "用户文件已备份" fi # 备份配置文件 if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then cp "${PROJECT_DIR}/backend/.env" "$TEMP_BACKUP/" print_success "配置文件已备份" fi print_success "备份完成: $TEMP_BACKUP" echo "" } update_stop_services() { print_step "停止服务..." if command -v pm2 &> /dev/null; then if pm2 list | grep -q "${PROJECT_NAME}-backend"; then pm2 stop ${PROJECT_NAME}-backend print_success "后端服务已停止" fi fi } update_pull_latest_code() { print_step "正在从Gitee拉取最新代码..." cd /tmp if [[ -d "${PROJECT_NAME}-update" ]]; then rm -rf "${PROJECT_NAME}-update" fi # 克隆最新代码 git clone "$REPO_URL" "${PROJECT_NAME}-update" # 更新前端文件 print_info "更新前端文件..." if [[ -d "/tmp/${PROJECT_NAME}-update/frontend" ]]; then rm -rf "${PROJECT_DIR}/frontend" cp -r "/tmp/${PROJECT_NAME}-update/frontend" "${PROJECT_DIR}/" fi # 更新上传工具 - 询问用户是否保留 if [[ -d "/tmp/${PROJECT_NAME}-update/upload-tool" ]]; then # 检查是否已存在上传工具可执行文件 if [[ -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" ]]; then FILE_SIZE=$(stat -f%z "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || echo "0") if [[ $FILE_SIZE -gt 30000000 ]]; then FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 )) echo "" print_info "检测到已存在上传工具(${FILE_SIZE_MB}MB)" echo "" echo "╔════════════════════════════════════════════════════════════╗" echo "║ 上传工具更新选项 ║" echo "╠════════════════════════════════════════════════════════════╣" echo "║ 1) 保留现有上传工具(推荐,节省下载时间) ║" echo "║ 2) 删除并重新下载(如果工具有更新) ║" echo "╚════════════════════════════════════════════════════════════╝" echo "" # 强制从终端读取用户输入 read -p "▶ 请选择 [1/2, 默认:1]: " KEEP_UPLOAD_TOOL < /dev/tty KEEP_UPLOAD_TOOL=${KEEP_UPLOAD_TOOL:-1} if [[ "$KEEP_UPLOAD_TOOL" == "1" ]]; then print_success "保留现有上传工具" # 只更新upload-tool目录的脚本文件,保留dist目录 mkdir -p "${PROJECT_DIR}/upload-tool-temp" cp -r "${PROJECT_DIR}/upload-tool/dist" "${PROJECT_DIR}/upload-tool-temp/" rm -rf "${PROJECT_DIR}/upload-tool" cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/" rm -rf "${PROJECT_DIR}/upload-tool/dist" mv "${PROJECT_DIR}/upload-tool-temp/dist" "${PROJECT_DIR}/upload-tool/" rm -rf "${PROJECT_DIR}/upload-tool-temp" print_success "已保留现有上传工具,仅更新脚本文件" else print_info "将删除现有工具并在后续步骤重新下载..." rm -rf "${PROJECT_DIR}/upload-tool" cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/" # 删除dist目录以触发后续重新下载 rm -rf "${PROJECT_DIR}/upload-tool/dist" fi else print_warning "现有上传工具文件大小异常,将重新下载..." rm -rf "${PROJECT_DIR}/upload-tool" cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/" fi else # 不存在上传工具,直接复制 rm -rf "${PROJECT_DIR}/upload-tool" cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/" fi fi # 更新后端代码文件(但不覆盖 data、storage、.env) print_info "更新后端代码..." if [[ -d "/tmp/${PROJECT_NAME}-update/backend" ]]; then cd "/tmp/${PROJECT_NAME}-update/backend" # 复制后端文件,但排除 data、storage、.env、node_modules for item in *; do if [[ "$item" != "data" ]] && [[ "$item" != "storage" ]] && [[ "$item" != ".env" ]] && [[ "$item" != "node_modules" ]]; then rm -rf "${PROJECT_DIR}/backend/$item" cp -r "$item" "${PROJECT_DIR}/backend/" fi done fi # 确保备份的重要文件存在 print_info "确保数据完整性..." # 如果 data 目录不存在,从备份恢复 if [[ ! -d "${PROJECT_DIR}/backend/data" ]] && [[ -d "$TEMP_BACKUP/data" ]]; then print_warning "检测到 data 目录丢失,正在从备份恢复..." cp -r "$TEMP_BACKUP/data" "${PROJECT_DIR}/backend/" print_success "数据库已恢复" fi # 如果 storage 目录不存在,从备份恢复 if [[ ! -d "${PROJECT_DIR}/backend/storage" ]] && [[ -d "$TEMP_BACKUP/storage" ]]; then print_warning "检测到 storage 目录丢失,正在从备份恢复..." cp -r "$TEMP_BACKUP/storage" "${PROJECT_DIR}/backend/" print_success "用户文件已恢复" fi # 如果 .env 文件不存在,从备份恢复 if [[ ! -f "${PROJECT_DIR}/backend/.env" ]] && [[ -f "$TEMP_BACKUP/.env" ]]; then print_warning "检测到 .env 文件丢失,正在从备份恢复..." cp "$TEMP_BACKUP/.env" "${PROJECT_DIR}/backend/" print_success "配置文件已恢复" fi # 清理临时文件 rm -rf "/tmp/${PROJECT_NAME}-update" rm -rf "$TEMP_BACKUP" print_success "代码更新完成" echo "" } update_install_dependencies() { print_step "更新后端依赖..." cd "${PROJECT_DIR}/backend" # 确保Python可用(node-gyp需要) if ! command -v python &> /dev/null; then if command -v python3 &> /dev/null; then if [[ "$OS" == "ubuntu" ]] || [[ "$OS" == "debian" ]]; then ln -sf /usr/bin/python3 /usr/bin/python || true else alternatives --install /usr/bin/python python /usr/bin/python3 1 &> /dev/null || \ ln -sf /usr/bin/python3 /usr/bin/python || true fi print_info "已配置Python环境" fi fi # 使用国内镜像加速(如果之前选择了) if command -v npm &> /dev/null; then current_registry=$(npm config get registry) if [[ "$current_registry" =~ "npmmirror" ]] || [[ "$current_registry" =~ "taobao" ]]; then print_info "检测到使用国内镜像源" fi fi # 清理旧的node_modules # 检查并升级C++编译器(如果需要) check_cpp_compiler if [[ -d "node_modules" ]]; then print_info "清理旧依赖..." rm -rf node_modules package-lock.json fi print_info "正在重新安装依赖(可能需要几分钟)..." if PYTHON=python3 npm install --production; then print_success "依赖更新完成" else print_error "依赖更新失败" print_warning "请检查错误日志: ~/.npm/_logs/" fi echo "" } update_migrate_database() { print_step "迁移数据库配置..." cd "${PROJECT_DIR}/backend" # 检查是否需要升级上传限制(从100MB升级到10GB) if command -v sqlite3 &> /dev/null; then if [[ -f "ftp-manager.db" ]]; then CURRENT_LIMIT=$(sqlite3 ftp-manager.db "SELECT value FROM system_settings WHERE key = 'max_upload_size';" 2>/dev/null || echo "") if [[ "$CURRENT_LIMIT" == "104857600" ]]; then print_info "检测到旧的上传限制(100MB),正在升级到10GB..." sqlite3 ftp-manager.db "UPDATE system_settings SET value = '10737418240' WHERE key = 'max_upload_size';" print_success "上传限制已升级: 100MB → 10GB" elif [[ "$CURRENT_LIMIT" == "10737418240" ]]; then print_success "上传限制已是最新: 10GB" elif [[ -n "$CURRENT_LIMIT" ]]; then print_info "当前上传限制: ${CURRENT_LIMIT} 字节" else print_info "数据库配置正常" fi fi else print_warning "sqlite3未安装,跳过数据库迁移检查" fi # ========== 安全配置迁移 ========== print_step "检查安全配置..." if [[ -f ".env" ]]; then # 检查 CORS 配置 CURRENT_CORS=$(grep "^ALLOWED_ORIGINS=" .env | cut -d'=' -f2-) if [[ "$CURRENT_CORS" == "*" ]]; then print_warning "⚠️ 检测到不安全的CORS配置: ALLOWED_ORIGINS=*" echo "" echo "这是一个严重的安全风险!攻击者可以从任何域名访问你的API。" echo "" # 尝试从域名配置自动修复 if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}" ]] || [[ -f "/etc/nginx/conf.d/${PROJECT_NAME}.conf" ]]; then # 尝试从Nginx配置读取域名 NGINX_DOMAIN=$(grep "server_name" /etc/nginx/sites-enabled/${PROJECT_NAME} 2>/dev/null | grep -v "_" | awk '{print $2}' | sed 's/;//g' | head -1) if [[ -z "$NGINX_DOMAIN" ]]; then NGINX_DOMAIN=$(grep "server_name" /etc/nginx/conf.d/${PROJECT_NAME}.conf 2>/dev/null | grep -v "_" | awk '{print $2}' | sed 's/;//g' | head -1) fi if [[ -n "$NGINX_DOMAIN" ]] && [[ "$NGINX_DOMAIN" != "localhost" ]]; then # 检测是否使用HTTPS if grep -q "listen.*443.*ssl" /etc/nginx/sites-enabled/${PROJECT_NAME} 2>/dev/null || \ grep -q "listen.*443.*ssl" /etc/nginx/conf.d/${PROJECT_NAME}.conf 2>/dev/null; then FIXED_CORS="https://${NGINX_DOMAIN}" else FIXED_CORS="http://${NGINX_DOMAIN}" fi print_info "检测到域名: ${NGINX_DOMAIN}" echo "" print_warning "建议将CORS设置为: ${FIXED_CORS}" echo "" read -p "是否自动修复CORS配置?[y/n]: " -n 1 -r < /dev/tty echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then # 备份原配置 cp .env .env.backup.$(date +%Y%m%d_%H%M%S) # 修复CORS配置 sed -i "s|^ALLOWED_ORIGINS=.*|ALLOWED_ORIGINS=${FIXED_CORS}|" .env print_success "✓ CORS配置已修复: ${FIXED_CORS}" print_info "原配置已备份到: .env.backup.*" else print_warning "跳过自动修复,请手动编辑 .env 文件修改 ALLOWED_ORIGINS" print_info "推荐值: ALLOWED_ORIGINS=${FIXED_CORS}" fi else print_warning "无法自动修复,请手动编辑backend/.env文件" print_info "将 ALLOWED_ORIGINS=* 改为你的实际域名" print_info "示例: ALLOWED_ORIGINS=https://yourdomain.com" fi else print_warning "无法自动修复,请手动编辑backend/.env文件" print_info "将 ALLOWED_ORIGINS=* 改为你的实际域名" print_info "示例: ALLOWED_ORIGINS=https://yourdomain.com" fi echo "" elif [[ -z "$CURRENT_CORS" ]]; then print_warning "⚠️ ALLOWED_ORIGINS未配置" print_info "生产环境必须配置具体的域名" else print_success "✓ CORS配置安全: ${CURRENT_CORS}" fi # 检查 NODE_ENV CURRENT_ENV=$(grep "^NODE_ENV=" .env | cut -d'=' -f2-) if [[ "$CURRENT_ENV" != "production" ]]; then print_warning "⚠️ 当前环境: ${CURRENT_ENV:-未设置}" print_info "生产环境建议设置为: NODE_ENV=production" else print_success "✓ 环境配置: production" fi else print_error "❌ .env 文件不存在!" fi echo "" } update_restart_services() { print_step "重启服务..." cd "${PROJECT_DIR}/backend" if command -v pm2 &> /dev/null; then pm2 restart ${PROJECT_NAME}-backend pm2 save print_success "后端服务已重启" fi # 重载Nginx(兼容宝塔/自管Nginx) restart_nginx_safe || print_warning "Nginx重载失败,请检查端口占用或手动重载" echo "" } update_check_version() { print_step "检查更新后的版本..." # 检查package.json版本 if [[ -f "${PROJECT_DIR}/backend/package.json" ]]; then VERSION=$(grep '"version"' "${PROJECT_DIR}/backend/package.json" | head -1 | awk -F'"' '{print $4}') if [[ -n "$VERSION" ]]; then print_success "当前版本: v$VERSION" fi fi echo "" } print_update_completion() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🎉 更新成功! ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" echo -e "${CYAN}更新内容:${NC}" echo " ✓ 代码已更新到最新版本" echo " ✓ 依赖已更新" echo " ✓ 服务已重启" echo "" echo -e "${CYAN}保留的数据:${NC}" echo " ✓ 数据库(用户、分享链接等)" echo " ✓ 用户文件(storage目录)" echo " ✓ 配置文件(.env)" echo "" echo -e "${YELLOW}常用命令:${NC}" echo " 查看服务状态: pm2 status" echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend" echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend" echo "" echo -e "${GREEN}更新完成,祝您使用愉快!${NC}" echo "" } update_patch_env() { print_step "检查 .env 新增配置..." if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then if ! grep -q "^ENFORCE_HTTPS=" "${PROJECT_DIR}/backend/.env"; then # 基于现有配置做一个合理的默认值:如果已启用安全Cookie或公开端口为443,优先设为true COOKIE_SECURE_CUR=$(grep "^COOKIE_SECURE=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2- | tr '[:upper:]' '[:lower:]') PUBLIC_PORT_CUR=$(grep "^PUBLIC_PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2-) ALLOWED_CUR=$(grep "^ALLOWED_ORIGINS=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2-) ENFORCE_DEFAULT="false" if [[ "$COOKIE_SECURE_CUR" == "true" ]] || [[ "$PUBLIC_PORT_CUR" == "443" ]]; then ENFORCE_DEFAULT="true" elif echo "$ALLOWED_CUR" | grep -qi "https://"; then ENFORCE_DEFAULT="true" fi echo "ENFORCE_HTTPS=${ENFORCE_DEFAULT}" >> "${PROJECT_DIR}/backend/.env" print_warning "已为现有 .env 补充 ENFORCE_HTTPS=${ENFORCE_DEFAULT}(如生产请确认设为 true 并重启)" else print_info ".env 已包含 ENFORCE_HTTPS,保持不变" fi else print_warning "未找到 ${PROJECT_DIR}/backend/.env,请手动确认配置" fi echo "" } update_main() { # 检查root权限 check_root # 确认更新 confirm_update # 检查项目 update_check_project # 备份重要文件 update_backup_important_files # 停止服务 update_stop_services # 拉取最新代码 update_pull_latest_code # 更新依赖 # 检查并重新下载上传工具(如果需要) if [[ ! -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" ]]; then print_info "检测到上传工具丢失,正在重新下载..." build_upload_tool else FILE_SIZE=$(stat -f%z "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || echo "0") if [[ $FILE_SIZE -lt 30000000 ]]; then print_warning "上传工具文件大小异常,正在重新下载..." rm -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" build_upload_tool else FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 )) print_success "上传工具完整(${FILE_SIZE_MB}MB)" fi fi update_install_dependencies # 迁移数据库配置 update_migrate_database # 补充新配置项 update_patch_env # 重启服务 update_restart_services # 健康检查 if ! health_check; then print_error "健康检查未通过,请检查日志" print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend" exit 1 fi # 检查版本 update_check_version # 完成提示 print_update_completion } ################################################################################ # 主流程 ################################################################################ main() { print_banner # 检查root权限 check_root # 如果没有通过命令行参数指定模式,则显示交互式选择 if [[ "$MODE" == "install" ]] && [[ "$1" != "--skip-mode-select" ]]; then # 检测是否可以使用交互式输入 if [[ -t 0 ]] || [[ -c /dev/tty ]]; then print_step "请选择操作模式" echo "" echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云" echo -e "${BLUE}[2]${NC} 更新/升级 玩玩云" echo -e "${YELLOW}[3]${NC} 修复/重新配置 玩玩云" echo -e "${PURPLE}[4]${NC} SSL证书管理(安装/续签/更换证书)" echo -e "${RED}[5]${NC} 卸载 玩玩云" echo -e "${GRAY}[0]${NC} 退出脚本" echo "" while true; do read -p "请输入选项 [0-5]: " mode_choice < /dev/tty case $mode_choice in 1) print_success "已选择: 安装模式" echo "" break ;; 2) print_info "切换到更新模式..." echo "" update_main exit 0 ;; 3) print_info "切换到修复模式..." echo "" repair_main exit 0 ;; 4) print_info "切换到SSL证书管理模式..." echo "" ssl_main exit 0 ;; 5) print_info "切换到卸载模式..." echo "" uninstall_main exit 0 ;; 0) print_info "正在退出脚本..." echo "" exit 0 ;; *) print_error "无效选项,请重新选择" ;; esac done else # 管道执行时的提示 print_info "检测到通过管道执行脚本" print_info "默认进入安装模式" print_warning "如需其他操作,请下载脚本后运行" echo "" echo -e "${YELLOW}提示:${NC}" echo " 安装: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh" echo " 更新: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --update" echo " 修复: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --repair" echo " SSL管理: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --ssl" echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall" echo "" sleep 2 fi fi # 系统检测 system_check # 选择软件源 choose_mirror # 安装依赖 install_dependencies # 选择访问模式 choose_access_mode # 端口配置 configure_ports # 配置管理员账号 configure_admin_account # 创建项目目录 create_project_directory # 下载项目 download_project # 安装后端依赖 install_backend_dependencies # 创建配置文件 create_env_file # 创建数据目录 create_data_directories # 打包上传工具 build_upload_tool # 先配置基础HTTP Nginx(SSL证书申请需要) configure_nginx_http_first # 部署SSL证书(需要HTTP server block进行验证) deploy_ssl # 根据SSL结果配置最终Nginx configure_nginx_final # 启动后端服务 start_backend_service # 健康检查 if ! health_check; then print_error "健康检查未通过,请检查日志" exit 1 fi # 完成提示 print_completion } uninstall_main() { # 检查root权限 check_root # 确认卸载 confirm_uninstall # 备份数据 uninstall_backup_data # 停止PM2进程 uninstall_stop_pm2 # 删除Nginx配置 uninstall_nginx_config # 清理SSL证书 uninstall_ssl_certificates # 删除项目目录 uninstall_project_directory # 清理临时文件 uninstall_temp_files # 检查残留 uninstall_check_residual # 完成提示 print_uninstall_completion } ################################################################################ # 修复功能 ################################################################################ print_repair_banner() { clear echo -e "${BLUE}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🔧 玩玩云 修复模式 ║" echo "║ ║" echo "║ Repair Mode ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" } confirm_repair() { print_repair_banner echo -e "${YELLOW}" echo "本脚本将执行以下操作:" echo "" echo "【将会重新配置】" echo " ✓ 重新生成Nginx配置(应用最新配置)" echo " ✓ 重启后端服务" echo " ✓ 重载Nginx服务" echo "" echo "【将会保留】" echo " ✓ 数据库文件(用户数据)" echo " ✓ 用户上传的文件" echo " ✓ .env 配置文件" echo " ✓ SSL证书" echo -e "${NC}" echo "" print_info "适用场景: 更新配置、修复nginx配置错误、重新应用设置" echo "" read -p "确定要执行修复吗? (y/n): " confirm < /dev/tty if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then print_info "已取消修复" exit 0 fi echo "" } repair_check_project() { print_step "检查项目是否已安装..." if [[ ! -d "$PROJECT_DIR" ]]; then print_error "项目未安装: $PROJECT_DIR" print_info "请先运行安装命令: bash install.sh" exit 1 fi if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then print_error "项目目录不完整" exit 1 fi print_success "项目已安装: $PROJECT_DIR" echo "" } repair_load_existing_config() { print_step "读取现有配置..." # 从.env读取端口配置 if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then BACKEND_PORT=$(grep "^PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2 || echo "40001") print_success "后端端口: $BACKEND_PORT" else print_warning ".env文件不存在,使用默认端口40001" BACKEND_PORT="40001" fi # 检查现有nginx配置(兼容宝塔/标准) local nginx_conf_path="" if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then nginx_conf_path="/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" elif [[ -f "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" ]]; then nginx_conf_path="/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" fi if [[ -n "$nginx_conf_path" ]]; then # 尝试从现有配置读取端口 EXISTING_HTTP_PORT=$(grep "listen" "$nginx_conf_path" | grep -v "ssl" | head -1 | awk '{print $2}' | tr -d ';' || echo "80") HTTP_PORT=${EXISTING_HTTP_PORT:-80} # 检查是否有HTTPS配置 if grep -q "listen.*ssl" "$nginx_conf_path"; then EXISTING_HTTPS_PORT=$(grep "listen.*ssl" "$nginx_conf_path" | head -1 | awk '{print $2}' | tr -d ';' || echo "443") HTTPS_PORT=${EXISTING_HTTPS_PORT:-443} SSL_METHOD="existing" print_success "检测到HTTPS配置,端口: $HTTPS_PORT" else SSL_METHOD="8" print_info "未检测到HTTPS配置" fi # 检查是否有域名 SERVER_NAME=$(grep "server_name" "$nginx_conf_path" | head -1 | awk '{print $2}' | tr -d ';' || echo "_") if [[ "$SERVER_NAME" != "_" ]] && [[ "$SERVER_NAME" != "localhost" ]]; then DOMAIN="$SERVER_NAME" USE_DOMAIN=true print_success "检测到域名: $DOMAIN" else USE_DOMAIN=false print_info "使用IP模式" fi print_success "HTTP端口: $HTTP_PORT" else print_warning "未找到现有nginx配置,将使用默认配置" HTTP_PORT="80" HTTPS_PORT="443" SSL_METHOD="8" USE_DOMAIN=false fi echo "" } repair_regenerate_nginx_config() { print_step "重新生成Nginx配置..." # 清理旧的备份文件(避免nginx读取到错误配置) rm -f /etc/nginx/sites-enabled/${PROJECT_NAME}.conf.backup.* 2>/dev/null || true rm -f /etc/nginx/sites-available/${PROJECT_NAME}.conf.backup.* 2>/dev/null || true rm -f /www/server/panel/vhost/nginx/${PROJECT_NAME}.conf.backup.* 2>/dev/null || true # 备份当前配置到 /root/ if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then cp "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" "/root/nginx-backup-${PROJECT_NAME}.conf.$(date +%Y%m%d%H%M%S)" print_success "已备份现有配置到 /root/" elif [[ -f "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" ]]; then cp "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" "/root/nginx-backup-${PROJECT_NAME}.conf.$(date +%Y%m%d%H%M%S)" print_success "已备份宝塔配置到 /root/" fi # 调用现有的configure_nginx函数 configure_nginx print_success "Nginx配置已重新生成" echo "" } repair_restart_services() { print_step "重启服务..." # 重启后端 if command -v pm2 &> /dev/null; then if pm2 list | grep -q "${PROJECT_NAME}-backend"; then pm2 restart ${PROJECT_NAME}-backend print_success "后端服务已重启" else print_warning "后端服务未运行,尝试启动..." cd "${PROJECT_DIR}/backend" pm2 start server.js --name ${PROJECT_NAME}-backend pm2 save print_success "后端服务已启动" fi fi # 重载Nginx(兼容宝塔/自管Nginx) restart_nginx_safe || print_warning "Nginx未能启动/重载,请检查端口占用或手动重载" echo "" } repair_verify_services() { print_step "验证服务状态..." # 等待服务启动 sleep 3 # 检查后端 if pm2 status | grep -q "${PROJECT_NAME}-backend.*online"; then print_success "后端服务运行正常" else print_error "后端服务状态异常" print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend" fi # 检查Nginx if systemctl is-active --quiet nginx 2>/dev/null || pgrep -x nginx > /dev/null || [[ -x /www/server/nginx/sbin/nginx ]] && pgrep -f "/www/server/nginx/sbin/nginx" > /dev/null; then print_success "Nginx服务运行正常" else print_error "Nginx服务异常" fi # 检查端口 if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then print_success "后端端口监听正常 (${BACKEND_PORT})" else print_warning "后端端口监听异常" fi echo "" } print_repair_completion() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ✓ 修复完成! ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" echo -e "${CYAN}修复内容:${NC}" echo " ✓ Nginx配置已更新到最新版本" echo " ✓ 服务已重启" echo "" echo -e "${CYAN}保留的数据:${NC}" echo " ✓ 数据库(用户、分享链接等)" echo " ✓ 用户文件(storage目录)" echo " ✓ 配置文件(.env)" echo "" # 显示访问地址 if [[ "$USE_DOMAIN" == "true" ]]; then if [[ "$SSL_METHOD" == "8" ]] || [[ "$SSL_METHOD" == "" ]]; then if [[ "$HTTP_PORT" == "80" ]]; then echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}" fi else if [[ "$HTTPS_PORT" == "443" ]]; then echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}" fi fi else PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "服务器IP") if [[ "$HTTP_PORT" == "80" ]]; then echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}" else echo -e "${CYAN}访问地址:${NC} http://${PUBLIC_IP}:${HTTP_PORT}" fi fi echo "" echo -e "${YELLOW}常用命令:${NC}" echo " 查看服务状态: pm2 status" echo " 查看日志: pm2 logs ${PROJECT_NAME}-backend" echo " 重启服务: pm2 restart ${PROJECT_NAME}-backend" echo "" echo -e "${GREEN}修复完成,请测试功能是否正常!${NC}" echo "" } repair_main() { # 检查root权限 check_root # 检测操作系统 detect_os # 确认修复 confirm_repair # 检查项目 repair_check_project # 读取现有配置 repair_load_existing_config # 重新生成nginx配置 repair_regenerate_nginx_config # 重启服务 repair_restart_services # 验证服务 repair_verify_services # 健康检查 if ! health_check; then print_warning "部分健康检查未通过,请查看日志" print_info "查看日志: pm2 logs ${PROJECT_NAME}-backend" fi # 完成提示 print_repair_completion } ################################################################################ # SSL证书管理功能 ################################################################################ print_ssl_banner() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ 🔐 SSL证书管理模式 ║" echo "║ ║" echo "║ SSL Certificate Manager ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" } confirm_ssl_operation() { print_ssl_banner echo -e "${YELLOW}" echo "本脚本将执行以下操作:" echo "" echo "【SSL证书管理】" echo " ✓ 检测现有域名配置" echo " ✓ 选择SSL证书部署方案" echo " ✓ 申请/更换/续签证书" echo " ✓ 更新Nginx HTTPS配置" echo " ✓ 重载服务" echo "" echo "【将会保留】" echo " ✓ 数据库文件(用户数据)" echo " ✓ 用户上传的文件" echo " ✓ 后端配置文件(.env)" echo " ✓ 现有HTTP配置" echo -e "${NC}" echo "" print_info "适用场景: 初次配置HTTPS、更换证书方案、证书续签" echo "" read -p "确定要继续吗? (y/n): " confirm < /dev/tty if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then print_info "已取消操作" exit 0 fi echo "" } ssl_check_project() { print_step "检查项目是否已安装..." if [[ ! -d "$PROJECT_DIR" ]]; then print_error "项目未安装: $PROJECT_DIR" print_info "请先运行安装命令: bash install.sh" exit 1 fi if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then print_error "项目目录不完整" exit 1 fi print_success "项目已安装: $PROJECT_DIR" echo "" } ssl_load_existing_config() { print_step "读取现有配置..." # 从.env读取后端端口 if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then BACKEND_PORT=$(grep "^PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2 || echo "40001") print_success "后端端口: $BACKEND_PORT" else BACKEND_PORT="40001" print_warning ".env文件不存在,使用默认端口: $BACKEND_PORT" fi # 检查现有nginx配置 local nginx_conf="" if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then nginx_conf="/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" elif [[ -f "/etc/nginx/conf.d/${PROJECT_NAME}.conf" ]]; then nginx_conf="/etc/nginx/conf.d/${PROJECT_NAME}.conf" elif [[ -f "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" ]]; then nginx_conf="/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" fi if [[ -n "$nginx_conf" ]]; then # 读取HTTP端口 EXISTING_HTTP_PORT=$(grep "listen" "$nginx_conf" | grep -v "ssl" | grep -v "#" | head -1 | awk '{print $2}' | tr -d ';' || echo "80") HTTP_PORT=${EXISTING_HTTP_PORT:-80} # 检查是否有HTTPS配置 if grep -q "listen.*ssl" "$nginx_conf"; then EXISTING_HTTPS_PORT=$(grep "listen.*ssl" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "443") HTTPS_PORT=${EXISTING_HTTPS_PORT:-443} print_info "检测到现有HTTPS配置,端口: $HTTPS_PORT" else HTTPS_PORT="443" print_info "未检测到HTTPS配置,将使用默认端口: 443" fi # 读取域名 SERVER_NAME=$(grep "server_name" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "") if [[ -n "$SERVER_NAME" ]] && [[ "$SERVER_NAME" != "_" ]] && [[ "$SERVER_NAME" != "localhost" ]]; then DOMAIN="$SERVER_NAME" USE_DOMAIN=true print_success "检测到域名: $DOMAIN" else USE_DOMAIN=false print_warning "未检测到域名配置" fi print_success "HTTP端口: $HTTP_PORT" else print_error "未找到Nginx配置文件" exit 1 fi echo "" } ssl_configure_domain() { print_step "配置域名" echo "" # 如果已有域名,询问是否使用 if [[ "$USE_DOMAIN" == "true" ]] && [[ -n "$DOMAIN" ]]; then print_info "检测到现有域名: $DOMAIN" read -p "是否使用此域名? (y/n): " use_existing < /dev/tty if [[ "$use_existing" == "y" || "$use_existing" == "Y" ]]; then print_success "使用现有域名: $DOMAIN" echo "" return 0 fi fi # 输入新域名 while true; do read -p "请输入您的域名 (例如: wwy.example.com): " DOMAIN < /dev/tty if [[ -z "$DOMAIN" ]]; then print_error "域名不能为空" continue fi # 验证域名格式 if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then print_error "域名格式不正确" continue fi # 验证域名解析 print_info "正在验证域名解析..." DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1 || nslookup "$DOMAIN" 2>/dev/null | grep -A1 "Name:" | tail -1 | awk '{print $2}') PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "") if [[ -n "$DOMAIN_IP" ]] && [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then print_success "域名已正确解析到当前服务器IP" USE_DOMAIN=true break else print_warning "域名未解析到当前服务器IP" print_info "域名解析IP: ${DOMAIN_IP:-未解析}" print_info "当前服务器IP: $PUBLIC_IP" read -p "是否继续? (y/n): " continue_choice < /dev/tty if [[ "$continue_choice" == "y" || "$continue_choice" == "Y" ]]; then USE_DOMAIN=true break fi fi done echo "" } ssl_choose_method() { print_step "选择SSL证书部署方式" echo "" echo -e "${YELLOW}【推荐方案】${NC}" echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)" echo " - 最稳定可靠,支持自动续期" echo "" echo -e "${YELLOW}【备选方案】${NC}" echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt" echo " - 纯Shell脚本,更轻量级" echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL" echo " - Let's Encrypt的免费替代品" echo -e "${GREEN}[5]${NC} acme.sh + Buypass" echo " - 挪威免费CA,有效期180天" echo "" echo -e "${YELLOW}【云服务商证书】${NC}" echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)" echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)" echo "" echo -e "${YELLOW}【其他选项】${NC}" echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)" echo -e "${GREEN}[8]${NC} 移除HTTPS配置 (改回HTTP)" echo -e "${GREEN}[0]${NC} 取消操作" echo "" while true; do read -p "请输入选项 [0-8]: " ssl_choice < /dev/tty case $ssl_choice in 1|2|3|4|5|6|7) SSL_METHOD=$ssl_choice break ;; 8) SSL_METHOD=$ssl_choice print_warning "将移除HTTPS配置,改回HTTP模式" read -p "确定要继续吗? (y/n): " confirm_remove < /dev/tty if [[ "$confirm_remove" == "y" || "$confirm_remove" == "Y" ]]; then break fi ;; 0) print_info "已取消操作" exit 0 ;; *) print_error "无效选项,请重新选择" ;; esac done echo "" } ssl_deploy_certificate() { print_step "部署SSL证书..." # 如果选择移除HTTPS if [[ "$SSL_METHOD" == "8" ]]; then print_info "将移除HTTPS配置..." # 配置为HTTP模式 configure_nginx_http return 0 fi # 部署证书 deploy_ssl # 检查证书是否部署成功 if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]] && [[ -f "/etc/nginx/ssl/${DOMAIN}.key" ]]; then print_success "证书文件已部署" else print_warning "证书文件未找到,将使用HTTP配置" SSL_METHOD="8" fi } ssl_update_nginx_config() { print_step "更新Nginx配置..." if [[ "$SSL_METHOD" == "8" ]]; then # HTTP配置 configure_nginx_http else # HTTPS配置 configure_nginx_https fi # 测试nginx配置 if ! nginx -t 2>&1; then print_error "Nginx配置测试失败" print_info "请检查配置文件" return 1 fi print_success "Nginx配置已更新" echo "" } ssl_reload_services() { print_step "重载服务..." # 重载Nginx if [[ -d /www/server/nginx ]]; then # 宝塔面板 print_info "宝塔环境,重载Nginx..." if [[ -f /www/server/nginx/sbin/nginx ]]; then /www/server/nginx/sbin/nginx -s reload 2>/dev/null if [[ $? -eq 0 ]]; then print_success "Nginx已重载" else /www/server/nginx/sbin/nginx 2>/dev/null print_success "Nginx已启动" fi fi systemctl reload nginx 2>/dev/null || true else # 标准Nginx systemctl reload nginx print_success "Nginx已重载" fi # 重启后端服务(更新PUBLIC_PORT配置) if command -v pm2 &> /dev/null; then if pm2 list | grep -q "${PROJECT_NAME}-backend"; then pm2 restart ${PROJECT_NAME}-backend print_success "后端服务已重启" fi fi echo "" } ssl_verify_deployment() { print_step "验证部署..." # 检查Nginx if [[ -d /www/server/nginx ]]; then if pgrep -x nginx > /dev/null; then print_success "Nginx运行正常" else print_error "Nginx未运行" fi else if systemctl is-active --quiet nginx; then print_success "Nginx运行正常" else print_error "Nginx未运行" fi fi # 检查SSL证书 if [[ "$SSL_METHOD" != "8" ]]; then if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]]; then print_success "SSL证书已部署: /etc/nginx/ssl/${DOMAIN}.crt" # 显示证书信息 CERT_EXPIRY=$(openssl x509 -enddate -noout -in "/etc/nginx/ssl/${DOMAIN}.crt" 2>/dev/null | cut -d= -f2) if [[ -n "$CERT_EXPIRY" ]]; then print_info "证书有效期至: $CERT_EXPIRY" fi else print_warning "SSL证书文件未找到" fi fi echo "" } print_ssl_completion() { clear echo -e "${GREEN}" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ✓ SSL配置完成! ║" echo "║ ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" # 显示访问地址 if [[ "$SSL_METHOD" == "8" ]]; then if [[ "$HTTP_PORT" == "80" ]]; then echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}" fi echo -e "${YELLOW}模式:${NC} HTTP" else if [[ "$HTTPS_PORT" == "443" ]]; then echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}" else echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}" fi echo -e "${YELLOW}模式:${NC} HTTPS" # SSL信息 echo "" echo -e "${YELLOW}SSL证书:${NC}" case $SSL_METHOD in 1) echo " 方案: Certbot (Let's Encrypt)" echo " 续期: 自动续期已配置" ;; 2) echo " 方案: acme.sh + Let's Encrypt" echo " 续期: 自动续期已配置" ;; 3) echo " 方案: acme.sh + ZeroSSL" echo " 续期: 自动续期已配置" ;; 4) echo " 方案: acme.sh + Buypass" echo " 续期: 自动续期已配置" ;; 7) echo " 方案: 手动上传证书" echo " 续期: 需手动更新证书文件" ;; esac fi echo "" echo -e "${YELLOW}常用命令:${NC}" echo " 查看证书信息: openssl x509 -text -noout -in /etc/nginx/ssl/${DOMAIN}.crt" echo " 测试HTTPS: curl -I https://${DOMAIN}" echo " 查看Nginx日志: tail -f /var/log/nginx/error.log" echo " 重新配置SSL: bash install.sh --ssl" echo "" echo -e "${GREEN}SSL配置完成!${NC}" echo "" } ssl_main() { # 检查root权限 check_root # 检测操作系统 detect_os # 确认操作 confirm_ssl_operation # 检查项目 ssl_check_project # 读取现有配置 ssl_load_existing_config # 配置域名 if [[ "$USE_DOMAIN" != "true" ]] || [[ -z "$DOMAIN" ]]; then ssl_configure_domain fi # 选择SSL方案 ssl_choose_method # 先配置基础HTTP Nginx(SSL验证需要) configure_nginx_http_first # 部署SSL证书 ssl_deploy_certificate # 更新Nginx配置 ssl_update_nginx_config # 重载服务 ssl_reload_services # 验证部署 ssl_verify_deployment # 完成提示 print_ssl_completion } # 执行主流程 if [[ "$MODE" == "uninstall" ]]; then uninstall_main elif [[ "$MODE" == "update" ]]; then update_main elif [[ "$MODE" == "repair" ]]; then repair_main elif [[ "$MODE" == "ssl" ]]; then ssl_main else main fi