Files
thebet365/scripts/deploy-lib.sh
2026-06-13 17:38:25 +08:00

210 lines
6.2 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
DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$DEPLOY_LIB_DIR/.." && pwd)"
COMPOSE_FILE="$ROOT/docker-compose.prod.yml"
ENV_FILE="$ROOT/.env.docker"
ENV_EXAMPLE_FILE="$ROOT/.env.docker.example"
BACKUP_DIR="$ROOT/backups"
log() {
printf '%s\n' "[$(basename "$0")] $*"
}
warn() {
printf '%s\n' "警告: $*" >&2
}
die() {
printf '%s\n' "错误: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "未找到命令: $1"
}
require_docker() {
require_command docker
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2请确认可执行 docker compose"
}
compose() {
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
}
ensure_env_file() {
if [ -f "$ENV_FILE" ]; then
return 0
fi
if [ ! -f "$ENV_EXAMPLE_FILE" ]; then
die "未找到 .env.docker 或 .env.docker.example"
fi
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
warn "已从 .env.docker.example 创建 .env.docker"
warn "请先修改 POSTGRES_PASSWORD、JWT_SECRET、短信配置和端口后再重新执行部署脚本"
return 1
}
env_value() {
local key="$1"
local line
line="$(grep -E "^[[:space:]]*${key}=" "$ENV_FILE" | tail -n 1 || true)"
line="${line#*=}"
line="${line%$'\r'}"
line="${line%\"}"
line="${line#\"}"
line="${line%\'}"
line="${line#\'}"
printf '%s' "$line"
}
validate_prod_env() {
local allow_defaults="${1:-false}"
local postgres_password jwt_secret seed_database
postgres_password="$(env_value POSTGRES_PASSWORD)"
jwt_secret="$(env_value JWT_SECRET)"
seed_database="$(env_value SEED_DATABASE)"
[ -n "$postgres_password" ] || die ".env.docker 缺少 POSTGRES_PASSWORD"
[ -n "$jwt_secret" ] || die ".env.docker 缺少 JWT_SECRET"
if [ "$allow_defaults" != "true" ]; then
[ "$postgres_password" != "thebet365" ] || die "POSTGRES_PASSWORD 仍是示例值;如确为测试环境,请加 --allow-default-secrets"
[ "$jwt_secret" != "change-me-in-production-use-long-random-string" ] || die "JWT_SECRET 仍是示例值;如确为测试环境,请加 --allow-default-secrets"
fi
if [ "$seed_database" = "true" ]; then
warn ".env.docker 中 SEED_DATABASE=true 会让 api 每次启动都执行 seed生产建议设为 false首次部署脚本会按需一次性 seed"
fi
if [ "$(env_value CHUANGLAN_ACCOUNT)" = "your_account" ] || [ "$(env_value CHUANGLAN_PASSWORD)" = "your_password" ]; then
warn "创蓝短信账号仍是示例值,短信验证码功能上线前需要改为真实配置"
fi
}
wait_for_service_health() {
local service="$1"
local timeout="${2:-120}"
local start now container_id status
start="$(date +%s)"
while true; do
container_id="$(compose ps -q "$service" 2>/dev/null || true)"
if [ -n "$container_id" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)"
if [ "$status" = "healthy" ] || [ "$status" = "running" ]; then
return 0
fi
fi
now="$(date +%s)"
if [ $((now - start)) -ge "$timeout" ]; then
compose logs --tail=80 "$service" || true
die "$service${timeout}s 内未就绪"
fi
sleep 2
done
}
wait_for_service_running() {
local service="$1"
local timeout="${2:-120}"
local start now container_id running
start="$(date +%s)"
while true; do
container_id="$(compose ps -q "$service" 2>/dev/null || true)"
if [ -n "$container_id" ]; then
running="$(docker inspect -f '{{.State.Running}}' "$container_id" 2>/dev/null || true)"
if [ "$running" = "true" ]; then
return 0
fi
fi
now="$(date +%s)"
if [ $((now - start)) -ge "$timeout" ]; then
compose logs --tail=120 "$service" || true
die "$service${timeout}s 内未保持运行"
fi
sleep 2
done
}
start_infra() {
log "启动 PostgreSQL / Redis"
compose up -d postgres redis
wait_for_service_health postgres 120
wait_for_service_health redis 120
}
build_app_images() {
log "构建 api / player / admin 镜像"
compose build api player admin
}
load_image_tar() {
local image_tar="$1"
[ -f "$image_tar" ] || die "镜像包不存在: $image_tar"
log "加载镜像包: $image_tar"
docker load -i "$image_tar"
}
run_prisma_migrations() {
log "执行 Prisma 迁移"
compose run --rm --no-deps --entrypoint sh api -c 'cd /app/apps/api && npx prisma migrate deploy && npx prisma generate'
}
show_prisma_status() {
log "检查 Prisma 迁移状态"
compose exec -T api sh -c 'cd /app/apps/api && npx prisma migrate status'
}
backup_database() {
local prefix="${1:-manual}"
local stamp backup_file
mkdir -p "$BACKUP_DIR"
stamp="$(date +%Y%m%d-%H%M%S)"
backup_file="$BACKUP_DIR/thebet365-${prefix}-${stamp}.sql"
log "备份 PostgreSQL -> $backup_file"
compose exec -T postgres pg_dump -U thebet365 -d thebet365 -F p > "$backup_file"
log "数据库备份完成: $backup_file"
}
admin_user_count() {
compose exec -T postgres psql -U thebet365 -d thebet365 -tAc "select count(*) from users where username = 'admin';" 2>/dev/null | tr -d '[:space:]'
}
seed_production_if_missing_admin() {
local count
count="$(admin_user_count || true)"
[ -n "$count" ] || die "无法检查 admin 用户,请确认数据库迁移已成功"
if [ "$count" = "0" ]; then
log "未发现 admin 用户,执行一次生产 seed"
compose run --rm --no-deps --entrypoint sh -e SEED_MODE=production -e NODE_ENV=production api -c 'cd /app/apps/api && node dist/infrastructure/database/seed-cli.js'
else
log "已存在 admin 用户,跳过生产 seed"
fi
}
print_stack_urls() {
local player_port admin_port
player_port="$(env_value PLAYER_PORT)"
admin_port="$(env_value ADMIN_PORT)"
player_port="${player_port:-8082}"
admin_port="${admin_port:-8081}"
printf '\n'
printf '%s\n' "部署完成:"
printf '%s\n' " 玩家端: http://服务器IP:${player_port}"
printf '%s\n' " 管理端: http://服务器IP:${admin_port}"
printf '%s\n' " API: 仅在 Docker 网络内暴露,由 player/admin 容器和宝塔反代链路访问"
printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml --env-file .env.docker ps"
}