私有化部署场景下用户通常有自己的 nginx,内置 nginx 容器会占用 80/443 与宿主机冲突,且域名/HTTPS 交互配置对用户是噪音。 - docker-compose.yml: nginx 容器改为 profile=nginx-bundled(默认不启动) 各业务容器增加 ports 暴露宿主机端口(9001/8086/8082/8083/8084/8085/8080/8081) - deploy.sh: 去除 域名/HTTPS/certbot 交互,改为询问一个外部访问地址(用于 SDK 配置) 健康检查和 import API 直接打 127.0.0.1:9001,不再依赖 nginx 容器 末尾输出端口表和可直接复制的 nginx location 配置参考 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
961 行
38 KiB
Bash
可执行文件
961 行
38 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
||
# deploy.sh — XuqmGroup 私有化一键部署脚本
|
||
#
|
||
# 用途:在目标机器上完成以下全部步骤:
|
||
# 0. 交互式选择租户初始化方式(新建 / 迁移)并收集必要信息
|
||
# 1. 预检(Docker、Compose、磁盘、端口)
|
||
# 2. 写入配置(.env / secrets.env / xuqm.env / bootstrap.env / nginx)
|
||
# 3. 登录 ACR 镜像仓库
|
||
# 4. 启动基础设施容器(MySQL、Redis)并等待就绪
|
||
# 5. 拉取镜像并启动所有业务容器(base + im + push + update + license)
|
||
# 6. Schema 扩展(is_default / deletable 列 + 删除保护触发器)+ 系统应用初始化
|
||
# 7. 租户初始化(新建:填写信息并验证;迁移:凭迁移密钥调用 API 导入)
|
||
# 8. 运行一键验证脚本确认所有服务正常
|
||
#
|
||
# 幂等性:可重复执行。已运行的容器不会被重建。
|
||
# 迁移模式:若检测到已有租户数据,会在执行前提示确认。
|
||
#
|
||
# 前提:
|
||
# - Docker 和 Docker Compose v2 已安装
|
||
# - python3 和 curl 已安装
|
||
# - 在 XuqmGroup-PrivateDeploy 仓库根目录下执行本脚本
|
||
#
|
||
# 覆盖默认值(可通过环境变量传入):
|
||
# DEPLOY_HOST 目标机器 IP / 主机名(默认 localhost)
|
||
# 部署向导会提示配置域名与 HTTPS
|
||
# REGISTRY_PASSWORD ACR 密码(默认 xuqinmin1022)
|
||
# MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、REDIS_PASSWORD 同理
|
||
|
||
set -euo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 常量
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
# 镜像仓库
|
||
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"
|
||
|
||
# 部署主机(用于健康检查 HTTP 请求,直接打服务端口)
|
||
DEPLOY_HOST="${DEPLOY_HOST:-localhost}"
|
||
CONSOLE_BASE="http://${DEPLOY_HOST}"
|
||
_WS_SCHEME="ws"
|
||
|
||
# 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}"
|
||
|
||
# 公有化平台地址(迁移时调用 export API)
|
||
PUBLIC_PLATFORM_URL="https://dev.xuqinmin.com"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 内部变量
|
||
# ---------------------------------------------------------------------------
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
STEP=0
|
||
TOTAL_STEPS=7
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具函数
|
||
# ---------------------------------------------------------------------------
|
||
log() { printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$STEP" "$TOTAL_STEPS" "$*"; }
|
||
info() { printf ' \033[1;36m→\033[0m %s\n' "$*"; }
|
||
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'
|
||
printf '\033[1;35m XuqmGroup 私有化一键部署脚本(全量部署)\033[0m\n'
|
||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||
printf ' 部署目录: %s\n' "$ROOT_DIR"
|
||
printf ' 镜像仓库: %s\n' "$REGISTRY"
|
||
printf ' 服务集: base + im + push + update + license\n\n'
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 0b. 租户初始化方式选择(交互式,在 Step 1 预检前执行)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
command -v python3 >/dev/null 2>&1 || \
|
||
fail "python3 未安装: apt install -y python3"
|
||
command -v curl >/dev/null 2>&1 || \
|
||
fail "curl 未安装: apt install -y curl"
|
||
|
||
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||
printf '\033[1;33m 租户初始化方式\033[0m\n'
|
||
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||
printf ' 1) 新建租户 — 填写租户信息,系统将为您创建全新账号\n'
|
||
printf ' 2) 迁移租户 — 从公有平台导入现有租户数据(需迁移密钥)\n\n'
|
||
|
||
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 配置 + 认证,再继续后续步骤 ──
|
||
# ── 迁移模式:获取迁移密钥 ──
|
||
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
|
||
read -rp " 请粘贴迁移密钥: " _MIGRATE_KEY
|
||
[[ "$_MIGRATE_KEY" == pmk_* ]] || {
|
||
warn "密钥格式不正确(应以 pmk_ 开头),请重新输入"
|
||
_MIGRATE_KEY=""
|
||
}
|
||
done
|
||
ok "迁移密钥已接收"
|
||
|
||
DEPLOY_TENANT_EMAIL="migrate_placeholder@private.local"
|
||
DEPLOY_TENANT_USERNAME="migrate_placeholder"
|
||
DEPLOY_TENANT_NICKNAME=""
|
||
_BOOTSTRAP_PASSWORD="already-migrated-do-not-use"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 0c. 外部访问地址(用于 SDK 配置,填写用户自己 nginx 代理后的地址)
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||
printf '\033[1;33m 外部访问地址\033[0m\n'
|
||
printf '\033[1;33m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n'
|
||
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=""
|
||
fi
|
||
done
|
||
CONSOLE_BASE="${_EXT_BASE%/}"
|
||
if printf '%s' "$CONSOLE_BASE" | grep -q '^https://'; then
|
||
_WS_SCHEME="wss"
|
||
fi
|
||
DEPLOY_HOST="$(printf '%s' "$CONSOLE_BASE" | sed 's|https\?://||')"
|
||
ok "外部地址: ${CONSOLE_BASE}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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 — 写入配置
|
||
# ---------------------------------------------------------------------------
|
||
step "写入配置(全服务)"
|
||
|
||
# .env — 主配置文件,包含所有 profile
|
||
cat > "$ROOT_DIR/.env" <<EOF
|
||
# =============================================================================
|
||
# XuqmGroup 私有化部署主配置
|
||
# 部署时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||
# 部署主机: ${DEPLOY_HOST}
|
||
# =============================================================================
|
||
|
||
PRIVATE_VERSION=2026.05.19-private.1
|
||
REGISTRY=${REGISTRY}
|
||
REGISTRY_USER=${REGISTRY_USER}
|
||
REGISTRY_PASSWORD=${REGISTRY_PASSWORD}
|
||
IMAGE_TAG=${IMAGE_TAG}
|
||
|
||
# 启用全量服务(nginx 容器默认不启动,用户自行配置宿主机 nginx)
|
||
COMPOSE_PROFILES=base,infra-mysql,infra-redis,im,push,update,license
|
||
|
||
# MySQL(managed 模式,Docker 容器托管)
|
||
MYSQL_MODE=managed
|
||
MYSQL_HOST=mysql
|
||
MYSQL_PORT=3306
|
||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||
|
||
# Redis(managed 模式)
|
||
REDIS_MODE=managed
|
||
REDIS_HOST=redis
|
||
REDIS_PORT=6379
|
||
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}
|
||
UPDATE_DOMAIN=${CONSOLE_BASE}
|
||
LICENSE_DOMAIN=${CONSOLE_BASE}
|
||
PUSH_DOMAIN=${CONSOLE_BASE}
|
||
|
||
# 功能开关(全量启用)
|
||
ENABLE_FILE=true
|
||
ENABLE_IM=true
|
||
ENABLE_PUSH=true
|
||
ENABLE_UPDATE=true
|
||
ENABLE_LICENSE=true
|
||
|
||
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||
EOF
|
||
ok ".env 已写入"
|
||
|
||
# config/secrets.env — 敏感信息(权限 600)
|
||
mkdir -p "$ROOT_DIR/config"
|
||
cat > "$ROOT_DIR/config/secrets.env" <<EOF
|
||
# XuqmGroup 私有化部署 — 敏感凭据(请妥善保管,chmod 600)
|
||
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)"
|
||
|
||
# config/xuqm.env — 业务服务容器内配置(全服务开启)
|
||
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
||
# =============================================================================
|
||
# XuqmGroup 私有化部署 — 业务配置
|
||
# =============================================================================
|
||
|
||
# 私有化部署模式(必须为 PRIVATE)
|
||
DEPLOYMENT_MODE=PRIVATE
|
||
TENANT_REGISTER_ENABLED=false
|
||
TENANT_BOOTSTRAP_ENABLED=true
|
||
|
||
# 功能开关(全服务启用)
|
||
ENABLE_FILE=true
|
||
ENABLE_IM=true
|
||
ENABLE_PUSH=true
|
||
ENABLE_UPDATE=true
|
||
ENABLE_LICENSE=true
|
||
|
||
# 数据库(容器内使用 Docker 服务名)
|
||
MYSQL_HOST=mysql
|
||
MYSQL_PORT=3306
|
||
MYSQL_DATABASE=${MYSQL_DATABASE}
|
||
MYSQL_USERNAME=${MYSQL_USERNAME}
|
||
|
||
# Redis(容器内使用 Docker 服务名)
|
||
REDIS_HOST=redis
|
||
REDIS_PORT=6379
|
||
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}
|
||
UPDATE_DOMAIN=${CONSOLE_BASE}
|
||
LICENSE_DOMAIN=${CONSOLE_BASE}
|
||
PUSH_DOMAIN=${CONSOLE_BASE}
|
||
|
||
# SDK 对外服务地址(客户端 SDK 使用)
|
||
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 保持一致
|
||
SYSTEM_APP_KEY=ak_409e217e4aa14254ad73ad3c
|
||
EOF
|
||
ok "config/xuqm.env 已写入"
|
||
|
||
# config/tenant/bootstrap.env — 使用收集到的租户信息(迁移模式为占位符,后续由迁移步骤覆盖)
|
||
mkdir -p "$ROOT_DIR/config/tenant"
|
||
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<EOF
|
||
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||
TENANT_BOOTSTRAP_USERNAME=${DEPLOY_TENANT_USERNAME}
|
||
TENANT_BOOTSTRAP_PASSWORD=${_BOOTSTRAP_PASSWORD}
|
||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||
EOF
|
||
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
|
||
ok "config/tenant/bootstrap.env 已写入 (chmod 600)"
|
||
|
||
# (nginx 容器默认不启动,用户使用宿主机自己的 nginx 代理到各服务端口)
|
||
|
||
if false; then
|
||
# 以下为内置 nginx 配置示例,仅在 COMPOSE_PROFILES 包含 nginx-bundled 时使用
|
||
cat > /dev/null << NGINX_CONF
|
||
# =============================================================================
|
||
# 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
|
||
|
||
# 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
|
||
|
||
# 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 和文档站运行时配置
|
||
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,
|
||
"configVersion": "2026.05.19-private.1",
|
||
"deployment": "PRIVATE",
|
||
"appKey": "ak_private_default",
|
||
"controlBaseUrl": "${CONSOLE_BASE}",
|
||
"fileBaseUrl": "${CONSOLE_BASE}",
|
||
"imApiBaseUrl": "${CONSOLE_BASE}",
|
||
"imWsUrl": "${_WS_SCHEME}://${DEPLOY_HOST}/ws/im",
|
||
"features": {
|
||
"file": true,
|
||
"im": true,
|
||
"push": true,
|
||
"update": true,
|
||
"license": true
|
||
},
|
||
"connectTimeoutMs": 10000,
|
||
"readTimeoutMs": 30000,
|
||
"logLevel": "WARN"
|
||
}
|
||
EOF
|
||
|
||
cat > "$ROOT_DIR/config/docs/docs-runtime.json" <<EOF
|
||
{
|
||
"deployment": "PRIVATE",
|
||
"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 }
|
||
}
|
||
EOF
|
||
|
||
# 创建所有数据目录
|
||
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 环境"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 5 — 启动所有业务容器(全服务)
|
||
# ---------------------------------------------------------------------------
|
||
step "拉取镜像并启动所有业务容器(base + im + push + update + license)"
|
||
|
||
docker compose \
|
||
--env-file "$ROOT_DIR/.env" \
|
||
-f "$ROOT_DIR/docker-compose.yml" \
|
||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||
--profile base \
|
||
--profile infra-mysql --profile infra-redis \
|
||
--profile im --profile push --profile update --profile license \
|
||
up -d
|
||
|
||
# 等待 tenant-service 健康(直接打 9001 端口,不经过 nginx)
|
||
printf ' 等待 tenant-service 启动'
|
||
for i in $(seq 1 40); do
|
||
code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
|
||
"http://127.0.0.1:9001/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 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" \
|
||
ps --format 'table {{.Name}}\t{{.Status}}' 2>/dev/null || \
|
||
docker ps --format ' {{.Names}}\t{{.Status}}' | grep -E "xuqm|mysql|redis" || true
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 5b — Schema 扩展 + 系统应用创建(在迁移之前,Hibernate 已建表后执行)
|
||
# ---------------------------------------------------------------------------
|
||
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
|
||
|
||
# 触发器:先删再建,使用 --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" \
|
||
mysql -u "$MYSQL_USERNAME" -p"${MYSQL_PASSWORD}" "$MYSQL_DATABASE" \
|
||
-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" << 'SYSAPP_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
|
||
|
||
# 为 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"
|
||
|
||
else
|
||
# ── 迁移租户:调用公有平台 export API → 私有平台 import API ──
|
||
|
||
# 检查是否有现有租户数据
|
||
_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'
|
||
printf ' 迁移操作将 \033[1;31m删除所有现有租户信息\033[0m(账号、应用、功能配置)。\n\n'
|
||
read -rp " 确认删除并继续迁移?请输入 yes: " _confirm
|
||
[ "$_confirm" = "yes" ] || fail "操作已取消"
|
||
fi
|
||
|
||
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'
|
||
_IMPORT_RESP=$(printf '%s' "$_EXPORT_DATA" | curl -s --max-time 30 -X POST \
|
||
-H "Content-Type: application/json" \
|
||
--data-binary @- \
|
||
-w "\n__HTTP_STATUS__:%{http_code}" \
|
||
"http://127.0.0.1:9001/api/private/deployment/migrate/import")
|
||
_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
|
||
|
||
# 从导出数据中提取租户信息用于最终汇总
|
||
_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} 数据已成功导入"
|
||
|
||
# 为迁移的所有 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"
|
||
|
||
# 更新 bootstrap.env,防止重启时重新创建占位租户
|
||
cat > "$ROOT_DIR/config/tenant/bootstrap.env" <<BSENV
|
||
TENANT_BOOTSTRAP_EMAIL=${DEPLOY_TENANT_EMAIL}
|
||
TENANT_BOOTSTRAP_USERNAME=${DEPLOY_TENANT_USERNAME}
|
||
TENANT_BOOTSTRAP_PASSWORD=already-migrated-do-not-use
|
||
TENANT_BOOTSTRAP_APP_KEY=ak_private_default
|
||
BSENV
|
||
chmod 600 "$ROOT_DIR/config/tenant/bootstrap.env"
|
||
ok "bootstrap.env 已更新(迁移后)"
|
||
|
||
unset _MIGRATE_KEY _EXPORT_BODY _EXPORT_RESP _EXPORT_DATA _IMPORT_RESP _TENANT_INFO
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 7 — 一键验证(调用 verify.sh)
|
||
# ---------------------------------------------------------------------------
|
||
step "一键验证所有服务"
|
||
|
||
# 将 DEPLOY_HOST 注入到 verify.sh 中使用
|
||
export DEPLOY_HOST="${DEPLOY_HOST}"
|
||
|
||
if bash "$ROOT_DIR/scripts/verify.sh"; then
|
||
ok "全量验证通过"
|
||
else
|
||
printf '\n\033[33m 部分验证项未通过,请查看上方输出。\033[0m\n'
|
||
printf ' 可重新运行:DEPLOY_HOST=%s bash %s/scripts/verify.sh\n' "$DEPLOY_HOST" "$ROOT_DIR"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 最终汇总
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||
printf '\033[1;35m XuqmGroup 私有化部署完成\033[0m\n'
|
||
printf '\033[1;35m══════════════════════════════════════════════════\033[0m\n'
|
||
printf '\n \033[1m租户信息:\033[0m\n'
|
||
printf ' 邮箱: %s\n' "${DEPLOY_TENANT_EMAIL}"
|
||
printf ' 用户名: %s\n' "${DEPLOY_TENANT_USERNAME}"
|
||
if [ "$DEPLOY_MODE" = "new" ]; then
|
||
printf ' 密码: 部署时设置的密码\n'
|
||
else
|
||
printf ' 密码: 同生产平台密码(原样迁移,未重置)\n'
|
||
fi
|
||
printf '\n \033[1m容器端口(请在您的 nginx 中配置代理):\033[0m\n'
|
||
printf ' 控制台前端 127.0.0.1:8080 → 代理 /\n'
|
||
printf ' 运营后台 127.0.0.1:8081 → 代理 /ops\n'
|
||
printf ' 核心 API 127.0.0.1:9001 → 代理 /api/ /actuator/\n'
|
||
printf ' 文件服务 127.0.0.1:8086 → 代理 /file/ /api/file/\n'
|
||
printf ' IM 服务 127.0.0.1:8082 → 代理 /api/im/ /ws/im\n'
|
||
printf ' 版本管理 127.0.0.1:8084 → 代理 /api/v1/updates/ /api/v1/rn/\n'
|
||
printf ' License 服务 127.0.0.1:8085 → 代理 /api/license/\n'
|
||
printf ' 推送服务 127.0.0.1:8083 (厂商回调,按需代理)\n'
|
||
printf '\n \033[1mNginx 配置参考(复制到您的 nginx server 块):\033[0m\n'
|
||
printf '\033[0;37m'
|
||
cat <<'NGINX_REF'
|
||
charset utf-8;
|
||
client_max_body_size 100m;
|
||
|
||
location /api/v1/updates/ { proxy_pass http://127.0.0.1:8084/api/v1/updates/; }
|
||
location /api/v1/rn/ { proxy_pass http://127.0.0.1:8084/api/v1/rn/; }
|
||
location /api/im/ { proxy_pass http://127.0.0.1:8082/api/im/; }
|
||
location /ws/im {
|
||
proxy_pass http://127.0.0.1:8082/ws/im;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_read_timeout 3600s;
|
||
}
|
||
location /api/license/ { proxy_pass http://127.0.0.1:8085/api/license/; }
|
||
location /file/ {
|
||
proxy_pass http://127.0.0.1:8086/file/;
|
||
client_max_body_size 500m;
|
||
proxy_read_timeout 300s;
|
||
}
|
||
location /api/ { proxy_pass http://127.0.0.1:9001/api/; }
|
||
location /actuator/ { proxy_pass http://127.0.0.1:9001/actuator/; }
|
||
location /ops { proxy_pass http://127.0.0.1:8081; }
|
||
location / { proxy_pass http://127.0.0.1:8080; }
|
||
NGINX_REF
|
||
printf '\033[0m'
|
||
printf '\n \033[1m部署目录:\033[0m %s\n' "$ROOT_DIR"
|
||
printf ' \033[1m审计日志:\033[0m %s/logs/audit.log\n' "$ROOT_DIR"
|
||
printf '\n\033[1;32m 部署成功!配置好 nginx 后即可访问:%s\033[0m\n\n' "${CONSOLE_BASE}"
|