Files
ctms/scripts/install-ctms.sh
T
2026-06-22 15:35:00 +08:00

647 lines
23 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────
# CTMS 一键安装脚本
# ─────────────────────────────────────────────
TARGET_ENV=""
BASE_URL=""
ASSUME_YES=0
SKIP_BUILD=0
SKIP_MIGRATE=0
VERBOSE=0
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"
LOG_DIR="$ROOT_DIR/.install-logs"
# ── 颜色 & 样式 ──────────────────────────────
_has_color() { [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null)" -ge 8 ]]; }
if _has_color; then
C_RESET="$(tput sgr0)"
C_BOLD="$(tput bold)"
C_DIM="$(tput dim 2>/dev/null || printf '')"
C_BLUE="$(tput setaf 4)"
C_CYAN="$(tput setaf 6)"
C_GREEN="$(tput setaf 2)"
C_YELLOW="$(tput setaf 3)"
C_RED="$(tput setaf 1)"
C_WHITE="$(tput setaf 7)"
else
C_RESET="" C_BOLD="" C_DIM="" C_BLUE="" C_CYAN=""
C_GREEN="" C_YELLOW="" C_RED="" C_WHITE=""
fi
# ── 输出函数 ─────────────────────────────────
_banner() {
printf '\n'
printf '%s╔══════════════════════════════════════════════╗%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
printf '%s║%s ██████╗████████╗███╗ ███╗███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s ██╔════╝╚══██╔══╝████╗ ████║██╔════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s ██║ ██║ ██╔████╔██║███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s ██║ ██║ ██║╚██╔╝██║╚════██║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s ╚██████╗ ██║ ██║ ╚═╝ ██║███████║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
printf '%s║%s 临床试验管理系统 安装程序 %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_DIM}${C_WHITE}" "${C_BLUE}" "${C_RESET}"
printf '%s╚══════════════════════════════════════════════╝%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
printf '\n'
}
log() { printf ' %s·%s %s\n' "${C_DIM}" "${C_RESET}" "$*"; }
ok() { printf ' %s✔%s %s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$*"; }
warn() { printf ' %s⚠%s %s\n' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$*" >&2; }
step() {
local idx="${_STEP_IDX:-0}"
_STEP_IDX=$(( idx + 1 ))
printf '\n%s┌─ 步骤 %s · %s%s\n' \
"${C_BLUE}${C_BOLD}" "$_STEP_IDX" "$*" "${C_RESET}"
}
fail() {
printf '\n %s✖ 安装中止%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" >&2
printf ' %s%s%s\n\n' "${C_RED}" "$*" "${C_RESET}" >&2
exit 1
}
run() {
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
"$@"
}
compose_cmd() {
if [[ "$TARGET_ENV" == "dev" ]]; then
docker compose -f "$ROOT_DIR/docker-compose.dev.yaml" "$@"
else
docker compose "$@"
fi
}
# 运行长命令,默认折叠输出。失败或 --verbose 时回放完整日志。
# 用法: run_quiet <提示语> -- <命令...>
run_quiet() {
local label="$1"; shift
[[ "${1:-}" == "--" ]] && shift
if [[ "$VERBOSE" -eq 1 ]]; then
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
"$@"
return $?
fi
mkdir -p "$LOG_DIR"
local log_file
log_file="$(mktemp "$LOG_DIR/cmd_XXXXXXXX")"
local start_ts elapsed exit_code
start_ts="$(date +%s)"
set +e
if [[ -t 1 ]]; then
"$@" >"$log_file" 2>&1 &
local cmd_pid=$!
_spinner_for "$cmd_pid" "$label" "$start_ts" &
local spin_pid=$!
wait "$cmd_pid"
exit_code=$?
kill "$spin_pid" 2>/dev/null || true
wait "$spin_pid" 2>/dev/null || true
printf '\r\033[K'
else
printf ' · %s …\n' "$label"
"$@" >"$log_file" 2>&1
exit_code=$?
fi
set -e
elapsed=$(( $(date +%s) - start_ts ))
if [[ "$exit_code" -eq 0 ]]; then
printf ' %s✔%s %s %s(%ss)%s\n' \
"${C_GREEN}${C_BOLD}" "${C_RESET}" "$label" "${C_DIM}" "$elapsed" "${C_RESET}"
return 0
fi
printf '\n %s✖%s %s 失败(耗时 %ss),完整输出:\n\n' \
"${C_RED}${C_BOLD}" "${C_RESET}" "$label" "$elapsed" >&2
sed 's/^/ /' "$log_file" >&2
printf '\n' >&2
fail "命令执行失败,详细日志保留于:${log_file#$ROOT_DIR/}(追加 --verbose 可看实时输出)"
}
_spinner_for() {
local cmd_pid="$1" label="$2" start_ts="$3"
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$cmd_pid" 2>/dev/null; do
local elapsed=$(( $(date +%s) - start_ts ))
printf '\r\033[K %s%s%s %s %s(%ss)%s' \
"${C_CYAN}${C_BOLD}" "${frames[i]}" "${C_RESET}" \
"$label" "${C_DIM}" "$elapsed" "${C_RESET}"
i=$(( (i + 1) % ${#frames[@]} ))
sleep 0.1
done
}
# ── 帮助 ─────────────────────────────────────
usage() {
cat <<EOF
${C_BOLD}用法:${C_RESET}
bash scripts/install-ctms.sh ${C_CYAN}<环境>${C_RESET} [选项]
${C_BOLD}环境:${C_RESET}
${C_CYAN}dev${C_RESET} 本地开发环境(使用默认密钥,不生成 RSA 密钥对)
前端通过 Vite dev server 热加载,入口仍为 http://127.0.0.1:8888
${C_CYAN}main${C_RESET} 内网测试 / 预发布环境
${C_CYAN}release${C_RESET} 生产环境(强制 HTTPS,自动生成密钥对)
${C_BOLD}选项:${C_RESET}
${C_WHITE}--base-url <url>${C_RESET} 健康检查使用的访问地址
dev/main 默认 http://127.0.0.1:8888
release 必须通过此参数或交互输入提供
${C_WHITE}--yes${C_RESET} 跳过所有交互确认,适合 CI/CD 自动化执行
${C_WHITE}--skip-build${C_RESET} 跳过镜像构建,仍会启动容器(docker compose up -d
${C_WHITE}--skip-migrate${C_RESET} 跳过数据库迁移(alembic upgrade head
${C_WHITE}--verbose${C_RESET} 展开所有命令的实时输出(默认折叠为 spinner)
${C_WHITE}-h, --help${C_RESET} 显示此帮助信息
${C_BOLD}示例:${C_RESET}
bash scripts/install-ctms.sh dev
bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888
bash scripts/install-ctms.sh release --base-url https://ctms.example.com --yes
EOF
}
# ── 参数解析 ─────────────────────────────────
parse_args() {
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
TARGET_ENV="$1"
shift
case "$TARGET_ENV" in
dev|main|release) ;;
-h|--help) usage; exit 0 ;;
*)
usage
fail "未知环境 \"$TARGET_ENV\",有效值为 dev、main 或 release"
;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--base-url)
[[ $# -ge 2 ]] || fail "--base-url 需要提供一个 URL 参数"
BASE_URL="$2"
shift 2
;;
--yes) ASSUME_YES=1; shift ;;
--skip-build) SKIP_BUILD=1; shift ;;
--skip-migrate) SKIP_MIGRATE=1; shift ;;
--verbose) VERBOSE=1; shift ;;
-h|--help) usage; exit 0 ;;
*)
usage
fail "未知参数: $1"
;;
esac
done
}
# ── 依赖检查 ─────────────────────────────────
_require_command() {
local cmd="$1"
local hint="$2"
if ! command -v "$cmd" >/dev/null 2>&1; then
warn "缺少依赖: ${C_BOLD}${cmd}${C_RESET}"
warn "安装方式: $hint"
return 1
fi
}
check_dependencies() {
step "检查系统依赖"
local missing=0
_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
_require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1
[[ "$missing" -eq 0 ]] || fail "系统依赖不完整,请按上方提示安装后重试"
if ! docker compose version >/dev/null 2>&1; then
fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行"
fi
ok "系统依赖检查通过"
}
# ── 环境参数计算 ──────────────────────────────
compose_project_name() {
case "$TARGET_ENV" in
dev) printf 'ctms_dev' ;;
main) printf 'ctms_main' ;;
release) printf 'ctms_release' ;;
esac
}
app_env() {
[[ "$TARGET_ENV" == "dev" ]] && printf 'development' || printf 'production'
}
key_id() {
local today
today="$(date +%Y%m%d)"
case "$TARGET_ENV" in
dev) printf 'default' ;;
main) printf 'main-%s' "$today" ;;
release) printf 'release-%s' "$today" ;;
esac
}
default_base_url() {
case "$TARGET_ENV" in
dev|main) printf 'http://127.0.0.1:8888' ;;
release) printf '' ;;
esac
}
# ── .env 读写 ─────────────────────────────────
read_env_value() {
local key="$1"
[[ -f "$ENV_FILE" ]] || return 0
awk -v key="$key" '
BEGIN { prefix = key "=" }
index($0, prefix) == 1 {
value = substr($0, length(prefix) + 1)
if (value ~ /^".*"$/ || value ~ /^\047.*\047$/) {
value = substr(value, 2, length(value) - 2)
}
print value
exit
}
' "$ENV_FILE"
}
resolve_effective_config() {
local desired_project_name="$1"
local desired_runtime_env="$2"
local desired_login_key_id="$3"
EFFECTIVE_PROJECT_NAME="$desired_project_name"
EFFECTIVE_RUNTIME_ENV="$desired_runtime_env"
EFFECTIVE_LOGIN_KEY_ID="$desired_login_key_id"
[[ -f "$ENV_FILE" ]] || return 0
local env_project_name env_runtime_env env_login_key_id
env_project_name="$(read_env_value COMPOSE_PROJECT_NAME || true)"
env_runtime_env="$(read_env_value ENV || true)"
env_login_key_id="$(read_env_value LOGIN_RSA_KEY_ID || true)"
[[ -n "$env_project_name" ]] || fail ".env 缺少 COMPOSE_PROJECT_NAME,请补齐后重试"
[[ -n "$env_runtime_env" ]] || fail ".env 缺少 ENV,请补齐后重试"
[[ -n "$env_login_key_id" ]] || fail ".env 缺少 LOGIN_RSA_KEY_ID,请补齐后重试"
if [[ "$env_project_name" != "$desired_project_name" ]]; then
fail ".env 的 COMPOSE_PROJECT_NAME=$env_project_name 与目标环境 $TARGET_ENV 期望值 $desired_project_name 不一致"
fi
if [[ "$env_runtime_env" != "$desired_runtime_env" ]]; then
fail ".env 的 ENV=$env_runtime_env 与目标环境 $TARGET_ENV 期望值 $desired_runtime_env 不一致"
fi
EFFECTIVE_PROJECT_NAME="$env_project_name"
EFFECTIVE_RUNTIME_ENV="$env_runtime_env"
EFFECTIVE_LOGIN_KEY_ID="$env_login_key_id"
}
generate_escaped_private_key() {
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \
| awk '{printf "%s\\n", $0}'
}
generate_jwt_secret() {
openssl rand -hex 32
}
# ── 地址解析 ─────────────────────────────────
resolve_base_url() {
[[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)"
if [[ "$TARGET_ENV" == "release" && -z "$BASE_URL" && "$ASSUME_YES" -eq 0 ]]; then
printf ' %s▸%s 请输入 release 健康检查域名(例如 https://ctms.example.com: ' \
"${C_YELLOW}${C_BOLD}" "${C_RESET}"
read -r BASE_URL
fi
[[ -n "$BASE_URL" ]] || fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址"
if [[ "$TARGET_ENV" == "release" && "$BASE_URL" != https://* ]]; then
fail "release 环境的访问地址必须使用 HTTPS"
fi
}
# ── 安装确认 ─────────────────────────────────
confirm_install() {
local project_name="$1"
local runtime_env="$2"
step "确认安装参数"
local env_status build_status migrate_status
env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在' || printf '新建')"
build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
printf '\n'
printf ' %s%-18s%s %s\n' "${C_BOLD}" "目标环境" "${C_RESET}" "$TARGET_ENV"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "运行模式" "${C_RESET}" "$runtime_env"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL"
printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在' || printf '新建')"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status"
printf '\n'
if [[ "$ASSUME_YES" -eq 1 ]]; then
log "已启用 --yes,跳过确认"
return
fi
printf ' %s▸%s 确认以上参数并继续安装?输入 %syes%s 继续: ' \
"${C_YELLOW}${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
local answer
read -r answer
[[ "$answer" == "yes" ]] || fail "用户取消安装"
}
# ── .env 写入 ─────────────────────────────────
write_env_file() {
local project_name="$1" runtime_env="$2" login_key_id="$3"
local jwt_secret="$4" rsa_private_key="$5"
local temp_file
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
{
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
printf 'ENV=%s\n' "$runtime_env"
printf 'VITE_RUNTIME_ENV=%s\n' "$runtime_env"
printf 'VITE_ALLOW_INSECURE_DEV_LOGIN=%s\n' "$([[ "$runtime_env" == "development" ]] && printf 'true' || printf 'false')"
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
} >> "$temp_file"
mv "$temp_file" "$ENV_FILE"
ok ".env 写入完成"
}
upsert_env_value() {
local key="$1" value="$2"
local temp_file
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
if [[ -f "$ENV_FILE" && -n "$(read_env_value "$key" || true)" ]]; then
awk -v key="$key" -v value="$value" '
BEGIN { prefix = key "=" }
index($0, prefix) == 1 { print key "=" value; next }
{ print }
' "$ENV_FILE" > "$temp_file"
else
[[ -f "$ENV_FILE" ]] && cat "$ENV_FILE" > "$temp_file"
printf '%s=%s\n' "$key" "$value" >> "$temp_file"
fi
mv "$temp_file" "$ENV_FILE"
}
sync_frontend_build_env() {
local runtime_env="$1"
local allow_insecure_dev_login="false"
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
upsert_env_value VITE_RUNTIME_ENV "$runtime_env"
upsert_env_value VITE_ALLOW_INSECURE_DEV_LOGIN "$allow_insecure_dev_login"
ok "前端构建环境变量已同步"
}
prepare_env_file() {
step "准备环境配置文件"
local project_name="$1" runtime_env="$2" login_key_id="$3"
local jwt_secret="" rsa_private_key=""
if [[ -f "$ENV_FILE" ]]; then
ok "检测到现有 .env,保留原文件并跳过写入"
return
fi
if [[ "$TARGET_ENV" == "dev" ]]; then
jwt_secret="dev-secret"
rsa_private_key=""
else
jwt_secret="$(generate_jwt_secret)"
ok "已生成新的 JWT_SECRET_KEY"
log "正在生成 RSA 2048 密钥对,请稍候…"
rsa_private_key="$(generate_escaped_private_key)"
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
fi
write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
}
# ── Docker 流程 ───────────────────────────────
run_compose_config() {
step "校验 Docker Compose 配置"
run_quiet "Docker Compose 配置语法校验" -- compose_cmd config
}
run_backend_init() {
[[ "$TARGET_ENV" != "dev" ]] || return 0
step "初始化数据库与管理员账号"
run_quiet "运行 backend-init 容器" -- compose_cmd run --rm backend-init
}
run_build_and_start() {
if [[ "$SKIP_BUILD" -eq 1 ]]; then
step "启动服务(跳过镜像构建)"
if [[ "$TARGET_ENV" == "dev" ]]; then
run_quiet "启动开发容器并刷新后端/Nginx/Vite" -- compose_cmd up -d --force-recreate backend nginx frontend-dev
else
run_quiet "启动容器" -- compose_cmd up -d
fi
else
step "构建镜像并启动服务"
if [[ "$TARGET_ENV" == "dev" ]]; then
run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev
else
run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build
fi
fi
}
run_migrations() {
if [[ "$SKIP_MIGRATE" -eq 1 ]]; then
log "已跳过数据库迁移(--skip-migrate"
return
fi
step "执行数据库迁移"
run_quiet "Alembic upgrade head" -- compose_cmd run --rm backend python -m alembic upgrade head
}
# ── 健康检查 ─────────────────────────────────
check_container_status() {
step "检查容器运行状态"
local running
running="$(compose_cmd ps --services --filter status=running 2>/dev/null || true)"
local services=(db backend nginx)
[[ "$TARGET_ENV" != "dev" ]] || services+=(frontend-dev)
for svc in "${services[@]}"; do
if printf '%s\n' "$running" | grep -qx "$svc"; then
printf ' %s✔%s %-8s 运行中\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$svc"
else
printf ' %s✖%s %-8s %s未运行%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" "$svc" "${C_RED}" "${C_RESET}"
fail "$svc 容器未正常运行,请执行 'bash scripts/install-ctms.sh $TARGET_ENV --skip-build --skip-migrate --verbose' 或对应 compose ps 命令排查"
fi
done
}
check_backend_environment() {
step "校验后端运行时环境变量"
local expected_env="$1" expected_key_id="$2" expect_rsa="$3"
local check_code
check_code=$(cat <<'PY'
import os, sys
from app.core.config import settings
from app.core.login_crypto import _normalize_pem
expected_env = os.environ["EXPECTED_ENV"]
expected_key_id = os.environ["EXPECTED_KEY_ID"]
expect_rsa = os.environ["EXPECT_RSA"] == "1"
if settings.ENV != expected_env:
sys.exit(f"ENV 不匹配: {settings.ENV} != {expected_env}")
if settings.LOGIN_RSA_KEY_ID != expected_key_id:
sys.exit("LOGIN_RSA_KEY_ID 不匹配")
if expected_env == "production" and settings.JWT_SECRET_KEY == "dev-secret":
sys.exit("生产环境不允许使用默认 JWT_SECRET_KEY")
if expect_rsa:
if not settings.LOGIN_RSA_PRIVATE_KEY:
sys.exit("LOGIN_RSA_PRIVATE_KEY 未配置")
from cryptography.hazmat.primitives import serialization
serialization.load_pem_private_key(
_normalize_pem(settings.LOGIN_RSA_PRIVATE_KEY).encode("utf-8"),
password=None,
)
PY
)
run_quiet "执行后端配置自检" -- compose_cmd exec -T \
-e EXPECTED_ENV="$expected_env" \
-e EXPECTED_KEY_ID="$expected_key_id" \
-e EXPECT_RSA="$expect_rsa" \
backend python -c "$check_code"
}
check_http_endpoint() {
local path="$1"
local max_attempts="${2:-10}"
local url="${BASE_URL%/}$path"
log "探测接口: $url"
local attempt=1 delay=2 warmup_delay=3
log "等待服务预热 ${warmup_delay}s …"
sleep "$warmup_delay"
while [[ "$attempt" -le "$max_attempts" ]]; do
if curl -fsS "$url" >/dev/null 2>&1; then
ok "接口就绪: $path"
return 0
fi
if [[ "$attempt" -lt "$max_attempts" ]]; then
log "接口尚未就绪,${delay}s 后重试(${attempt}/${max_attempts})…"
sleep "$delay"
fi
attempt=$(( attempt + 1 ))
done
if [[ "$path" == "/api/v1/auth/login-key" ]]; then
warn "登录公钥接口无响应,请检查 .env 中 LOGIN_RSA_PRIVATE_KEY 是否为完整的单行转义 PEM 文本"
fi
fail "接口探测失败: $url"
}
run_health_checks() {
local runtime_env="$1" login_key_id="$2"
local expect_rsa=0
[[ "$runtime_env" == "production" ]] && expect_rsa=1
check_container_status
check_dev_nginx_mode
check_backend_environment "$runtime_env" "$login_key_id" "$expect_rsa"
step "探测 HTTP 接口可用性"
check_http_endpoint "/health"
check_http_endpoint "/api/v1/auth/login-key"
if [[ "$TARGET_ENV" == "dev" ]]; then
check_http_endpoint "/" 60
fi
ok "HTTP 接口全部就绪"
}
check_dev_nginx_mode() {
[[ "$TARGET_ENV" == "dev" ]] || return 0
step "校验开发 Nginx 代理配置"
if compose_cmd exec -T nginx sh -c 'grep -q "frontend_dev" /etc/nginx/nginx.conf && grep -q "proxy_pass http://frontend_dev" /etc/nginx/nginx.conf' >/dev/null 2>&1; then
ok "Nginx 已代理到 Vite dev server"
return
fi
fail "当前 Nginx 未加载开发配置,请重试:bash scripts/install-ctms.sh dev --skip-migrate --verbose"
}
# ── 完成提示 ─────────────────────────────────
show_success() {
printf '\n'
printf '%s┌──────────────────────────────────────────────┐%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
printf '%s│%s ✔ CTMS 安装成功 %s│%s\n' "${C_GREEN}${C_BOLD}" "${C_GREEN}" "${C_GREEN}${C_BOLD}" "${C_RESET}"
printf '%s└──────────────────────────────────────────────┘%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
printf '\n'
printf ' %s访问地址%s %s%s%s\n' "${C_BOLD}" "${C_RESET}" "${C_CYAN}" "$BASE_URL" "${C_RESET}"
printf ' %s环境模式%s %s\n' "${C_BOLD}" "${C_RESET}" "$TARGET_ENV"
printf '\n'
}
# ── 入口 ─────────────────────────────────────
main() {
_banner
parse_args "$@"
cd "$ROOT_DIR"
local project_name runtime_env login_key_id
project_name="$(compose_project_name)"
runtime_env="$(app_env)"
login_key_id="$(key_id)"
resolve_base_url
resolve_effective_config "$project_name" "$runtime_env" "$login_key_id"
check_dependencies
confirm_install "$EFFECTIVE_PROJECT_NAME" "$EFFECTIVE_RUNTIME_ENV"
prepare_env_file "$project_name" "$runtime_env" "$login_key_id"
sync_frontend_build_env "$runtime_env"
run_compose_config
run_backend_init
run_build_and_start
run_migrations
resolve_effective_config "$project_name" "$runtime_env" "$login_key_id"
run_health_checks "$EFFECTIVE_RUNTIME_ENV" "$EFFECTIVE_LOGIN_KEY_ID"
show_success
}
main "$@"