# Piano Plan 部署脚本 - 钢琴练习方案系统 # 版本:v2.0 (安全版) # 核心原则:备份优先、不删除、自动验证 # # 使用前: # 1. 修改下方 CONFIG 部分,填写正确的值 # 2. 确认本地代码已验证通过 # 3. 确认目标环境正确 # ============================================================ # 配置(请根据实际情况修改) # ============================================================ $IMAGE_NAME = "piano-plan" $IMAGE_TAG = "latest" $CONTAINER_NAME = "piano-plan" $REMOTE_HOST = "root@47.106.65.108" $LOCAL_PATH = "D:\F\NewI\opencode\daily-workspace\projects\青年钢琴集体课\练习方案系统" $REMOTE_IMAGE_PATH = "/tmp/piano-plan.tar" # 必须保留的挂载(不要修改) # ============================================================ $ErrorActionPreference = "Stop" Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " Piano Plan 部署脚本 (安全版 v2.0)" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # ---------------------------------------------------------- # 部署前确认(必须!) # ---------------------------------------------------------- Write-Host "【重要】部署前必须获得用户明确同意" -ForegroundColor Yellow Write-Host "" $confirm = Read-Host "是否确认要执行部署?(输入 'YES' 继续)" if ($confirm -ne "YES") { Write-Host "部署已取消" -ForegroundColor Red exit 0 } Write-Host "" # ---------------------------------------------------------- # 安全检查函数 # ---------------------------------------------------------- function Test-ContainerExists { param([string]$Name) $result = ssh -i ~/.ssh/id_rsa $REMOTE_HOST "docker ps -a --filter name=$Name --format '{{.Names}}'" return ($result -eq $Name) } function Backup-File { param([string]$Source, [string]$Dest) $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $backupPath = "$Dest backup_$timestamp" Write-Host " [备份] $Source -> $backupPath" -ForegroundColor Yellow ssh -i ~/.ssh/id_rsa $REMOTE_HOST "cp -r '$Source' '$backupPath'" if ($LASTEXITCODE -eq 0) { Write-Host " [OK] 备份成功" -ForegroundColor Green } else { Write-Host " [ERROR] 备份失败!" -ForegroundColor Red throw "备份失败,终止部署" } } function Get-CurrentMounts { $mounts = ssh -i ~/.ssh/id_rsa $REMOTE_HOST "docker inspect $CONTAINER_NAME --format '{{json .HostConfig.Binds}}'" 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($mounts)) { Write-Host " [WARNING] 无法获取当前挂载,将使用默认挂载配置" -ForegroundColor Yellow return $null } return $mounts } function Write-Section { param([string]$Title) Write-Host "" Write-Host "[$Title]" -ForegroundColor Cyan } function Write-Step { param([int]$Num, [int]$Total, [string]$Msg) Write-Host " Step $Num/$Total`: $Msg" -ForegroundColor White } function Test-ServiceHealth { Write-Host " 检查服务健康状态..." -ForegroundColor Yellow $status = ssh -i ~/.ssh/id_rsa $REMOTE_HOST "curl -s -o /dev/null -w '%{http_code}' http://localhost:5001/ 2>`$null" if ($status -eq "302" -or $status -eq "200") { Write-Host " [OK] 服务正常 (HTTP $status)" -ForegroundColor Green return $true } else { Write-Host " [WARNING] 服务异常 (HTTP $status)" -ForegroundColor Red return $false } } function Test-DataIntegrity { Write-Host " 检查数据完整性..." -ForegroundColor Yellow $fileCount = ssh -i ~/.ssh/id_rsa $REMOTE_HOST "docker exec piano-plan ls -1 /app/个性化方案/*.md 2>/dev/null | wc -l" if ($fileCount -ge 15) { Write-Host " [OK] 问题文件完整 ($fileCount 个 md 文件)" -ForegroundColor Green return $true } else { Write-Host " [ERROR] 问题文件不完整!只有 $fileCount 个" -ForegroundColor Red return $false } } # ============================================================ # 主流程 # ============================================================ # Step 1: 前置检查 Write-Section "1. 前置检查" Write-Step 1 7 "确认容器存在" if (-not (Test-ContainerExists $CONTAINER_NAME)) { Write-Host " [ERROR] 容器 '$CONTAINER_NAME' 不存在!" -ForegroundColor Red Write-Host " 请检查 CONTAINER_NAME 配置是否正确" -ForegroundColor Red exit 1 } Write-Host " [OK] 容器存在" -ForegroundColor Green # Step 2: 获取当前挂载(关键!) Write-Step 2 7 "获取当前挂载配置" Write-Host " [INFO] 将保留原有挂载配置" -ForegroundColor Cyan $currentMounts = Get-CurrentMounts # Step 3: 构建镜像 Write-Section "2. 构建 Docker 镜像" Set-Location $LOCAL_PATH Write-Host " 正在构建镜像..." -ForegroundColor Yellow docker build -t "$IMAGE_NAME`:$IMAGE_TAG" . 2>&1 | ForEach-Object { Write-Host " $_" } if ($LASTEXITCODE -ne 0) { Write-Host " [ERROR] Docker 构建失败!" -ForegroundColor Red exit 1 } Write-Host " [OK] 镜像构建成功" -ForegroundColor Green # Step 4: 备份当前容器配置(挂载信息) Write-Section "3. 备份" Write-Step 3 7 "备份容器配置" $mountBackupPath = "/tmp/piano_plan_mounts_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" ssh -i ~/.ssh/id_rsa $REMOTE_HOST "echo '$currentMounts' > '$mountBackupPath'" Write-Host " [OK] 挂载配置已备份到 $mountBackupPath" -ForegroundColor Green # Step 5: 保存并上传镜像 Write-Section "4. 上传镜像" Write-Step 4 7 "保存镜像" docker save "$IMAGE_NAME`:$IMAGE_TAG" -o "$LOCAL_PATH\$IMAGE_NAME.tar" Write-Host " [OK] 镜像已保存" -ForegroundColor Green Write-Step 5 7 "上传镜像到服务器" scp -i ~/.ssh/id_rsa "$LOCAL_PATH\$IMAGE_NAME.tar" "$REMOTE_HOST`:$REMOTE_IMAGE_PATH" Write-Host " [OK] 镜像已上传" -ForegroundColor Green # Step 6: 停止旧容器(关键:不删除!) Write-Section "5. 停止旧容器" Write-Step 6 7 "停止容器 (NOT 删除)" Write-Host " [WARNING] 即将停止容器,配置和挂载将保留" -ForegroundColor Yellow ssh -i ~/.ssh/id_rsa $REMOTE_HOST "docker stop $CONTAINER_NAME" Write-Host " [OK] 容器已停止" -ForegroundColor Green # Step 7: 加载新镜像 Write-Section "6. 启动新容器" Write-Step 7 7 "加载新镜像并启动" # 构建挂载参数(使用原有挂载) $mountArgs = "-v /opt/piano-plan/个性化方案:/app/个性化方案 -v piano-plan-data:/app/data -v piano-plan-output:/app/output -v piano-plan-config:/app/config" $runCmd = @" docker rm $CONTAINER_NAME 2>/dev/null docker run -d --name $CONTAINER_NAME -p 5001:5001 --restart unless-stopped -e FLASK_ENV=production -e PYTHONDONTWRITEBYTECODE=1 $mountArgs $IMAGE_NAME`:$IMAGE_TAG "@ Write-Host " [INFO] 启动命令: $runCmd" -ForegroundColor Cyan ssh -i ~/.ssh/id_rsa $REMOTE_HOST $runCmd Write-Host " [OK] 新容器已启动" -ForegroundColor Green # 等待容器启动 Start-Sleep -Seconds 3 # ============================================================ # 部署后验证 # ============================================================ Write-Section "7. 部署后验证" $allPassed = $true Write-Host "" Write-Host " 检查项:" -ForegroundColor White # 检查1: 容器状态 Write-Host " [1/3] 容器状态" -ForegroundColor White $containerStatus = ssh -i ~/.ssh/id_rsa $REMOTE_HOST "docker ps --filter name=$CONTAINER_NAME --format '{{.Status}}'" if ($containerStatus -like "Up *") { Write-Host " [OK] 容器运行中: $containerStatus" -ForegroundColor Green } else { Write-Host " [ERROR] 容器状态异常: $containerStatus" -ForegroundColor Red $allPassed = $false } # 检查2: 服务健康 Write-Host " [2/3] 服务健康检查" -ForegroundColor White if (-not (Test-ServiceHealth)) { $allPassed = $false } # 检查3: 数据完整性 Write-Host " [3/3] 数据完整性检查" -ForegroundColor White if (-not (Test-DataIntegrity)) { $allPassed = $false } # ============================================================ # 完成 # ============================================================ Write-Host "" Write-Host "========================================" -ForegroundColor Cyan if ($allPassed) { Write-Host " 部署完成!" -ForegroundColor Green } else { Write-Host " 部署完成(但有警告,请检查!)" -ForegroundColor Yellow } Write-Host "" Write-Host " 访问地址: https://piano.yoin.fun" -ForegroundColor Cyan Write-Host " 备份位置: $mountBackupPath" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # 清理本地临时文件 Remove-Item "$LOCAL_PATH\$IMAGE_NAME.tar" -ErrorAction SilentlyContinue