From 4a95793d2174511246637b2e3b03da2380bbec26 Mon Sep 17 00:00:00 2001 From: mars <3361409208@qq.com> Date: Fri, 10 Jul 2026 13:58:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E6=94=AF=E6=8C=81=20full-themes?= =?UTF-8?q?=20=E5=85=AD=E5=90=88=E4=B8=80=E5=8C=85=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=20shell=20=E8=84=9A=E6=9C=AC=20LF?= =?UTF-8?q?=20=E6=8D=A2=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 3 + docs/docker/build-and-export-all-themes.ps1 | 1 + docs/docker/build-and-export-images.bat | 2 + scripts/deploy-first.sh | 18 ++- scripts/deploy-lib.sh | 164 ++++++++++++++++++-- scripts/deploy-update.sh | 18 ++- scripts/rollback.sh | 27 +++- 7 files changed, 212 insertions(+), 21 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7bdc42f..6f22d33 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,7 @@ apps/api/dist mcps assets agent-transcripts +.agents +.cursor +.ui-ux-pro-max-skill **/.vite diff --git a/docs/docker/build-and-export-all-themes.ps1 b/docs/docker/build-and-export-all-themes.ps1 index 5460292..cbff560 100644 --- a/docs/docker/build-and-export-all-themes.ps1 +++ b/docs/docker/build-and-export-all-themes.ps1 @@ -172,6 +172,7 @@ function Invoke-PlayerThemeBuild { Write-Host "[INFO] Switched to $Branch" Set-Location $Root + $env:PLAYER_IMAGE_TAG = $Tag if ((Invoke-BuildExport -Service "player" -Tag $Tag) -ne 0) { Write-Host "[ERROR] Player build failed for $Branch" $script:FailedThemes.Add($Branch) | Out-Null diff --git a/docs/docker/build-and-export-images.bat b/docs/docker/build-and-export-images.bat index ea6e690..f829b0e 100644 --- a/docs/docker/build-and-export-images.bat +++ b/docs/docker/build-and-export-images.bat @@ -131,6 +131,8 @@ goto do_export :do_build echo ==^> Building %SERVICE% (tag: %TAG%) set "IMAGE_TAG=%TAG%" +REM player 在 compose 里默认 PLAYER_IMAGE_TAG=main;与 --tag 对齐以便导出/部署 +set "PLAYER_IMAGE_TAG=%TAG%" if "%NO_CACHE%"=="1" goto build_no_cache if /i "%SERVICE%"=="all" ( docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api player admin diff --git a/scripts/deploy-first.sh b/scripts/deploy-first.sh index 25ca764..bc90153 100755 --- a/scripts/deploy-first.sh +++ b/scripts/deploy-first.sh @@ -13,6 +13,7 @@ INIT_DB=false SKIP_BACKUP=false ALLOW_DEFAULT_SECRETS=false IMAGE_TAR="" +WITH_THEMES=false usage() { cat <<'EOF' @@ -30,6 +31,7 @@ usage() { --no-build 不构建镜像,直接使用服务器已有镜像 --images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建 --tag TAG 使用指定镜像 tag,并写入 .env.docker 的 IMAGE_TAG + --themes 启用四套 player(player2/3/4);加载 thebet365-full-themes-*.tar 时自动开启 --no-seed 不执行生产 seed --init-db 调用 scripts/prod-init-db.sh 清空业务数据并初始化生产数据,仅限全新库或明确重置 --skip-backup 与 --init-db 一起使用,跳过 prod-init-db 的备份 @@ -39,6 +41,7 @@ usage() { 示例: ./scripts/deploy-first.sh ./scripts/deploy-first.sh --images thebet365-images-v1.2.3.tar --tag v1.2.3 + ./scripts/deploy-first.sh --images thebet365-full-themes-latest.tar --tag latest ./scripts/deploy-first.sh --no-build ./scripts/deploy-first.sh --init-db EOF @@ -59,6 +62,10 @@ while [ $# -gt 0 ]; do set_deploy_image_tag "${2:?缺少 --tag 参数值}" shift 2 ;; + --themes) + WITH_THEMES=true + shift + ;; --no-seed) RUN_SEED=false shift @@ -99,6 +106,12 @@ if [ -n "$IMAGE_TAR" ] && [ -z "${DEPLOY_IMAGE_TAG:-}" ]; then fi fi +if [ "$WITH_THEMES" = true ]; then + enable_themes_deploy +elif [ -n "$IMAGE_TAR" ]; then + maybe_enable_themes_from_tar "$IMAGE_TAR" +fi + if [ -n "$IMAGE_TAR" ]; then load_image_tar "$IMAGE_TAR" fi @@ -118,8 +131,9 @@ if [ "$INIT_DB" = false ] && [ "$RUN_SEED" = true ]; then seed_production_if_missing_admin fi -log "启动 api / player / admin" -compose up -d api player admin +log "启动 $(stack_app_services)" +# shellcheck disable=SC2046 +compose up -d $(stack_app_services) wait_for_stack_ready if [ "$INIT_DB" = true ]; then diff --git a/scripts/deploy-lib.sh b/scripts/deploy-lib.sh index 6bcb41e..5715182 100755 --- a/scripts/deploy-lib.sh +++ b/scripts/deploy-lib.sh @@ -3,6 +3,7 @@ DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$DEPLOY_LIB_DIR/.." && pwd)" COMPOSE_FILE="$ROOT/docker-compose.prod.yml" +COMPOSE_THEMES_FILE="$ROOT/docker-compose.themes.yml" ENV_FILE="$ROOT/.env.docker" ENV_EXAMPLE_FILE="$ROOT/.env.docker.example" BACKUP_DIR="$ROOT/backups" @@ -12,6 +13,7 @@ CURRENT_RELEASE_FILE="$DEPLOY_STATE_DIR/current-release.env" PREVIOUS_RELEASE_FILE="$DEPLOY_STATE_DIR/previous-release.env" DEPLOY_IMAGE_TAG="${DEPLOY_IMAGE_TAG:-}" +DEPLOY_WITH_THEMES="${DEPLOY_WITH_THEMES:-false}" LAST_DB_BACKUP="" LAST_UPLOADS_BACKUP="" @@ -55,18 +57,95 @@ infer_image_tag_from_tar() { local base tag base="$(basename "$image_tar")" + tag="${base#thebet365-full-themes-}" + tag="${tag%.tar}" + if [ "$tag" != "$base" ] && [ -n "$tag" ]; then + printf '%s' "$tag" + return 0 + fi + tag="${base#thebet365-images-}" tag="${tag%.tar}" - if [ "$tag" != "$base" ] && [ -n "$tag" ]; then printf '%s' "$tag" fi } -compose() { +is_full_themes_tar() { + local image_tar="$1" + case "$(basename "$image_tar")" in + thebet365-full-themes-*.tar) return 0 ;; + *) return 1 ;; + esac +} + +themes_enabled() { + [ "$DEPLOY_WITH_THEMES" = true ] +} + +player_image_tag() { local tag + tag="$(env_value PLAYER_IMAGE_TAG)" + tag="${tag:-main}" + validate_image_tag "$tag" + printf '%s' "$tag" +} + +theme_player_tag() { + local key="$1" + local default_tag="$2" + local tag + tag="$(env_value "$key")" + tag="${tag:-$default_tag}" + validate_image_tag "$tag" + printf '%s' "$tag" +} + +compose() { + local tag player_tag compose_args=(-f "$COMPOSE_FILE") tag="$(current_image_tag)" - IMAGE_TAG="$tag" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@" + player_tag="$(player_image_tag)" + + if themes_enabled; then + [ -f "$COMPOSE_THEMES_FILE" ] || die "缺少 $COMPOSE_THEMES_FILE,无法启动四套 player" + compose_args+=(-f "$COMPOSE_THEMES_FILE") + fi + + IMAGE_TAG="$tag" \ + PLAYER_IMAGE_TAG="$player_tag" \ + docker compose "${compose_args[@]}" --env-file "$ENV_FILE" "$@" +} + +stack_app_services() { + if themes_enabled; then + printf '%s' "api player player2 player3 player4 admin" + else + printf '%s' "api player admin" + fi +} + +enable_themes_deploy() { + DEPLOY_WITH_THEMES=true + export DEPLOY_WITH_THEMES + log "四套 player 主题部署已启用" +} + +maybe_enable_themes_from_tar() { + local image_tar="$1" + if is_full_themes_tar "$image_tar"; then + enable_themes_deploy + fi +} + +load_themes_from_release() { + local val + [ -f "$CURRENT_RELEASE_FILE" ] || return 1 + val="$(grep -E '^deploy_with_themes=' "$CURRENT_RELEASE_FILE" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ "$val" = true ]; then + enable_themes_deploy + return 0 + fi + return 1 } ensure_env_file() { @@ -203,6 +282,11 @@ wait_for_service_health() { wait_for_stack_ready() { wait_for_service_health api 180 wait_for_service_health player 120 + if themes_enabled; then + wait_for_service_health player2 120 + wait_for_service_health player3 120 + wait_for_service_health player4 120 + fi wait_for_service_health admin 120 } @@ -214,8 +298,13 @@ start_infra() { } build_app_images() { - log "构建 api / player / admin 镜像: $(current_image_tag)" - compose build api player admin + if themes_enabled; then + log "构建 api / player / player2 / player3 / player4 / admin 镜像" + compose build api player player2 player3 player4 admin + else + log "构建 api / player / admin 镜像: $(current_image_tag)" + compose build api player admin + fi } load_image_tar() { @@ -225,12 +314,28 @@ load_image_tar() { docker load -i "$image_tar" } +require_image() { + local ref="$1" + docker image inspect "$ref" >/dev/null 2>&1 || die "缺少镜像: $ref" +} + require_images_for_current_tag() { - local tag + local tag player_tag t2 t3 t4 tag="$(current_image_tag)" - docker image inspect "thebet365-api:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-api:$tag" - docker image inspect "thebet365-player:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-player:$tag" - docker image inspect "thebet365-admin:$tag" >/dev/null 2>&1 || die "缺少镜像: thebet365-admin:$tag" + player_tag="$(player_image_tag)" + + require_image "thebet365-api:$tag" + require_image "thebet365-player:$player_tag" + require_image "thebet365-admin:$tag" + + if themes_enabled; then + t2="$(theme_player_tag PLAYER2_IMAGE_TAG theme-2)" + t3="$(theme_player_tag PLAYER3_IMAGE_TAG theme-3)" + t4="$(theme_player_tag PLAYER4_IMAGE_TAG theme-4)" + require_image "thebet365-player:$t2" + require_image "thebet365-player:$t3" + require_image "thebet365-player:$t4" + fi } run_prisma_migrations() { @@ -343,9 +448,10 @@ current_release_tag() { record_release_state() { local source="${1:-deploy}" - local tag stamp safe_tag manifest previous_tag + local tag player_tag t2 t3 t4 stamp safe_tag manifest previous_tag tag="$(current_image_tag)" + player_tag="$(player_image_tag)" stamp="$(date -u +%Y%m%dT%H%M%SZ)" safe_tag="$(printf '%s' "$tag" | tr -c 'A-Za-z0-9_.-' '-')" manifest="$RELEASES_DIR/${stamp}-${safe_tag}.env" @@ -361,9 +467,18 @@ record_release_state() { printf 'previous_tag=%s\n' "$previous_tag" printf 'deployed_at=%s\n' "$stamp" printf 'source=%s\n' "$source" + printf 'deploy_with_themes=%s\n' "$DEPLOY_WITH_THEMES" printf 'api_image=thebet365-api:%s\n' "$tag" - printf 'player_image=thebet365-player:%s\n' "$tag" + printf 'player_image=thebet365-player:%s\n' "$player_tag" printf 'admin_image=thebet365-admin:%s\n' "$tag" + if themes_enabled; then + t2="$(theme_player_tag PLAYER2_IMAGE_TAG theme-2)" + t3="$(theme_player_tag PLAYER3_IMAGE_TAG theme-3)" + t4="$(theme_player_tag PLAYER4_IMAGE_TAG theme-4)" + printf 'player2_image=thebet365-player:%s\n' "$t2" + printf 'player3_image=thebet365-player:%s\n' "$t3" + printf 'player4_image=thebet365-player:%s\n' "$t4" + fi [ -n "$LAST_DB_BACKUP" ] && printf 'db_backup=%s\n' "$LAST_DB_BACKUP" [ -n "$LAST_UPLOADS_BACKUP" ] && printf 'uploads_backup=%s\n' "$LAST_UPLOADS_BACKUP" } > "$manifest" @@ -373,21 +488,40 @@ record_release_state() { } print_stack_urls() { - local player_port admin_port bind_addr tag + local player_port player2_port player3_port player4_port admin_port bind_addr tag player_tag player_port="$(env_value PLAYER_PORT)" + player2_port="$(env_value PLAYER2_PORT)" + player3_port="$(env_value PLAYER3_PORT)" + player4_port="$(env_value PLAYER4_PORT)" admin_port="$(env_value ADMIN_PORT)" bind_addr="$(env_value BIND_ADDR)" tag="$(current_image_tag)" + player_tag="$(player_image_tag)" player_port="${player_port:-8082}" + player2_port="${player2_port:-8083}" + player3_port="${player3_port:-8084}" + player4_port="${player4_port:-8085}" admin_port="${admin_port:-8081}" bind_addr="${bind_addr:-127.0.0.1}" printf '\n' printf '%s\n' "部署完成:" - printf '%s\n' " 镜像 tag: ${tag}" - printf '%s\n' " 玩家端: http://${bind_addr}:${player_port}" + printf '%s\n' " api/admin 镜像 tag: ${tag}" + printf '%s\n' " player 镜像 tag: ${player_tag}" + if themes_enabled; then + printf '%s\n' " 玩家端 main: http://${bind_addr}:${player_port}" + printf '%s\n' " 玩家端 theme-2: http://${bind_addr}:${player2_port}" + printf '%s\n' " 玩家端 theme-3: http://${bind_addr}:${player3_port}" + printf '%s\n' " 玩家端 theme-4: http://${bind_addr}:${player4_port}" + else + printf '%s\n' " 玩家端: http://${bind_addr}:${player_port}" + fi printf '%s\n' " 管理端: http://${bind_addr}:${admin_port}" printf '%s\n' " API: 仅在 Docker 网络内暴露,由 player/admin 容器和宝塔反代链路访问" - printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml --env-file .env.docker ps" + if themes_enabled; then + printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker ps" + else + printf '%s\n' " 状态: docker compose -f docker-compose.prod.yml --env-file .env.docker ps" + fi printf '%s\n' " 当前发布: .deploy/current-release.env" } diff --git a/scripts/deploy-update.sh b/scripts/deploy-update.sh index 16b7e42..349b1d3 100755 --- a/scripts/deploy-update.sh +++ b/scripts/deploy-update.sh @@ -12,6 +12,7 @@ NO_BUILD=false SKIP_BACKUP=false ALLOW_DEFAULT_SECRETS=false IMAGE_TAR="" +WITH_THEMES=false usage() { cat <<'EOF' @@ -30,6 +31,7 @@ usage() { --pull 先执行 git pull --ff-only --images PATH 先 docker load 指定镜像 tar,并自动跳过本机构建 --tag TAG 使用指定镜像 tag,并写入 .env.docker 的 IMAGE_TAG + --themes 启用四套 player(player2/3/4);加载 thebet365-full-themes-*.tar 时自动开启 --no-build 不构建镜像,直接使用服务器已有镜像 --no-backup 跳过 PostgreSQL 与 uploads 备份 --allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用 @@ -38,6 +40,7 @@ usage() { 示例: ./scripts/deploy-update.sh --pull ./scripts/deploy-update.sh --images thebet365-images-v1.2.3.tar --tag v1.2.3 + ./scripts/deploy-update.sh --images thebet365-full-themes-latest.tar --tag latest ./scripts/deploy-update.sh --no-build EOF } @@ -57,6 +60,10 @@ while [ $# -gt 0 ]; do set_deploy_image_tag "${2:?缺少 --tag 参数值}" shift 2 ;; + --themes) + WITH_THEMES=true + shift + ;; --no-build) NO_BUILD=true shift @@ -92,6 +99,12 @@ if [ -n "$IMAGE_TAR" ] && [ -z "${DEPLOY_IMAGE_TAG:-}" ]; then fi fi +if [ "$WITH_THEMES" = true ]; then + enable_themes_deploy +elif [ -n "$IMAGE_TAR" ]; then + maybe_enable_themes_from_tar "$IMAGE_TAR" +fi + if [ "$PULL_CODE" = true ]; then require_command git log "拉取代码: git pull --ff-only" @@ -121,8 +134,9 @@ fi run_prisma_migrations -log "启动/更新 api / player / admin" -compose up -d api player admin +log "启动/更新 $(stack_app_services)" +# shellcheck disable=SC2046 +compose up -d $(stack_app_services) wait_for_stack_ready show_prisma_status compose ps diff --git a/scripts/rollback.sh b/scripts/rollback.sh index 17a536f..25a7691 100755 --- a/scripts/rollback.sh +++ b/scripts/rollback.sh @@ -10,6 +10,7 @@ source "$SCRIPT_DIR/deploy-lib.sh" TARGET_TAG="" SKIP_BACKUP=false ALLOW_DEFAULT_SECRETS=false +WITH_THEMES="auto" usage() { cat <<'EOF' @@ -25,9 +26,12 @@ usage() { 注意: 本脚本只回滚镜像,不回滚数据库结构或数据。 如果目标版本涉及不可逆迁移,必须先按备份文件手工恢复数据库。 + 若当前发布启用了四套 player,默认会一并回滚 player2/3/4;可用 --no-themes 仅回滚单套 player。 选项: --to TAG 目标镜像 tag,必填 + --themes 回滚时启用四套 player(player2/3/4) + --no-themes 仅回滚单套 player,忽略当前发布的 themes 状态 --no-backup 跳过 PostgreSQL 与 uploads 备份 --allow-default-secrets 允许 .env.docker 使用示例密钥,仅测试环境使用 -h, --help 显示帮助 @@ -43,6 +47,14 @@ while [ $# -gt 0 ]; do TARGET_TAG="${2:?缺少 --to 参数值}" shift 2 ;; + --themes) + WITH_THEMES=true + shift + ;; + --no-themes) + WITH_THEMES=false + shift + ;; --no-backup) SKIP_BACKUP=true shift @@ -68,6 +80,16 @@ cd "$ROOT" require_docker ensure_env_file || exit 1 validate_prod_env "$ALLOW_DEFAULT_SECRETS" + +if [ "$WITH_THEMES" = true ]; then + enable_themes_deploy +elif [ "$WITH_THEMES" = false ]; then + DEPLOY_WITH_THEMES=false + export DEPLOY_WITH_THEMES +else + load_themes_from_release || true +fi + require_images_for_current_tag start_infra @@ -80,8 +102,9 @@ else warn "已跳过 PostgreSQL 与 uploads 备份" fi -log "回滚 api / player / admin 到 tag: $(current_image_tag)" -compose up -d api player admin +log "回滚 $(stack_app_services) 到 tag: $(current_image_tag)" +# shellcheck disable=SC2046 +compose up -d $(stack_app_services) wait_for_stack_ready show_prisma_status compose ps