#!/bin/bash #=============================================================================== # 敲敲云(QiaoQiaoYun) 一键部署脚本 - Linux # 自动安装: JDK17 + MySQL 8.0 + MongoDB 6.0 + Redis + 敲敲云v2.3.0 # 支持: Ubuntu/Debian, CentOS/RHEL/Rocky/AlmaLinux, TencentOS/OpenEuler/Anolis # # 快速安装: # curl -fsSL https://www.qiaoqiaoyun.com/install.sh | bash # # 自定义安装: # curl -fsSL https://www.qiaoqiaoyun.com/install.sh -o install.sh # sudo bash install.sh [all|env|mysql|mongodb|redis|jdk17|qqyun] [选项] # # 组件说明: # all - 安装全部环境 + 部署敲敲云 (默认) # env - 仅安装环境 (MySQL+MongoDB+Redis+JDK17) # qqyun - 仅部署敲敲云 (需已安装环境) # # 选项 (也可用同名环境变量): # --pc-url URL PC 端访问地址 (JEECG_DOMAIN_URL_PC, 默认 http://<服务器IP>) # --app-url URL H5/移动端访问地址 (JEECG_DOMAIN_URL_APP, 默认 http://<服务器IP>:8051) # 钉钉消息「立刻办理」链接取此地址, 须为钉钉客户端能访问的外网 H5 地址 # --baidu-ak AK 百度地图 AK (BAIDU_MAP_AK), 表单地图控件需要 # 申请: https://lbsyun.baidu.com/apiconsole/key # --force 数据库已有数据时仍重新导入初始化 SQL (会清空已有数据!) # # 示例: # curl -fsSL https://www.qiaoqiaoyun.com/install.sh | bash -s -- --app-url http://h5.example.com --baidu-ak 你的AK #=============================================================================== # 兼容 curl | bash 模式: 将整个脚本包裹在函数中,确保完整下载后再执行 _main() { set -e # 安装统计(前台限时 3 秒,失败不影响安装) curl -s --max-time 3 "https://qiaoqiaoyun.com/setup?type=install&t=$(date +%s)" > /dev/null 2>&1 || true # ========================== 配置 ========================== QQYUN_VERSION="v2.4.0" # 下载源 (按顺序尝试). 七牛 CDN 旧测试域名已失效, 暂不使用 QQYUN_DOWNLOAD_URL_PRIMARY="https://upload.jeecg.com/qqyun/download/qqy${QQYUN_VERSION}.zip" QQYUN_DOWNLOAD_URL_BACKUP="https://github.com/jeecgboot/qiaoqiaoyun/releases/download/${QQYUN_VERSION}/${QQYUN_VERSION}.zip" QQYUN_DEPLOY_DIR="/opt/qiaoqiaoyun" MYSQL_VERSION="8.0" MONGODB_VERSION="6.0" # MySQL 配置 MYSQL_ROOT_PASSWORD="QiaoQiao@2024" MYSQL_QQYUN_DB="qiaoqiaoyun" # 服务端口 QQYUN_PORT=80 # 移动端 H5 (独立 nginx 实例, 不影响系统 nginx) H5_PORT=8051 QQYUN_H5_DIR="/opt/qiaoqiaoyun-h5" H5_DEPLOYED=0 # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } # ========================== OS 检测 ========================== detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" elif [ -f /etc/redhat-release ]; then OS_ID="centos" OS_VERSION=$(rpm -q --queryformat '%{VERSION}' centos-release 2>/dev/null || echo "7") else log_error "不支持的Linux发行版" exit 1 fi case "$OS_ID" in ubuntu|debian) PKG_MANAGER="apt" ;; centos|rhel|rocky|almalinux|fedora|tencentos|openEuler|anolis|alinux|opencloudos) PKG_MANAGER="yum" command -v dnf &>/dev/null && PKG_MANAGER="dnf" ;; *) if echo "$ID_LIKE" | grep -qiE "rhel|centos|fedora"; then PKG_MANAGER="yum" command -v dnf &>/dev/null && PKG_MANAGER="dnf" elif echo "$ID_LIKE" | grep -qiE "debian|ubuntu"; then PKG_MANAGER="apt" else log_error "不支持的发行版: $OS_ID (ID_LIKE=$ID_LIKE)" exit 1 fi ;; esac # 检测 RHEL 兼容版本号 (el7/el8/el9) EL_VERSION="" if [ "$PKG_MANAGER" != "apt" ]; then if [ -n "$PLATFORM_ID" ]; then EL_VERSION=$(echo "$PLATFORM_ID" | grep -oP '(?:el|oc)\K[0-9]+' 2>/dev/null || true) fi if [ -z "$EL_VERSION" ]; then EL_VERSION=$(rpm -E '%{rhel}' 2>/dev/null | grep -oP '^[0-9]+$' || true) fi if [ -z "$EL_VERSION" ]; then case "$OS_ID" in centos|rhel|rocky|almalinux) EL_VERSION="${OS_VERSION%%.*}" ;; tencentos) local major_ver="${OS_VERSION%%.*}" if [ "$major_ver" -ge 4 ] 2>/dev/null; then EL_VERSION="9"; else EL_VERSION="8"; fi ;; opencloudos) local major_ver="${OS_VERSION%%.*}" if [ "$major_ver" -ge 9 ] 2>/dev/null; then EL_VERSION="9"; else EL_VERSION="8"; fi ;; *) EL_VERSION="8" ;; esac fi log_info "RHEL 兼容版本: el${EL_VERSION}" fi # el10+: 包名变化较大 (mysql8.4-server, valkey 替代 redis, MongoDB 需回退 el9 repo) # 脚本已适配,不再拦截 ARCH=$(uname -m) log_info "系统: $OS_ID $OS_VERSION ($ARCH), 包管理器: $PKG_MANAGER" } # ========================== 基础工具 ========================== install_prerequisites() { log_step "安装基础工具 (wget, curl, unzip)..." if [ "$PKG_MANAGER" = "apt" ]; then apt-get update -y apt-get install -y wget curl unzip gnupg lsb-release else $PKG_MANAGER install -y wget curl unzip fi } # ========================== JDK 17 ========================== install_jdk17() { log_step "========== 安装 JDK 17 ==========" if command -v java &>/dev/null; then local ver=$(java -version 2>&1 | head -1) if java -version 2>&1 | grep -q '"17\.'; then log_info "已是 JDK 17,跳过安装 ($ver)" return 0 fi log_warn "当前 Java 不是 17: $ver,继续安装..." fi local installed=false if [ "$PKG_MANAGER" = "apt" ]; then apt-get install -y openjdk-17-jdk && installed=true else $PKG_MANAGER install -y java-17-openjdk-devel && installed=true fi # 回退: 包管理器无 JDK 17 时,从镜像站下载 tar 包安装 if [ "$installed" = false ]; then log_warn "包管理器无 JDK 17,尝试下载 Adoptium (Eclipse Temurin) 二进制包..." local jdk_dir="/usr/lib/jvm/temurin-17" local tmp_tar="/tmp/openjdk17.tar.gz" local jdk_filename="OpenJDK17U-jdk_x64_linux_hotspot_17.0.18_8.tar.gz" local mirror_url="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jdk/x64/linux/${jdk_filename}" local official_url="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse?project=jdk" local downloaded=false for url in "$mirror_url" "$official_url"; do log_info "正在下载: $url" if curl -fSL --connect-timeout 15 --max-time 300 -o "$tmp_tar" "$url"; then downloaded=true break fi log_warn "下载失败,尝试下一个源..." done if [ "$downloaded" = true ]; then mkdir -p "$jdk_dir" tar -xzf "$tmp_tar" -C "$jdk_dir" --strip-components=1 rm -f "$tmp_tar" installed=true log_info "Adoptium JDK 17 已解压到 $jdk_dir" # tar 包安装不含字体依赖,需手动安装 (验证码等 AWT 图形功能需要) log_info "安装字体依赖 (fontconfig)..." $PKG_MANAGER install -y fontconfig 2>/dev/null || true else log_error "JDK 17 下载失败,请检查网络后重试" return 1 fi fi local java_path java_path=$(find /usr/lib/jvm -maxdepth 1 \( -name "java-17-*" -o -name "temurin-17" \) 2>/dev/null | head -1) if [ -n "$java_path" ]; then cat > /etc/profile.d/jdk17.sh <&1 | head -1)" } # ========================== MySQL 8.0 ========================== install_mysql() { log_step "========== 安装 MySQL $MYSQL_VERSION ==========" if command -v mysql &>/dev/null; then log_warn "MySQL 已安装: $(mysql --version 2>/dev/null)" return 0 fi if [ "$PKG_MANAGER" = "apt" ]; then # 优先检查系统仓库是否已有 mysql-server (Ubuntu 22.04+/Debian 12+ 自带 MySQL 8.0) local apt_mysql_avail=$(apt-cache show mysql-server 2>/dev/null | grep -c "Package:" || true) if [ "$apt_mysql_avail" -gt 0 ]; then log_info "系统仓库已提供 mysql-server,直接安装" # 关键: 在安装前预配置 lower_case_table_names=1 # MySQL 8.0 只能在数据目录初始化时设置此参数,之后无法更改 mkdir -p /etc/mysql/conf.d cat > /etc/mysql/conf.d/qiaoqiaoyun.cnf <<'MYCNF' [mysqld] lower_case_table_names=1 MYCNF log_info "已预配置 lower_case_table_names=1 (安装前)" DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server else log_info "系统仓库无 mysql-server,使用 MySQL 官方 apt 源安装" local tmp_deb="/tmp/mysql-apt-config.deb" wget -q "https://mirrors.cloud.tencent.com/mysql/apt/pool/mysql-apt-config_0.8.29-1_all.deb" -O "$tmp_deb" \ || wget -q "https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb" -O "$tmp_deb" DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp_deb" || true sed -i 's|repo.mysql.com|mirrors.cloud.tencent.com/mysql|g' /etc/apt/sources.list.d/mysql.list 2>/dev/null || true apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server rm -f "$tmp_deb" fi else local el_ver="${EL_VERSION:-8}" local mysql_installed=false # 方式1: 优先检查系统 AppStream 是否直接提供 MySQL (速度快且兼容性好) if [ "$el_ver" -ge 8 ] 2>/dev/null; then # el10+ 包名为 mysql8.4-server/mysql8.4,el8/el9 为 mysql-server/mysql local mysql_server_pkg="mysql-server" local mysql_client_pkg="mysql" if $PKG_MANAGER list mysql8.4-server 2>/dev/null | grep -q "mysql8.4-server"; then mysql_server_pkg="mysql8.4-server" mysql_client_pkg="mysql8.4" log_info "检测到 el10+ 新包名: mysql8.4-server" fi local pkg_avail=$($PKG_MANAGER list "$mysql_server_pkg" 2>/dev/null | grep -c "$mysql_server_pkg" || true) if [ "$pkg_avail" -gt 0 ]; then log_info "检测到系统 AppStream 提供 $mysql_server_pkg,直接安装" # 如果模块被禁用,先重置 $PKG_MANAGER module reset -y mysql 2>/dev/null || true if $PKG_MANAGER install -y "$mysql_server_pkg" "$mysql_client_pkg"; then mysql_installed=true log_info "MySQL 通过系统 AppStream 安装成功" else log_warn "AppStream 安装失败,回退到 MySQL 官方 repo" fi fi fi # 方式2: 回退使用 MySQL 官方 yum repo if [ "$mysql_installed" = false ]; then log_info "使用 MySQL 官方仓库安装..." rpm -Uvh "https://dev.mysql.com/get/mysql80-community-release-el${el_ver}-9.noarch.rpm" 2>/dev/null || true rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 2>/dev/null || true rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 2>/dev/null || true if curl -sf "https://mirrors.cloud.tencent.com/mysql/yum/mysql-${MYSQL_VERSION}-community/el/${el_ver}/x86_64/repodata/repomd.xml" -o /dev/null 2>/dev/null; then sed -i 's|repo.mysql.com|mirrors.cloud.tencent.com/mysql|g' /etc/yum.repos.d/mysql-community*.repo 2>/dev/null || true log_info "MySQL 使用腾讯云镜像加速" fi if [ "$el_ver" -ge 8 ] 2>/dev/null; then $PKG_MANAGER module disable -y mysql 2>/dev/null || true fi if $PKG_MANAGER install -y mysql-community-server; then mysql_installed=true else log_warn "MySQL 官方 repo 安装失败,回退到系统 mysql-server" # 方式3: 最终回退,直接安装系统自带的 mysql-server $PKG_MANAGER module reset -y mysql 2>/dev/null || true $PKG_MANAGER install -y mysql-server mysql fi fi fi # 配置 lower_case_table_names=1 (敲敲云/JeecgBoot 要求表名不区分大小写) # 注意: MySQL 8.0 此参数只能在数据目录初始化时设置,之后不可更改 # 策略: 直接写入 /etc/my.cnf 的 [mysqld] 段 (最可靠,不依赖 includedir) if [ "$PKG_MANAGER" = "apt" ]; then # apt 路径: 配置文件在 /etc/mysql/ 下,使用 conf.d 目录 local mycnf_dir="/etc/mysql/conf.d" mkdir -p "$mycnf_dir" if [ ! -f "$mycnf_dir/qiaoqiaoyun.cnf" ]; then cat > "$mycnf_dir/qiaoqiaoyun.cnf" <<'MYCNF' [mysqld] lower_case_table_names=1 MYCNF log_info "已设置 lower_case_table_names=1 (apt conf.d)" fi else # yum/dnf 路径: 直接写入 /etc/my.cnf (MySQL 官方 RPM 的 my.cnf 可能不含 includedir) if ! grep -q 'lower_case_table_names' /etc/my.cnf 2>/dev/null; then if grep -q '^\[mysqld\]' /etc/my.cnf 2>/dev/null; then sed -i '/^\[mysqld\]/a lower_case_table_names=1' /etc/my.cnf else # my.cnf 无 [mysqld] 段 (部分 AppStream 安装仅含 includedir) printf '\n[mysqld]\nlower_case_table_names=1\n' >> /etc/my.cnf fi log_info "已设置 lower_case_table_names=1 (/etc/my.cnf)" fi fi systemctl enable mysqld || systemctl enable mysql || true # MySQL 8.0 的 lower_case_table_names 只能在初始化时设置 # 如果数据目录已初始化但参数未生效,需要重新初始化 if [ "$PKG_MANAGER" = "apt" ]; then systemctl start mysql 2>/dev/null || true local lct=$(mysql -u root -Nse "SHOW VARIABLES LIKE 'lower_case_table_names';" 2>/dev/null | awk '{print $2}') if [ "$lct" != "1" ]; then log_warn "lower_case_table_names 未生效 (当前值: ${lct:-unknown}),需要重新初始化 MySQL 数据目录..." systemctl stop mysql 2>/dev/null || true local datadir=$(mysql -u root -Nse "SHOW VARIABLES LIKE 'datadir';" 2>/dev/null | awk '{print $2}' || echo "/var/lib/mysql") [ -z "$datadir" ] && datadir="/var/lib/mysql" rm -rf "${datadir:?}"/* mysqld --initialize-insecure --user=mysql 2>/dev/null || true systemctl start mysql log_info "MySQL 数据目录已重新初始化,lower_case_table_names=1 已生效" fi else # yum/dnf 路径: 如果 MySQL 在 rpm 安装时已自动初始化了数据目录,需要重新初始化 local datadir=$(awk -F= '/^datadir/{gsub(/[ \t]/,"",$2); print $2}' /etc/my.cnf 2>/dev/null) [ -z "$datadir" ] && datadir="/var/lib/mysql" if [ -d "$datadir" ] && [ "$(ls -A "$datadir" 2>/dev/null)" ]; then # 数据目录已存在内容,说明 rpm 安装时已初始化,需要重新初始化以应用 lower_case_table_names log_info "检测到 MySQL 数据目录已初始化,重新初始化以应用 lower_case_table_names=1..." systemctl stop mysqld 2>/dev/null || systemctl stop mysql 2>/dev/null || true rm -rf "${datadir:?}"/* mysqld --initialize --user=mysql 2>/dev/null || true systemctl start mysqld || systemctl start mysql || true log_info "MySQL 数据目录已重新初始化" else systemctl start mysqld || systemctl start mysql || true fi fi # 设置 root 密码 local tmp_pass=$(grep 'temporary password' /var/log/mysqld.log 2>/dev/null | tail -1 | awk '{print $NF}') if [ -n "$tmp_pass" ]; then log_info "正在修改 MySQL root 密码..." mysql --connect-expired-password -u root -p"$tmp_pass" -e \ "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}';" 2>/dev/null || true else # Ubuntu/Debian 默认使用 auth_socket,需要用 sudo mysql 或直接 mysql mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${MYSQL_ROOT_PASSWORD}'; FLUSH PRIVILEGES;" 2>/dev/null || true fi log_info "MySQL 安装完成: $(mysql --version 2>/dev/null)" } # ========================== MongoDB 6.0 ========================== install_mongodb() { log_step "========== 安装 MongoDB $MONGODB_VERSION ==========" if command -v mongod &>/dev/null; then log_warn "MongoDB 已安装: $(mongod --version 2>/dev/null | head -1)" return 0 fi if [ "$PKG_MANAGER" = "apt" ]; then apt-get install -y gnupg curl curl -fsSL "https://www.mongodb.org/static/pgp/server-${MONGODB_VERSION}.asc" | \ gpg --dearmor -o /usr/share/keyrings/mongodb-server-${MONGODB_VERSION}.gpg 2>/dev/null || true local codename if [ "$OS_ID" = "ubuntu" ]; then codename=$(lsb_release -cs 2>/dev/null || echo "jammy") else codename=$(lsb_release -cs 2>/dev/null || echo "bullseye") fi local mongo_apt_url="https://repo.mongodb.org/apt/${OS_ID}" # 检查当前 codename 是否有对应的 MongoDB 仓库,如果没有则回退 # MongoDB 6.0 不支持 Ubuntu 24.04 (noble),需要回退到 jammy if ! curl -sf "${mongo_apt_url}/dists/${codename}/mongodb-org/${MONGODB_VERSION}/Release" -o /dev/null 2>/dev/null; then local fallback_codename="" if [ "$OS_ID" = "ubuntu" ]; then fallback_codename="jammy" else fallback_codename="bullseye" fi log_warn "MongoDB ${MONGODB_VERSION} 没有 ${codename} 仓库,回退到 ${fallback_codename}" codename="$fallback_codename" fi echo "deb [signed-by=/usr/share/keyrings/mongodb-server-${MONGODB_VERSION}.gpg] ${mongo_apt_url} ${codename}/mongodb-org/${MONGODB_VERSION} multiverse" | \ tee /etc/apt/sources.list.d/mongodb-org-${MONGODB_VERSION}.list apt-get update -y apt-get install -y mongodb-org else local el_ver="${EL_VERSION:-8}" local mongo_repo_ver="$el_ver" # MongoDB 6.0 没有 el10 仓库,回退到 el9 if ! curl -sf "https://repo.mongodb.org/yum/redhat/${el_ver}/mongodb-org/${MONGODB_VERSION}/x86_64/repodata/repomd.xml" -o /dev/null 2>/dev/null; then if [ "$el_ver" -ge 10 ] 2>/dev/null; then mongo_repo_ver="9" log_warn "MongoDB ${MONGODB_VERSION} 没有 el${el_ver} 仓库,回退到 el${mongo_repo_ver}" fi fi local mongo_baseurl="https://repo.mongodb.org/yum/redhat/${mongo_repo_ver}/mongodb-org/${MONGODB_VERSION}/x86_64/" # 尝试清华大学镜像加速 local tuna_url="https://mirrors.tuna.tsinghua.edu.cn/mongodb/yum/el${mongo_repo_ver}-${MONGODB_VERSION}/" if curl -sf "${tuna_url}repodata/repomd.xml" -o /tmp/_mongo_repomd.xml 2>/dev/null; then local primary_href=$(grep -oP 'href="repodata/[^"]*primary[^"]*"' /tmp/_mongo_repomd.xml 2>/dev/null | head -1 | tr -d '"' | sed 's/href=//') if [ -n "$primary_href" ] && curl -sf "${tuna_url}${primary_href}" -o /dev/null 2>/dev/null; then mongo_baseurl="$tuna_url" log_info "MongoDB 使用清华大学镜像加速" fi rm -f /tmp/_mongo_repomd.xml fi # el10 加密策略更严格,MongoDB 6.0 的 GPG 密钥绑定签名不被接受,需跳过 GPG 检查 local mongo_gpgcheck=1 if [ "$el_ver" -ge 10 ] 2>/dev/null && [ "$mongo_repo_ver" != "$el_ver" ]; then mongo_gpgcheck=0 log_warn "el${el_ver} 使用 el${mongo_repo_ver} 仓库,跳过 GPG 检查" fi cat > /etc/yum.repos.d/mongodb-org-${MONGODB_VERSION}.repo </dev/null | head -1)" } # ========================== Redis ========================== install_redis() { log_step "========== 安装 Redis ==========" if command -v redis-server &>/dev/null; then log_warn "Redis 已安装: $(redis-server --version 2>/dev/null)" return 0 fi if [ "$PKG_MANAGER" = "apt" ]; then apt-get update -y apt-get install -y redis-server else # el10+: EPEL 没有 redis 包,使用 valkey (Redis 兼容替代品) local use_valkey=false $PKG_MANAGER install -y epel-release || true if $PKG_MANAGER list redis 2>/dev/null | grep -q "^redis\\."; then $PKG_MANAGER install -y redis elif $PKG_MANAGER list valkey 2>/dev/null | grep -q "^valkey\\."; then log_info "系统仓库无 redis,使用 valkey (Redis 兼容替代品)" $PKG_MANAGER install -y valkey use_valkey=true # 创建兼容 symlink,让 redis-server/redis-cli 命令可用 for cmd in server cli benchmark; do if [ -x "/usr/bin/valkey-${cmd}" ] && [ ! -e "/usr/bin/redis-${cmd}" ]; then ln -sf "/usr/bin/valkey-${cmd}" "/usr/bin/redis-${cmd}" fi done log_info "已创建 redis-server/redis-cli → valkey 兼容链接" else log_error "无法找到 redis 或 valkey 包" return 1 fi fi if [ "${use_valkey:-false}" = true ]; then systemctl enable valkey || true systemctl start valkey || true else systemctl enable redis || systemctl enable redis-server || true systemctl start redis || systemctl start redis-server || true fi log_info "Redis 安装完成: $(redis-server --version 2>/dev/null)" } # ========================== 敲敲云部署 ========================== # ========================== 移动端 H5 ========================== # 用法: deploy_h5 <安装包目录> # 独立 nginx 实例监听 H5_PORT, systemd 服务 qiaoqiaoyun-h5 管理, 不使用/不修改系统 nginx 服务 deploy_h5() { local app_dir="$1" local h5_zip h5_zip=$(find "$app_dir" -maxdepth 2 -name "qiaoqiaoyun_app_h5.zip" -type f 2>/dev/null | head -1) if [ -z "$h5_zip" ]; then log_warn "安装包中没有 qiaoqiaoyun_app_h5.zip,跳过移动端 H5 部署" return 0 fi log_step "========== 部署移动端 H5 (端口 ${H5_PORT}) ==========" # 端口已被其他程序占用 (且不是本脚本的 H5 服务) 时跳过 if ss -tln 2>/dev/null | grep -q ":${H5_PORT} " && ! systemctl is-active -q qiaoqiaoyun-h5 2>/dev/null; then log_warn "端口 ${H5_PORT} 已被其他程序占用,跳过 H5 部署" log_warn " 手工部署参考安装包内 docs/h5-deploy.md" return 0 fi # 安装 nginx local nginx_bin nginx_bin=$(command -v nginx 2>/dev/null || true) if [ -z "$nginx_bin" ]; then log_info "安装 nginx..." if [ "$PKG_MANAGER" = "apt" ]; then DEBIAN_FRONTEND=noninteractive apt-get install -y nginx || true else $PKG_MANAGER install -y nginx \ || { $PKG_MANAGER install -y epel-release && $PKG_MANAGER install -y nginx; } \ || true fi nginx_bin=$(command -v nginx 2>/dev/null || true) if [ -z "$nginx_bin" ]; then log_warn "nginx 安装失败,跳过 H5 部署 (手工部署参考安装包内 docs/h5-deploy.md)" return 0 fi # 系统 nginx 默认站点监听 80,会和敲敲云冲突:本脚本只用独立实例,停用系统 nginx 服务 systemctl disable --now nginx 2>/dev/null || true fi # 解压静态文件 rm -rf "${QQYUN_H5_DIR}/html" mkdir -p "${QQYUN_H5_DIR}/html" "${QQYUN_H5_DIR}/logs" if ! unzip -q -o "$h5_zip" -d "${QQYUN_H5_DIR}/html"; then log_warn "H5 解压失败,跳过 H5 部署" return 0 fi # 后端接口地址: 默认用「访问 H5 的主机 + 后端端口」, 与 Docker 部署一致 local api_expr='location.protocol + "//" + location.hostname' [ "$QQYUN_PORT" != "80" ] && api_expr="${api_expr} + \":${QQYUN_PORT}\"" echo "window._appConfig = {\"API_BASE_URL\": ${api_expr}, \"SHARE_URL\": location.origin};" \ > "${QQYUN_H5_DIR}/html/_app.config.js" chmod -R a+rX "${QQYUN_H5_DIR}" # 独立 nginx 配置 local ng_user=nobody id nginx &>/dev/null && ng_user=nginx id www-data &>/dev/null && ng_user=www-data local mime=/etc/nginx/mime.types if [ ! -f "$mime" ]; then mime="$(dirname "$("$nginx_bin" -V 2>&1 | grep -o 'conf-path=[^ ]*' | cut -d= -f2)")/mime.types" fi local conf="${QQYUN_H5_DIR}/nginx.conf" cat > "$conf" </dev/null && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then command -v semanage &>/dev/null || $PKG_MANAGER install -y policycoreutils-python-utils 2>/dev/null || $PKG_MANAGER install -y policycoreutils-python 2>/dev/null || true semanage port -a -t http_port_t -p tcp "$H5_PORT" 2>/dev/null || semanage port -m -t http_port_t -p tcp "$H5_PORT" 2>/dev/null || true chcon -R -t httpd_sys_content_t "${QQYUN_H5_DIR}/html" 2>/dev/null || true chcon -R -t httpd_log_t "${QQYUN_H5_DIR}/logs" 2>/dev/null || true fi if ! "$nginx_bin" -t -c "$conf" >/dev/null 2>&1; then log_warn "H5 nginx 配置检查失败,跳过 H5 部署:" "$nginx_bin" -t -c "$conf" 2>&1 | tail -3 return 0 fi # systemd 服务 (开机自启) if command -v systemctl &>/dev/null; then cat > /etc/systemd/system/qiaoqiaoyun-h5.service </dev/null 2>&1 || true systemctl restart qiaoqiaoyun-h5 || true else "$nginx_bin" -c "$conf" -s quit 2>/dev/null || true "$nginx_bin" -c "$conf" || true fi sleep 1 if curl -sf -o /dev/null --max-time 5 "http://127.0.0.1:${H5_PORT}/"; then H5_DEPLOYED=1 log_info "移动端 H5 已部署: ${QQYUN_H5_DIR}/html (端口 ${H5_PORT}, 服务 qiaoqiaoyun-h5)" else log_warn "H5 服务启动失败,请检查: systemctl status qiaoqiaoyun-h5 / ${QQYUN_H5_DIR}/logs/error.log" fi } deploy_qqyun() { log_step "========== 部署敲敲云 $QQYUN_VERSION ==========" # 检查环境 local missing="" command -v java &>/dev/null || missing="${missing} JDK17" command -v mysql &>/dev/null || missing="${missing} MySQL" command -v mongod &>/dev/null || missing="${missing} MongoDB" command -v redis-cli &>/dev/null || missing="${missing} Redis" if [ -n "$missing" ]; then log_error "缺少依赖:${missing}" log_error "请先运行: curl -fsSL https://www.qiaoqiaoyun.com/install.sh | bash -s env" exit 1 fi # 停止已有的敲敲云进程 if [ -f "${QQYUN_DEPLOY_DIR}/qqyun.pid" ]; then local old_pid=$(cat "${QQYUN_DEPLOY_DIR}/qqyun.pid" 2>/dev/null) if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then log_info "停止旧的敲敲云进程 (PID: $old_pid)..." kill "$old_pid" 2>/dev/null || true sleep 2 fi fi # 同时清理残留的 java 进程 (占用部署端口) local port_pid=$(ss -tlnp 2>/dev/null | grep ":${QQYUN_PORT} " | grep -oP 'pid=\K[0-9]+' | head -1) if [ -n "$port_pid" ]; then log_info "端口 ${QQYUN_PORT} 被占用 (PID: $port_pid),正在停止..." kill "$port_pid" 2>/dev/null || true sleep 3 fi mkdir -p "$QQYUN_DEPLOY_DIR" # 下载敲敲云 local zip_file="/tmp/qiaoqiaoyun-${QQYUN_VERSION}.zip" if [ ! -f "$zip_file" ]; then log_info "下载敲敲云 $QQYUN_VERSION (约 200MB)..." # 尝试多个下载源: 官网优先, GitHub 备用 local downloaded=false local urls=( "$QQYUN_DOWNLOAD_URL_PRIMARY" "$QQYUN_DOWNLOAD_URL_BACKUP" ) for url in "${urls[@]}"; do log_info "下载地址: $url" if curl -fL --connect-timeout 15 --max-time 600 --progress-bar "$url" -o "$zip_file" 2>&1; then # 校验文件大小 (至少 100MB) local fsize fsize=$(stat -c%s "$zip_file" 2>/dev/null || stat -f%z "$zip_file" 2>/dev/null || echo 0) if [ "$fsize" -gt 104857600 ]; then downloaded=true break else log_warn "下载文件过小 (${fsize} bytes),可能不完整,尝试下一个源..." rm -f "$zip_file" fi else rm -f "$zip_file" fi done if [ "$downloaded" = false ]; then log_error "下载失败! 请手动下载后重试:" log_error " 1. 下载: $QQYUN_DOWNLOAD_URL_PRIMARY" log_error " 备用: $QQYUN_DOWNLOAD_URL_BACKUP" log_error " 2. 上传到服务器: $zip_file" log_error " 3. 重新运行: curl -fsSL https://www.qiaoqiaoyun.com/install.sh | bash -s qqyun" exit 1 fi else log_info "安装包已存在: $zip_file" # 验证已有文件是否有效 if ! unzip -t "$zip_file" > /dev/null 2>&1; then log_warn "已有 ZIP 文件损坏,删除后重新下载..." rm -f "$zip_file" log_error "请重新运行安装脚本" exit 1 fi fi # 解压 log_info "解压敲敲云到 $QQYUN_DEPLOY_DIR ..." if ! unzip -o "$zip_file" -d "$QQYUN_DEPLOY_DIR"; then log_error "解压失败! ZIP 文件可能损坏,请删除后重试: rm -f $zip_file" exit 1 fi # 查找解压后的目录结构 local app_dir="$QQYUN_DEPLOY_DIR" if [ -d "$QQYUN_DEPLOY_DIR/${QQYUN_VERSION}" ]; then app_dir="$QQYUN_DEPLOY_DIR/${QQYUN_VERSION}" fi local sub_dir=$(find "$QQYUN_DEPLOY_DIR" -maxdepth 1 -type d -name "qiaoqiaoyun*" 2>/dev/null | head -1) if [ -n "$sub_dir" ]; then app_dir="$sub_dir" fi # 初始化 MySQL 数据库 log_info "初始化 MySQL 数据库..." local sql_file=$(find "$app_dir" -name "qiaoqiaoyun.sql" -type f 2>/dev/null | head -1) # 初始化 SQL 含 DROP TABLE, 已有数据时导入会清空业务数据 -> 默认跳过 local has_data has_data=$(mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${MYSQL_QQYUN_DB}' AND table_name='sys_user'" 2>/dev/null || echo 0) if [ "$has_data" = "1" ] && [ "$QQYUN_FORCE" != "1" ]; then log_warn "数据库 ${MYSQL_QQYUN_DB} 已有数据,跳过初始化 SQL (保留现有数据)" log_warn " 如需全新初始化 (会清空数据!),请加 --force 重新执行" log_warn " 从旧版本升级如需执行升级 SQL,请参考安装包内 _UpgradeSql 目录" elif [ -n "$sql_file" ]; then mysql -u root -p"${MYSQL_ROOT_PASSWORD}" < "$sql_file" 2>/dev/null && \ log_info "数据库初始化完成" || \ log_warn "数据库初始化失败,请检查: mysql -uroot -p'${MYSQL_ROOT_PASSWORD}' < $sql_file" else log_warn "未找到 qiaoqiaoyun.sql,请手动导入数据库" fi local server_ip server_ip=$(curl -sf --max-time 3 ifconfig.me 2>/dev/null \ || curl -sf --max-time 3 ip.sb 2>/dev/null \ || curl -sf --max-time 3 ipinfo.io/ip 2>/dev/null \ || hostname -I 2>/dev/null | awk '{print $1}') [ -z "$server_ip" ] && server_ip="<服务器IP>" # PC 地址未指定时默认用服务器 IP [ -z "$QQYUN_PC_URL" ] && [ "$server_ip" != "<服务器IP>" ] && QQYUN_PC_URL="http://${server_ip}" # 部署移动端 H5 (在启动后端之前: apt 安装 nginx 会自动启动并占用 80 端口, 需先停用) deploy_h5 "$app_dir" # H5 地址未指定且本机 H5 部署成功时,默认用 服务器IP:H5端口 (钉钉「立刻办理」链接) if [ -z "$QQYUN_APP_URL" ] && [ "$H5_DEPLOYED" = "1" ] && [ "$server_ip" != "<服务器IP>" ]; then QQYUN_APP_URL="http://${server_ip}:${H5_PORT}" fi # 修改配置文件 (排除 docker 目录) local config_file=$(find "$app_dir" -path "*/config/application-prod.yml" -not -path "*/docker/*" -type f 2>/dev/null | head -1) if [ -z "$config_file" ]; then config_file=$(find "$app_dir" -name "application-prod.yml" -not -path "*/docker/*" -type f 2>/dev/null | head -1) fi if [ -n "$config_file" ]; then log_info "配置文件: $config_file" cp "$config_file" "${config_file}.bak" # MySQL: 替换 datasource.master 下的密码和地址 sed -i '/datasource:/,/driver-class-name:/s|password:.*|password: '"${MYSQL_ROOT_PASSWORD}"'|' "$config_file" sed -i "s|jdbc:mysql://[^/]*/|jdbc:mysql://127.0.0.1:3306/|g" "$config_file" # MongoDB: 去掉认证信息,替换地址 sed -i "s|mongodb://[^@]*@\([^/]*\)/|mongodb://\1/|g" "$config_file" sed -i "s|mongodb://[^/]*/|mongodb://127.0.0.1:27017/|g" "$config_file" # Redis: 替换地址 sed -i '/^ redis:/,/^ [a-z]/{s|host:.*|host: 127.0.0.1|}' "$config_file" # Flowable: 启用自动 schema 更新 sed -i 's|database-schema-update: false|database-schema-update: true|' "$config_file" # 前端访问地址 jeecg.domainUrl (钉钉消息「立刻办理」链接取 app 地址) if [ -n "$QQYUN_PC_URL" ]; then sed -i "/^ domainUrl:/,/^ [a-z]/{s|^\( pc:\).*|\1 \"${QQYUN_PC_URL}\"|}" "$config_file" fi if [ -n "$QQYUN_APP_URL" ]; then sed -i "/^ domainUrl:/,/^ [a-z]/{s|^\( app:\).*|\1 \"${QQYUN_APP_URL}\"|}" "$config_file" fi # 百度地图 AK (表单地图控件) if [ -n "$QQYUN_BAIDU_AK" ]; then sed -i "/^ map:/,/baidu:/{s|^\([[:space:]]*baidu:\).*|\1 ${QQYUN_BAIDU_AK}|}" "$config_file" fi log_info "配置文件已更新" else log_warn "未找到 application-prod.yml,请手动配置" fi # 设置权限并启动 find "$app_dir" -name "*.sh" -exec chmod +x {} \; local start_script=$(find "$app_dir" -name "start.sh" -type f 2>/dev/null | head -1) if [ -n "$start_script" ]; then local start_dir=$(dirname "$start_script") cd "$start_dir" nohup bash "$start_script" >> "${start_dir}/qqyun.log" 2>&1 & local qqyun_pid=$! echo "$qqyun_pid" > "${start_dir}/qqyun.pid" log_info "敲敲云正在启动 (PID: $qqyun_pid),首次启动需 1-2 分钟..." # 配置开机自启 (crontab @reboot, 服务器重启后自动拉起敲敲云) if command -v crontab &>/dev/null; then local cron_cmd="cd ${start_dir} && nohup bash start.sh >> ${start_dir}/qqyun.log 2>&1 &" # 注意: 干净系统下 crontab -l 返回非零, set -e 模式会让子 shell 提前退出导致写入空 # 因此分两步: 先收集现有非本条记录(失败兜底为空), 再拼接写回 local existing existing=$(crontab -l 2>/dev/null | grep -vF "$cron_cmd" || true) { [ -n "$existing" ] && printf '%s\n' "$existing"; printf '@reboot %s\n' "$cron_cmd"; } | crontab - log_info "已配置开机自启 (crontab @reboot)" else log_warn "系统未安装 crontab,跳过开机自启配置" fi else log_warn "未找到 start.sh,请手动启动: cd $app_dir && ./start.sh" fi echo "" echo -e "${CYAN}======================================================${NC}" echo -e "${GREEN} 敲敲云 $QQYUN_VERSION 部署完成!${NC}" echo -e "${CYAN}======================================================${NC}" echo "" echo -e " 访问地址: ${GREEN}http://${server_ip}${NC}" if [ "$H5_DEPLOYED" = "1" ]; then echo -e " 移动端H5: ${GREEN}${QQYUN_APP_URL:-http://${server_ip}:${H5_PORT}}${NC} (钉钉「立刻办理」链接地址)" fi echo -e " 默认账号: ${GREEN}admin${NC}" echo -e " 默认密码: ${GREEN}123456${NC}" echo "" echo -e " 部署目录: $app_dir" echo -e " 开机自启: ${GREEN}已配置${NC} (重启服务器后自动拉起)" echo -e " 查看日志: tail -f ${app_dir}/qqyun.log" echo -e " 停止服务: pkill -f 'qiaoqiaoyun-start.*\\.jar'" echo -e " 启动服务: cd ${app_dir} && nohup bash start.sh >> qqyun.log 2>&1 &" [ -n "$config_file" ] && echo -e " 配置文件: $config_file" [ "$H5_DEPLOYED" = "1" ] && echo -e " H5 服务: systemctl restart qiaoqiaoyun-h5 (目录 ${QQYUN_H5_DIR})" echo "" echo -e " ${YELLOW}提示: 云服务器请在安全组放行 ${QQYUN_PORT} 和 ${H5_PORT} 端口${NC}" echo -e "${CYAN}======================================================${NC}" if [ -n "$config_file" ] && { [ -z "$QQYUN_APP_URL" ] || [ -z "$QQYUN_BAIDU_AK" ]; }; then echo "" echo -e "${YELLOW}[私有化部署注意] 以下配置尚未设置 (修改配置文件后重启敲敲云生效):${NC}" [ -z "$QQYUN_APP_URL" ] && echo -e " - ${YELLOW}jeecg.domainUrl.app${NC}: 钉钉消息「立刻办理」链接地址,需配置为钉钉客户端能访问的外网 H5 地址" [ -z "$QQYUN_BAIDU_AK" ] && echo -e " - ${YELLOW}jeecg.desform.map.baidu${NC}: 百度地图 AK,表单地图控件需要 (申请: https://lbsyun.baidu.com/apiconsole/key)" echo " 也可重新执行并传参: bash install.sh qqyun --app-url http://h5.example.com --baidu-ak 你的AK" fi } # ========================== Main ========================== main() { # 通过 curl | bash 运行时,自动提升 root 权限 if [ "$(id -u)" -ne 0 ]; then log_error "请使用 root 权限运行:" echo "" echo " curl -fsSL https://www.qiaoqiaoyun.com/install.sh | sudo bash" echo "" exit 1 fi echo "" echo -e "${CYAN}========================================${NC}" echo -e "${CYAN} 敲敲云 一键部署脚本${NC}" echo -e "${CYAN} 版本: $QQYUN_VERSION${NC}" echo -e "${CYAN}========================================${NC}" echo "" # 解析参数: 组件名 + 选项 (环境变量为默认值, 命令行优先) QQYUN_PC_URL="${JEECG_DOMAIN_URL_PC:-}" QQYUN_APP_URL="${JEECG_DOMAIN_URL_APP:-}" QQYUN_BAIDU_AK="${BAIDU_MAP_AK:-}" QQYUN_FORCE=0 local components=() while [ $# -gt 0 ]; do case "$1" in --pc-url) QQYUN_PC_URL="${2:?--pc-url 需要值}"; shift 2 ;; --app-url) QQYUN_APP_URL="${2:?--app-url 需要值}"; shift 2 ;; --baidu-ak) QQYUN_BAIDU_AK="${2:?--baidu-ak 需要值}"; shift 2 ;; --force) QQYUN_FORCE=1; shift ;; -*) log_error "未知选项: $1"; echo "选项: --pc-url URL --app-url URL --baidu-ak AK --force"; exit 1 ;; *) components+=("$1"); shift ;; esac done detect_os install_prerequisites # 支持 curl | bash -s 传参 if [ ${#components[@]} -eq 0 ] || [ "${components[0]}" = "all" ]; then components=("jdk17" "mysql" "mongodb" "redis" "qqyun") elif [ "${components[0]}" = "env" ]; then components=("jdk17" "mysql" "mongodb" "redis") fi for comp in "${components[@]}"; do case "$comp" in jdk17) install_jdk17 ;; mysql) install_mysql ;; mongodb) install_mongodb ;; redis) install_redis ;; qqyun) deploy_qqyun ;; env) install_jdk17; install_mysql; install_mongodb; install_redis ;; all) install_jdk17; install_mysql; install_mongodb; install_redis; deploy_qqyun ;; *) log_error "未知组件: $comp" echo "用法: curl -fsSL https://www.qiaoqiaoyun.com/install.sh | bash -s [all|env|mysql|mongodb|redis|jdk17|qqyun]" exit 1 ;; esac done echo "" log_info "==================== 环境检查 ====================" command -v java &>/dev/null && log_info "JDK: $(java -version 2>&1 | head -1)" || log_warn "JDK: 未安装" command -v mysql &>/dev/null && log_info "MySQL: $(mysql --version 2>/dev/null)" || log_warn "MySQL: 未安装" command -v mongod &>/dev/null && log_info "MongoDB: $(mongod --version 2>/dev/null | head -1)" || log_warn "MongoDB: 未安装" command -v redis-server &>/dev/null && log_info "Redis: $(redis-server --version 2>/dev/null)" || log_warn "Redis: 未安装" log_info "==================================================" } main "$@" } # 执行包裹函数 (确保 curl | bash 模式下脚本完整下载后再执行) _main "$@"