- Rename deploy-szyx.sh → deploy.sh, remove all customer-specific branding - Migrate mode: prompt for pmk_ key, call public platform export API, pipe to private import API — no MySQL credentials needed - Remove bcrypt dependency (no longer used in script logic) - Update install.sh and verify.sh references Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
445 行
16 KiB
Bash
445 行
16 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
|
||
|
||
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:-}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 颜色和工具函数
|
||
# ---------------------------------------------------------------------------
|
||
|
||
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_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 nginx tenant-web ops-web"
|
||
for CTR_SUFFIX in $REQUIRED_CONTAINERS; do
|
||
CTR_NAME=$(docker ps --filter "name=$CTR_SUFFIX" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
if [ -n "$CTR_NAME" ]; then
|
||
pass "容器运行中: $CTR_NAME"
|
||
else
|
||
fail "容器未运行: *-${CTR_SUFFIX}-*"
|
||
fi
|
||
done
|
||
|
||
OPTIONAL_CONTAINERS="file-service im-service push-service update-service license-service docs-site mysql redis"
|
||
for CTR_SUFFIX in $OPTIONAL_CONTAINERS; do
|
||
CTR_NAME=$(docker ps --filter "name=$CTR_SUFFIX" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
if [ -n "$CTR_NAME" ]; then
|
||
pass "容器运行中: $CTR_NAME"
|
||
else
|
||
warn "容器未运行(可选): *-${CTR_SUFFIX}-*"
|
||
fi
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 数据库和 Redis
|
||
# ---------------------------------------------------------------------------
|
||
section "2. 数据库和缓存"
|
||
|
||
# MySQL
|
||
MYSQL_CTR=$(docker ps --filter "name=mysql" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
if [ -n "$MYSQL_CTR" ]; then
|
||
if docker exec "$MYSQL_CTR" mysqladmin -u root -p"${MYSQL_ROOT_PASSWORD:-}" ping --silent 2>/dev/null; then
|
||
pass "MySQL 响应 PONG"
|
||
else
|
||
fail "MySQL ping 失败"
|
||
fi
|
||
else
|
||
warn "MySQL 容器未运行(external 模式跳过此项)"
|
||
fi
|
||
|
||
# Redis
|
||
REDIS_CTR=$(docker ps --filter "name=redis" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
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')
|
||
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=$(http_body "$BASE_URL/actuator/health")
|
||
HEALTH_STATUS=$(echo "$HEALTH_BODY" | grep -o '"status":"[^"]*"' | head -1)
|
||
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
|
||
|
||
OPS_CODE=$(http_get "$BASE_URL/ops")
|
||
if echo "$OPS_CODE" | grep -qE '^(200|301|302)$'; then
|
||
pass "运营后台可访问 (HTTP $OPS_CODE)"
|
||
else
|
||
fail "运营后台不可访问 (HTTP $OPS_CODE)"
|
||
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)
|
||
|
||
LOGIN_OK=false
|
||
TOKEN=""
|
||
NICK=""
|
||
|
||
if [ -n "$CAPTCHA_KEY" ]; then
|
||
# 从 Redis 读取验证码
|
||
REDIS_CTR=$(docker ps --filter "name=redis" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
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')
|
||
|
||
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]*')
|
||
if [ "$LOGIN_CODE" = "200" ]; then
|
||
TOKEN=$(echo "$LOGIN_RESP" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
||
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=$(docker ps --filter "name=mysql" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
||
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' ' ')
|
||
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
|
||
SDK_CODE=$(http_get "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID")
|
||
SDK_BODY=$(http_body "$BASE_URL/api/sdk/config?appKey=${APP_KEY}&platform=ANDROID")
|
||
if [ "$SDK_CODE" = "200" ] && echo "$SDK_BODY" | grep -q '"code":200'; then
|
||
pass "SDK config: $APP_KEY"
|
||
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
|
||
}
|
||
|
||
check_optional_service "IM 服务" \
|
||
"$BASE_URL/api/im/platform-events/token?appKey=test" "$TOKEN"
|
||
|
||
check_optional_service "版本管理 (update)" \
|
||
"$BASE_URL/api/v1/updates/app/list?appKey=test&platform=ANDROID" "$TOKEN"
|
||
|
||
check_optional_service "RN 热更新 (rn)" \
|
||
"$BASE_URL/api/v1/rn/list?appKey=test" "$TOKEN"
|
||
|
||
# 文件服务
|
||
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/ops${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 ]
|