530 行
19 KiB
Bash
530 行
19 KiB
Bash
#!/usr/bin/env bash
|
||
# verify.sh — 私有化部署一键验证脚本
|
||
#
|
||
# 用途:
|
||
# 部署完成后,全面验证所有服务是否正常运行,输出通过/失败清单。
|
||
# 可独立运行,也可作为 deploy.sh 的最终步骤。
|
||
#
|
||
# 使用方法:
|
||
# # 从部署目录读取配置自动推断
|
||
# ./scripts/verify.sh
|
||
#
|
||
# # 指定部署地址(可覆盖 .env 中的值)
|
||
# DEPLOY_HOST=192.168.1.100 ./scripts/verify.sh
|
||
#
|
||
# # 指定完整 URL
|
||
# BASE_URL=http://192.168.1.100 ./scripts/verify.sh
|
||
#
|
||
# 验证项目:
|
||
# 1. Docker 容器运行状态
|
||
# 2. 数据库和 Redis 连通性
|
||
# 3. tenant-service 健康(actuator/health)
|
||
# 4. PRIVATE 模式激活
|
||
# 5. 注册接口已阻断
|
||
# 6. 前端页面可访问
|
||
# 7. SDK 配置接口
|
||
# 8. IM 服务(如已启动)
|
||
# 9. 版本管理服务(如已启动)
|
||
# 10. 文档站(如已启动)
|
||
# 11. 租户登录和中文字符集
|
||
#
|
||
# 退出码:
|
||
# 0 = 全部通过
|
||
# 1 = 有项目失败
|
||
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$ROOT_DIR"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 配置读取
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# 优先使用环境变量,否则从 .env 读取
|
||
if [ -f "$ROOT_DIR/.env" ]; then
|
||
# shellcheck disable=SC1090
|
||
set -a; . "$ROOT_DIR/.env" 2>/dev/null; set +a
|
||
fi
|
||
if [ -f "$ROOT_DIR/config/secrets.env" ]; then
|
||
# shellcheck disable=SC1090
|
||
set -a; . "$ROOT_DIR/config/secrets.env" 2>/dev/null; set +a
|
||
fi
|
||
if [ -f "$ROOT_DIR/config/tenant/bootstrap.env" ]; then
|
||
# shellcheck disable=SC1090
|
||
set -a; . "$ROOT_DIR/config/tenant/bootstrap.env" 2>/dev/null; set +a
|
||
fi
|
||
|
||
DEPLOY_HOST="${DEPLOY_HOST:-127.0.0.1}"
|
||
BASE_URL="${BASE_URL:-http://${DEPLOY_HOST}}"
|
||
# 去掉末尾斜杠
|
||
BASE_URL="${BASE_URL%/}"
|
||
|
||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||
|
||
# 所有容器定位均限定在当前 Compose 项目。宿主机可能同时运行其他 MySQL、
|
||
# Redis 或 Xuqm 环境,按名称模糊匹配会误检、误执行数据库命令。
|
||
compose() {
|
||
docker compose --env-file "$ROOT_DIR/.env" \
|
||
-f "$ROOT_DIR/docker-compose.yml" \
|
||
-f "$ROOT_DIR/docker-compose.infra.yml" "$@"
|
||
}
|
||
|
||
container_id() {
|
||
compose ps -q "$1" 2>/dev/null | head -1 || true
|
||
}
|
||
|
||
container_name() {
|
||
local id
|
||
id="$(container_id "$1")"
|
||
[ -n "$id" ] && docker inspect --format '{{.Name}}' "$id" 2>/dev/null | sed 's|^/||'
|
||
}
|
||
|
||
profile_enabled() {
|
||
case ",${COMPOSE_PROFILES:-base}," in
|
||
*,"$1",*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 颜色和工具函数
|
||
# ---------------------------------------------------------------------------
|
||
|
||
RED='\033[1;31m'; GREEN='\033[1;32m'; YELLOW='\033[1;33m'
|
||
CYAN='\033[1;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||
|
||
PASS=0; FAIL=0; WARN=0
|
||
RESULTS=() # 收集结果用于最终报告
|
||
|
||
pass() {
|
||
local msg="$1"
|
||
printf " ${GREEN}✓${RESET} PASS %s\n" "$msg"
|
||
PASS=$((PASS+1))
|
||
RESULTS+=("PASS|$msg")
|
||
}
|
||
|
||
fail() {
|
||
local msg="$1" detail="${2:-}"
|
||
printf " ${RED}✗${RESET} FAIL %s" "$msg"
|
||
[ -n "$detail" ] && printf " ${YELLOW}(%s)${RESET}" "$detail"
|
||
printf "\n"
|
||
FAIL=$((FAIL+1))
|
||
RESULTS+=("FAIL|$msg")
|
||
}
|
||
|
||
warn() {
|
||
local msg="$1" detail="${2:-}"
|
||
printf " ${YELLOW}⚠${RESET} WARN %s" "$msg"
|
||
[ -n "$detail" ] && printf " (%s)" "$detail"
|
||
printf "\n"
|
||
WARN=$((WARN+1))
|
||
RESULTS+=("WARN|$msg")
|
||
}
|
||
|
||
section() {
|
||
printf "\n${CYAN}── %s ${RESET}\n" "$1"
|
||
}
|
||
|
||
# HTTP 请求(自动跳过代理,直连内网)
|
||
http_get() {
|
||
local url="$1" timeout="${2:-8}"
|
||
curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$timeout" "$url" 2>/dev/null || echo 000
|
||
}
|
||
|
||
http_body() {
|
||
local url="$1" timeout="${2:-8}"
|
||
curl -sL --noproxy '*' --max-time "$timeout" "$url" 2>/dev/null || true
|
||
}
|
||
|
||
http_headers() {
|
||
local url="$1" timeout="${2:-8}"
|
||
curl -sS --noproxy '*' -D - -o /dev/null --max-time "$timeout" "$url" 2>/dev/null || true
|
||
}
|
||
|
||
http_body_auth() {
|
||
local url="$1" token="$2" timeout="${3:-8}"
|
||
curl -sL --noproxy '*' --max-time "$timeout" \
|
||
-H "Authorization: Bearer $token" "$url" 2>/dev/null || true
|
||
}
|
||
|
||
http_code_auth() {
|
||
local url="$1" token="$2" timeout="${3:-8}"
|
||
curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$timeout" \
|
||
-H "Authorization: Bearer $token" "$url" 2>/dev/null || echo 000
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 开始验证
|
||
# ---------------------------------------------------------------------------
|
||
|
||
printf "\n${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||
printf "${BOLD} XuqmGroup 私有化部署验证${RESET}\n"
|
||
printf "${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||
printf " 部署地址: ${BOLD}%s${RESET}\n" "$BASE_URL"
|
||
printf " 部署目录: %s\n" "$ROOT_DIR"
|
||
printf " 验证时间: %s\n" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. Docker 容器状态
|
||
# ---------------------------------------------------------------------------
|
||
section "1. 容器状态"
|
||
|
||
REQUIRED_CONTAINERS="tenant-service file-service nginx tenant-web"
|
||
for CTR_SUFFIX in $REQUIRED_CONTAINERS; do
|
||
CTR_NAME="$(container_name "$CTR_SUFFIX")"
|
||
if [ -n "$CTR_NAME" ]; then
|
||
pass "容器运行中: $CTR_NAME"
|
||
else
|
||
fail "当前部署容器未运行: $CTR_SUFFIX"
|
||
fi
|
||
done
|
||
|
||
OPTIONAL_SERVICES="im:im-service push:push-service update:update-service bugcollect:bugcollect-service bugcollect:symbolicator infra-mysql:mysql infra-redis:redis"
|
||
for PROFILE_SERVICE in $OPTIONAL_SERVICES; do
|
||
PROFILE="${PROFILE_SERVICE%%:*}"
|
||
CTR_SUFFIX="${PROFILE_SERVICE#*:}"
|
||
CTR_NAME="$(container_name "$CTR_SUFFIX")"
|
||
if [ -n "$CTR_NAME" ]; then
|
||
pass "容器运行中: $CTR_NAME"
|
||
elif profile_enabled "$PROFILE"; then
|
||
fail "已启用 profile 的容器未运行: $CTR_SUFFIX"
|
||
else
|
||
warn "容器未启用(profile: $PROFILE): $CTR_SUFFIX"
|
||
fi
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 数据库和 Redis
|
||
# ---------------------------------------------------------------------------
|
||
section "2. 数据库和缓存"
|
||
|
||
# MySQL
|
||
MYSQL_CTR="$(container_id mysql)"
|
||
if [ -n "$MYSQL_CTR" ]; then
|
||
if docker exec "$MYSQL_CTR" mysql -u root -p"${MYSQL_ROOT_PASSWORD:-}" \
|
||
--connect-timeout=5 -N -e 'SELECT 1' 2>/dev/null | grep -q '^1$'; then
|
||
pass "MySQL 查询响应正常"
|
||
else
|
||
fail "当前部署 MySQL 查询失败"
|
||
fi
|
||
else
|
||
warn "MySQL 容器未运行(external 模式跳过此项)"
|
||
fi
|
||
|
||
# Redis
|
||
REDIS_CTR="$(container_id redis)"
|
||
if [ -n "$REDIS_CTR" ]; then
|
||
PING=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning PING 2>/dev/null | tr -d '\r' || true)
|
||
if [ "$PING" = "PONG" ]; then
|
||
pass "Redis 响应 PONG"
|
||
else
|
||
fail "Redis ping 失败 (got: $PING)"
|
||
fi
|
||
else
|
||
warn "Redis 容器未运行(external 模式跳过此项)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. 核心服务健康
|
||
# ---------------------------------------------------------------------------
|
||
section "3. 核心服务"
|
||
|
||
# actuator/health
|
||
HEALTH_BODY=""
|
||
HEALTH_STATUS=""
|
||
for _attempt in 1 2 3 4 5; do
|
||
HEALTH_BODY=$(http_body "$BASE_URL/actuator/health")
|
||
HEALTH_STATUS=$(echo "$HEALTH_BODY" | grep -o '"status":"[^"]*"' | head -1 || true)
|
||
[ "$HEALTH_STATUS" = '"status":"UP"' ] && break
|
||
sleep 2
|
||
done
|
||
if [ "$HEALTH_STATUS" = '"status":"UP"' ]; then
|
||
pass "tenant-service actuator/health = UP"
|
||
else
|
||
fail "tenant-service 未就绪" "$(echo "$HEALTH_BODY" | head -c 100)"
|
||
fi
|
||
|
||
# 私有化模式
|
||
DEPLOY_STATUS=$(http_body "$BASE_URL/api/private/deployment/status")
|
||
if echo "$DEPLOY_STATUS" | grep -q '"mode":"PRIVATE"'; then
|
||
pass "PRIVATE 模式已激活"
|
||
else
|
||
fail "PRIVATE 模式未激活" "$(echo "$DEPLOY_STATUS" | head -c 100)"
|
||
fi
|
||
|
||
if echo "$DEPLOY_STATUS" | grep -q '"tenantRegisterEnabled":false'; then
|
||
pass "租户注册已禁用"
|
||
else
|
||
fail "租户注册未禁用(安全风险)"
|
||
fi
|
||
|
||
# 注册接口阻断
|
||
REG_CODE=$(curl -sL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 8 \
|
||
-X POST "$BASE_URL/api/auth/register" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"email":"verify-test@block.test","username":"verifytest","password":"Test@123456"}' 2>/dev/null || echo 000)
|
||
if [ "$REG_CODE" != "200" ] && [ "$REG_CODE" != "000" ]; then
|
||
pass "注册接口已阻断 (HTTP $REG_CODE)"
|
||
else
|
||
fail "注册接口未阻断 (HTTP $REG_CODE)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. 前端页面
|
||
# ---------------------------------------------------------------------------
|
||
section "4. 前端页面"
|
||
|
||
WEB_CODE=$(http_get "$BASE_URL/")
|
||
if [ "$WEB_CODE" = "200" ]; then
|
||
pass "控制台前端可访问 (HTTP 200)"
|
||
else
|
||
fail "控制台前端不可访问 (HTTP $WEB_CODE)"
|
||
fi
|
||
|
||
WEB_CACHE="$(http_headers "$BASE_URL/" | tr -d '\r' | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')"
|
||
if printf '%s' "$WEB_CACHE" | grep -qi 'no-store'; then
|
||
pass "入口 HTML 禁止持久缓存"
|
||
else
|
||
fail "入口 HTML 缺少 no-store" "${WEB_CACHE:-无 Cache-Control}"
|
||
fi
|
||
|
||
WEB_HTML="$(http_body "$BASE_URL/")"
|
||
WEB_ASSET="$(printf '%s' "$WEB_HTML" | grep -oE '/releases/[^/]+/assets/[^"[:space:]]+\.(js|css)' | head -1 || true)"
|
||
if [ -n "$WEB_ASSET" ]; then
|
||
ASSET_HEADERS="$(http_headers "$BASE_URL$WEB_ASSET" | tr -d '\r')"
|
||
ASSET_CODE="$(printf '%s' "$ASSET_HEADERS" | awk '/^HTTP/{code=$2} END{print code}')"
|
||
ASSET_CACHE="$(printf '%s' "$ASSET_HEADERS" | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')"
|
||
if [ "$ASSET_CODE" = "200" ] && printf '%s' "$ASSET_CACHE" | grep -qi 'immutable'; then
|
||
pass "版本化哈希资源可访问且 immutable"
|
||
else
|
||
fail "版本化资源缓存契约异常" "HTTP ${ASSET_CODE:-000}, ${ASSET_CACHE:-无 Cache-Control}"
|
||
fi
|
||
else
|
||
fail "入口 HTML 未引用版本化 releases assets"
|
||
fi
|
||
|
||
DOCS_CODE=$(http_get "$BASE_URL/docs/")
|
||
if echo "$DOCS_CODE" | grep -qE '^(200|301|302)$'; then
|
||
pass "文档站可访问 (HTTP $DOCS_CODE)"
|
||
else
|
||
warn "文档站不可访问 (HTTP $DOCS_CODE)(可选服务)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. 租户登录和中文字符集
|
||
# ---------------------------------------------------------------------------
|
||
section "5. 租户登录"
|
||
|
||
# 获取验证码
|
||
CAPTCHA_RESP=$(http_body "$BASE_URL/api/auth/captcha")
|
||
CAPTCHA_KEY=$(echo "$CAPTCHA_RESP" | grep -o '"key":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
||
|
||
LOGIN_OK=false
|
||
TOKEN=""
|
||
NICK=""
|
||
|
||
if [ -n "$CAPTCHA_KEY" ]; then
|
||
# 从 Redis 读取验证码
|
||
REDIS_CTR="$(container_id redis)"
|
||
if [ -n "$REDIS_CTR" ]; then
|
||
CAPTCHA_CODE=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning \
|
||
GET "captcha:${CAPTCHA_KEY}" 2>/dev/null | tr -d '\r' || true)
|
||
|
||
if [ -n "$CAPTCHA_CODE" ]; then
|
||
# 尝试登录(用 bootstrap 邮箱)
|
||
BOOTSTRAP_EMAIL="${TENANT_BOOTSTRAP_EMAIL:-admin@company.com}"
|
||
BOOTSTRAP_PASS="${TENANT_BOOTSTRAP_PASSWORD:-}"
|
||
|
||
LOGIN_RESP=$(curl -sL --noproxy '*' --max-time 10 \
|
||
-X POST "$BASE_URL/api/auth/login" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"account\":\"${BOOTSTRAP_EMAIL}\",\"password\":\"${BOOTSTRAP_PASS}\",\"captchaKey\":\"${CAPTCHA_KEY}\",\"captchaCode\":\"${CAPTCHA_CODE}\"}" 2>/dev/null || true)
|
||
|
||
LOGIN_CODE=$(echo "$LOGIN_RESP" | grep -o '"code":[0-9]*' | head -1 | grep -o '[0-9]*' || true)
|
||
if [ "$LOGIN_CODE" = "200" ]; then
|
||
TOKEN=$(echo "$LOGIN_RESP" | grep -o '"token":"[^"]*"' | cut -d'"' -f4 || true)
|
||
LOGIN_OK=true
|
||
pass "租户登录成功"
|
||
elif [ "$BOOTSTRAP_PASS" = "change-me-on-first-login" ] || [ -z "$BOOTSTRAP_PASS" ]; then
|
||
# bootstrap 密码是占位符,说明已迁移生产数据,用生产密码登录,此处跳过
|
||
warn "租户登录跳过(bootstrap 密码为占位符,迁移后请用生产密码验证)"
|
||
|
||
# 解码 JWT 检查中文字符集
|
||
if command -v python3 >/dev/null 2>&1 && [ -n "$TOKEN" ]; then
|
||
NICK=$(python3 -c "
|
||
import base64, json, sys
|
||
try:
|
||
payload = '${TOKEN}'.split('.')[1]
|
||
payload += '=' * (4 - len(payload) % 4)
|
||
d = json.loads(base64.urlsafe_b64decode(payload).decode('utf-8'))
|
||
print(d.get('nickname',''))
|
||
except:
|
||
print('')
|
||
" 2>/dev/null)
|
||
if [ -n "$NICK" ] && echo "$NICK" | python3 -c "import sys; s=sys.stdin.read().strip(); sys.exit(0 if s == s.encode('utf-8').decode('utf-8') and any(ord(c) > 0x7F for c in s) else 0)" 2>/dev/null; then
|
||
pass "JWT 中文字段正常: $NICK"
|
||
elif [ -n "$NICK" ]; then
|
||
pass "JWT 昵称字段: $NICK"
|
||
else
|
||
warn "JWT 昵称字段为空(bootstrap 租户可能未迁移)"
|
||
fi
|
||
fi
|
||
else
|
||
fail "租户登录失败" "code=$LOGIN_CODE (resp: $(echo "$LOGIN_RESP" | head -c 80))"
|
||
fi
|
||
else
|
||
warn "无法从 Redis 读取验证码(跳过登录测试)"
|
||
fi
|
||
else
|
||
warn "Redis 容器未运行(跳过登录测试)"
|
||
fi
|
||
else
|
||
fail "无法获取验证码(captcha API 异常)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. SDK 配置接口
|
||
# ---------------------------------------------------------------------------
|
||
section "6. SDK 配置"
|
||
|
||
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
|
||
DEFAULT_APP_KEY=$(python3 -c "
|
||
import json
|
||
try:
|
||
d = json.load(open('$ROOT_DIR/config/sdk/xuqm-private-sdk.json'))
|
||
print(d.get('appKey',''))
|
||
except:
|
||
print('')
|
||
" 2>/dev/null)
|
||
fi
|
||
|
||
# 从数据库查询已有 App
|
||
MYSQL_CTR="$(container_id mysql)"
|
||
APP_KEYS=""
|
||
if [ -n "$MYSQL_CTR" ]; then
|
||
APP_KEYS=$(docker exec "$MYSQL_CTR" sh -c \
|
||
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
|
||
-e 'SELECT app_key FROM t_app LIMIT 5;'" 2>/dev/null | tr '\n' ' ' || true)
|
||
fi
|
||
|
||
if [ -z "$APP_KEYS" ] && [ -n "${DEFAULT_APP_KEY:-}" ]; then
|
||
APP_KEYS="$DEFAULT_APP_KEY"
|
||
fi
|
||
|
||
if [ -n "$APP_KEYS" ]; then
|
||
for APP_KEY in $APP_KEYS; do
|
||
# packageName 为必填参数;fresh 部署无已注册包名时返回 403(正常)
|
||
SDK_CODE=$(http_get "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID&packageName=com.xuqm.verify")
|
||
SDK_BODY=$(http_body "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID&packageName=com.xuqm.verify")
|
||
if [ "$SDK_CODE" = "200" ] && echo "$SDK_BODY" | grep -q '"code":200'; then
|
||
pass "SDK config: $APP_KEY"
|
||
elif echo "$SDK_CODE" | grep -qE '^(403|404)$'; then
|
||
# 403 = 包名未注册(fresh 部署正常);404 = appKey 不存在
|
||
warn "SDK config: $APP_KEY (HTTP $SDK_CODE,包名未注册或 App 未配置 — 部署后正常)"
|
||
else
|
||
fail "SDK config: $APP_KEY (HTTP $SDK_CODE)"
|
||
fi
|
||
done
|
||
else
|
||
warn "未找到 App Key(数据库可能为空或尚未迁移)"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 7. 可选服务
|
||
# ---------------------------------------------------------------------------
|
||
section "7. 可选服务(需认证,401 = 正常)"
|
||
|
||
check_optional_service() {
|
||
local label="$1" url="$2" token="$3"
|
||
local code
|
||
if [ -n "$token" ]; then
|
||
code=$(http_code_auth "$url" "$token")
|
||
else
|
||
code=$(http_get "$url")
|
||
fi
|
||
if echo "$code" | grep -qE '^(200|201|400|401|403|404)$'; then
|
||
pass "$label 响应正常 (HTTP $code)"
|
||
elif [ "$code" = "000" ]; then
|
||
warn "$label 无法连接(未部署或未启动)"
|
||
else
|
||
fail "$label 返回错误 (HTTP $code)"
|
||
fi
|
||
}
|
||
|
||
# 应用级接口必须使用当前登录租户真正拥有的 App。直接从全库挑第一个 App 会把
|
||
# 正确的租户隔离误报为 500/403,也无法证明当前租户的业务链路可用。
|
||
_IM_APP_KEY=""
|
||
if [ -n "$TOKEN" ]; then
|
||
_OWNED_APPS="$(http_body_auth "$BASE_URL/api/apps" "$TOKEN")"
|
||
_IM_APP_KEY="$(printf '%s' "$_OWNED_APPS" |
|
||
grep -o '"appKey":"[^"]*"' | head -1 | cut -d'"' -f4 || true)"
|
||
fi
|
||
if [ -z "$_IM_APP_KEY" ] && [ "$LOGIN_OK" != "true" ] && [ -n "$MYSQL_CTR" ]; then
|
||
_IM_APP_KEY=$(docker exec "$MYSQL_CTR" sh -c \
|
||
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
|
||
-e \"SELECT app_key FROM t_app WHERE is_default=1 LIMIT 1;\"" 2>/dev/null | tr -d '\r\n' || true)
|
||
fi
|
||
|
||
if [ -n "$_IM_APP_KEY" ]; then
|
||
check_optional_service "IM 服务" \
|
||
"$BASE_URL/api/im/platform-events/token?appKey=${_IM_APP_KEY}" "$TOKEN"
|
||
|
||
check_optional_service "版本管理 (update)" \
|
||
"$BASE_URL/api/v1/updates/app/list?appKey=${_IM_APP_KEY}&platform=ANDROID" "$TOKEN"
|
||
|
||
check_optional_service "RN 热更新 (rn)" \
|
||
"$BASE_URL/api/v1/rn/list?appKey=${_IM_APP_KEY}" "$TOKEN"
|
||
else
|
||
warn "当前租户没有应用,跳过 IM/update/RN 的应用级接口验证"
|
||
fi
|
||
|
||
# 文件服务
|
||
FILE_CODE=$(http_get "$BASE_URL/file/")
|
||
if echo "$FILE_CODE" | grep -qE '^(200|400|401|403|404)$'; then
|
||
pass "文件服务响应正常 (HTTP $FILE_CODE)"
|
||
else
|
||
warn "文件服务响应: HTTP $FILE_CODE"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 最终汇总
|
||
# ---------------------------------------------------------------------------
|
||
|
||
printf "\n${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||
printf "${BOLD} 验证结果${RESET}\n"
|
||
printf "${BOLD}════════════════════════════════════════════════════${RESET}\n"
|
||
printf " ${GREEN}PASS${RESET}: %d 项\n" "$PASS"
|
||
printf " ${YELLOW}WARN${RESET}: %d 项(预期降级,不影响功能)\n" "$WARN"
|
||
printf " ${RED}FAIL${RESET}: %d 项\n" "$FAIL"
|
||
|
||
if [ "$FAIL" -eq 0 ]; then
|
||
printf "\n ${GREEN}${BOLD}✓ 验证通过!所有必选服务运行正常。${RESET}\n"
|
||
[ "$WARN" -gt 0 ] && printf " ${YELLOW}(%d 项检查为预期降级,所有服务均已全量启动)${RESET}\n" "$WARN"
|
||
else
|
||
printf "\n ${RED}${BOLD}✗ %d 项验证失败,请检查容器日志:${RESET}\n" "$FAIL"
|
||
printf " docker compose logs --tail 50 tenant-service\n"
|
||
fi
|
||
|
||
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$BASE_URL"
|
||
printf " 文档站 :${BOLD}%s/docs/${RESET}\n" "$BASE_URL"
|
||
printf "\n"
|
||
|
||
# 输出结果到 JSON
|
||
RESULT_JSON="$ROOT_DIR/.deploy-state/last-verify.json"
|
||
{
|
||
printf '{"timestamp":"%s","base_url":"%s","pass":%d,"warn":%d,"fail":%d,"items":[' \
|
||
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BASE_URL" "$PASS" "$WARN" "$FAIL"
|
||
first=true
|
||
for r in "${RESULTS[@]}"; do
|
||
STATUS="${r%%|*}"; MSG="${r##*|}"
|
||
$first || printf ','
|
||
printf '{"status":"%s","message":"%s"}' "$STATUS" "$MSG"
|
||
first=false
|
||
done
|
||
printf ']}\n'
|
||
} > "$RESULT_JSON" 2>/dev/null || true
|
||
|
||
[ "$FAIL" -eq 0 ]
|