2026-05-19 08:01:19 +08:00
#!/usr/bin/env bash
2026-05-19 15:14:23 +08:00
# deploy.sh — XuqmGroup 私有化一键部署脚本
2026-05-19 08:01:19 +08:00
#
# 用途:在目标机器上完成以下全部步骤:
2026-05-19 14:29:57 +08:00
# 0. 交互式选择租户初始化方式(新建 / 迁移)并收集必要信息
2026-05-19 15:14:23 +08:00
# 1. 预检( Docker、Compose、磁盘、端口)
2026-05-19 14:29:57 +08:00
# 2. 写入配置(.env / secrets.env / xuqm.env / bootstrap.env / nginx)
2026-05-19 08:01:19 +08:00
# 3. 登录 ACR 镜像仓库
# 4. 启动基础设施容器( MySQL、Redis) 并等待就绪
2026-05-19 14:29:57 +08:00
# 5. 拉取镜像并启动所有业务容器( base + im + push + update + license)
# 6. Schema 扩展( is_default / deletable 列 + 删除保护触发器)+ 系统应用初始化
2026-05-19 15:14:23 +08:00
# 7. 租户初始化(新建:填写信息并验证;迁移:凭迁移密钥调用 API 导入)
2026-05-19 14:29:57 +08:00
# 8. 运行一键验证脚本确认所有服务正常
2026-05-19 08:01:19 +08:00
#
2026-05-19 14:29:57 +08:00
# 幂等性:可重复执行。已运行的容器不会被重建。
# 迁移模式:若检测到已有租户数据,会在执行前提示确认。
2026-05-19 08:01:19 +08:00
#
# 前提:
# - Docker 和 Docker Compose v2 已安装
2026-05-19 15:14:23 +08:00
# - python3 和 curl 已安装
2026-05-19 08:01:19 +08:00
# - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本
#
# 覆盖默认值(可通过环境变量传入):
2026-05-19 18:12:48 +08:00
# DEPLOY_HOST 目标机器 IP / 主机名(默认 localhost)
# 部署向导会提示配置域名与 HTTPS
2026-05-19 08:01:19 +08:00
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022)
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
set -euo pipefail
# ---------------------------------------------------------------------------
2026-05-19 15:14:23 +08:00
# 常量
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
# 镜像仓库
REGISTRY = "crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com/xuqmgroup"
REGISTRY_HOST = "crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com"
REGISTRY_USER = "xuqinmin12"
REGISTRY_PASSWORD = " ${ REGISTRY_PASSWORD :- xuqinmin1022 } "
IMAGE_TAG = "latest"
2026-05-20 15:30:55 +08:00
# 部署主机(用于健康检查 HTTP 请求,直接打服务端口)
2026-05-19 18:12:48 +08:00
DEPLOY_HOST = " ${ DEPLOY_HOST :- localhost } "
2026-05-19 08:01:19 +08:00
CONSOLE_BASE = " http:// ${ DEPLOY_HOST } "
2026-05-19 18:12:48 +08:00
_WS_SCHEME = "ws"
2026-05-19 08:01:19 +08:00
# MySQL( managed 模式,由 Docker 容器托管)
MYSQL_ROOT_PASSWORD = " ${ MYSQL_ROOT_PASSWORD :- XuqmRoot @2026 } "
MYSQL_PASSWORD = " ${ MYSQL_PASSWORD :- XuqmMysql @2026 } "
MYSQL_DATABASE = "xuqm_private"
MYSQL_USERNAME = "xuqm"
# Redis( managed 模式)
REDIS_PASSWORD = " ${ REDIS_PASSWORD :- XuqmRedis @2026 } "
2026-05-19 15:14:23 +08:00
# 公有化平台地址(迁移时调用 export API)
PUBLIC_PLATFORM_URL = "https://dev.xuqinmin.com"
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
# 内部变量
# ---------------------------------------------------------------------------
ROOT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) /.. " && pwd ) "
STEP = 0
2026-05-20 15:30:55 +08:00
TOTAL_STEPS = 7
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
log( ) { printf '\n\033[1;36m[%d/%d] %s\033[0m\n' " $STEP " " $TOTAL_STEPS " " $* " ; }
2026-05-19 18:12:48 +08:00
info( ) { printf ' \033[1;36m→\033[0m %s\n' " $* " ; }
2026-05-19 08:01:19 +08:00
ok( ) { printf ' \033[32m✓\033[0m %s\n' " $* " ; }
warn( ) { printf ' \033[33m⚠\033[0m %s\n' " $* " ; }
fail( ) { printf '\n\033[1;31mERROR: %s\033[0m\n' " $* " >& 2; exit 1; }
step( ) { STEP = $(( STEP+1)) ; log " $* " ; }
container_running( ) {
docker ps --filter " name= $1 " --filter "status=running" --format '{{.Names}}' 2>/dev/null | grep -q " $1 "
}
# ---------------------------------------------------------------------------
# 0. 确认工作目录
# ---------------------------------------------------------------------------
[ -f " $ROOT_DIR /docker-compose.yml " ] || \
fail " 请在 XuqmGroup-PrivateDeploy 仓库根目录执行本脚本(当前: $ROOT_DIR ) "
cd " $ROOT_DIR "
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
2026-05-19 15:14:23 +08:00
printf '\033[1;35m XuqmGroup 私有化一键部署脚本(全量部署)\033[0m\n'
2026-05-19 08:01:19 +08:00
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
printf ' 部署目录: %s\n' " $ROOT_DIR "
printf ' 镜像仓库: %s\n' " $REGISTRY "
2026-05-19 14:29:57 +08:00
printf ' 服务集: base + im + push + update + license\n\n'
# ---------------------------------------------------------------------------
# 0b. 租户初始化方式选择(交互式,在 Step 1 预检前执行)
# ---------------------------------------------------------------------------
command -v python3 >/dev/null 2>& 1 || \
2026-05-19 15:14:23 +08:00
fail "python3 未安装: apt install -y python3"
command -v curl >/dev/null 2>& 1 || \
fail "curl 未安装: apt install -y curl"
2026-05-19 14:29:57 +08:00
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf '\033[1;33m 租户初始化方式\033[0m\n'
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
printf ' 1) 新建租户 — 填写租户信息,系统将为您创建全新账号\n'
2026-05-19 15:14:23 +08:00
printf ' 2) 迁移租户 — 从公有平台导入现有租户数据(需迁移密钥)\n\n'
2026-05-19 14:29:57 +08:00
DEPLOY_MODE = ""
while [ -z " $DEPLOY_MODE " ] ; do
read -rp " 请选择 [1/2]: " _choice
case " $_choice " in
1) DEPLOY_MODE = "new" ; ;
2) DEPLOY_MODE = "migrate" ; ;
*) printf ' 请输入 1 或 2\n' ; ;
esac
done
DEPLOY_TENANT_EMAIL = ""
DEPLOY_TENANT_USERNAME = ""
DEPLOY_TENANT_NICKNAME = ""
_BOOTSTRAP_PASSWORD = ""
if [ " $DEPLOY_MODE " = "new" ] ; then
printf '\n ── 新建租户信息 ──\n'
while [ -z " $DEPLOY_TENANT_EMAIL " ] ; do
read -rp " 租户邮箱: " DEPLOY_TENANT_EMAIL
printf '%s' " $DEPLOY_TENANT_EMAIL " | grep -qE '^[^@]+@[^@]+\.[^@]+$' || {
warn "邮箱格式不正确,请重新输入"
DEPLOY_TENANT_EMAIL = ""
}
done
while [ -z " $DEPLOY_TENANT_USERNAME " ] ; do
read -rp " 用户名(登录用): " DEPLOY_TENANT_USERNAME
done
read -rp " 显示名称(昵称,默认同用户名): " DEPLOY_TENANT_NICKNAME
[ -z " $DEPLOY_TENANT_NICKNAME " ] && DEPLOY_TENANT_NICKNAME = " $DEPLOY_TENANT_USERNAME "
_pw = ""
while [ -z " $_pw " ] ; do
read -rsp " 登录密码( ≥8 位): " _pw; printf '\n'
[ " ${# _pw } " -ge 8 ] || { warn "密码不能少于 8 位,请重新输入" ; _pw = "" ; continue ; }
read -rsp " 确认密码: " _pw2; printf '\n'
[ " $_pw " = " $_pw2 " ] || { warn "两次密码不一致,请重新输入" ; _pw = "" ; }
done
unset _pw2
# Spring Boot bootstrap 接收明文密码并在存储时自行 bcrypt( bootstrap.env chmod 600)
_BOOTSTRAP_PASSWORD = " $_pw "
unset _pw
ok "租户信息已收集"
else
# ── 迁移模式:先收集 SRC 配置 + 认证,再继续后续步骤 ──
2026-05-19 15:14:23 +08:00
# ── 迁移模式:获取迁移密钥 ──
printf '\n\033[1;33m 请先访问租户平台安全中心生成迁移密钥:\033[0m\n'
printf ' 地址: %s\n' " $PUBLIC_PLATFORM_URL "
printf ' 路径: 安全中心 → 私有化部署迁移 → 生成迁移密钥\n'
printf ' (密钥仅在生成时显示一次,请及时复制后回到此处)\n\n'
_MIGRATE_KEY = ""
while [ -z " $_MIGRATE_KEY " ] ; do
2026-05-19 15:42:35 +08:00
read -rp " 请粘贴迁移密钥: " _MIGRATE_KEY
2026-05-19 15:14:23 +08:00
[ [ " $_MIGRATE_KEY " = = pmk_* ] ] || {
warn "密钥格式不正确(应以 pmk_ 开头),请重新输入"
_MIGRATE_KEY = ""
2026-05-19 14:29:57 +08:00
}
done
2026-05-19 15:14:23 +08:00
ok "迁移密钥已接收"
2026-05-19 14:29:57 +08:00
2026-05-19 15:14:23 +08:00
DEPLOY_TENANT_EMAIL = "migrate_placeholder@private.local"
DEPLOY_TENANT_USERNAME = "migrate_placeholder"
2026-05-19 14:29:57 +08:00
DEPLOY_TENANT_NICKNAME = ""
2026-05-19 15:14:23 +08:00
_BOOTSTRAP_PASSWORD = "already-migrated-do-not-use"
2026-05-19 14:29:57 +08:00
fi
2026-05-19 08:01:19 +08:00
2026-05-19 18:12:48 +08:00
# ---------------------------------------------------------------------------
2026-05-20 15:30:55 +08:00
# 0c. 外部访问地址(用于 SDK 配置,填写用户自己 nginx 代理后的地址)
2026-05-19 18:12:48 +08:00
# ---------------------------------------------------------------------------
printf '\n\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
2026-05-20 15:30:55 +08:00
printf '\033[1;33m 外部访问地址\033[0m\n'
2026-05-19 18:12:48 +08:00
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
2026-05-20 15:30:55 +08:00
printf ' 填写您的 nginx 对外暴露的地址,SDK 客户端将使用此地址。\n'
printf ' 例: http://192.168.1.100 或 https://xuqm.example.com\n\n'
_EXT_BASE = ""
while [ -z " $_EXT_BASE " ] ; do
read -rp " 外部访问地址: " _EXT_BASE
if ! printf '%s' " $_EXT_BASE " | grep -qE '^https?://' ; then
warn "请以 http:// 或 https:// 开头"
_EXT_BASE = ""
2026-05-19 18:12:48 +08:00
fi
2026-05-20 15:30:55 +08:00
done
CONSOLE_BASE = " ${ _EXT_BASE %/ } "
if printf '%s' " $CONSOLE_BASE " | grep -q '^https://' ; then
_WS_SCHEME = "wss"
2026-05-19 18:12:48 +08:00
fi
2026-05-20 15:30:55 +08:00
DEPLOY_HOST = " $( printf '%s' " $CONSOLE_BASE " | sed 's|https\?://||' ) "
ok " 外部地址: ${ CONSOLE_BASE } "
2026-05-19 18:12:48 +08:00
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
# Step 1 — 预检
# ---------------------------------------------------------------------------
step "预检( Docker / Compose / 磁盘 / 端口)"
command -v docker >/dev/null 2>& 1 || fail "Docker 未安装"
docker info >/dev/null 2>& 1 || fail "Docker daemon 未运行"
ok " Docker $( docker --version | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | head -1) "
docker compose version >/dev/null 2>& 1 || fail "Docker Compose v2 未安装"
ok " Docker Compose $( docker compose version --short 2>/dev/null || echo 'v2' ) "
DISK_FREE_GB = " $( df -BG " $ROOT_DIR " | awk 'NR==2{gsub(/G/,"",$4); print $4}' ) "
[ " ${ DISK_FREE_GB :- 0 } " -ge 10 ] || \
fail " 磁盘可用空间不足(需 ≥10 GB,当前 ${ DISK_FREE_GB :- ? } GB) "
ok " 磁盘可用: ${ DISK_FREE_GB } GB "
for port in 80 443; do
if ss -tlnp 2>/dev/null | grep -q " : ${ port } " || \
netstat -tlnp 2>/dev/null | grep -q " : ${ port } " ; then
warn " 端口 ${ port } 已被占用(继续执行,如已是本脚本容器则无影响) "
else
ok " 端口 ${ port } 空闲 "
fi
done
# ---------------------------------------------------------------------------
# Step 2 — 写入配置
# ---------------------------------------------------------------------------
2026-05-19 15:14:23 +08:00
step "写入配置(全服务)"
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# .env — 主配置文件,包含所有 profile
2026-05-19 08:01:19 +08:00
cat > " $ROOT_DIR /.env " <<EOF
2026-05-19 14:29:57 +08:00
# =============================================================================
2026-05-19 15:14:23 +08:00
# XuqmGroup 私有化部署主配置
2026-05-19 14:29:57 +08:00
# 部署时间: $(date '+%Y-%m-%d %H:%M:%S')
# 部署主机: ${DEPLOY_HOST}
# =============================================================================
PRIVATE_VERSION = 2026.05.19-private.1
2026-05-19 08:01:19 +08:00
REGISTRY = ${ REGISTRY }
REGISTRY_USER = ${ REGISTRY_USER }
REGISTRY_PASSWORD = ${ REGISTRY_PASSWORD }
IMAGE_TAG = ${ IMAGE_TAG }
2026-05-20 15:30:55 +08:00
# 启用全量服务( nginx 容器默认不启动,用户自行配置宿主机 nginx)
2026-05-19 14:29:57 +08:00
COMPOSE_PROFILES = base,infra-mysql,infra-redis,im,push,update,license
# MySQL( managed 模式,Docker 容器托管)
2026-05-19 08:01:19 +08:00
MYSQL_MODE = managed
MYSQL_HOST = mysql
MYSQL_PORT = 3306
MYSQL_DATABASE = ${ MYSQL_DATABASE }
MYSQL_USERNAME = ${ MYSQL_USERNAME }
2026-05-19 14:29:57 +08:00
# Redis( managed 模式)
2026-05-19 08:01:19 +08:00
REDIS_MODE = managed
REDIS_HOST = redis
REDIS_PORT = 6379
REDIS_DATABASE = 0
2026-05-19 14:29:57 +08:00
# 访问域名( IP 部署时直接使用 IP,无需配置域名)
2026-05-19 08:01:19 +08:00
CONSOLE_DOMAIN = ${ CONSOLE_BASE }
OPS_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 14:29:57 +08:00
DOCS_DOMAIN = ${ CONSOLE_BASE } /docs
2026-05-19 08:01:19 +08:00
FILE_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 14:29:57 +08:00
IM_DOMAIN = ${ CONSOLE_BASE }
UPDATE_DOMAIN = ${ CONSOLE_BASE }
LICENSE_DOMAIN = ${ CONSOLE_BASE }
PUSH_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# 功能开关(全量启用)
2026-05-19 08:01:19 +08:00
ENABLE_FILE = true
2026-05-19 14:29:57 +08:00
ENABLE_IM = true
ENABLE_PUSH = true
ENABLE_UPDATE = true
ENABLE_LICENSE = true
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
TENANT_BOOTSTRAP_EMAIL = ${ DEPLOY_TENANT_EMAIL }
2026-05-19 08:01:19 +08:00
TENANT_BOOTSTRAP_APP_KEY = ak_private_default
EOF
ok ".env 已写入"
2026-05-19 14:29:57 +08:00
# config/secrets.env — 敏感信息(权限 600)
2026-05-19 08:01:19 +08:00
mkdir -p " $ROOT_DIR /config "
cat > " $ROOT_DIR /config/secrets.env " <<EOF
2026-05-19 15:14:23 +08:00
# XuqmGroup 私有化部署 — 敏感凭据( 请妥善保管,chmod 600)
2026-05-19 08:01:19 +08:00
MYSQL_ROOT_PASSWORD = ${ MYSQL_ROOT_PASSWORD }
MYSQL_PASSWORD = ${ MYSQL_PASSWORD }
REDIS_PASSWORD = ${ REDIS_PASSWORD }
EOF
chmod 600 " $ROOT_DIR /config/secrets.env "
ok "config/secrets.env 已写入 (chmod 600)"
2026-05-19 14:29:57 +08:00
# config/xuqm.env — 业务服务容器内配置(全服务开启)
2026-05-19 08:01:19 +08:00
cat > " $ROOT_DIR /config/xuqm.env " <<EOF
2026-05-19 14:29:57 +08:00
# =============================================================================
2026-05-19 15:14:23 +08:00
# XuqmGroup 私有化部署 — 业务配置
2026-05-19 14:29:57 +08:00
# =============================================================================
# 私有化部署模式(必须为 PRIVATE)
2026-05-19 08:01:19 +08:00
DEPLOYMENT_MODE = PRIVATE
TENANT_REGISTER_ENABLED = false
TENANT_BOOTSTRAP_ENABLED = true
2026-05-19 14:29:57 +08:00
# 功能开关(全服务启用)
2026-05-19 08:01:19 +08:00
ENABLE_FILE = true
2026-05-19 14:29:57 +08:00
ENABLE_IM = true
ENABLE_PUSH = true
ENABLE_UPDATE = true
ENABLE_LICENSE = true
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# 数据库(容器内使用 Docker 服务名)
2026-05-19 08:01:19 +08:00
MYSQL_HOST = mysql
MYSQL_PORT = 3306
MYSQL_DATABASE = ${ MYSQL_DATABASE }
MYSQL_USERNAME = ${ MYSQL_USERNAME }
2026-05-19 14:29:57 +08:00
# Redis( 容器内使用 Docker 服务名)
2026-05-19 08:01:19 +08:00
REDIS_HOST = redis
REDIS_PORT = 6379
REDIS_DATABASE = 0
2026-05-19 14:29:57 +08:00
# 访问域名( IP 部署时直接使用 IP)
2026-05-19 08:01:19 +08:00
CONSOLE_DOMAIN = ${ CONSOLE_BASE }
OPS_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 14:29:57 +08:00
DOCS_DOMAIN = ${ CONSOLE_BASE } /docs
2026-05-19 08:01:19 +08:00
FILE_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 14:29:57 +08:00
IM_DOMAIN = ${ CONSOLE_BASE }
UPDATE_DOMAIN = ${ CONSOLE_BASE }
LICENSE_DOMAIN = ${ CONSOLE_BASE }
PUSH_DOMAIN = ${ CONSOLE_BASE }
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# SDK 对外服务地址(客户端 SDK 使用)
2026-05-19 08:01:19 +08:00
SDK_FILE_SERVICE_URL = ${ CONSOLE_BASE }
2026-05-19 14:29:57 +08:00
SDK_IM_API_URL = ${ CONSOLE_BASE }
2026-05-19 18:12:48 +08:00
SDK_IM_WS_URL = ${ _WS_SCHEME } ://${ DEPLOY_HOST } /ws/im
2026-05-19 14:29:57 +08:00
# 系统 IM 通信应用 key( 私有化服务间消息通知使用此 app_key 连接 IM 服务)
# 与公有化平台 xuqinmin12 租户下的平台系统应用 key 保持一致
SYSTEM_APP_KEY = ak_409e217e4aa14254ad73ad3c
2026-05-19 08:01:19 +08:00
EOF
ok "config/xuqm.env 已写入"
2026-05-19 14:29:57 +08:00
# config/tenant/bootstrap.env — 使用收集到的租户信息(迁移模式为占位符,后续由迁移步骤覆盖)
2026-05-19 08:01:19 +08:00
mkdir -p " $ROOT_DIR /config/tenant "
cat > " $ROOT_DIR /config/tenant/bootstrap.env " <<EOF
2026-05-19 14:29:57 +08:00
TENANT_BOOTSTRAP_EMAIL = ${ DEPLOY_TENANT_EMAIL }
TENANT_BOOTSTRAP_USERNAME = ${ DEPLOY_TENANT_USERNAME }
TENANT_BOOTSTRAP_PASSWORD = ${ _BOOTSTRAP_PASSWORD }
2026-05-19 08:01:19 +08:00
TENANT_BOOTSTRAP_APP_KEY = ak_private_default
EOF
2026-05-19 14:29:57 +08:00
chmod 600 " $ROOT_DIR /config/tenant/bootstrap.env "
ok "config/tenant/bootstrap.env 已写入 (chmod 600)"
2026-05-19 08:01:19 +08:00
2026-05-20 15:30:55 +08:00
# ( nginx 容器默认不启动,用户使用宿主机自己的 nginx 代理到各服务端口)
2026-05-19 18:12:48 +08:00
2026-05-20 15:30:55 +08:00
if false; then
# 以下为内置 nginx 配置示例,仅在 COMPOSE_PROFILES 包含 nginx-bundled 时使用
cat > /dev/null << NGIN X_CONF
2026-05-19 18:12:48 +08:00
# =============================================================================
# XuqmGroup 私有化部署 — Nginx HTTPS 配置(由 deploy.sh 自动生成)
# 域名: ${DEPLOY_DOMAIN}
# =============================================================================
# HTTP → HTTPS 重定向
server {
listen 80;
server_name ${ DEPLOY_DOMAIN } ;
return 301 https://\$ host\$ request_uri;
}
server {
listen 443 ssl;
server_name ${ DEPLOY_DOMAIN } ;
ssl_certificate /etc/letsencrypt/live/${ DEPLOY_DOMAIN } /fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${ DEPLOY_DOMAIN } /privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
charset utf-8;
client_max_body_size 100m;
location /health {
return 200 "ok\n" ;
add_header Content-Type text/plain;
}
location /api/v1/updates/ {
proxy_pass http://update-service:8084/api/v1/updates/;
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_read_timeout 60s;
}
location /api/v1/rn/ {
proxy_pass http://update-service:8084/api/v1/rn/;
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_read_timeout 120s;
}
location /api/im/ {
proxy_pass http://im-service:8082/api/im/;
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_read_timeout 60s;
}
location /ws/im {
proxy_pass http://im-service:8082/ws/im;
proxy_http_version 1.1;
proxy_set_header Upgrade \$ http_upgrade;
proxy_set_header Connection "upgrade" ;
proxy_set_header Host \$ host;
proxy_set_header X-Real-IP \$ remote_addr;
proxy_read_timeout 3600s;
}
location /api/license/ {
proxy_pass http://license-service:8085/api/license/;
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_read_timeout 60s;
}
location /api/file/ {
proxy_pass http://file-service:8086/api/file/;
proxy_set_header Host \$ host;
proxy_set_header X-Real-IP \$ remote_addr;
proxy_set_header X-Forwarded-For \$ proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /api/ {
proxy_pass http://tenant-service:9001/api/;
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_read_timeout 60s;
}
location /actuator/ {
proxy_pass http://tenant-service:9001/actuator/;
proxy_set_header Host \$ host;
proxy_set_header X-Real-IP \$ remote_addr;
}
location /file/ {
proxy_pass http://file-service:8086/file/;
proxy_set_header Host \$ host;
proxy_set_header X-Real-IP \$ remote_addr;
proxy_set_header X-Forwarded-For \$ proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /docs/ {
proxy_pass http://tenant-web:80/docs/;
proxy_set_header Host \$ host;
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;
}
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://dev.xuqinmin.com' 'https://\$host' ;
sub_filter_once off;
sub_filter_types text/javascript application/javascript;
}
}
NGINX_CONF
fi
2026-05-19 14:29:57 +08:00
# config/vendors/push.env — 推送服务厂商凭据(初始为关闭状态,按需开启)
mkdir -p " $ROOT_DIR /config/vendors "
if [ ! -f " $ROOT_DIR /config/vendors/push.env " ] ; then
cat > " $ROOT_DIR /config/vendors/push.env " <<'PUSH_ENV'
# 推送厂商凭据 — 按需填写并将对应 ENABLED 改为 true
HUAWEI_PUSH_ENABLED = false
MI_PUSH_ENABLED = false
OPPO_PUSH_ENABLED = false
VIVO_PUSH_ENABLED = false
HONOR_PUSH_ENABLED = false
APNS_PUSH_ENABLED = false
FCM_PUSH_ENABLED = false
PUSH_ENV
fi
# config/vendors/store-submit.env — 应用市场发布凭据(初始为关闭状态)
if [ ! -f " $ROOT_DIR /config/vendors/store-submit.env " ] ; then
cat > " $ROOT_DIR /config/vendors/store-submit.env " <<'STORE_ENV'
# 应用市场自动发布凭据 — 按需填写并将对应 ENABLED 改为 true
HUAWEI_STORE_ENABLED = false
MI_STORE_ENABLED = false
OPPO_STORE_ENABLED = false
VIVO_STORE_ENABLED = false
HONOR_STORE_ENABLED = false
STORE_ENV
fi
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# config/mail/smtp.env — 邮件服务配置(按需填写)
mkdir -p " $ROOT_DIR /config/mail "
if [ ! -f " $ROOT_DIR /config/mail/smtp.env " ] ; then
cat > " $ROOT_DIR /config/mail/smtp.env " <<'SMTP_ENV'
# 邮件服务配置(按需填写,不填则邮件功能不可用)
SMTP_HOST =
SMTP_PORT = 465
SMTP_USERNAME =
SMTP_TLS = true
SMTP_SSL = true
SMTP_ENV
fi
# SDK JSON 和文档站运行时配置
2026-05-19 08:01:19 +08:00
mkdir -p " $ROOT_DIR /config/sdk " " $ROOT_DIR /config/docs " " $ROOT_DIR /.deploy-state "
cat > " $ROOT_DIR /config/sdk/xuqm-private-sdk.json " <<EOF
{
"schemaVersion" : 1,
2026-05-19 14:29:57 +08:00
"configVersion" : "2026.05.19-private.1" ,
2026-05-19 08:01:19 +08:00
"deployment" : "PRIVATE" ,
"appKey" : "ak_private_default" ,
"controlBaseUrl" : " ${ CONSOLE_BASE } " ,
"fileBaseUrl" : " ${ CONSOLE_BASE } " ,
2026-05-19 14:29:57 +08:00
"imApiBaseUrl" : " ${ CONSOLE_BASE } " ,
2026-05-19 18:12:48 +08:00
"imWsUrl" : " ${ _WS_SCHEME } :// ${ DEPLOY_HOST } /ws/im " ,
2026-05-19 08:01:19 +08:00
"features" : {
"file" : true,
2026-05-19 14:29:57 +08:00
"im" : true,
"push" : true,
"update" : true,
"license" : true
2026-05-19 08:01:19 +08:00
} ,
"connectTimeoutMs" : 10000,
"readTimeoutMs" : 30000,
"logLevel" : "WARN"
}
EOF
cat > " $ROOT_DIR /config/docs/docs-runtime.json " <<EOF
{
"deployment" : "PRIVATE" ,
2026-05-19 14:29:57 +08:00
"privateVersion" : "2026.05.19-private.1" ,
"domains" : {
"console" : " ${ CONSOLE_BASE } " ,
"file" : " ${ CONSOLE_BASE } " ,
"im" : " ${ CONSOLE_BASE } " ,
"update" : " ${ CONSOLE_BASE } "
} ,
"features" : { "file" : true, "im" : true, "push" : true, "update" : true, "license" : true }
2026-05-19 08:01:19 +08:00
}
EOF
2026-05-19 14:29:57 +08:00
# 创建所有数据目录
mkdir -p \
" $ROOT_DIR /data/uploads " \
" $ROOT_DIR /data/mysql " \
" $ROOT_DIR /data/redis " \
" $ROOT_DIR /data/update " \
" $ROOT_DIR /data/backups " \
" $ROOT_DIR /logs "
ok "SDK JSON、文档站配置和目录结构就绪"
# 将 secrets 导出到 shell 环境,使 docker compose 变量替换可读取密码
set -a
# shellcheck disable=SC1090
. " $ROOT_DIR /config/secrets.env "
set +a
ok "secrets.env 已加载到 shell 环境"
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
# Step 3 — 登录镜像仓库
# ---------------------------------------------------------------------------
step "登录 ACR 镜像仓库"
printf '%s' " $REGISTRY_PASSWORD " | \
docker login " $REGISTRY_HOST " -u " $REGISTRY_USER " --password-stdin 2>/dev/null \
&& ok " 登录成功: $REGISTRY_HOST " \
|| fail "ACR 登录失败,请检查 REGISTRY_USER / REGISTRY_PASSWORD"
# ---------------------------------------------------------------------------
# Step 4 — 启动基础设施( MySQL + Redis)
# ---------------------------------------------------------------------------
step "启动基础设施容器( MySQL / Redis) "
docker compose \
--env-file " $ROOT_DIR /.env " \
-f " $ROOT_DIR /docker-compose.yml " \
-f " $ROOT_DIR /docker-compose.infra.yml " \
--profile infra-mysql --profile infra-redis \
up -d mysql redis
# 等待 MySQL 就绪(最多 90 秒)
printf ' 等待 MySQL 启动'
for i in $( seq 1 30) ; do
if docker exec " $( docker ps -qf name = mysql | head -1) " \
mysqladmin -u root -p" ${ MYSQL_ROOT_PASSWORD } " ping --silent 2>/dev/null; then
printf '\n'
ok "MySQL 就绪"
break
fi
printf '.'
sleep 3
[ " $i " -eq 30 ] && { printf '\n' ; fail "MySQL 启动超时( 90s) " ; }
done
# 等待 Redis 就绪
for i in $( seq 1 10) ; do
if docker exec " $( docker ps -qf name = redis | head -1) " \
redis-cli -a " $REDIS_PASSWORD " --no-auth-warning PING 2>/dev/null | grep -q PONG; then
ok "Redis 就绪"
break
fi
sleep 2
[ " $i " -eq 10 ] && fail "Redis 启动超时( 20s) "
done
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
# Step 5 — 启动所有业务容器(全服务)
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
step "拉取镜像并启动所有业务容器( base + im + push + update + license) "
2026-05-19 08:01:19 +08:00
docker compose \
--env-file " $ROOT_DIR /.env " \
-f " $ROOT_DIR /docker-compose.yml " \
-f " $ROOT_DIR /docker-compose.infra.yml " \
2026-05-19 14:29:57 +08:00
--profile base \
--profile infra-mysql --profile infra-redis \
--profile im --profile push --profile update --profile license \
2026-05-19 08:01:19 +08:00
up -d
2026-05-20 15:30:55 +08:00
# 等待 tenant-service 健康(直接打 9001 端口,不经过 nginx)
2026-05-19 08:01:19 +08:00
printf ' 等待 tenant-service 启动'
for i in $( seq 1 40) ; do
2026-05-19 14:29:57 +08:00
code = " $( curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
2026-05-20 15:43:05 +08:00
"http://127.0.0.1:11224/actuator/health" 2>/dev/null || echo 000) "
2026-05-19 08:01:19 +08:00
if [ " $code " = "200" ] ; then
printf '\n'
ok "tenant-service 健康 (HTTP 200)"
break
fi
printf '.'
sleep 3
[ " $i " -eq 40 ] && {
printf '\n'
warn "tenant-service 未在 120s 内响应,继续执行(可能仍在初始化)"
break
}
done
# 简要容器状态
printf '\n 容器状态:\n'
docker compose \
--env-file " $ROOT_DIR /.env " \
-f " $ROOT_DIR /docker-compose.yml " \
-f " $ROOT_DIR /docker-compose.infra.yml " \
2026-05-19 14:29:57 +08:00
ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null || \
docker ps --format ' {{.Names}}\t{{.Status}}' | grep -E "xuqm|mysql|redis" || true
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
# Step 5b — Schema 扩展 + 系统应用创建( 在迁移之前,Hibernate 已建表后执行)
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
step "Schema 扩展 + 系统 IM 应用初始化"
MYSQL_CTR_TMP = " $( docker ps -qf name = mysql | head -1) "
# 检查并添加列(兼容 MySQL 8.x,不依赖 ADD COLUMN IF NOT EXISTS)
for _col in is_default deletable; do
_exists = " $( docker exec " $MYSQL_CTR_TMP " mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
-N -e " SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t_app' AND COLUMN_NAME=' ${ _col } ' " 2>/dev/null || echo 0) "
if [ " ${ _exists :- 0 } " -eq 0 ] ; then
_dv = " $( [ " $_col " = "is_default" ] && echo 0 || echo 1 ) "
docker exec " $MYSQL_CTR_TMP " mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
-e " ALTER TABLE t_app ADD COLUMN ${ _col } BIT(1) NOT NULL DEFAULT ${ _dv } " 2>/dev/null \
&& ok " 列 ${ _col } 已添加 " || warn " 列 ${ _col } 添加失败,继续 "
else
ok " 列 ${ _col } 已存在,跳过 "
fi
done
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# 触发器:先删再建,使用 --delimiter='$$'( MySQL 批量模式不支持 DELIMITER 语句)
docker exec " $MYSQL_CTR_TMP " mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
-e "DROP TRIGGER IF EXISTS prevent_default_app_delete" 2>/dev/null
_TRIG_FILE = " $( mktemp /tmp/xuqm-trig-XXXXXX.sql) "
cat > " $_TRIG_FILE " << 'TRIG_SQL'
CREATE TRIGGER prevent_default_app_delete
BEFORE DELETE ON t_app
FOR EACH ROW
BEGIN
IF OLD.is_default = 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete default application' ;
END IF;
END
TRIG_SQL
if docker exec -i " $MYSQL_CTR_TMP " mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
--delimiter= '$$' < " $_TRIG_FILE " 2>/dev/null; then
ok "删除保护触发器已创建"
else
warn "触发器创建遇到警告,继续"
fi
rm -f " $_TRIG_FILE "
# 创建系统 IM 应用( bootstrap 阶段,迁移会重建;此处保证无迁移时也可用)
_SYS_APP_KEY = "ak_409e217e4aa14254ad73ad3c"
_SYS_EXIST = " $( docker exec " $MYSQL_CTR_TMP " \
2026-05-19 08:01:19 +08:00
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
2026-05-19 14:29:57 +08:00
-N -e " SELECT COUNT(*) FROM t_app WHERE app_key=' ${ _SYS_APP_KEY } ' " 2>/dev/null || echo 0) "
if [ " ${ _SYS_EXIST :- 0 } " -eq 0 ] ; then
_SYS_SQL = " $( mktemp /tmp/xuqm-sysapp-XXXXXX.sql) "
cat > " $_SYS_SQL " << 'SYS APP_SQL'
INSERT IGNORE INTO t_app ( id, app_key, app_secret, created_at, description, name, package_name, tenant_id)
SELECT UUID( ) , 'ak_409e217e4aa14254ad73ad3c' ,
CONCAT( 'as_sys_' , LEFT( MD5( UUID( ) ) , 16) ) ,
NOW( ) ,
'系统内置应用 — 私有化服务间 IM 通信专用,is_default=1,不可删除' ,
'平台系统应用' , 'com.xuqmgroup.platform' ,
id
FROM t_tenant ORDER BY created_at LIMIT 1;
INSERT INTO t_feature_service ( id, app_key, created_at, enabled, platform, secret_key, service_type)
SELECT UUID( ) , 'ak_409e217e4aa14254ad73ad3c' , NOW( ) , 1, 'ANDROID' , CONCAT( 'sk_sys_a_' , LEFT( MD5( UUID( ) ) , 8) ) , 'IM'
WHERE NOT EXISTS ( SELECT 1 FROM t_feature_service WHERE app_key = 'ak_409e217e4aa14254ad73ad3c' AND platform = 'ANDROID' AND service_type = 'IM' ) ;
INSERT INTO t_feature_service ( id, app_key, created_at, enabled, platform, secret_key, service_type)
SELECT UUID( ) , 'ak_409e217e4aa14254ad73ad3c' , NOW( ) , 1, 'IOS' , CONCAT( 'sk_sys_i_' , LEFT( MD5( UUID( ) ) , 8) ) , 'IM'
WHERE NOT EXISTS ( SELECT 1 FROM t_feature_service WHERE app_key = 'ak_409e217e4aa14254ad73ad3c' AND platform = 'IOS' AND service_type = 'IM' ) ;
INSERT INTO t_feature_service ( id, app_key, created_at, enabled, platform, secret_key, service_type)
SELECT UUID( ) , 'ak_409e217e4aa14254ad73ad3c' , NOW( ) , 1, 'HARMONY' , CONCAT( 'sk_sys_h_' , LEFT( MD5( UUID( ) ) , 8) ) , 'IM'
WHERE NOT EXISTS ( SELECT 1 FROM t_feature_service WHERE app_key = 'ak_409e217e4aa14254ad73ad3c' AND platform = 'HARMONY' AND service_type = 'IM' ) ;
UPDATE t_app SET is_default = 1, deletable = 0 WHERE app_key = 'ak_409e217e4aa14254ad73ad3c' ;
SYSAPP_SQL
docker exec -i " $MYSQL_CTR_TMP " \
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
--default-character-set= utf8mb4 < " $_SYS_SQL " 2>/dev/null || warn "系统应用创建遇到警告,继续"
rm -f " $_SYS_SQL "
ok " 系统应用 ${ _SYS_APP_KEY } 已创建( IM 三端启用,is_default=1) "
else
ok " 系统应用 ${ _SYS_APP_KEY } 已存在,跳过创建 "
fi
# ---------------------------------------------------------------------------
# Step 6 — 租户初始化(新建 / 迁移)
# ---------------------------------------------------------------------------
step " 租户初始化(模式: ${ DEPLOY_MODE } ) "
MYSQL_CTR = " $( docker ps -qf name = mysql | head -1) "
if [ " $DEPLOY_MODE " = "new" ] ; then
# ── 新建租户: bootstrap 已在容器启动时执行,验证租户记录即可 ──
_NEW_TENANT_CNT = " $( docker exec " $MYSQL_CTR " \
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
-N -e " SELECT COUNT(*) FROM t_tenant WHERE email=' ${ DEPLOY_TENANT_EMAIL } ' " 2>/dev/null || echo 0) "
if [ " ${ _NEW_TENANT_CNT :- 0 } " -ge 1 ] ; then
ok " 租户 ${ DEPLOY_TENANT_EMAIL } 已由 bootstrap 创建 "
else
warn "租户记录暂未就绪( tenant-service 可能仍在初始化),继续部署"
fi
2026-05-19 08:01:19 +08:00
2026-05-20 12:11:19 +08:00
# 为 bootstrap 创建的 app 补写 app_licenses( license-service 独立 DB 写入)
_LIC_SQL_NEW = " $( mktemp /tmp/xuqm-lic-new-XXXXXX.sql) "
cat > " $_LIC_SQL_NEW " <<'LIC_SQL _NEW'
INSERT INTO app_licenses ( app_key, name, max_devices, registered_devices,
expires_at, is_active, remark, created_at, updated_at)
SELECT a.app_key, a.name, 1000, 0, NULL, 1, '私有化新建' , NOW( ) , NOW( )
FROM t_app a
WHERE a.app_key != 'ak_409e217e4aa14254ad73ad3c'
ON DUPLICATE KEY UPDATE updated_at = updated_at;
LIC_SQL_NEW
docker exec -i " $MYSQL_CTR " \
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
--default-character-set= utf8mb4 < " $_LIC_SQL_NEW " 2>/dev/null \
&& ok "app_licenses 记录已为所有应用创建" \
|| warn "app_licenses 写入遇到警告,继续"
rm -f " $_LIC_SQL_NEW "
2026-05-19 08:01:19 +08:00
else
2026-05-19 15:14:23 +08:00
# ── 迁移租户:调用公有平台 export API → 私有平台 import API ──
# 检查是否有现有租户数据
2026-05-19 14:29:57 +08:00
_EXIST_CNT = " $( docker exec " $MYSQL_CTR " \
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
-N -e "SELECT COUNT(*) FROM t_tenant" 2>/dev/null || echo 0) "
if [ " ${ _EXIST_CNT :- 0 } " -ge 1 ] ; then
printf '\n \033[1;31m⚠ 警告:数据库中已存在租户数据!\033[0m\n'
2026-05-19 15:14:23 +08:00
printf ' 迁移操作将 \033[1;31m删除所有现有租户信息\033[0m( 账号、应用、功能配置) 。\n\n'
2026-05-19 14:29:57 +08:00
read -rp " 确认删除并继续迁移?请输入 yes: " _confirm
[ " $_confirm " = "yes" ] || fail "操作已取消"
fi
2026-05-19 15:14:23 +08:00
printf ' 正在从公有平台导出租户数据 ...\n'
_EXPORT_BODY = $( python3 -c "import json, sys; print(json.dumps({'migrationKey': sys.argv[1]}))" " $_MIGRATE_KEY " )
_EXPORT_RESP = $( curl -sf --max-time 30 -X POST \
-H "Content-Type: application/json" \
-d " $_EXPORT_BODY " \
" ${ PUBLIC_PLATFORM_URL } /api/migrate/export " ) \
|| fail " 导出失败:请确认迁移密钥有效且未过期( ${ PUBLIC_PLATFORM_URL } ) "
_EXPORT_DATA = $( printf '%s' " $_EXPORT_RESP " | python3 -c "
import json, sys
resp = json.load( sys.stdin)
code = resp.get( 'code' , 0)
if code != 200:
sys.exit( '导出失败: ' + resp.get( 'message' , 'unknown error' ) )
print( json.dumps( resp[ 'data' ] ) )
") || fail " 解析导出数据失败"
ok "租户数据已从公有平台导出"
printf ' 正在导入租户数据到私有部署 ...\n'
2026-05-19 16:07:47 +08:00
_IMPORT_RESP = $( printf '%s' " $_EXPORT_DATA " | curl -s --max-time 30 -X POST \
2026-05-19 15:14:23 +08:00
-H "Content-Type: application/json" \
--data-binary @- \
2026-05-19 16:07:47 +08:00
-w "\n__HTTP_STATUS__:%{http_code}" \
2026-05-20 15:43:05 +08:00
"http://127.0.0.1:11224/api/private/deployment/migrate/import" )
2026-05-19 16:07:47 +08:00
_IMPORT_STATUS = $( printf '%s' " $_IMPORT_RESP " | grep -o '__HTTP_STATUS__:[0-9]*' | cut -d: -f2)
_IMPORT_BODY = $( printf '%s' " $_IMPORT_RESP " | sed 's/__HTTP_STATUS__:[0-9]*//' )
if [ " ${ _IMPORT_STATUS } " != "200" ] ; then
fail " 导入失败( HTTP ${ _IMPORT_STATUS } ) : $( printf '%s' " $_IMPORT_BODY " | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('message','unknown'))" 2>/dev/null || printf '%s' " $_IMPORT_BODY " ) "
fi
2026-05-19 15:14:23 +08:00
# 从导出数据中提取租户信息用于最终汇总
_TENANT_INFO = $( printf '%s' " $_EXPORT_DATA " | python3 -c "
import json, sys
d = json.load( sys.stdin)
t = d.get( 'tenant' , { } )
print( t.get( 'email' , '' ) , t.get( 'username' , '' ) )
" )
DEPLOY_TENANT_EMAIL = $( printf '%s' " $_TENANT_INFO " | awk '{print $1}' )
DEPLOY_TENANT_USERNAME = $( printf '%s' " $_TENANT_INFO " | awk '{print $2}' )
ok " 租户 ${ DEPLOY_TENANT_EMAIL } 数据已成功导入 "
2026-05-20 12:11:19 +08:00
# 为迁移的所有 app 在 app_licenses 表创建 license 记录( license-service 独立 DB 写入)
_LIC_SQL = " $( mktemp /tmp/xuqm-lic-XXXXXX.sql) "
cat > " $_LIC_SQL " <<'LIC_SQL '
INSERT INTO app_licenses ( app_key, name, max_devices, registered_devices,
expires_at, is_active, remark, created_at, updated_at)
SELECT a.app_key, a.name, 1000, 0, NULL, 1, '私有化迁移' , NOW( ) , NOW( )
FROM t_app a
WHERE a.app_key != 'ak_409e217e4aa14254ad73ad3c'
ON DUPLICATE KEY UPDATE updated_at = updated_at;
LIC_SQL
docker exec -i " $MYSQL_CTR " \
mysql -u " $MYSQL_USERNAME " -p" ${ MYSQL_PASSWORD } " " $MYSQL_DATABASE " \
--default-character-set= utf8mb4 < " $_LIC_SQL " 2>/dev/null \
&& ok "app_licenses 记录已为所有迁移应用创建" \
|| warn "app_licenses 写入遇到警告,继续"
rm -f " $_LIC_SQL "
2026-05-19 15:14:23 +08:00
# 更新 bootstrap.env,防止重启时重新创建占位租户
cat > " $ROOT_DIR /config/tenant/bootstrap.env " <<BSENV
2026-05-19 14:29:57 +08:00
TENANT_BOOTSTRAP_EMAIL = ${ DEPLOY_TENANT_EMAIL }
2026-05-19 15:14:23 +08:00
TENANT_BOOTSTRAP_USERNAME = ${ DEPLOY_TENANT_USERNAME }
2026-05-19 14:29:57 +08:00
TENANT_BOOTSTRAP_PASSWORD = already-migrated-do-not-use
TENANT_BOOTSTRAP_APP_KEY = ak_private_default
2026-05-19 15:14:23 +08:00
BSENV
2026-05-19 14:29:57 +08:00
chmod 600 " $ROOT_DIR /config/tenant/bootstrap.env "
ok "bootstrap.env 已更新(迁移后)"
2026-05-19 15:14:23 +08:00
unset _MIGRATE_KEY _EXPORT_BODY _EXPORT_RESP _EXPORT_DATA _IMPORT_RESP _TENANT_INFO
2026-05-19 08:01:19 +08:00
fi
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
# Step 7 — 一键验证(调用 verify.sh)
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
step "一键验证所有服务"
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
# 将 DEPLOY_HOST 注入到 verify.sh 中使用
export DEPLOY_HOST = " ${ DEPLOY_HOST } "
2026-05-19 08:01:19 +08:00
2026-05-19 14:29:57 +08:00
if bash " $ROOT_DIR /scripts/verify.sh " ; then
ok "全量验证通过"
2026-05-19 08:01:19 +08:00
else
2026-05-19 14:29:57 +08:00
printf '\n\033[33m 部分验证项未通过,请查看上方输出。\033[0m\n'
printf ' 可重新运行: DEPLOY_HOST=%s bash %s/scripts/verify.sh\n' " $DEPLOY_HOST " " $ROOT_DIR "
2026-05-19 08:01:19 +08:00
fi
# ---------------------------------------------------------------------------
2026-05-19 14:29:57 +08:00
# 最终汇总
2026-05-19 08:01:19 +08:00
# ---------------------------------------------------------------------------
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
2026-05-19 15:14:23 +08:00
printf '\033[1;35m XuqmGroup 私有化部署完成\033[0m\n'
2026-05-19 08:01:19 +08:00
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
2026-05-19 14:29:57 +08:00
printf '\n \033[1m租户信息: \033[0m\n'
2026-05-20 15:30:55 +08:00
printf ' 邮箱: %s\n' " ${ DEPLOY_TENANT_EMAIL } "
printf ' 用户名: %s\n' " ${ DEPLOY_TENANT_USERNAME } "
2026-05-19 14:29:57 +08:00
if [ " $DEPLOY_MODE " = "new" ] ; then
2026-05-20 15:30:55 +08:00
printf ' 密码: 部署时设置的密码\n'
2026-05-19 14:29:57 +08:00
else
2026-05-20 15:30:55 +08:00
printf ' 密码: 同生产平台密码(原样迁移,未重置)\n'
2026-05-19 08:01:19 +08:00
fi
2026-05-20 15:30:55 +08:00
printf '\n \033[1m容器端口( 请在您的 nginx 中配置代理):\033[0m\n'
2026-05-20 15:43:05 +08:00
printf ' 控制台前端 127.0.0.1:11226 → 代理 /\n'
printf ' 运营后台 127.0.0.1:11227 → 代理 /ops\n'
printf ' 核心 API 127.0.0.1:11224 → 代理 /api/ /actuator/\n'
printf ' 文件服务 127.0.0.1:11225 → 代理 /file/ /api/file/\n'
printf ' IM 服务 127.0.0.1:11228 → 代理 /api/im/ /ws/im\n'
printf ' 版本管理 127.0.0.1:11230 → 代理 /api/v1/updates/ /api/v1/rn/\n'
printf ' License 服务 127.0.0.1:11231 → 代理 /api/license/\n'
printf ' 推送服务 127.0.0.1:11229 (厂商回调,按需代理)\n'
2026-05-20 15:30:55 +08:00
printf '\n \033[1mNginx 配置参考(复制到您的 nginx server 块):\033[0m\n'
printf '\033[0;37m'
cat <<'NGIN X_REF'
charset utf-8;
client_max_body_size 100m;
2026-05-20 15:43:05 +08:00
location /api/v1/updates/ { proxy_pass http://127.0.0.1:11230/api/v1/updates/; }
location /api/v1/rn/ { proxy_pass http://127.0.0.1:11230/api/v1/rn/; }
location /api/im/ { proxy_pass http://127.0.0.1:11228/api/im/; }
2026-05-20 15:30:55 +08:00
location /ws/im {
2026-05-20 15:43:05 +08:00
proxy_pass http://127.0.0.1:11228/ws/im;
2026-05-20 15:30:55 +08:00
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade ;
proxy_set_header Connection "upgrade" ;
proxy_read_timeout 3600s;
}
2026-05-20 15:43:05 +08:00
location /api/license/ { proxy_pass http://127.0.0.1:11231/api/license/; }
2026-05-20 15:30:55 +08:00
location /file/ {
2026-05-20 15:43:05 +08:00
proxy_pass http://127.0.0.1:11225/file/;
2026-05-20 15:30:55 +08:00
client_max_body_size 500m;
proxy_read_timeout 300s;
}
2026-05-20 15:43:05 +08:00
location /api/ { proxy_pass http://127.0.0.1:11224/api/; }
location /actuator/ { proxy_pass http://127.0.0.1:11224/actuator/; }
location /ops { proxy_pass http://127.0.0.1:11227; }
location / { proxy_pass http://127.0.0.1:11226; }
2026-05-20 15:30:55 +08:00
NGINX_REF
printf '\033[0m'
printf '\n \033[1m部署目录: \033[0m %s\n' " $ROOT_DIR "
2026-05-19 14:29:57 +08:00
printf ' \033[1m审计日志: \033[0m %s/logs/audit.log\n' " $ROOT_DIR "
2026-05-20 15:30:55 +08:00
printf '\n\033[1;32m 部署成功!配置好 nginx 后即可访问:%s\033[0m\n\n' " ${ CONSOLE_BASE } "