- 新增 install.sh:curl 一键下载依赖安装 + 自动解压部署包 + 启动部署向导 - deploy-szyx.sh:移除硬编码租户常量,改为交互式选择(新建/迁移) - 新建租户:收集邮箱/用户名/密码,bcrypt 写入 bootstrap.env - 迁移租户:提示输入生产 MySQL 配置,bcrypt 验证主账号后执行迁移 - 已有数据时迁移前显示红色警告要求 yes 确认 - 移除 docs-site 独立容器(文档已内置于 tenant-web/docs/) - nginx 和 docker-compose 同步清理 docs-site 残留配置 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
445 行
20 KiB
Bash
可执行文件
445 行
20 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
||
# migrate-tenant.sh — Migrate a tenant from the public platform to this private deployment.
|
||
#
|
||
# What it does:
|
||
# 1. Connects to the source (public) MySQL and locates the tenant by email or username.
|
||
# 2. Exports t_tenant, t_app, and t_feature_service rows for that tenant.
|
||
# 3. Replaces the bootstrap tenant in the private deployment's MySQL with the migrated data.
|
||
# 4. Restarts tenant-service to flush any in-memory state.
|
||
# 5. Verifies the migration via the SDK config API.
|
||
#
|
||
# Usage:
|
||
# ./scripts/migrate-tenant.sh [options]
|
||
#
|
||
# Required options:
|
||
# --src-host HOST Source MySQL host (production server)
|
||
# --src-user USER Source MySQL username
|
||
# --src-password PASS Source MySQL password
|
||
# --tenant EMAIL|USERNAME Tenant to migrate (matched against email or username)
|
||
#
|
||
# Optional options:
|
||
# --src-port PORT Source MySQL port (default: 3306)
|
||
# --src-db DB Source database name (default: xuqm_tenant)
|
||
# --dry-run Print generated SQL but do not apply it
|
||
# --reset-password PASS Override tenant password after migration (bcrypt-hashed on server)
|
||
# -h, --help Show this help
|
||
#
|
||
# The source MySQL client is resolved in order: docker exec (managed container),
|
||
# then a local mysql binary. At least one must be available.
|
||
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
. "$ROOT_DIR/scripts/lib.sh"
|
||
load_env
|
||
|
||
SRC_HOST=""
|
||
SRC_PORT="3306"
|
||
SRC_USER=""
|
||
SRC_PASSWORD=""
|
||
SRC_DB="xuqm_tenant"
|
||
TENANT_IDENT=""
|
||
DRY_RUN=false
|
||
RESET_PASSWORD=""
|
||
|
||
usage() {
|
||
awk '/^# Usage:/,/^[^#]/' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
|
||
}
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--src-host) SRC_HOST="${2:-}"; shift 2 ;;
|
||
--src-host=*) SRC_HOST="${1#--src-host=}"; shift ;;
|
||
--src-port) SRC_PORT="${2:-}"; shift 2 ;;
|
||
--src-port=*) SRC_PORT="${1#--src-port=}"; shift ;;
|
||
--src-user) SRC_USER="${2:-}"; shift 2 ;;
|
||
--src-user=*) SRC_USER="${1#--src-user=}"; shift ;;
|
||
--src-password) SRC_PASSWORD="${2:-}"; shift 2 ;;
|
||
--src-password=*) SRC_PASSWORD="${1#--src-password=}"; shift ;;
|
||
--src-db) SRC_DB="${2:-}"; shift 2 ;;
|
||
--src-db=*) SRC_DB="${1#--src-db=}"; shift ;;
|
||
--tenant) TENANT_IDENT="${2:-}"; shift 2 ;;
|
||
--tenant=*) TENANT_IDENT="${1#--tenant=}"; shift ;;
|
||
--dry-run) DRY_RUN=true; shift ;;
|
||
--reset-password) RESET_PASSWORD="${2:-}"; shift 2 ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) fail_json "XUQM_PRIVATE_5001" "unknown option: $1" "migrate-tenant" ;;
|
||
esac
|
||
done
|
||
|
||
[ -n "$SRC_HOST" ] || fail_json "XUQM_PRIVATE_5002" "--src-host is required" "migrate-tenant"
|
||
[ -n "$SRC_USER" ] || fail_json "XUQM_PRIVATE_5003" "--src-user is required" "migrate-tenant"
|
||
[ -n "$SRC_PASSWORD" ] || fail_json "XUQM_PRIVATE_5004" "--src-password is required" "migrate-tenant"
|
||
[ -n "$TENANT_IDENT" ] || fail_json "XUQM_PRIVATE_5005" "--tenant is required" "migrate-tenant"
|
||
|
||
audit "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT dry_run=$DRY_RUN"
|
||
progress "migrate-tenant" "STARTED" "src=$SRC_HOST tenant=$TENANT_IDENT"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Resolve source and destination MySQL clients
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Source: always a remote host, so require a local mysql binary
|
||
require_cmd mysql
|
||
|
||
src_mysql() {
|
||
MYSQL_PWD="$SRC_PASSWORD" mysql \
|
||
-h "$SRC_HOST" -P "$SRC_PORT" -u "$SRC_USER" \
|
||
--default-character-set=utf8mb4 --connect-timeout=10 \
|
||
"$SRC_DB" "$@"
|
||
}
|
||
|
||
# Destination: prefer managed container, fall back to local binary
|
||
DST_MYSQL_CTR="$(compose ps -q mysql 2>/dev/null | head -1 || true)"
|
||
|
||
dst_mysql() {
|
||
if [ -n "$DST_MYSQL_CTR" ]; then
|
||
docker exec -i "$DST_MYSQL_CTR" \
|
||
mysql -u "${MYSQL_USERNAME:-xuqm}" -p"${MYSQL_PASSWORD:-}" \
|
||
--default-character-set=utf8mb4 \
|
||
"${MYSQL_DATABASE:-xuqm_private}" "$@"
|
||
elif command -v mysql >/dev/null 2>&1; then
|
||
MYSQL_PWD="${MYSQL_PASSWORD:-}" mysql \
|
||
-h "${MYSQL_HOST:-127.0.0.1}" -P "${MYSQL_PORT:-3306}" \
|
||
-u "${MYSQL_USERNAME:-xuqm}" \
|
||
--default-character-set=utf8mb4 \
|
||
"${MYSQL_DATABASE:-xuqm_private}" "$@"
|
||
else
|
||
fail_json "XUQM_PRIVATE_5006" "no destination MySQL client: container not running and mysql binary not found" "migrate-tenant"
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 1 — Locate the tenant in the source database
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[1/5] Locating tenant "%s" in %s/%s ...\n' "$TENANT_IDENT" "$SRC_HOST" "$SRC_DB"
|
||
|
||
TENANT_ROW="$(src_mysql -N -e "
|
||
SELECT id, username, nickname, email, status
|
||
FROM t_tenant
|
||
WHERE (email = '${TENANT_IDENT}' OR username = '${TENANT_IDENT}')
|
||
AND type = 'MAIN'
|
||
LIMIT 1" 2>/dev/null || true)"
|
||
|
||
[ -n "$TENANT_ROW" ] || \
|
||
fail_json "XUQM_PRIVATE_5007" "tenant not found in source DB: $TENANT_IDENT" "migrate-tenant"
|
||
|
||
TENANT_ID="$(printf '%s' "$TENANT_ROW" | awk '{print $1}')"
|
||
TENANT_USERNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $2}')"
|
||
TENANT_NICKNAME="$(printf '%s' "$TENANT_ROW" | awk '{print $3}')"
|
||
TENANT_EMAIL="$(printf '%s' "$TENANT_ROW" | awk '{print $4}')"
|
||
TENANT_STATUS="$(printf '%s' "$TENANT_ROW" | awk '{print $5}')"
|
||
|
||
printf ' Found: id=%s username=%s nickname=%s email=%s status=%s\n' \
|
||
"$TENANT_ID" "$TENANT_USERNAME" "$TENANT_NICKNAME" "$TENANT_EMAIL" "$TENANT_STATUS"
|
||
|
||
# Count associated data
|
||
APP_COUNT="$(src_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)"
|
||
FS_COUNT="$(src_mysql -N -e "
|
||
SELECT COUNT(*) FROM t_feature_service
|
||
WHERE app_key IN (SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}')" 2>/dev/null || echo 0)"
|
||
|
||
printf ' Apps: %s FeatureServices: %s\n' "$APP_COUNT" "$FS_COUNT"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 2 — Collect app keys for this tenant
|
||
# ---------------------------------------------------------------------------
|
||
APP_KEYS_RAW="$(src_mysql -N -e "SELECT app_key FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || true)"
|
||
|
||
if [ -z "$APP_KEYS_RAW" ]; then
|
||
printf ' WARNING: tenant has no apps; only tenant record will be migrated.\n'
|
||
APP_KEYS_SQL="''"
|
||
else
|
||
# Build SQL IN-list: 'key1','key2',...
|
||
APP_KEYS_SQL="$(printf '%s' "$APP_KEYS_RAW" | awk '{printf "'"'"'%s'"'"',",$1}' | sed 's/,$//')"
|
||
fi
|
||
|
||
FIRST_APP_KEY="$(printf '%s' "$APP_KEYS_RAW" | head -1 || true)"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 3 — Generate migration SQL
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[2/5] Generating migration SQL ...\n'
|
||
|
||
SQL_FILE="$(mktemp /tmp/xuqm-migrate-XXXXXX.sql)"
|
||
trap 'rm -f "$SQL_FILE"' EXIT
|
||
|
||
{
|
||
printf -- '-- XuqmGroup private deployment tenant migration\n'
|
||
printf -- '-- Source: %s/%s Tenant: %s (%s)\n' "$SRC_HOST" "$SRC_DB" "$TENANT_NICKNAME" "$TENANT_ID"
|
||
printf -- '-- Generated: %s\n\n' "$(now)"
|
||
|
||
printf 'SET FOREIGN_KEY_CHECKS=0;\n\n'
|
||
|
||
# Clear existing data (private deployment is single-tenant)
|
||
printf -- '-- Clear existing tenant data\n'
|
||
printf 'DELETE FROM t_feature_service;\n'
|
||
printf 'DELETE FROM t_app;\n'
|
||
printf 'DELETE FROM t_tenant;\n\n'
|
||
|
||
# t_tenant — use explicit column names to be schema-order agnostic
|
||
printf -- '-- Tenant record\n'
|
||
src_mysql -N -e "
|
||
SELECT CONCAT(
|
||
'INSERT INTO t_tenant (id,created_at,email,nickname,parent_id,password,phone,status,type,username) VALUES (',
|
||
QUOTE(id),',',QUOTE(created_at),',',QUOTE(email),',',QUOTE(nickname),',',
|
||
IFNULL(QUOTE(parent_id),'NULL'),',',QUOTE(password),',',
|
||
IFNULL(QUOTE(phone),'NULL'),',',QUOTE(status),',',QUOTE(type),',',QUOTE(username),');'
|
||
) FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null
|
||
|
||
printf '\n'
|
||
|
||
# t_app — explicit columns matching private schema (alphabetical: id,app_key,...,tenant_id)
|
||
if [ "$APP_COUNT" -gt 0 ]; then
|
||
printf -- '-- Apps\n'
|
||
src_mysql -N -e "
|
||
SELECT CONCAT(
|
||
'INSERT INTO t_app (id,app_key,app_secret,created_at,description,harmony_bundle_name,icon_url,ios_bundle_id,name,package_name,tenant_id) VALUES (',
|
||
QUOTE(id),',',QUOTE(app_key),',',QUOTE(app_secret),',',QUOTE(created_at),',',
|
||
IFNULL(QUOTE(description),'NULL'),',',IFNULL(QUOTE(harmony_bundle_name),'NULL'),',',
|
||
IFNULL(QUOTE(icon_url),'NULL'),',',IFNULL(QUOTE(ios_bundle_id),'NULL'),',',
|
||
QUOTE(name),',',QUOTE(package_name),',',QUOTE(tenant_id),');'
|
||
) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null
|
||
printf '\n'
|
||
fi
|
||
|
||
# t_feature_service — explicit columns matching private schema (alphabetical: id,app_key,config,...)
|
||
if [ "$FS_COUNT" -gt 0 ]; then
|
||
printf -- '-- Feature services\n'
|
||
src_mysql -N -e "
|
||
SELECT CONCAT(
|
||
'INSERT INTO t_feature_service (id,app_key,config,created_at,enabled,platform,secret_key,service_type) VALUES (',
|
||
QUOTE(id),',',QUOTE(app_key),',',IFNULL(QUOTE(config),'NULL'),',',QUOTE(created_at),',',
|
||
CAST(enabled AS UNSIGNED),',',QUOTE(platform),',',QUOTE(secret_key),',',QUOTE(service_type),');'
|
||
) FROM t_feature_service WHERE app_key IN (${APP_KEYS_SQL})" 2>/dev/null
|
||
printf '\n'
|
||
fi
|
||
|
||
# Optional password reset via bcrypt (requires htpasswd or python3 available on the deployment host)
|
||
if [ -n "$RESET_PASSWORD" ]; then
|
||
printf -- '-- Password reset\n'
|
||
if command -v python3 >/dev/null 2>&1 && python3 -c "import bcrypt" 2>/dev/null; then
|
||
BCRYPT_HASH="$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${RESET_PASSWORD}', bcrypt.gensalt(rounds=10)).decode())")"
|
||
elif command -v htpasswd >/dev/null 2>&1; then
|
||
BCRYPT_HASH="$(htpasswd -bnBC 10 '' "${RESET_PASSWORD}" | tr -d ':\n' | sed 's/\$2y/\$2a/')"
|
||
else
|
||
printf -- '-- WARNING: cannot generate bcrypt hash (install python3-bcrypt or apache2-utils)\n'
|
||
BCRYPT_HASH=""
|
||
fi
|
||
if [ -n "$BCRYPT_HASH" ]; then
|
||
printf "UPDATE t_tenant SET password='%s' WHERE id='%s';\n" "$BCRYPT_HASH" "$TENANT_ID"
|
||
fi
|
||
fi
|
||
|
||
printf 'SET FOREIGN_KEY_CHECKS=1;\n'
|
||
printf -- '-- Migration SQL complete (%s apps, %s feature services)\n' "$APP_COUNT" "$FS_COUNT"
|
||
} > "$SQL_FILE"
|
||
|
||
LINE_COUNT="$(wc -l < "$SQL_FILE")"
|
||
printf ' Generated %s lines of SQL\n' "$LINE_COUNT"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 4 — Apply (or dry-run)
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$DRY_RUN" = "true" ]; then
|
||
printf '\n[DRY RUN] SQL that would be applied:\n'
|
||
printf '%s\n' "---"
|
||
cat "$SQL_FILE"
|
||
printf '%s\n' "---"
|
||
printf '\nDry run complete. Re-run without --dry-run to apply.\n'
|
||
audit "migrate-tenant" "DRY_RUN" "tenant=$TENANT_ID apps=$APP_COUNT"
|
||
exit 0
|
||
fi
|
||
|
||
printf '\n[3/5] Applying migration to private deployment ...\n'
|
||
dst_mysql < "$SQL_FILE"
|
||
audit "migrate-tenant" "SQL_APPLIED" "tenant=$TENANT_ID apps=$APP_COUNT fs=$FS_COUNT"
|
||
printf ' Done.\n'
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 4b — Schema 扩展:is_default / deletable 列 + 删除保护触发器
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[3b/5] 应用 Schema 扩展(is_default / deletable / 删除保护触发器)...\n'
|
||
|
||
_SCHEMA_EXT_FILE="$(mktemp /tmp/xuqm-schema-ext-XXXXXX.sql)"
|
||
|
||
# 用 information_schema 检查列是否存在,再决定是否 ALTER TABLE(兼容所有 MySQL 8.x)
|
||
for _col in is_default deletable; do
|
||
_exists="$(dst_mysql -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
|
||
_default_val="$( [ "$_col" = "is_default" ] && echo 0 || echo 1 )"
|
||
dst_mysql -e "ALTER TABLE t_app ADD COLUMN ${_col} BIT(1) NOT NULL DEFAULT ${_default_val}" 2>/dev/null \
|
||
&& printf ' 列 %s 已添加\n' "$_col" \
|
||
|| warn "列 ${_col} 添加失败,继续"
|
||
else
|
||
printf ' 列 %s 已存在,跳过\n' "$_col"
|
||
fi
|
||
done
|
||
|
||
# 触发器:用 --delimiter='$$' 创建(MySQL 批量模式不支持 DELIMITER 语句)
|
||
dst_mysql -e "DROP TRIGGER IF EXISTS prevent_default_app_delete" 2>/dev/null
|
||
cat > "$_SCHEMA_EXT_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 dst_mysql --delimiter='$$' < "$_SCHEMA_EXT_FILE" 2>/dev/null; then
|
||
printf ' 删除保护触发器已创建\n'
|
||
else
|
||
warn "触发器创建遇到警告,继续"
|
||
fi
|
||
rm -f "$_SCHEMA_EXT_FILE"
|
||
|
||
# 创建系统 IM 应用(固定 app_key,私有化服务间通信专用)
|
||
# 与公有化平台的 ak_409e217e4aa14254ad73ad3c 保持相同 key,services 通过 SYSTEM_APP_KEY 读取
|
||
_SYS_APP_KEY="ak_409e217e4aa14254ad73ad3c"
|
||
_SYS_APP_EXISTS="$(dst_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE app_key='${_SYS_APP_KEY}'" 2>/dev/null || echo 0)"
|
||
|
||
if [ "${_SYS_APP_EXISTS:-0}" -eq 0 ]; then
|
||
_SYS_SQL="$(mktemp /tmp/xuqm-sysapp-XXXXXX.sql)"
|
||
cat > "$_SYS_SQL" << SYSAPP_SQL
|
||
-- 系统 IM 应用:置于迁移后的唯一租户下
|
||
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;
|
||
|
||
-- 为系统应用启用三端 IM(NOT EXISTS 保证幂等)
|
||
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');
|
||
SYSAPP_SQL
|
||
if dst_mysql < "$_SYS_SQL" 2>/dev/null; then
|
||
printf ' 系统应用 %s 已创建(IM 三端已启用)\n' "$_SYS_APP_KEY"
|
||
else
|
||
warn "系统应用创建遇到警告,继续"
|
||
fi
|
||
rm -f "$_SYS_SQL"
|
||
else
|
||
printf ' 系统应用 %s 已存在,跳过创建\n' "$_SYS_APP_KEY"
|
||
fi
|
||
|
||
# 标记系统应用为 is_default=1 / deletable=0(schema 扩展已在上步添加列)
|
||
dst_mysql -e "UPDATE t_app SET is_default=1, deletable=0 WHERE app_key='${_SYS_APP_KEY}'" 2>/dev/null
|
||
printf ' %s → is_default=1 / deletable=0,DB 触发器保护已激活\n' "$_SYS_APP_KEY"
|
||
|
||
# 将 SYSTEM_APP_KEY 追加到 xuqm.env(若未配置)
|
||
_XUQM_ENV_FILE="$ROOT_DIR/config/xuqm.env"
|
||
if [ -f "$_XUQM_ENV_FILE" ] && ! grep -q "^SYSTEM_APP_KEY=" "$_XUQM_ENV_FILE"; then
|
||
printf '\n# 系统 IM 通信应用 key(私有化服务间消息通知使用此 app_key 连接 IM 服务)\nSYSTEM_APP_KEY=%s\n' \
|
||
"$_SYS_APP_KEY" >> "$_XUQM_ENV_FILE"
|
||
printf ' SYSTEM_APP_KEY=%s 已追加到 config/xuqm.env\n' "$_SYS_APP_KEY"
|
||
fi
|
||
audit "migrate-tenant" "SYSTEM_APP_CREATED" "app_key=$_SYS_APP_KEY tenant=$TENANT_ID"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 5 — Restart tenant-service to flush any cached state
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[4/5] Restarting tenant-service ...\n'
|
||
TENANT_SVC_CTR="$(compose ps -q tenant-service 2>/dev/null | head -1 || true)"
|
||
if [ -n "$TENANT_SVC_CTR" ]; then
|
||
compose restart tenant-service
|
||
# Wait up to 30 s for the service to come back
|
||
WAITED=0
|
||
while [ "$WAITED" -lt 30 ]; do
|
||
sleep 3
|
||
WAITED=$((WAITED + 3))
|
||
HTTP_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 3 \
|
||
"http://localhost:${NGINX_PORT:-80}/actuator/health" 2>/dev/null || echo '000')"
|
||
[ "$HTTP_CODE" = "200" ] && break
|
||
done
|
||
[ "$HTTP_CODE" = "200" ] || printf ' WARNING: actuator/health not 200 after restart (HTTP %s); continuing.\n' "$HTTP_CODE"
|
||
printf ' tenant-service restarted.\n'
|
||
else
|
||
printf ' WARNING: tenant-service container not found; skipping restart.\n'
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 6 — Verify
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[5/5] Verifying migration ...\n'
|
||
|
||
BASE_URL="http://localhost:${NGINX_PORT:-80}"
|
||
[ -n "${CONSOLE_DOMAIN:-}" ] && BASE_URL="${CONSOLE_DOMAIN}"
|
||
|
||
# Check tenant record
|
||
DST_TENANT="$(dst_mysql -N -e "
|
||
SELECT id, username, nickname, email FROM t_tenant WHERE id='${TENANT_ID}'" 2>/dev/null || true)"
|
||
|
||
if [ -n "$DST_TENANT" ]; then
|
||
printf ' DB check: PASS tenant record present (%s)\n' "$TENANT_NICKNAME"
|
||
else
|
||
fail_json "XUQM_PRIVATE_5008" "tenant record not found in destination after migration" "migrate-tenant"
|
||
fi
|
||
|
||
DST_APPS="$(dst_mysql -N -e "SELECT COUNT(*) FROM t_app WHERE tenant_id='${TENANT_ID}'" 2>/dev/null || echo 0)"
|
||
printf ' DB check: PASS %s app(s) migrated\n' "$DST_APPS"
|
||
|
||
# Check SDK config endpoint for the first migrated app
|
||
if [ -n "$FIRST_APP_KEY" ] && command -v curl >/dev/null 2>&1; then
|
||
SDK_CODE="$(curl -skL -o /dev/null -w '%{http_code}' --max-time 5 \
|
||
"${BASE_URL}/api/sdk/config?appKey=${FIRST_APP_KEY}&platform=ANDROID" 2>/dev/null || echo '000')"
|
||
if [ "$SDK_CODE" = "200" ]; then
|
||
printf ' API check: PASS /api/sdk/config returned 200 for %s\n' "$FIRST_APP_KEY"
|
||
else
|
||
printf ' API check: WARN /api/sdk/config returned %s for %s (service may still be warming up)\n' \
|
||
"$SDK_CODE" "$FIRST_APP_KEY"
|
||
fi
|
||
fi
|
||
|
||
# Confirm deployment status
|
||
if command -v curl >/dev/null 2>&1; then
|
||
DEPLOY_STATUS="$(curl -skL --max-time 5 "${BASE_URL}/api/private/deployment/status" 2>/dev/null || true)"
|
||
if printf '%s' "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then
|
||
REG_ENABLED="$(printf '%s' "$DEPLOY_STATUS" | grep -o '"tenantRegisterEnabled":[^,}]*' | cut -d: -f2)"
|
||
printf ' API check: PASS PRIVATE mode active, tenantRegisterEnabled=%s\n' "$REG_ENABLED"
|
||
else
|
||
printf ' API check: WARN deployment/status did not confirm PRIVATE mode\n'
|
||
fi
|
||
fi
|
||
|
||
printf '\nMigration complete.\n'
|
||
printf ' Tenant: %s (%s)\n' "$TENANT_NICKNAME" "$TENANT_EMAIL"
|
||
printf ' Apps: %s\n' "$DST_APPS"
|
||
[ -n "$RESET_PASSWORD" ] && printf ' Password: reset to provided value\n'
|
||
printf ' Login URL: %s\n' "${BASE_URL:-http://localhost}"
|
||
|
||
audit "migrate-tenant" "DONE" "tenant=$TENANT_ID apps=$DST_APPS"
|
||
progress "migrate-tenant" "DONE" "tenant=$TENANT_NICKNAME email=$TENANT_EMAIL apps=$DST_APPS"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Step 6 — 全量验证(调用 verify.sh)
|
||
# ---------------------------------------------------------------------------
|
||
printf '\n[6/5] 运行全量验证脚本 ...\n'
|
||
VERIFY_SCRIPT="$ROOT_DIR/scripts/verify.sh"
|
||
if [ -f "$VERIFY_SCRIPT" ]; then
|
||
if bash "$VERIFY_SCRIPT"; then
|
||
printf ' 全量验证通过。\n'
|
||
else
|
||
printf ' 部分验证项未通过,请查看上方输出。\n'
|
||
printf ' 可重新运行:bash %s\n' "$VERIFY_SCRIPT"
|
||
fi
|
||
else
|
||
printf ' verify.sh 未找到,跳过全量验证。\n'
|
||
fi
|