This commit is contained in:
wchino
2026-06-13 17:38:25 +08:00
parent e7e938f261
commit 7b33d9f9fa
190 changed files with 23222 additions and 4336 deletions

209
scripts/deploy-lib.sh Executable file
View File

@@ -0,0 +1,209 @@
#!/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"
}