#!/usr/bin/env bash set -Eeuo pipefail readonly INSTALL_DIR="${SHOP_PRO_INSTALL_DIR:-/opt/shop-pro}" readonly COMMAND_PATH="${SHOP_PRO_COMMAND_PATH:-/usr/local/bin/shop-pro}" readonly SOURCE_URL="${SHOP_PRO_SOURCE_URL:-https://github.com/yunmengnb/jingpro-shop-reverse/archive/refs/heads/main.tar.gz}" fail() { printf '错误:%s\n' "$1" >&2; exit 1; } validate_shop_url() { [[ "$1" =~ ^https?://[^/]+/shop/[A-Za-z0-9_-]+/?$ ]]; } validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535)); } validate_domain() { [[ "$1" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; } require_root() { ((EUID == 0)) || fail "请使用 root 用户运行安装命令。"; } detect_server_ip() { local ip for svc in "https://api.ipify.org" "https://ifconfig.me/ip" "https://ip.sb" "https://icanhazip.com"; do ip=$(curl -fsSL --connect-timeout 3 --max-time 5 "$svc" 2>/dev/null | tr -d '\r\n ') && [[ -n "$ip" ]] && { printf '%s' "$ip"; return; } done ip=$(hostname -I 2>/dev/null | awk '{print $1}') [[ -n "$ip" ]] && { printf '%s' "$ip"; return; } printf '服务器IP' } is_bt_panel() { [[ -d /www/server/panel ]] && [[ -f /www/server/nginx/conf/nginx.conf ]]; } port_in_use() { local port=$1 if command -v ss >/dev/null 2>&1; then ss -tln 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${port}$"; return $?; fi if command -v netstat >/dev/null 2>&1; then netstat -tln 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${port}$"; return $?; fi if command -v lsof >/dev/null 2>&1; then lsof -i ":${port}" -sTCP:LISTEN >/dev/null 2>&1; return $?; fi return 1 } PKG_MANAGER='' detect_pkg_manager() { if command -v apt-get >/dev/null 2>&1; then PKG_MANAGER=apt-get elif command -v dnf >/dev/null 2>&1; then PKG_MANAGER=dnf elif command -v yum >/dev/null 2>&1; then PKG_MANAGER=yum elif command -v apk >/dev/null 2>&1; then PKG_MANAGER=apk elif command -v zypper >/dev/null 2>&1; then PKG_MANAGER=zypper fi } pkg_install() { case "$PKG_MANAGER" in apt-get) DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1 || true; DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" || return 1 ;; dnf) dnf install -y "$@" || return 1 ;; yum) yum install -y "$@" || return 1 ;; apk) apk add --no-cache "$@" || return 1 ;; zypper) zypper --non-interactive install "$@" || return 1 ;; *) return 1 ;; esac } ensure_command() { local cmd=$1 pkg=$2 if command -v "$cmd" >/dev/null 2>&1; then printf '已检测到 %s,跳过安装。\n' "$cmd" return 0 fi [[ -n "$PKG_MANAGER" ]] || fail "未检测到 $cmd,且无法识别系统包管理器,请手动安装后重试。" printf '未检测到 %s,正在自动安装...\n' "$cmd" pkg_install "$pkg" || fail "$pkg 自动安装失败,请手动安装后重试。" command -v "$cmd" >/dev/null 2>&1 || fail "$pkg 已安装但未找到 $cmd 命令,请检查 PATH。" printf '%s 安装完成。\n' "$cmd" } ensure_docker() { if command -v docker >/dev/null 2>&1; then printf '已检测到 Docker,跳过安装。\n' else printf '未检测到 Docker,正在自动安装 Docker Engine...\n' if curl -fsSL https://get.docker.com -o /tmp/get-docker.sh && sh /tmp/get-docker.sh; then rm -f /tmp/get-docker.sh else rm -f /tmp/get-docker.sh [[ -n "$PKG_MANAGER" ]] || fail "Docker 自动安装失败,请手动安装 Docker Engine。" case "$PKG_MANAGER" in apt-get) pkg_install docker.io docker-compose-v2 || pkg_install docker.io || fail "Docker 自动安装失败,请手动安装 Docker Engine。" ;; dnf|yum) pkg_install docker-ce docker-ce-cli containerd.io || fail "Docker 自动安装失败,请手动安装 Docker Engine。" ;; apk|zypper) pkg_install docker || fail "Docker 自动安装失败,请手动安装 Docker Engine。" ;; esac fi command -v docker >/dev/null 2>&1 || fail "Docker 已安装但未找到 docker 命令,请检查 PATH。" printf 'Docker 安装完成。\n' fi if docker info >/dev/null 2>&1; then printf 'Docker 服务运行正常。\n' else printf 'Docker 服务未运行,正在启动...\n' if command -v systemctl >/dev/null 2>&1; then systemctl enable --now docker >/dev/null 2>&1 || true elif command -v service >/dev/null 2>&1; then service docker start >/dev/null 2>&1 || true fi local i for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break sleep 1 done docker info >/dev/null 2>&1 || fail "Docker 服务启动失败,请手动启动 Docker 后重试。" printf 'Docker 服务已启动。\n' fi } ensure_compose() { if docker compose version >/dev/null 2>&1; then printf '已检测到 Docker Compose 插件,跳过安装。\n' return 0 fi printf '未检测到 Docker Compose 插件,正在自动安装...\n' case "$PKG_MANAGER" in apt-get) pkg_install docker-compose-plugin || pkg_install docker-compose-v2 || true ;; dnf|yum) pkg_install docker-compose-plugin || true ;; apk) pkg_install docker-cli-compose || true ;; zypper) pkg_install docker-compose-plugin || true ;; esac if docker compose version >/dev/null 2>&1; then printf 'Docker Compose 插件安装完成。\n' return 0 fi fail "Docker Compose 插件自动安装失败,请手动安装 docker-compose-plugin 后重试。" } ensure_dependencies() { detect_pkg_manager ensure_command curl curl ensure_command tar tar ensure_docker ensure_compose } show_agreement() { cat <<'AGREEMENT' ================ 使用协议与免责声明 ================ 1. 本程序仅限合法合规用途,只能连接你拥有或已获明确授权的店铺及接口。 2. 禁止用于欺诈、钓鱼、洗钱、窃取数据、侵犯隐私、绕过访问控制或网络攻击。 3. 你须自行保障服务器、店铺配置、订单数据、支付信息及用户隐私安全。 4. 你须独立承担商品、交易、支付、履约、退款、售后及数据处理的全部责任。 5. 本程序按"现状"提供,不保证持续可用或永久兼容第三方接口。 6. 因上游变更、网络或支付异常、服务器配置、第三方服务、错误操作造成的损失,程序提供方不承担责任。 7. 因违法违规、违反第三方协议或超出授权范围使用产生的一切责任由使用者承担。 8. 完整协议随程序提供于 AGREEMENT.md。输入 yes 表示你已完整阅读、理解并接受协议。 ==================================================== AGREEMENT local agreement_answer printf '请输入 yes 确认接受协议,输入其他内容将退出安装:' IFS= read -r agreement_answer < /dev/tty [[ "$agreement_answer" == 'yes' ]] || fail "你未接受使用协议与免责声明,安装已终止。" } open_port() { local port=$1 if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null firewall-cmd --reload >/dev/null elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then ufw allow "${port}/tcp" >/dev/null elif command -v iptables >/dev/null 2>&1; then iptables -C INPUT -p tcp --dport "$port" -j ACCEPT >/dev/null 2>&1 || iptables -I INPUT -p tcp --dport "$port" -j ACCEPT else printf '提示:未检测到受支持的防火墙,Docker 已发布端口 %s。\n' "$port" fi } install_source() { local archive extract_root source_root backup archive=$(mktemp) extract_root=$(mktemp -d) backup=$(mktemp -d) trap 'rm -f "${archive:-}"; rm -rf "${extract_root:-}" "${backup:-}"' RETURN printf '正在下载程序文件...\n' curl --fail --location --silent --show-error "$SOURCE_URL" --output "$archive" || fail "程序文件下载失败。" tar -xzf "$archive" -C "$extract_root" || fail "程序压缩包解压失败。" source_root=$(find "$extract_root" -mindepth 1 -maxdepth 1 -type d -print -quit) [[ -n "$source_root" ]] || fail "压缩包解压为空。" if [[ ! -f "$source_root/compose.yaml" ]]; then source_root=$(find "$extract_root" -mindepth 2 -maxdepth 2 -type d -name node-docker -print -quit) [[ -n "$source_root" && -f "$source_root/compose.yaml" ]] || fail "压缩包中未找到 compose.yaml。" fi mkdir -p "$INSTALL_DIR" [[ ! -f "$INSTALL_DIR/.env" ]] || cp "$INSTALL_DIR/.env" "$backup/.env" cp -a "$source_root/." "$INSTALL_DIR/" [[ ! -f "$backup/.env" ]] || cp "$backup/.env" "$INSTALL_DIR/.env" mkdir -p "$INSTALL_DIR/nginx/certs" "$INSTALL_DIR/nginx/vhost" } write_env() { local shop_url=$1 port=$2 umask 077 cat > "$INSTALL_DIR/.env" </dev/null 2>&1; then nginx -t 2>/dev/null && nginx -s reload 2>/dev/null || /etc/init.d/nginx reload 2>/dev/null || true elif command -v nginx >/dev/null 2>&1; then nginx -t 2>/dev/null && nginx -s reload 2>/dev/null || true fi } DOMAINS_FILE="$INSTALL_DIR/nginx/domains.conf" install_command() { cat > "$COMMAND_PATH" <<'MENU' #!/usr/bin/env bash set -Eeuo pipefail INSTALL_DIR="${SHOP_PRO_INSTALL_DIR:-/opt/shop-pro}" COMMAND_PATH="${SHOP_PRO_COMMAND_PATH:-/usr/local/bin/shop-pro}" SOURCE_URL="${SHOP_PRO_SOURCE_URL:-__SOURCE_URL__}" ENV_FILE="$INSTALL_DIR/.env" DOMAINS_FILE="$INSTALL_DIR/nginx/domains.conf" validate_shop_url() { [[ "$1" =~ ^https?://[^/]+/shop/[A-Za-z0-9_-]+/?$ ]]; } validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535)); } validate_domain() { [[ "$1" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; } get_env() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1; } set_env() { local key=$1 value=$2 temporary temporary=$(mktemp) awk -v key="$key" -v value="$value" 'BEGIN{found=0} index($0,key "=")==1{print key "=" value;found=1;next}{print} END{if(!found)print key "=" value}' "$ENV_FILE" > "$temporary" chmod 600 "$temporary"; mv "$temporary" "$ENV_FILE" } open_port() { local port=$1 if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null; firewall-cmd --reload >/dev/null elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q '^Status: active'; then ufw allow "${port}/tcp" >/dev/null elif command -v iptables >/dev/null 2>&1; then iptables -C INPUT -p tcp --dport "$port" -j ACCEPT >/dev/null 2>&1 || iptables -I INPUT -p tcp --dport "$port" -j ACCEPT else printf '提示:未检测到受支持的防火墙,Docker 已发布端口 %s。\n' "$port"; fi } is_bt_panel() { [[ -d /www/server/panel ]] && [[ -f /www/server/nginx/conf/nginx.conf ]]; } reload_nginx() { if is_bt_panel && command -v nginx >/dev/null 2>&1; then nginx -t 2>/dev/null && nginx -s reload 2>/dev/null || /etc/init.d/nginx reload 2>/dev/null || true elif command -v nginx >/dev/null 2>&1; then nginx -t 2>/dev/null && nginx -s reload 2>/dev/null || true fi } list_domains() { if [[ ! -f "$DOMAINS_FILE" ]] || [[ ! -s "$DOMAINS_FILE" ]]; then printf '(尚未绑定任何域名)\n'; return; fi printf '当前绑定域名:\n' local n=1 while IFS='|' read -r domain cert_file key_file; do [[ -z "$domain" || "$domain" == \#* ]] && continue local ssl_status='未配置 SSL' [[ -n "$cert_file" && -n "$key_file" ]] && ssl_status="已配置 SSL" printf ' %d. %s [%s]\n' "$n" "$domain" "$ssl_status" n=$((n+1)) done < "$DOMAINS_FILE" } set_domain_record() { local domain=$1 cert_file=$2 key_file=$3 mkdir -p "$(dirname "$DOMAINS_FILE")" touch "$DOMAINS_FILE" awk -v d="$domain" -v cf="$cert_file" -v kf="$key_file" -F'|' 'BEGIN{OFS="|"} $1==d{print d,cf,kf; found=1; next}{print} END{if(!found) print d,cf,kf}' "$DOMAINS_FILE" > "${DOMAINS_FILE}.new" && mv "${DOMAINS_FILE}.new" "$DOMAINS_FILE" } remove_domain_record() { local domain=$1; grep -v "^${domain}|" "$DOMAINS_FILE" > "${DOMAINS_FILE}.new" 2>/dev/null || true; mv "${DOMAINS_FILE}.new" "$DOMAINS_FILE"; } get_domain_record() { local domain=$1; grep "^${domain}|" "$DOMAINS_FILE" 2>/dev/null | head -n1 || true; } build_proxy_block() { local domain=$1 proxy_host=$2 proxy_port=$3 cert_file=$4 key_file=$5 cert_dir=$6 local ssl_server='' if [[ -n "$cert_file" && -n "$key_file" && -f "${cert_dir}/${cert_file}" && -f "${cert_dir}/${key_file}" ]]; then ssl_server=$'\nserver {\n listen 443 ssl http2;\n server_name '"${domain}"$';\n ssl_certificate '"${cert_dir}"'/'"${cert_file}"$';\n ssl_certificate_key '"${cert_dir}"'/'"${key_file}"$';\n ssl_protocols TLSv1.2 TLSv1.3;\n ssl_ciphers HIGH:!aNULL:!MD5;\n ssl_prefer_server_ciphers on;\n\n location / {\n proxy_pass http://'"${proxy_host}"':'"${proxy_port}"$';\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n }\n}\n' fi printf '%s\nserver {\n listen 80;\n server_name %s;\n\n location / {\n proxy_pass http://%s:%s;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n }\n}\n' "$ssl_server" "$domain" "$proxy_host" "$proxy_port" } apply_domain_conf() { local port cert_dir conf_path _d _cf _kf port=$(get_env PORT); [[ -n "$port" ]] || port="3000" [[ ! -f "$DOMAINS_FILE" || ! -s "$DOMAINS_FILE" ]] && return 0 while IFS='|' read -r _d _cf _kf; do [[ -z "$_d" || "$_d" == \#* ]] && continue if is_bt_panel; then conf_path="/www/server/panel/vhost/nginx/${_d}.conf"; cert_dir="/www/server/panel/vhost/cert/${_d}"; mkdir -p "$cert_dir" else conf_path="$INSTALL_DIR/nginx/vhost/${_d}.conf"; cert_dir="$INSTALL_DIR/nginx/certs"; mkdir -p "$INSTALL_DIR/nginx/vhost" fi build_proxy_block "$_d" "127.0.0.1" "$port" "$_cf" "$_kf" "$cert_dir" > "$conf_path" printf ' 反代配置已写入:%s\n' "$conf_path" done < "$DOMAINS_FILE" reload_nginx } # 多行粘贴:允许证书链中存在空行,连续两个空行才结束输入 paste_pem() { local line tmp empty_count=0 tmp=$(mktemp) printf '(粘贴完整内容后,连续按两次回车确认)\n' >&2 while IFS= read -r line < /dev/tty; do if [[ -z "$line" ]]; then empty_count=$((empty_count + 1)) ((empty_count >= 2)) && break printf '\n' >> "$tmp" continue fi empty_count=0 printf '%s\n' "$line" >> "$tmp" done cat "$tmp" rm -f "$tmp" } reset_shop_url() { [[ -f "$ENV_FILE" ]] || { printf '程序尚未安装,请选择 3。\n'; return; } local value printf '请输入新的完整店铺链接:'; IFS= read -r value < /dev/tty validate_shop_url "$value" || { printf '错误:店铺链接格式无效。\n' >&2; return; } set_env SHOP_URL "$value" docker compose --project-directory "$INSTALL_DIR" up -d --force-recreate shop printf '店铺链接已重置。\n' } reset_port() { [[ -f "$ENV_FILE" ]] || { printf '程序尚未安装,请选择 3。\n'; return; } local value printf '请输入新的运行端口:'; IFS= read -r value < /dev/tty validate_port "$value" || { printf '错误:端口必须是 1-65535 的数字。\n' >&2; return; } set_env PORT "$value"; open_port "$value" apply_domain_conf docker compose --project-directory "$INSTALL_DIR" up -d --force-recreate shop printf '运行端口已重置为 %s。\n' "$value" } install_or_upgrade() { local archive extract_root source_root shop_url port saved_env archive=$(mktemp); extract_root=$(mktemp -d); saved_env=$(mktemp) [[ ! -f "$ENV_FILE" ]] || cp "$ENV_FILE" "$saved_env" printf '正在下载并安装/升级...\n' if ! curl -fsSL "$SOURCE_URL" -o "$archive" || ! tar -xzf "$archive" -C "$extract_root"; then rm -rf "$archive" "$extract_root" "$saved_env"; printf '错误:下载或解压失败。\n' >&2; return; fi source_root=$(find "$extract_root" -mindepth 1 -maxdepth 1 -type d -print -quit) if [[ -z "$source_root" || ! -f "$source_root/compose.yaml" ]]; then source_root=$(find "$extract_root" -mindepth 2 -maxdepth 2 -type d -name node-docker -print -quit) fi [[ -n "$source_root" && -f "$source_root/compose.yaml" ]] || { rm -rf "$archive" "$extract_root" "$saved_env"; printf '错误:未找到程序目录。\n' >&2; return; } mkdir -p "$INSTALL_DIR"; cp -a "$source_root/." "$INSTALL_DIR/" mkdir -p "$INSTALL_DIR/nginx/certs"; mkdir -p "$INSTALL_DIR/nginx/vhost" if [[ -s "$saved_env" ]]; then cp "$saved_env" "$ENV_FILE" else printf '请输入完整店铺链接(支持 http:// 或 https://):'; IFS= read -r shop_url < /dev/tty; validate_shop_url "$shop_url" || { printf '错误:店铺链接格式无效。\n' >&2; return; } printf '请输入运行端口 [3000]:'; IFS= read -r port < /dev/tty; port=${port:-3000}; validate_port "$port" || { printf '错误:端口无效。\n' >&2; return; } umask 077; printf 'SHOP_URL=%s\nPORT=%s\nVERIFY_SSL=false\nCACHE_TTL=60\n' "$shop_url" "$port" > "$ENV_FILE" fi port=$(get_env PORT); open_port "$port" docker compose --project-directory "$INSTALL_DIR" up -d --build apply_domain_conf if [[ -f "$source_root/install.sh" ]]; then SHOP_PRO_COMMAND_ONLY=1 SHOP_PRO_SOURCE_URL="$SOURCE_URL" SHOP_PRO_COMMAND_PATH="$COMMAND_PATH" bash "$source_root/install.sh" printf '管理菜单已同步更新。\n' fi rm -rf "$archive" "$extract_root" "$saved_env" printf '安装/升级完成,正在切换到新版菜单...\n' exec "$COMMAND_PATH" } uninstall_program() { if [[ -f "$DOMAINS_FILE" ]] && [[ -s "$DOMAINS_FILE" ]]; then while IFS='|' read -r domain _ _; do [[ -z "$domain" || "$domain" == \#* ]] && continue if is_bt_panel; then rm -f "/www/server/panel/vhost/nginx/${domain}.conf" 2>/dev/null || true else rm -f "$INSTALL_DIR/nginx/vhost/${domain}.conf" 2>/dev/null || true fi done < "$DOMAINS_FILE" fi if [[ -f "$INSTALL_DIR/compose.yaml" ]]; then docker compose --project-directory "$INSTALL_DIR" down --remove-orphans || true; fi rm -rf "$INSTALL_DIR" reload_nginx printf '程序已卸载,shop-pro 菜单已保留。\n' } add_domain() { [[ -f "$ENV_FILE" ]] || { printf '程序尚未安装,请选择 3。\n'; return; } local domain port port=$(get_env PORT); [[ -n "$port" ]] || port="3000" printf '当前已绑定的域名:\n'; list_domains printf '\n请输入要绑定的域名(多个用空格分隔):'; IFS= read -r domain < /dev/tty [[ -n "$domain" ]] || return mkdir -p "$INSTALL_DIR/nginx/certs"; mkdir -p "$INSTALL_DIR/nginx/vhost" for d in $domain; do validate_domain "$d" || { printf '错误:域名 %s 格式无效,跳过。\n' "$d" >&2; continue; } if get_domain_record "$d" | grep -q .; then printf '域名 %s 已存在,跳过。\n' "$d"; continue; fi set_domain_record "$d" "" "" printf '已添加:%s(未配置 SSL)\n' "$d" done apply_domain_conf printf '域名绑定已生效。如需 SSL,请选择菜单 7。\n' } remove_domain() { [[ -f "$ENV_FILE" ]] || { printf '程序尚未安装,请选择 3。\n'; return; } local domain cert_file key_file cert_dir printf '当前已绑定的域名:\n'; list_domains printf '\n请输入要移除的域名:'; IFS= read -r domain < /dev/tty [[ -n "$domain" ]] || return local record record=$(get_domain_record "$domain") if [[ -z "$record" ]]; then printf '域名 %s 未绑定。\n' "$domain"; return; fi cert_file=$(echo "$record" | cut -d'|' -f2) key_file=$(echo "$record" | cut -d'|' -f3) if is_bt_panel; then rm -f "/www/server/panel/vhost/nginx/${domain}.conf" 2>/dev/null || true cert_dir="/www/server/panel/vhost/cert/${domain}" else rm -f "$INSTALL_DIR/nginx/vhost/${domain}.conf" 2>/dev/null || true cert_dir="$INSTALL_DIR/nginx/certs" fi [[ -n "$cert_file" && -f "${cert_dir}/${cert_file}" ]] && rm -f "${cert_dir}/${cert_file}" [[ -n "$key_file" && -f "${cert_dir}/${key_file}" ]] && rm -f "${cert_dir}/${key_file}" remove_domain_record "$domain" reload_nginx printf '域名 %s 已移除。\n' "$domain" } add_ssl() { [[ -f "$ENV_FILE" ]] || { printf '程序尚未安装,请选择 3。\n'; return; } local domain cert_file key_file cert_dir cert_content key_content printf '请输入要配置 SSL 的域名:'; IFS= read -r domain < /dev/tty [[ -n "$domain" ]] || return if ! get_domain_record "$domain" | grep -q .; then printf '域名 %s 未绑定,请先选择菜单 5 添加。\n' "$domain"; return; fi cert_file="${domain}.pem" key_file="${domain}.key" printf '\n==== 粘贴证书内容 ====\n' cert_content=$(paste_pem) [[ "$cert_content" == *"-----BEGIN CERTIFICATE-----"* && "$cert_content" == *"-----END CERTIFICATE-----"* ]] || { printf '错误:证书内容不完整。\n' >&2; return; } printf '\n==== 粘贴私钥内容 ====\n' key_content=$(paste_pem) [[ "$key_content" == *"-----BEGIN "*"PRIVATE KEY-----"* && "$key_content" == *"-----END "*"PRIVATE KEY-----"* ]] || { printf '错误:私钥内容不完整。\n' >&2; return; } if is_bt_panel; then cert_dir="/www/server/panel/vhost/cert/${domain}" else cert_dir="$INSTALL_DIR/nginx/certs" fi mkdir -p "$cert_dir" printf '%s\n' "$cert_content" > "${cert_dir}/${cert_file}" printf '%s\n' "$key_content" > "${cert_dir}/${key_file}" chmod 600 "${cert_dir}/${cert_file}" "${cert_dir}/${key_file}" set_domain_record "$domain" "$cert_file" "$key_file" apply_domain_conf printf 'SSL 已配置完成。证书目录:%s\n' "$cert_dir" if is_bt_panel; then printf '提示:请在宝塔面板 - 网站 - %s - SSL 中确认(面板可能会覆盖)。\n' "$domain"; fi } ((EUID == 0)) || { printf '错误:请使用 root 用户运行 shop-pro。\n' >&2; exit 1; } command -v docker >/dev/null 2>&1 || { printf '错误:未安装 Docker。\n' >&2; exit 1; } while true; do printf '\nshop-pro 管理菜单\n1. 重置店铺链接\n2. 重置运行端口\n3. 安装/升级\n4. 卸载程序(不包括菜单文件)\n5. 添加绑定域名\n6. 移除绑定域名\n7. 给绑定域名添加 SSL 证书\n0. 退出菜单\n请选择:' IFS= read -r choice < /dev/tty || true case "$choice" in 1) reset_shop_url;; 2) reset_port;; 3) install_or_upgrade;; 4) uninstall_program;; 5) add_domain;; 6) remove_domain;; 7) add_ssl;; 0) exit 0;; *) printf '错误:请输入 0-7。\n' >&2;; esac done MENU sed -i "s|__SOURCE_URL__|$SOURCE_URL|g" "$COMMAND_PATH" chmod 0755 "$COMMAND_PATH" } main() { local shop_url port server_ip domain _d require_root show_agreement printf '请输入完整店铺链接(支持 http:// 或 https://):'; IFS= read -r shop_url < /dev/tty validate_shop_url "$shop_url" || fail "店铺链接格式无效。" printf '请输入运行端口 [3000]:'; IFS= read -r port < /dev/tty; port=${port:-3000} validate_port "$port" || fail "端口必须是 1-65535 的数字。" if port_in_use "$port"; then printf '警告:端口 %s 已被占用。\n' "$port" printf '请重新输入运行端口:'; IFS= read -r port < /dev/tty validate_port "$port" || fail "端口必须是 1-65535 的数字。" fi printf '请输入要绑定的域名(多个用空格分隔,留回车跳过):'; IFS= read -r domain < /dev/tty printf '\n配置完成,开始检测 Docker 及必要依赖...\n' ensure_dependencies install_source write_env "$shop_url" "$port" install_command mkdir -p "$INSTALL_DIR/nginx/certs" "$INSTALL_DIR/nginx/vhost" : > "$DOMAINS_FILE" if [[ -n "$domain" ]]; then for _d in $domain; do if validate_domain "$_d"; then printf '%s||\n' "$_d" >> "$DOMAINS_FILE" else printf '提示:域名 %s 格式无效,已跳过。\n' "$_d"; fi done fi open_port "$port" docker compose --project-directory "$INSTALL_DIR" up -d --build if [[ -s "$DOMAINS_FILE" ]]; then local cert_dir conf_path while IFS='|' read -r _d _cf _kf; do [[ -z "$_d" || "$_d" == \#* ]] && continue if is_bt_panel; then conf_path="/www/server/panel/vhost/nginx/${_d}.conf"; cert_dir="/www/server/panel/vhost/cert/${_d}"; mkdir -p "$cert_dir" else conf_path="$INSTALL_DIR/nginx/vhost/${_d}.conf"; cert_dir="$INSTALL_DIR/nginx/certs"; mkdir -p "$INSTALL_DIR/nginx/vhost" fi build_proxy_block "$_d" "127.0.0.1" "$port" "$_cf" "$_kf" "$cert_dir" > "$conf_path" printf ' 反代配置已写入:%s\n' "$conf_path" done < "$DOMAINS_FILE" reload_nginx fi server_ip=$(detect_server_ip) printf '\n安装完成!\n' printf ' 直接访问:http://%s:%s/\n' "$server_ip" "$port" if [[ -s "$DOMAINS_FILE" ]]; then printf ' 域名访问:\n' while IFS='|' read -r _d _ _; do [[ -n "$_d" && "$_d" != \#* ]] && printf ' http://%s/\n' "$_d" done < "$DOMAINS_FILE" if is_bt_panel; then printf '\n 反代配置已写入宝塔 vhost:/www/server/panel/vhost/nginx/\n' printf ' 如需 SSL,运行 shop-pro 选 7,或在宝塔面板 - 网站里手动添加。\n' else printf '\n 如需 SSL,请运行 shop-pro 选 7。\n' fi fi printf '终端管理命令:shop-pro\n' } if [[ "${SHOP_PRO_COMMAND_ONLY:-0}" == "1" ]]; then require_root install_command exit 0 fi main "$@"