feat(deploy): 移除 ops-web、修复 SDK URL 注入、新增一键升级

核心变更:
- 完全移除 ops-web 容器(私有化部署无需运营后台)
- nginx sub_filter 替换前端 JS bundle 中的公网 SDK URL
- deploy.sh 写入正确的 SDK_IM_WS_URL / SDK_IM_API_URL / SDK_FILE_SERVICE_URL
- 新增 scripts/update.sh:热更新脚本,修复配置 + 可选拉镜像 + 重启 + 验证
- 新增 upgrade.sh:一键升级入口,curl 下载后直接执行,流程同 install.sh
- install.sh 检测已有部署(.env 存在),自动路由到 update.sh 而非重跑向导
- 关键配置文件(.env / secrets.env / xuqm.env)在 tarball 解压前备份后恢复

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-05-20 18:25:12 +08:00
父节点 0c4802b20a
当前提交 a327a262dd
共有 14 个文件被更改,包括 496 次插入59 次删除

查看文件

@ -2,6 +2,8 @@
## 快速开始
**首次部署:**
```bash
curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/install.sh \
-o install.sh && bash install.sh
@ -9,6 +11,15 @@ curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/mai
脚本自动完成:依赖检测 → 配置生成 → 镜像拉取 → 容器启动 → 租户初始化(新建或迁移)→ 全量验证。
**升级已有部署:**
```bash
curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/upgrade.sh \
-o upgrade.sh && bash upgrade.sh
```
升级脚本自动完成:下载最新脚本 → 保留全部数据和配置 → 修复配置问题 → 可选拉取新镜像 → 重启容器 → 全量验证。
部署完成后根据输出的端口表配置宿主机 nginx,详见 [docs/runbook.md](docs/runbook.md)。
## 部署架构
@ -21,7 +32,6 @@ curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/mai
├── tenant-service /api/ /actuator/
├── file-service /file/
├── tenant-web /(兜底)
├── ops-web /ops
├── im-service /api/im/ /ws/im
├── update-service /api/v1/updates/ /api/v1/rn/
├── license-service /api/license/
@ -41,7 +51,7 @@ curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/mai
| Profile | 服务 | 说明 |
|---------|------|------|
| base | tenant-service, file-service, tenant-web, ops-web, nginx | 必选核心服务(含内置路由 nginx |
| base | tenant-service, file-service, tenant-web, nginx | 必选核心服务(含内置路由 nginx |
| infra-mysql | mysql | 托管数据库 |
| infra-redis | redis | 托管缓存 |
| im | im-service | IM HTTP + WebSocket |

查看文件

@ -1,2 +1 @@
2026.05.18-private.1
2026.05.20-private.1

查看文件

@ -83,17 +83,17 @@ server {
proxy_set_header X-Real-IP $remote_addr;
}
# 运营后台
location /ops {
proxy_pass http://ops-web:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 控制台前端(兜底路由,必须最后)
# sub_filter 替换前端 JS bundle 中硬编码的公有平台地址为私有部署地址
location / {
proxy_pass http://tenant-web:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Accept-Encoding "";
sub_filter 'wss://im.dev.xuqinmin.com/ws/im' 'wss://$host/ws/im';
sub_filter 'https://im.dev.xuqinmin.com' 'https://$host';
sub_filter 'https://dev.xuqinmin.com' 'https://$host';
sub_filter_once off;
sub_filter_types text/html text/javascript application/javascript;
}
}

查看文件

@ -71,17 +71,6 @@ services:
- "127.0.0.1:11226:80"
restart: unless-stopped
# ---------------------------------------------------------------------------
# 运营后台前端(必须)
# 管理员登录界面,nginx 代理 /ops
# ---------------------------------------------------------------------------
ops-web:
image: ${REGISTRY}/ops-web:${IMAGE_TAG}
profiles: ["base"]
ports:
- "127.0.0.1:11227:80"
restart: unless-stopped
# ---------------------------------------------------------------------------
# 内置路由 nginx必须
# 统一处理所有内部路由,绑定端口由 .env NGINX_BIND 控制:
@ -101,8 +90,6 @@ services:
condition: service_started
tenant-web:
condition: service_started
ops-web:
condition: service_started
restart: unless-stopped
# ---------------------------------------------------------------------------

查看文件

@ -74,8 +74,7 @@ environment:
| 11224 | tenant-service | 9001 | `/api/` `/actuator/` |
| 11225 | file-service | 8086 | `/file/` |
| 11226 | tenant-web | 80 | `/`(兜底路由) |
| 11227 | ops-web | 80 | `/ops` |
| 11228 | im-service | 8082 | `/api/im/` `/ws/im` |
| 11227 | im-service | 8082 | `/api/im/` `/ws/im` |
| 11229 | update-service | 8084 | `/api/v1/updates/` `/api/v1/rn/` |
| 11230 | license-service | 8085 | `/api/license/` |
| 11231 | push-service | 8083 | 厂商回调(按需) |

查看文件

@ -43,8 +43,7 @@
| 11224 | tenant-service | 9001 |
| 11225 | file-service | 8086 |
| 11226 | tenant-web | 80 |
| 11227 | ops-web | 80 |
| 11228 | im-service | 8082 |
| 11227 | im-service | 8082 |
| 11229 | update-service | 8084 |
| 11230 | license-service | 8085 |
| 11231 | push-service | 8083 |

查看文件

@ -6,7 +6,6 @@
{"name": "tenant-service", "required": true},
{"name": "file-service", "required": true},
{"name": "tenant-web", "required": true},
{"name": "ops-web", "required": true},
{"name": "docs-site", "required": true},
{"name": "im-service", "required": false, "profile": "im"},
{"name": "push-service", "required": false, "profile": "push"},

查看文件

@ -1,11 +1,15 @@
#!/usr/bin/env bash
# XuqmGroup 私有化部署 — 一键安装脚本
# XuqmGroup 私有化部署 — 一键安装脚本(已部署环境重新执行将自动升级)
#
# 用法一(推荐,先下载再审查)
# 首次部署
# curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/install.sh \
# -o install.sh && bash install.sh
#
# 用法二(直接执行,保留终端交互):
# 升级已有部署(推荐使用专用升级脚本):
# curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/upgrade.sh \
# -o upgrade.sh && bash upgrade.sh
#
# 或直接执行bash <(curl ...) 形式保留终端交互):
# bash <(curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/install.sh)
#
# 环境变量(可选):
@ -169,17 +173,26 @@ printf '\n'
info "下载部署包 ${ARCHIVE_URL} ..."
TMP_PKG="$(mktemp /tmp/xuqm-deploy-XXXXXX.tar.gz)"
trap 'rm -f "$TMP_PKG"' EXIT
_BACKUP_DIR="$(mktemp -d /tmp/xuqm-backup-XXXXXX)"
trap 'rm -f "$TMP_PKG"; rm -rf "$_BACKUP_DIR"' EXIT
curl -fsSL --progress-bar "$ARCHIVE_URL" -o "$TMP_PKG" \
|| fail "部署包下载失败,请检查网络或 Gitea 是否可达: $ARCHIVE_URL"
# ---------------------------------------------------------------------------
# Step 7 — 解压到安装目录
# Step 7 — 解压到安装目录(更新时保留关键配置)
# ---------------------------------------------------------------------------
if [ -d "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/docker-compose.yml" ]; then
warn "安装目录已存在部署文件: $INSTALL_DIR"
printf ' 将覆盖脚本和配置模板(数据目录 data/ 不受影响)\n'
# 检测是否为已有部署
_IS_UPDATE=0
[ -f "$INSTALL_DIR/.env" ] && [ -f "$INSTALL_DIR/config/secrets.env" ] && _IS_UPDATE=1
# 备份关键配置,防止 tarball 中同名文件意外覆盖
if [ "$_IS_UPDATE" -eq 1 ]; then
for _cf in .env config/secrets.env config/xuqm.env; do
[ -f "$INSTALL_DIR/$_cf" ] && \
cp "$INSTALL_DIR/$_cf" "$_BACKUP_DIR/$(basename "$_cf").bak"
done
fi
mkdir -p "$INSTALL_DIR"
@ -187,11 +200,45 @@ tar -xzf "$TMP_PKG" -C "$INSTALL_DIR" --strip-components=1 --exclude='*/data'
chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/install.sh" 2>/dev/null || true
ok "部署包解压完成: $INSTALL_DIR"
# ---------------------------------------------------------------------------
# Step 8 — 启动交互式部署
# ---------------------------------------------------------------------------
printf '\n%b 依赖安装完毕,即将进入交互式部署向导 ...%b\n\n' "$GREEN" "$RESET"
# 恢复关键配置文件(确保现有配置不被模板覆盖)
if [ "$_IS_UPDATE" -eq 1 ]; then
for _cf in .env config/secrets.env config/xuqm.env; do
_bak="$_BACKUP_DIR/$(basename "$_cf").bak"
[ -f "$_bak" ] && cp "$_bak" "$INSTALL_DIR/$_cf"
done
ok "已恢复现有配置文件"
fi
# ---------------------------------------------------------------------------
# Step 8 — 路由到更新或全量安装
# ---------------------------------------------------------------------------
export DEPLOY_HOST
cd "$INSTALL_DIR"
exec bash scripts/deploy.sh
if [ "$_IS_UPDATE" -eq 1 ]; then
printf '\n'
printf '%b 检测到已有部署(%s/.env 存在)%b\n' "$YELLOW" "$INSTALL_DIR" "$RESET"
printf ' 请选择操作:\n\n'
printf ' %b1%b 仅更新 — 修复配置问题,可选拉取新镜像,重启容器(保留全部数据)\n' "$BOLD" "$RESET"
printf ' %b2%b 全量重部署 — 重新运行完整安装向导(会覆盖现有配置,谨慎使用)\n' "$BOLD" "$RESET"
printf ' %b3%b 退出\n\n' "$BOLD" "$RESET"
read -rp " 请输入 [1/2/3](回车默认 1: " _choice
_choice="${_choice:-1}"
case "$_choice" in
2)
printf '\n%b → 进入全量部署向导 ...%b\n\n' "$GREEN" "$RESET"
exec bash scripts/deploy.sh
;;
3)
printf ' 已退出\n'
exit 0
;;
*)
printf '\n%b → 进入更新流程 ...%b\n\n' "$GREEN" "$RESET"
exec bash scripts/update.sh
;;
esac
else
printf '\n%b 依赖安装完毕,即将进入交互式部署向导 ...%b\n\n' "$GREEN" "$RESET"
exec bash scripts/deploy.sh
fi

查看文件

@ -293,7 +293,6 @@ REDIS_DATABASE=0
# 访问域名IP 部署时直接使用 IP,无需配置域名
CONSOLE_DOMAIN=${CONSOLE_BASE}
OPS_DOMAIN=${CONSOLE_BASE}
DOCS_DOMAIN=${CONSOLE_BASE}/docs
FILE_DOMAIN=${CONSOLE_BASE}
IM_DOMAIN=${CONSOLE_BASE}
@ -355,7 +354,6 @@ REDIS_DATABASE=0
# 访问域名IP 部署时直接使用 IP
CONSOLE_DOMAIN=${CONSOLE_BASE}
OPS_DOMAIN=${CONSOLE_BASE}
DOCS_DOMAIN=${CONSOLE_BASE}/docs
FILE_DOMAIN=${CONSOLE_BASE}
IM_DOMAIN=${CONSOLE_BASE}
@ -368,8 +366,7 @@ SDK_FILE_SERVICE_URL=${CONSOLE_BASE}
SDK_IM_API_URL=${CONSOLE_BASE}
SDK_IM_WS_URL=${_WS_SCHEME}://${DEPLOY_HOST}/ws/im
# 系统 IM 通信应用 key私有化服务间消息通知使用此 app_key 连接 IM 服务)
# 与公有化平台 xuqinmin12 租户下的平台系统应用 key 保持一致
# 系统 IM 通信应用 key私有化服务间消息通知使用
SYSTEM_APP_KEY=ak_409e217e4aa14254ad73ad3c
EOF
ok "config/xuqm.env 已写入"

查看文件

@ -44,7 +44,7 @@ pull_and_save() {
}
# Always include base images
for svc in tenant-service file-service tenant-web ops-web docs-site; do
for svc in tenant-service file-service tenant-web docs-site; do
pull_and_save "$svc"
done

查看文件

@ -25,7 +25,7 @@ else
fi
# Container states — docs-site is optional (image may not exist in all registries)
for svc in tenant-service file-service tenant-web ops-web nginx; do
for svc in tenant-service file-service tenant-web nginx; do
STATE="$(compose ps -q "$svc" 2>/dev/null | head -1 || true)"
if [ -n "$STATE" ]; then
STATUS="$(docker inspect --format='{{.State.Status}}' "$STATE" 2>/dev/null || echo 'unknown')"

281
scripts/update.sh 可执行文件
查看文件

@ -0,0 +1,281 @@
#!/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. 全量验证
# ---------------------------------------------------------------------------
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"

查看文件

@ -133,7 +133,7 @@ printf " 验证时间: %s\n" "$(date '+%Y-%m-%d %H:%M:%S')"
# ---------------------------------------------------------------------------
section "1. 容器状态"
REQUIRED_CONTAINERS="tenant-service nginx tenant-web ops-web"
REQUIRED_CONTAINERS="tenant-service nginx tenant-web"
for CTR_SUFFIX in $REQUIRED_CONTAINERS; do
CTR_NAME=$(docker ps --filter "name=$CTR_SUFFIX" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
if [ -n "$CTR_NAME" ]; then
@ -234,13 +234,6 @@ else
fail "控制台前端不可访问 (HTTP $WEB_CODE)"
fi
OPS_CODE=$(http_get "$BASE_URL/ops")
if echo "$OPS_CODE" | grep -qE '^(200|301|302)$'; then
pass "运营后台可访问 (HTTP $OPS_CODE)"
else
fail "运营后台不可访问 (HTTP $OPS_CODE)"
fi
DOCS_CODE=$(http_get "$BASE_URL/docs/")
if echo "$DOCS_CODE" | grep -qE '^(200|301|302)$'; then
pass "文档站可访问 (HTTP $DOCS_CODE)"
@ -422,7 +415,6 @@ else
fi
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$BASE_URL"
printf " 运营后台:${BOLD}%s/ops${RESET}\n" "$BASE_URL"
printf " 文档站 ${BOLD}%s/docs/${RESET}\n" "$BASE_URL"
printf "\n"

127
upgrade.sh 普通文件
查看文件

@ -0,0 +1,127 @@
#!/usr/bin/env bash
# upgrade.sh — XuqmGroup 私有化部署一键升级脚本
#
# 用法一(推荐,先下载再审查):
# curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/upgrade.sh \
# -o upgrade.sh && bash upgrade.sh
#
# 用法二(直接执行,保留终端交互):
# bash <(curl -fsSL https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy/raw/branch/main/upgrade.sh)
#
# 环境变量(可选):
# INSTALL_DIR 安装目录(默认自动检测,优先 /opt/xuqm-private
# XUQM_BRANCH Gitea 分支(默认 main
#
# 升级内容:
# 1. 下载最新部署包更新脚本、配置模板、docker-compose 文件)
# 2. 保留全部数据目录和现有配置(.env / secrets.env / xuqm.env 不被覆盖)
# 3. 修复已知配置问题SDK 地址残留公网 URL 等)
# 4. 可选拉取最新镜像
# 5. 重启受影响容器并运行全量验证
set -euo pipefail
# 若 stdin 不是终端curl | bash,强制从 /dev/tty 读取用户输入
if [ ! -t 0 ]; then
exec bash "$0" "$@" </dev/tty
fi
# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------
GITEA_BASE="https://xuqinmin.com/xuqmGroup/XuqmGroup-PrivateDeploy"
XUQM_BRANCH="${XUQM_BRANCH:-main}"
ARCHIVE_URL="${GITEA_BASE}/archive/${XUQM_BRANCH}.tar.gz"
INSTALL_DIR="${INSTALL_DIR:-/opt/xuqm-private}"
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
BOLD='\033[1m'; RESET='\033[0m'
CYAN='\033[1;36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[1;31m'
info() { printf "${CYAN}${RESET} %s\n" "$*"; }
ok() { printf "${GREEN}${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}${RESET} %s\n" "$*"; }
fail() { printf "${RED}\nERROR: %s${RESET}\n" "$*" >&2; exit 1; }
# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
printf '\n%b══════════════════════════════════════════════════%b\n' "$CYAN" "$RESET"
printf '%b XuqmGroup 私有化部署 — 一键升级%b\n' "$BOLD" "$RESET"
printf '%b══════════════════════════════════════════════════%b\n\n' "$CYAN" "$RESET"
# ---------------------------------------------------------------------------
# 检测安装目录
# ---------------------------------------------------------------------------
if [ ! -f "$INSTALL_DIR/.env" ]; then
for _d in /opt/xuqm /root/xuqm-private; do
if [ -f "$_d/.env" ] && [ -f "$_d/docker-compose.yml" ]; then
INSTALL_DIR="$_d"
break
fi
done
fi
if [ ! -f "$INSTALL_DIR/.env" ]; then
read -rp " 请输入部署目录路径(如 /opt/xuqm-private: " INSTALL_DIR
[ -f "$INSTALL_DIR/docker-compose.yml" ] || \
fail "$INSTALL_DIR 下未找到 docker-compose.yml,请先执行 install.sh 完成初始部署"
[ -f "$INSTALL_DIR/.env" ] || \
fail "$INSTALL_DIR 下未找到 .env,请先执行 install.sh 完成初始部署"
fi
printf ' 安装目录: %b%s%b\n' "$BOLD" "$INSTALL_DIR" "$RESET"
# ---------------------------------------------------------------------------
# 前置检查
# ---------------------------------------------------------------------------
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 未运行,请执行: systemctl start docker"
[ -f "$INSTALL_DIR/config/secrets.env" ] || fail "未找到 config/secrets.env,请先执行 install.sh 完成初始部署"
[ -f "$INSTALL_DIR/config/xuqm.env" ] || fail "未找到 config/xuqm.env,请先执行 install.sh 完成初始部署"
ok "前置检查通过"
# ---------------------------------------------------------------------------
# 下载最新部署包
# ---------------------------------------------------------------------------
printf '\n'
info "下载最新版本 ${ARCHIVE_URL} ..."
TMP_PKG="$(mktemp /tmp/xuqm-upgrade-XXXXXX.tar.gz)"
_BACKUP_DIR="$(mktemp -d /tmp/xuqm-backup-XXXXXX)"
trap 'rm -f "$TMP_PKG"; rm -rf "$_BACKUP_DIR"' EXIT
curl -fsSL --progress-bar "$ARCHIVE_URL" -o "$TMP_PKG" \
|| fail "下载失败,请检查网络或 Gitea 是否可达: $ARCHIVE_URL"
# ---------------------------------------------------------------------------
# 备份关键配置 → 解压 → 恢复
# ---------------------------------------------------------------------------
for _cf in .env config/secrets.env config/xuqm.env; do
[ -f "$INSTALL_DIR/$_cf" ] && \
cp "$INSTALL_DIR/$_cf" "$_BACKUP_DIR/$(basename "$_cf").bak"
done
tar -xzf "$TMP_PKG" -C "$INSTALL_DIR" --strip-components=1 --exclude='*/data'
chmod +x "$INSTALL_DIR/scripts/"*.sh \
"$INSTALL_DIR/install.sh" \
"$INSTALL_DIR/upgrade.sh" 2>/dev/null || true
ok "脚本和配置模板已更新"
for _cf in .env config/secrets.env config/xuqm.env; do
_bak="$_BACKUP_DIR/$(basename "$_cf").bak"
[ -f "$_bak" ] && cp "$_bak" "$INSTALL_DIR/$_cf"
done
ok "已恢复现有配置文件"
# ---------------------------------------------------------------------------
# 执行升级(修复配置 + 重启 + 验证)
# ---------------------------------------------------------------------------
printf '\n%b → 进入升级流程 ...%b\n\n' "$GREEN" "$RESET"
cd "$INSTALL_DIR"
exec bash scripts/update.sh