XuqmGroup-PrivateDeploy/scripts/migrate-tenant.sh
徐勤民 3fe5ae0807 fix(migrate-tenant): 补全 app_licenses 迁移,修复租户迁移后 license 404
migrate-tenant.sh 迁移 t_app 时未同步写入 license-service 的 app_licenses
表,导致前端 /api/license/admin/apps/:appKey 返回 404。

新增 Step 4c:为所有迁移的 app 以 INSERT ... ON DUPLICATE KEY UPDATE
方式创建 app_licenses 记录(max_devices=1000,永不过期),操作幂等,
不影响已存在的 license 配置。

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

478 行
21 KiB
Bash
可执行文件

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

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

#!/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;
-- 为系统应用启用三端 IMNOT 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=0schema 扩展已在上步添加列)
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 4c — 为迁移的 app 在 app_licenses 表补录 license 记录
# ---------------------------------------------------------------------------
printf '\n[3c/5] 同步 app_licenses 记录license-service 授权表)...\n'
if [ -n "$APP_KEYS_RAW" ]; then
_LICENSE_SQL="$(mktemp /tmp/xuqm-license-XXXXXX.sql)"
{
printf 'SET FOREIGN_KEY_CHECKS=0;\n'
# 为每个迁移的 app 创建 license 记录(若已存在则跳过,保证幂等)
printf "INSERT INTO app_licenses (app_key, name, max_devices, registered_devices, expires_at, is_active, remark, created_at, updated_at)\n"
printf "SELECT a.app_key, a.name, 1000, 0, NULL, 1,\n"
printf " CONCAT('迁移自 %s — ', NOW()), NOW(), NOW()\n" "$TENANT_NICKNAME"
printf "FROM t_app a\n"
printf "WHERE a.app_key IN (%s)\n" "$APP_KEYS_SQL"
printf "ON DUPLICATE KEY UPDATE updated_at = updated_at;\n"
printf 'SET FOREIGN_KEY_CHECKS=1;\n'
} > "$_LICENSE_SQL"
if dst_mysql < "$_LICENSE_SQL" 2>/dev/null; then
printf ' app_licenses 记录已创建(共 %s 个应用,max_devices=1000,永不过期\n' "$APP_COUNT"
else
warn "app_licenses 写入遇到警告,请手动检查"
fi
rm -f "$_LICENSE_SQL"
# 验证写入结果
_LICENSE_COUNT="$(dst_mysql -N -e "SELECT COUNT(*) FROM app_licenses WHERE app_key IN (${APP_KEYS_SQL})" 2>/dev/null || echo 0)"
printf ' DB check: %s/%s 个 app_licenses 记录就绪\n' "${_LICENSE_COUNT:-0}" "$APP_COUNT"
fi
audit "migrate-tenant" "LICENSE_SYNCED" "tenant=$TENANT_ID apps=$APP_COUNT"
# ---------------------------------------------------------------------------
# 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