补回 swagger(Swashbuckle 6.6.2 + XML 注释)、socket.html 入库、固化本机构建与服务器替换脚本

- Directory.Build.props: 生成 XML 注释供 Swagger 展示中文说明
- AdminHost/FishGameHost: 接入 Swashbuckle,路由 adminswagger / roomswagger
- 后台前端 config.js 改为同源相对地址,适配反向代理
- socket.html(SignalR 协议文档)纳入仓库
- deploy/: 本机构建(容器内编译,规避杀软)、服务器替换脚本、Nginx 配置
This commit is contained in:
2026-10-18 17:28:45 +08:00
parent f0d678c5b6
commit d083d8524b
17 changed files with 1252 additions and 13 deletions
+67
View File
@@ -0,0 +1,67 @@
# ============================================================
# 容器内编译 + 打包镜像(多阶段)
#
# 用途:本机构建时使用。编译发生在 Docker 的 Linux 虚拟机内,
# Windows 上的杀软(如火绒)无法拦截中间产物 AdminApi.dll 等,
# 可规避「dll 生成后被删除导致 MSB3030」的问题。
#
# 由 deploy/build-local.ps1 调用,构建上下文 = 仓库根目录:
# docker build -f deploy/Dockerfile.build \
# --build-arg PROJECT=<项目csproj相对路径,正斜杠> \
# --build-arg SVC_NAME=<服务名,如 admin> \
# --build-arg APP_DLL=<入口dll,如 AdminHost.dll> \
# --build-arg PUBLIC_HOST=<捕鱼对外地址> \
# -t 0524<服务名> .
# ============================================================
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:8.0
ARG RUNTIME_IMAGE=mcr.microsoft.com/dotnet/aspnet:8.0
# ---------- 阶段一:编译发布 ----------
FROM ${SDK_IMAGE} AS build
ARG PROJECT
ARG SVC_NAME
ARG PUBLIC_HOST=localhost
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
DOTNET_NOLOGO=1
WORKDIR /src
COPY . .
# 编译(不依赖 PUBLIC_HOST,可复用构建缓存)
RUN dotnet publish "${PROJECT}" -c Release -o /out --nologo
# 部署配置覆盖 + 对外地址替换(单独一层:日后只改地址可秒级重建,无需重编译)
RUN set -eux; \
# 部署版配置覆盖(各 Host 的 Config/system.json 默认不随发布输出)
mkdir -p /out/Config; \
cp "/src/deploy/config/${SVC_NAME}/system.json" /out/Config/system.json; \
cp /src/nlog.config /out/nlog.config; \
# 捕鱼对局层:替换客户端要访问的对外地址
sed -i "s|__PUBLIC_HOST__|${PUBLIC_HOST}|g" /out/Config/system.json; \
# 网关:ocelot 下游地址由 localhost:524x 改写为「容器名:80」
if [ -f /out/ocelot.json ]; then \
sed -i \
-e 's|{ "Host": "localhost", "Port": 5241 }|{ "Host": "0524admin", "Port": 80 }|' \
-e 's|{ "Host": "localhost", "Port": 5245 }|{ "Host": "0524user", "Port": 80 }|' \
-e 's|{ "Host": "localhost", "Port": 5247 }|{ "Host": "0524customer", "Port": 80 }|' \
-e 's|{ "Host": "localhost", "Port": 5242 }|{ "Host": "0524chess", "Port": 80 }|' \
-e 's|{ "Host": "localhost", "Port": 5244 }|{ "Host": "0524fishroom", "Port": 80 }|' \
-e 's|{ "Host": "localhost", "Port": 5248 }|{ "Host": "0524fishgame", "Port": 80 }|' \
/out/ocelot.json; \
fi
# ---------- 阶段二:运行时镜像 ----------
FROM ${RUNTIME_IMAGE}
ARG APP_DLL
ENV APP_DLL=${APP_DLL} \
ASPNETCORE_URLS=http://+:80 \
ASPNETCORE_ENVIRONMENT=Production \
TZ=Asia/Shanghai
WORKDIR /app
COPY --from=build /out .
EXPOSE 80
ENTRYPOINT ["sh", "-c", "exec dotnet ${APP_DLL}"]
+157
View File
@@ -0,0 +1,157 @@
<#
本机构建镜像并导出 tar(编译在 Docker 容器内进行)
------------------------------------------------------------------
适用场景:服务器无法访问 mcr.microsoft.com;且本机杀软(如火绒)
会拦截/删除编译产物(AdminApi.dll 等)。
编译与镜像构建全部在容器内完成,Windows 杀软无法拦截中间产物。
用法:
# 1) 全量构建(7 个服务)
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com
# 2) 只构建/打包指定服务(多个逗号分隔)
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com -Services admin,fishgame
# 3) 按「改动的子项目」自动推导需要重建的服务(推荐,避免漏编)
powershell -ExecutionPolicy Bypass -File deploy\build-local.ps1 -PublicHost game.xxx.com -ForProject FishGameService
产物:deploy\dist\ 下的 tar(只含本次构建的镜像)
#>
param(
[string]$PublicHost = "CHANGE_ME",
[string]$Tag = (Get-Date -Format "yyyyMMdd-HHmm"),
[string[]]$Services = @(),
[string]$ForProject = "",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$Deploy = Join-Path $Root "deploy"
$Dist = Join-Path $Deploy "dist"
function Info($m) { Write-Host "[本地构建] $m" -ForegroundColor Green }
function Warn($m) { Write-Host "[警告] $m" -ForegroundColor Yellow }
# ---------- 服务定义:服务名 -> 项目文件 / 入口 dll / Host 项目名 ----------
$serviceMap = [ordered]@{
gateway = @{ proj = "Gateway/Gateway.csproj"; dll = "Gateway.dll"; host = "Gateway" }
admin = @{ proj = "AdminSrc/AdminHost/AdminHost.csproj"; dll = "AdminHost.dll"; host = "AdminHost" }
user = @{ proj = "UserSrc/UserHost/UserHost.csproj"; dll = "UserHost.dll"; host = "UserHost" }
customer = @{ proj = "CustomerSrc/CustomerHost/CustomerHost.csproj"; dll = "CustomerHost.dll"; host = "CustomerHost" }
chess = @{ proj = "ChessGameHost/ChessGameHost.csproj"; dll = "ChessGameHost.dll"; host = "ChessGameHost" }
fishroom = @{ proj = "FishGameSrc/FishRoomHost/FishRoomHost.csproj"; dll = "FishRoomHost.dll"; host = "FishRoomHost" }
fishgame = @{ proj = "FishGameSrc/FishGameHost/FishGameHost.csproj"; dll = "FishGameHost.dll"; host = "FishGameHost" }
}
# ---------- 依赖图:改动某项目 -> 需要重建的服务 ----------
function Get-AffectedServices([string]$changedProject) {
$refs = @{}
Get-ChildItem -Path $Root -Recurse -Filter *.csproj -ErrorAction SilentlyContinue | ForEach-Object {
$txt = [IO.File]::ReadAllText($_.FullName)
$deps = @()
foreach ($m in [regex]::Matches($txt, 'ProjectReference\s+Include="([^"]+)"')) {
$deps += [IO.Path]::GetFileNameWithoutExtension($m.Groups[1].Value)
}
$refs[$_.BaseName] = $deps
}
$result = @()
foreach ($name in $serviceMap.Keys) {
$hostName = $serviceMap[$name].host
if ($hostName -eq $changedProject) { $result += $name; continue }
$seen = New-Object System.Collections.Generic.HashSet[string]
$stack = New-Object System.Collections.Stack
foreach ($d in @($refs[$hostName])) { if ($d) { [void]$stack.Push($d) } }
while ($stack.Count -gt 0) {
$cur = $stack.Pop()
if (-not $seen.Add($cur)) { continue }
foreach ($d in @($refs[$cur])) { if ($d) { [void]$stack.Push($d) } }
}
if ($seen.Contains($changedProject)) { $result += $name }
}
return $result
}
# ---------- 1) 决定要构建哪些服务 ----------
if ($ForProject) {
$targets = @(Get-AffectedServices $ForProject)
if ($targets.Count -eq 0) {
throw "没有任何 Host 引用项目 '$ForProject'(项目名不带 .csproj)。请检查拼写。"
}
Info ("按项目 '$ForProject' 推导出需重建的服务: " + ($targets -join ', '))
} elseif ($Services.Count -gt 0) {
$svcList = @()
foreach ($s in $Services) { $svcList += ($s -split ',') }
$svcList = @($svcList | ForEach-Object { $_.Trim() } | Where-Object { $_ })
foreach ($s in $svcList) {
if (-not $serviceMap.Contains($s)) { throw "未知服务名 '$s'。可选: $($serviceMap.Keys -join ', ')" }
}
$targets = @($svcList)
Info ("指定构建服务: " + ($targets -join ', '))
} else {
$targets = @($serviceMap.Keys)
Info "全量构建 7 个服务"
}
if ($DryRun) {
Info ("[DryRun] 将构建的服务: " + ($targets -join ', '))
exit 0
}
# ---------- 2) 前置校验 + 准备基础镜像(经镜像站拉取并打回官方 tag)----------
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "未找到 docker,请先安装并启动 Docker Desktop" }
$prevEap = $ErrorActionPreference
$ErrorActionPreference = "Continue"
docker info 2>&1 | Out-Null
$dockerOk = ($LASTEXITCODE -eq 0)
$ErrorActionPreference = $prevEap
if (-not $dockerOk) { throw "Docker 引擎未就绪,请先启动 Docker Desktop 并等托盘图标变绿" }
if ($PublicHost -eq "CHANGE_ME") {
Warn "未指定 -PublicHost,捕鱼对局层地址会被写成 CHANGE_ME,客户端将连不上"
}
$baseImages = [ordered]@{
"mcr.microsoft.com/dotnet/sdk:8.0" = "mcr.m.daocloud.io/dotnet/sdk:8.0"
"mcr.microsoft.com/dotnet/aspnet:8.0" = "mcr.m.daocloud.io/dotnet/aspnet:8.0"
}
foreach ($official in $baseImages.Keys) {
docker image inspect $official *> $null
if ($LASTEXITCODE -ne 0) {
Info "本地缺少 $official,经镜像站拉取 ..."
docker pull $baseImages[$official]
if ($LASTEXITCODE -ne 0) { throw "拉取 $official 失败" }
docker tag $baseImages[$official] $official
}
}
# ---------- 3) 构建镜像(编译在容器内)----------
foreach ($name in $targets) {
$s = $serviceMap[$name]
Info "构建镜像 0524$name ..."
docker build -f (Join-Path $Deploy "Dockerfile.build") `
--build-arg "PROJECT=$($s.proj)" `
--build-arg "SVC_NAME=$name" `
--build-arg "APP_DLL=$($s.dll)" `
--build-arg "PUBLIC_HOST=$PublicHost" `
-t "0524$name" $Root
if ($LASTEXITCODE -ne 0) { throw "构建 0524$name 失败" }
}
# ---------- 4) 导出 tar(只含本次构建的镜像)----------
New-Item -ItemType Directory -Path $Dist -Force | Out-Null
$isAll = ($targets.Count -eq $serviceMap.Count)
$suffix = if ($isAll) { "" } else { "-" + ($targets -join "-") }
$tar = Join-Path $Dist "buyu-images$suffix-$Tag.tar"
$tags = @($targets | ForEach-Object { "0524$_" })
Info ("导出镜像到 $tar ...(含: " + ($tags -join ', ') + ")")
docker save -o $tar @tags
if ($LASTEXITCODE -ne 0) { throw "导出镜像失败" }
$sizeMB = [math]::Round((Get-Item $tar).Length / 1MB, 1)
Write-Host ""
Info "完成!镜像包: $tar ($sizeMB MB)"
Write-Host "下一步 —— 在本机上传到服务器:" -ForegroundColor Cyan
Write-Host " ssh root@<服务器IP> `"mkdir -p /opt/buyu/deploy/dist`"" -ForegroundColor Cyan
Write-Host " scp `"$tar`" root@<服务器IP>:/opt/buyu/deploy/dist/" -ForegroundColor Cyan
Write-Host "再登录服务器执行(只重启本次涉及的服务):" -ForegroundColor Cyan
Write-Host " cd /opt/buyu" -ForegroundColor Cyan
Write-Host (" bash deploy/load-and-up.sh deploy/dist/" + (Split-Path $tar -Leaf) + " " + ($targets -join ' ')) -ForegroundColor Cyan
+70
View File
@@ -0,0 +1,70 @@
<#
本机拉取中间件镜像并导出 tar
------------------------------------------------------------------
适用场景:服务器访问不了 docker.io(拉 mongo/redis/postgres/rabbitmq 报
"error from registry: unknown error"),改为在本机拉好后传过去。
用法:
powershell -ExecutionPolicy Bypass -File deploy\build-middleware.ps1
产物:
deploy\dist\middleware.tar
随后在服务器执行:
docker load -i middleware.tar
docker compose -f deploy/docker-compose.yml up -d
#>
param(
[string]$OutFile = "",
[string[]]$Images = @("redis:7", "mongo:6.0", "postgres:16", "rabbitmq:3.13-management")
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
$Dist = Join-Path $Root "deploy\dist"
if (-not $OutFile) { $OutFile = Join-Path $Dist "middleware.tar" }
$mirrors = @("docker.m.daocloud.io", "docker.1ms.run", "docker.xuanyuan.me", "hub.rat.dev")
function Info($m) { Write-Host "[中间件] $m" -ForegroundColor Green }
function Invoke-Quiet([scriptblock]$sb) {
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& $sb 2>&1 | Out-Null
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
return $code
}
# ---------- 引擎检查 ----------
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "未找到 docker,请先安装并启动 Docker Desktop" }
if ((Invoke-Quiet { docker info }) -ne 0) { throw "Docker 引擎未就绪,请先启动 Docker Desktop 并等托盘图标变绿" }
# ---------- 拉取镜像(逐个镜像站回退)----------
foreach ($img in $Images) {
if ((Invoke-Quiet { docker image inspect $img }) -eq 0) { Info "已存在,跳过: $img"; continue }
$done = $false
foreach ($m in $mirrors) {
Info "拉取 $img (经 $m)..."
if ((Invoke-Quiet { docker pull "$m/library/$img" }) -eq 0) {
Invoke-Quiet { docker tag "$m/library/$img" $img } | Out-Null
Info "OK: $img <= $m"
$done = $true
break
}
}
if (-not $done) { throw "镜像 $img 在所有镜像站都拉取失败,请稍后重试" }
}
# ---------- 导出 tar ----------
New-Item -ItemType Directory -Force -Path (Split-Path $OutFile) | Out-Null
Info "导出到 $OutFile (体积较大,稍候)..."
if ((Invoke-Quiet { docker save -o $OutFile @Images }) -ne 0) { throw "导出镜像失败" }
$mb = [math]::Round((Get-Item $OutFile).Length / 1MB, 1)
Info "完成!$OutFile ($mb MB)"
Write-Host "下一步 —— 本机上传:" -ForegroundColor Cyan
Write-Host " scp `"$OutFile`" root@<服务器IP>:/opt/buyu/" -ForegroundColor Cyan
Write-Host "服务器执行:" -ForegroundColor Cyan
Write-Host " cd /opt/buyu && docker load -i middleware.tar && docker compose -f deploy/docker-compose.yml up -d" -ForegroundColor Cyan
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# ============================================================
# 服务器侧:加载镜像包并按需启动(无需联网、无需 sdk 镜像)
#
# 用法:
# # 1) 加载完整包并启动全部
# bash deploy/load-and-up.sh deploy/dist/buyu-images-<tag>.tar
#
# # 2) 只加载某个服务的包并只重启该服务(增量更新)
# bash deploy/load-and-up.sh deploy/dist/buyu-images-admin-<tag>.tar admin
# bash deploy/load-and-up.sh deploy/dist/buyu-images-admin-fishgame-<tag>.tar admin fishgame
#
# 说明:镜像内的配置(网关 ocelot 下游改写、捕鱼对外地址)已在构建时固化;
# 服务器只做「加载镜像 + 按需重启」,不涉及编译。
# ============================================================
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE="$ROOT/deploy/docker-compose.yml"
TAR="${1:-}"
shift || true
SERVICES=("$@")
log() { echo -e "\033[1;32m[部署]\033[0m $*"; }
[ -n "$TAR" ] || { echo "用法: bash deploy/load-and-up.sh <镜像包.tar> [服务名...]"; exit 1; }
[ -f "$TAR" ] || { echo "找不到镜像包: $TAR"; exit 1; }
command -v docker >/dev/null 2>&1 || { echo "未检测到 docker,请先安装 Docker"; exit 1; }
docker compose version >/dev/null 2>&1 || { echo "未检测到 docker compose v2"; exit 1; }
log "1/3 加载镜像(tar 较大,需要一点时间)..."
docker load -i "$TAR"
log "2/3 启动容器..."
mkdir -p "$ROOT/deploy"/logs/{gateway,admin,user,customer,chess,fishroom,fishgame}
mkdir -p "$ROOT/deploy"/uploads/{admin,user,customer}
if [ ${#SERVICES[@]} -gt 0 ]; then
log "仅重建/重启: ${SERVICES[*]}"
docker compose -f "$COMPOSE" up -d --force-recreate "${SERVICES[@]}"
CHECK=("${SERVICES[@]}")
else
docker compose -f "$COMPOSE" up -d
CHECK=(gateway admin user customer chess fishroom fishgame)
fi
log "3/3 健康检查..."
sleep 6
declare -A PORT=( [gateway]=8001 [admin]=5241 [user]=5245 [customer]=5247 [chess]=5242 [fishroom]=5244 [fishgame]=5248 )
for name in "${CHECK[@]}"; do
port="${PORT[$name]:-}"
[ -n "$port" ] || continue
ok=""
for _ in $(seq 1 20); do
if curl -fsS -o /dev/null --max-time 2 "http://localhost:$port/health"; then ok="✔"; break; fi
sleep 2
done
echo " ${ok:-✘} $name http://localhost:$port/health"
done
echo ""
log "完成。网关统一入口: http://<服务器IP或域名>:8001"
+49
View File
@@ -0,0 +1,49 @@
# ============================================================
# 捕鱼/棋牌平台 - Nginx 反向代理
# 放到宝塔站点目录(该目录下的 *.conf 被全局 nginx.conf 的 http 块 include):
# /www/server/panel/vhost/nginx/buyu.conf
# 生效:nginx -t && systemctl reload nginx
# 说明:所有后端都在本机(Docker 映射到 127.0.0.1),Nginx 只代理到本机端口。
# ============================================================
# WebSocket 升级映射(名字加 buyu 前缀,避免与宝塔/其它站点冲突)
map $http_upgrade $buyu_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name 47.109.132.12; # 换成你的域名或服务器公网 IP
client_max_body_size 50m;
# 管理后台静态页面(AdminHost 的 wwwroot,访问 http://<IP>/admin/index.html)
location /admin/ {
proxy_pass http://127.0.0.1:5241;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 上传的图片(公告图、客服聊天图等)
location /uploads/ {
proxy_pass http://127.0.0.1:5241;
proxy_set_header Host $host;
}
# 其余全部走网关(HTTP 接口 + SignalR WebSocket)
location / {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # SignalR 必需
proxy_set_header Connection $buyu_upgrade; # SignalR 必需
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s; # 长连接不要被掐断
proxy_send_timeout 3600s;
}
}
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
# ============================================================
# 0524 平台:按现有约定替换/回滚单个(或多个)服务容器
# 位置:服务器 /mnt/web/0524-replace.sh
#
# 用法:
# bash 0524-replace.sh admin # 用 0524admin:latest 重建 admin
# bash 0524-replace.sh fishgame admin # 一次替换多个
# TAG=dual-20261008 bash 0524-replace.sh admin # 回滚 admin 到旧镜像
#
# 约定(与线上既有部署保持一致):
# - 网络 webapi_backend
# - 端口只绑 127.0.0.1
# - 配置挂载 /mnt/web/config -> /app/Config(全服务共用)
# - 日志挂载 /mnt/web/logs -> /app/logs
# - ASPNETCORE_URLS 按各服务内部端口
# ============================================================
set -u
NET=webapi_backend
CFG=/mnt/web/config
LOGS=/mnt/web/logs
UPLOADS=/mnt/web/uploads
TAG="${TAG:-latest}"
ROLLBACK_TAG="${ROLLBACK_TAG:-dual-20261008}"
ALL="gateway admin user customer chess fishroom fishgame"
# 容器内部监听端口
internal_port() {
case "$1" in
gateway) echo 8001 ;;
admin) echo 5241 ;;
user) echo 5245 ;;
customer) echo 5247 ;;
chess) echo 5242 ;;
fishroom) echo 5244 ;;
fishgame) echo 8001 ;;
esac
}
# 宿主机映射端口(只绑 127.0.0.1)
host_port() {
case "$1" in
gateway) echo 8001 ;;
admin) echo 5241 ;;
user) echo 5245 ;;
customer) echo 5247 ;;
chess) echo 5242 ;;
fishroom) echo 5244 ;;
fishgame) echo 5248 ;;
esac
}
# 额外的 -v/-e 参数
extra_args() {
case "$1" in
gateway) echo "-v $CFG/ocelot.json:/app/ocelot.json -v $UPLOADS:/app/uploads" ;;
admin) echo "-v $UPLOADS:/app/wwwroot/uploads" ;;
customer) echo "-v $UPLOADS:/app/wwwroot/uploads" ;;
*) echo "" ;;
esac
}
# INSTANCE_ID(日志文件名里用于区分实例)
instance_id() {
case "$1" in
gateway) echo "" ;;
fishgame) echo "1" ;;
*) echo "Container_Port_$(host_port "$1")" ;;
esac
}
run_container() {
local svc=$1 tag=$2
local ip hp inst extra
ip=$(internal_port "$svc"); hp=$(host_port "$svc"); inst=$(instance_id "$svc"); extra=$(extra_args "$svc")
docker rm -f "0524$svc" >/dev/null 2>&1
local envar="ASPNETCORE_URLS=http://+:$ip"
[ -n "$inst" ] && envar="$envar -e INSTANCE_ID=$inst"
# shellcheck disable=SC2086
docker run -d --name "0524$svc" --restart always --network "$NET" \
-e $envar $extra \
-p "127.0.0.1:$hp:$ip" \
-v "$CFG:/app/Config" -v "$LOGS:/app/logs" -v "$CFG/nlog.config:/app/nlog.config" \
"0524$svc:$tag" >/dev/null
}
health() {
local hp; hp=$(host_port "$1")
for _ in $(seq 1 15); do
local c; c=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$hp/health")
[ "$c" = "200" ] && return 0
sleep 2
done
return 1
}
replace_one() {
local svc=$1
echo "=== 替换 $svc (0524$svc:$TAG)==="
run_container "$svc" "$TAG"
if health "$svc"; then
echo " ✔ $svc 健康检查通过"
else
echo " ✘ $svc 未通过,输出日志并回滚到 $ROLLBACK_TAG"
docker logs --tail 40 "0524$svc" 2>&1 | tail -20
run_container "$svc" "$ROLLBACK_TAG"
health "$svc" && echo " ↩ 已回滚,$svc 恢复正常" || echo " !! 回滚后仍不健康,请人工介入"
return 1
fi
}
if [ $# -eq 0 ]; then
echo "用法: bash 0524-replace.sh <服务...> (可选服务: $ALL)"
echo " TAG=dual-20261008 bash 0524-replace.sh <服务...> # 回滚"
exit 1
fi
for svc in "$@"; do
case " $ALL " in
*" $svc "*) replace_one "$svc" ;;
*) echo "未知服务: $svc(可选: $ALL)" ;;
esac
done
echo ""
echo "=== 当前业务容器 ==="
docker ps --format '{{.Names}} => {{.Image}} => {{.Status}}' | grep 0524