XuqmGroup-PrivateDeploy/scripts/update.sh
徐勤民 28fd8c0793 feat(update): upgrade 后自动调用 approve-pending-requests 修复积压申请
update.sh 在 tenant-service 健康后调用
POST /api/private/admin/approve-pending-requests
自动开通所有 PENDING 状态的服务申请,无需用户手动操作。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:46:00 +08:00

299 行
12 KiB
Bash
可执行文件

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

#!/usr/bin/env bash
# update.sh — XuqmGroup 私有化部署热更新脚本
#
# 用途:在已部署的环境上执行,无需重新配置基础信息:
# 1. 自动读取现有配置(.env / config/secrets.env
# 2. 修复已知配置问题SDK 地址、残留公网 URL 等)
# 3. 可选:拉取最新镜像
# 4. 重启受影响的容器
# 5. 重新运行全量验证
#
# 前提:已执行过 install.sh 完成初始部署
set -euo pipefail
# 自动定位安装目录:优先脚本所在目录的上级,否则搜索常见路径
_script_parent="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ -f "$_script_parent/docker-compose.yml" ]; then
ROOT_DIR="$_script_parent"
else
ROOT_DIR=""
for _d in /opt/xuqm-private /opt/xuqm /root/xuqm-private; do
[ -f "$_d/docker-compose.yml" ] && ROOT_DIR="$_d" && break
done
if [ -z "$ROOT_DIR" ]; then
read -rp " 请输入部署目录路径(如 /opt/xuqm-private: " ROOT_DIR
[ -f "$ROOT_DIR/docker-compose.yml" ] || \
{ printf "\nERROR: %s 下未找到 docker-compose.yml\n" "$ROOT_DIR" >&2; exit 1; }
fi
fi
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
BOLD='\033[1m'; RESET='\033[0m'
CYAN='\033[1;36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[1;31m'
log() { printf "\n${CYAN}[update] %s${RESET}\n" "$*"; }
ok() { printf " ${GREEN}${RESET} %s\n" "$*"; }
warn() { printf " ${YELLOW}${RESET} %s\n" "$*"; }
fail() { printf "\n${RED}ERROR: %s${RESET}\n" "$*" >&2; exit 1; }
info() { printf " → %s\n" "$*"; }
# 修改 env 文件中某个 key 的值;若 key 不存在则追加
_set_env() {
local file="$1" key="$2" val="$3"
local tmp; tmp="$(mktemp)"
if grep -q "^${key}=" "$file" 2>/dev/null; then
# 用 python3 替换,避免 sed 在 URL 值中的斜杠转义问题
python3 - "$file" "$key" "$val" <<'PY'
import sys, re
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
content = open(path).read()
new = re.sub(r'^' + re.escape(key) + r'=.*$', key + '=' + val, content, flags=re.MULTILINE)
open(path, 'w').write(new)
PY
else
printf '%s=%s\n' "$key" "$val" >> "$file"
fi
rm -f "$tmp"
}
# ---------------------------------------------------------------------------
# 前置检查
# ---------------------------------------------------------------------------
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
printf "${BOLD} XuqmGroup 私有化部署热更新${RESET}\n"
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
printf " 部署目录: %s\n\n" "$ROOT_DIR"
[ -f "$ROOT_DIR/.env" ] || fail "未找到 .env,请先执行 install.sh 完成初始部署"
[ -f "$ROOT_DIR/config/xuqm.env" ] || fail "未找到 config/xuqm.env,请先执行 install.sh 完成初始部署"
[ -f "$ROOT_DIR/config/secrets.env" ] || fail "未找到 config/secrets.env,请先执行 install.sh 完成初始部署"
command -v docker >/dev/null 2>&1 || fail "Docker 未安装"
command -v python3 >/dev/null 2>&1 || fail "python3 未安装"
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行"
# ---------------------------------------------------------------------------
# 加载现有配置
# ---------------------------------------------------------------------------
log "加载现有配置"
# shellcheck disable=SC1090,SC1091
set -a
. "$ROOT_DIR/.env"
. "$ROOT_DIR/config/secrets.env"
set +a
# 从 config/xuqm.env 中读取 CONSOLE_DOMAIN不用 source,避免覆盖已加载值
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
_NGINX_BIND="${NGINX_BIND:-80}"
_NGINX_PORT="${_NGINX_BIND##*:}"
ok "CONSOLE_DOMAIN=${_CONSOLE_DOMAIN:-(未设置)}"
ok "NGINX_BIND=${_NGINX_BIND}"
[ -n "$_CONSOLE_DOMAIN" ] || fail "config/xuqm.env 中 CONSOLE_DOMAIN 未设置,请手动补充后重试"
# ---------------------------------------------------------------------------
# 1. 修复配置问题
# ---------------------------------------------------------------------------
log "检查并修复配置"
# 根据 CONSOLE_DOMAIN 派生正确的 SDK 地址
if printf '%s' "$_CONSOLE_DOMAIN" | grep -q '^https://'; then
_WS_SCHEME="wss"
else
_WS_SCHEME="ws"
fi
_DEPLOY_HOST="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||')"
_SDK_IM_WS_URL="${_WS_SCHEME}://${_DEPLOY_HOST}/ws/im"
_SDK_IM_API_URL="${_CONSOLE_DOMAIN}"
_SDK_FILE_URL="${_CONSOLE_DOMAIN}"
_FIXED=0
# 检查 SDK_IM_WS_URL
_CURRENT_WS="$(grep '^SDK_IM_WS_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
if [ -z "$_CURRENT_WS" ] || printf '%s' "$_CURRENT_WS" | grep -qi 'xuqinmin\.com'; then
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_WS_URL" "$_SDK_IM_WS_URL"
ok "SDK_IM_WS_URL 已更新 → ${_SDK_IM_WS_URL}"
_FIXED=1
else
ok "SDK_IM_WS_URL 正常: ${_CURRENT_WS}"
fi
# 检查 SDK_IM_API_URL
_CURRENT_IM_API="$(grep '^SDK_IM_API_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
if [ -z "$_CURRENT_IM_API" ] || printf '%s' "$_CURRENT_IM_API" | grep -qi 'xuqinmin\.com'; then
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_API_URL" "$_SDK_IM_API_URL"
ok "SDK_IM_API_URL 已更新 → ${_SDK_IM_API_URL}"
_FIXED=1
else
ok "SDK_IM_API_URL 正常: ${_CURRENT_IM_API}"
fi
# 检查 SDK_FILE_SERVICE_URL
_CURRENT_FILE="$(grep '^SDK_FILE_SERVICE_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
if [ -z "$_CURRENT_FILE" ] || printf '%s' "$_CURRENT_FILE" | grep -qi 'xuqinmin\.com'; then
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_FILE_SERVICE_URL" "$_SDK_FILE_URL"
ok "SDK_FILE_SERVICE_URL 已更新 → ${_SDK_FILE_URL}"
_FIXED=1
else
ok "SDK_FILE_SERVICE_URL 正常: ${_CURRENT_FILE}"
fi
# 检查 .env 中是否有残留 OPS_DOMAIN
if grep -q '^OPS_DOMAIN=' "$ROOT_DIR/.env" 2>/dev/null; then
python3 - "$ROOT_DIR/.env" <<'PY'
import sys, re
path = sys.argv[1]
content = open(path).read()
content = re.sub(r'^OPS_DOMAIN=.*\n?', '', content, flags=re.MULTILINE)
open(path, 'w').write(content)
PY
ok ".env 中已清理 OPS_DOMAIN"
_FIXED=1
fi
# 扫描 config/xuqm.env 是否还有其它公网地址残留
if grep -qi 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" 2>/dev/null; then
warn "config/xuqm.env 中仍有 xuqinmin.com 字样,请人工核查:"
grep -n 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" | while IFS= read -r line; do
printf ' %s\n' "$line"
done
fi
if [ "$_FIXED" -eq 0 ]; then
ok "配置无需修复"
fi
# 同步更新 config/sdk/xuqm-private-sdk.jsonSDK 初始化配置文件)
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
python3 - "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" \
"$_CONSOLE_DOMAIN" "$_SDK_IM_WS_URL" <<'PY'
import json, sys
path, console, ws = sys.argv[1], sys.argv[2], sys.argv[3]
try:
d = json.load(open(path))
d['controlBaseUrl'] = console
d['fileBaseUrl'] = console
d['imApiBaseUrl'] = console
d['imWsUrl'] = ws
json.dump(d, open(path, 'w'), ensure_ascii=False, indent=2)
except Exception as e:
print(f'warning: {e}', file=sys.stderr)
PY
ok "config/sdk/xuqm-private-sdk.json 已同步更新"
fi
# ---------------------------------------------------------------------------
# 2. 可选:拉取最新镜像
# ---------------------------------------------------------------------------
log "拉取最新镜像(可选)"
read -rp " 是否拉取最新镜像?(y/N): " _pull_choice
if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
_REGISTRY="${REGISTRY:-}"
_REGISTRY_HOST="${REGISTRY_HOST:-}"
_REGISTRY_USER="${REGISTRY_USER:-}"
_REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
if [ -n "$_REGISTRY_PASSWORD" ] && [ -n "$_REGISTRY_HOST" ]; then
printf '%s' "$_REGISTRY_PASSWORD" | \
docker login "$_REGISTRY_HOST" -u "$_REGISTRY_USER" --password-stdin 2>/dev/null \
&& ok "镜像仓库登录成功" \
|| warn "镜像仓库登录失败,将使用本地缓存镜像"
fi
docker compose \
--env-file "$ROOT_DIR/.env" \
-f "$ROOT_DIR/docker-compose.yml" \
-f "$ROOT_DIR/docker-compose.infra.yml" \
pull --ignore-pull-failures 2>/dev/null \
&& ok "镜像已拉取" \
|| warn "部分镜像拉取失败,继续使用本地缓存"
else
info "跳过镜像拉取"
fi
# ---------------------------------------------------------------------------
# 3. 重启受影响的容器
# ---------------------------------------------------------------------------
log "重启受影响的容器"
# 加载 secrets 到环境docker compose 需要)
set -a
# shellcheck disable=SC1090
. "$ROOT_DIR/config/secrets.env"
set +a
_COMPOSE="docker compose --env-file ${ROOT_DIR}/.env -f ${ROOT_DIR}/docker-compose.yml -f ${ROOT_DIR}/docker-compose.infra.yml"
# tenant-service读取 env_file包含 SDK_IM_WS_URL,必须重启
info "重启 tenant-service ..."
$_COMPOSE restart tenant-service
ok "tenant-service 已重启"
# nginx挂载 config/nginx/conf.d可能有 sub_filter 更新),重启生效
info "重启 nginx ..."
$_COMPOSE restart nginx
ok "nginx 已重启"
# 等待 tenant-service 健康
printf ' 等待 tenant-service 就绪'
for i in $(seq 1 30); do
code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
"http://127.0.0.1:11224/actuator/health" 2>/dev/null || echo 000)"
if [ "$code" = "200" ]; then
printf '\n'
ok "tenant-service 健康 (HTTP 200)"
break
fi
printf '.'
sleep 3
[ "$i" -eq 30 ] && {
printf '\n'
warn "tenant-service 未在 90s 内响应,请手动检查docker compose logs --tail 50 tenant-service"
break
}
done
# ---------------------------------------------------------------------------
# 4. 自动处理积压的 PENDING 服务开通申请
# ---------------------------------------------------------------------------
log "处理积压的服务开通申请"
_approve_resp="$(curl -sk --noproxy '*' -X POST \
"http://127.0.0.1:11224/api/private/admin/approve-pending-requests" \
--max-time 10 2>/dev/null || echo '')"
_approved="$(printf '%s' "$_approve_resp" | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('approved',0))" \
2>/dev/null || echo 0)"
if [ "${_approved:-0}" -gt 0 ]; then
ok "已自动开通 ${_approved} 条积压申请"
else
ok "无积压申请"
fi
# ---------------------------------------------------------------------------
# 5. 全量验证
# ---------------------------------------------------------------------------
log "运行全量验证"
if BASE_URL="http://127.0.0.1:${_NGINX_PORT}" bash "$ROOT_DIR/scripts/verify.sh"; then
ok "全量验证通过"
else
warn "部分验证项未通过,请查看上方输出"
fi
# ---------------------------------------------------------------------------
# 完成
# ---------------------------------------------------------------------------
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
printf "${BOLD} 热更新完成${RESET}\n"
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$_CONSOLE_DOMAIN"
printf " SDK IM WS${BOLD}%s${RESET}\n\n" "$_SDK_IM_WS_URL"