<# 本机构建镜像并导出 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