# 统一部署脚本 - 解决SSH认证冲突 param( [string]$Server = "45.249.208.118", [string]$User = "root", [string]$SshKeyPath = "$env:USERPROFILE\.ssh\id_rsa", [string]$Password = "", [string]$ProjectPath = "/root/youyidian-3d", [switch]$UsePassword = $false, [switch]$NoCache = $false, [ValidateSet("ssh-key", "password", "auto")] [string]$AuthMethod = "auto" ) $ErrorActionPreference = "Stop" Write-Host "========================================" -ForegroundColor Cyan Write-Host " 统一部署脚本 - 解决SSH冲突" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # 检查工具 $plinkPath = "C:\Program Files\PuTTY\plink.exe" $pscpPath = "C:\Program Files\PuTTY\pscp.exe" $toolsFound = $true if (-not (Test-Path $plinkPath)) { Write-Host "警告: 未找到 plink.exe" -ForegroundColor Yellow $toolsFound = $false } if (-not (Test-Path $pscpPath)) { Write-Host "警告: 未找到 pscp.exe" -ForegroundColor Yellow $toolsFound = $false } if (-not $toolsFound) { Write-Host "请安装 PuTTY 工具包" -ForegroundColor Red Write-Host "下载地址: https://www.putty.org/" -ForegroundColor Red exit 1 } # 自动选择认证方式 $useSshKey = $false $usePassword = $false switch ($AuthMethod) { "ssh-key" { $useSshKey = $true Write-Host "强制使用SSH密钥认证" -ForegroundColor Yellow } "password" { $usePassword = $true Write-Host "强制使用密码认证" -ForegroundColor Yellow } "auto" { # 自动检测最佳认证方式 if ((-not [string]::IsNullOrEmpty($SshKeyPath)) -and (Test-Path $SshKeyPath)) { $useSshKey = $true Write-Host "检测到SSH密钥文件,使用密钥认证" -ForegroundColor Green } elseif ($UsePassword -or (-not [string]::IsNullOrEmpty($Password))) { $usePassword = $true Write-Host "使用密码认证" -ForegroundColor Green } else { Write-Host "错误: 未找到SSH密钥且未提供密码" -ForegroundColor Red Write-Host "请使用 -SshKeyPath 指定密钥文件路径" -ForegroundColor Yellow Write-Host "或使用 -Password 提供密码" -ForegroundColor Yellow Write-Host "或使用 -UsePassword 参数启用密码认证" -ForegroundColor Yellow exit 1 } } } # 验证SSH密钥 if ($useSshKey) { if (-not (Test-Path $SshKeyPath)) { Write-Host "错误: SSH密钥文件不存在: $SshKeyPath" -ForegroundColor Red Write-Host "" Write-Host "创建SSH密钥的步骤:" -ForegroundColor Yellow Write-Host "1. 在PowerShell中运行: ssh-keygen -t rsa -b 4096 -C 'deploy@youyidian'" -ForegroundColor Gray Write-Host "2. 将生成的公钥复制到服务器: ssh-copy-id -i ~/.ssh/id_rsa.pub ${User}@${Server}" -ForegroundColor Gray Write-Host "3. 或手动复制公钥内容到服务器的 ~/.ssh/authorized_keys" -ForegroundColor Gray exit 1 } Write-Host "使用SSH密钥: $SshKeyPath" -ForegroundColor Green } # 验证密码 if ($usePassword -and [string]::IsNullOrEmpty($Password)) { Write-Host "错误: 使用密码认证需要提供 -Password 参数" -ForegroundColor Red exit 1 } Write-Host "服务器: $Server" -ForegroundColor Green Write-Host "用户: $User" -ForegroundColor Green Write-Host "目标路径: $ProjectPath" -ForegroundColor Green Write-Host "" # 需要上传的关键文件列表(完整列表) $filesToUpload = @( # 核心配置文件 "package.json", "package-lock.json", "next.config.js", ".env", "prisma/schema.prisma", "Dockerfile", "docker-compose.yml", # 应用核心文件 "app/layout.tsx", "app/page.tsx", "app/login/page.tsx", "app/register/page.tsx", "app/api/auth/register/route.ts", "app/api/auth/login/route.ts", "app/api/auth/logout/route.ts", "app/api/user/me/route.ts", "app/api/admin/users/route.ts", "app/api/admin/dashboard/route.ts", # 库文件 "lib/prisma.ts", "lib/auth.ts", "lib/config.ts", "lib/configLoader.ts", "lib/utils.ts", "middleware.ts", # 组件和样式 "components/ui", "components/auth", "components/admin", "public", "styles", # 其他重要文件 "README.md", "tsconfig.json", "tailwind.config.ts" ) Write-Host "[1/6] 检查文件完整性..." -ForegroundColor Yellow $missingFiles = @() foreach ($file in $filesToUpload) { if (-not (Test-Path $file)) { $missingFiles += $file } } if ($missingFiles.Count -gt 0) { Write-Host "以下文件不存在,将跳过:" -ForegroundColor Yellow $missingFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } Write-Host "" } # 测试SSH连接 Write-Host "[2/6] 测试SSH连接..." -ForegroundColor Yellow $testCmd = "echo 'SSH连接测试成功'" if ($useSshKey) { & $plinkPath -ssh -i $SshKeyPath -batch "${User}@${Server}" $testCmd | Out-Null } else { if ([string]::IsNullOrEmpty($Password)) { Write-Host "请输入服务器密码:" -ForegroundColor Yellow -NoNewline $securePwd = Read-Host -AsSecureString $passwordPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePwd) $Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordPtr) } & $plinkPath -ssh "${User}@${Server}" $testCmd | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Host "SSH连接测试失败" -ForegroundColor Red exit 1 } Write-Host " ✓ SSH连接正常" -ForegroundColor Green # 确保远程目录存在 Write-Host "[3/6] 准备远程目录..." -ForegroundColor Yellow $ensureDirCmd = "mkdir -p '$ProjectPath'" if ($useSshKey) { & $plinkPath -ssh -i $SshKeyPath -batch "${User}@${Server}" $ensureDirCmd | Out-Null } else { & $plinkPath -ssh -pw $Password "${User}@${Server}" $ensureDirCmd | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Host "创建远程目录失败" -ForegroundColor Red exit 1 } # 上传文件 Write-Host "[4/6] 上传文件到服务器..." -ForegroundColor Yellow $uploadedCount = 0 $totalFiles = ($filesToUpload | Where-Object { Test-Path $_ }).Count foreach ($file in $filesToUpload) { if (Test-Path $file) { $remotePath = "$ProjectPath/$file" $remoteDir = Split-Path $remotePath -Parent # 确保远程目录存在 $mkdirCmd = "mkdir -p '$remoteDir'" if ($useSshKey) { & $plinkPath -ssh -i $SshKeyPath -batch "${User}@${Server}" $mkdirCmd | Out-Null } else { & $plinkPath -ssh -pw $Password "${User}@${Server}" $mkdirCmd | Out-Null } # 上传文件 $uploadParams = @("-q") if ($useSshKey) { $uploadParams += "-i", $SshKeyPath, "-batch" } else { $uploadParams += "-pw", $Password } $uploadParams += $file, "${User}@${Server}:$remotePath" & $pscpPath @uploadParams 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { $uploadedCount++ Write-Host " ✓ $file" -ForegroundColor Green } else { Write-Host " ✗ $file (上传失败)" -ForegroundColor Red } } } Write-Host "文件上传完成: $uploadedCount/$totalFiles 个文件" -ForegroundColor $(if ($uploadedCount -eq $totalFiles) { "Green" } else { "Yellow" }) if ($uploadedCount -lt $totalFiles) { Write-Host "警告: 部分文件上传失败,可能影响部署" -ForegroundColor Yellow } # 创建部署脚本 Write-Host "[5/6] 创建并执行远程部署脚本..." -ForegroundColor Yellow $buildFlag = if ($NoCache) { "--no-cache" } else { "" } $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" # 创建本地临时脚本 $localDeployScript = Join-Path $env:TEMP "deploy-unified-$timestamp.sh" $utf8NoBom = New-Object System.Text.UTF8Encoding $false # 远程部署脚本内容 $deployScript = @" #!/bin/bash set -e echo "========================================" echo "开始部署应用..." echo "开始时间: $(date)" echo "========================================" cd $ProjectPath # 备份重要数据 echo "[1/7] 备份数据..." if [ -d "uploads" ] && [ -n "$(ls -A uploads 2>/dev/null)" ]; then mkdir -p backups backup_dir="backups/uploads-$(date +%Y%m%d-%H%M%S)" cp -r uploads "`$backup_dir" echo " ✓ 上传目录已备份到 `$(basename `$backup_dir)" fi # 检查Docker环境 echo "[2/7] 检查Docker环境..." if ! command -v docker &> /dev/null; then echo "错误: Docker 未安装" >&2 exit 1 fi if docker compose version &> /dev/null; then DOCKER_COMPOSE="docker compose" elif command -v docker-compose &> /dev/null; then DOCKER_COMPOSE="docker-compose" else echo "错误: Docker Compose 未安装" >&2 exit 1 fi echo " ✓ 使用Docker Compose: `$DOCKER_COMPOSE" # 停止旧容器 echo "[3/7] 停止旧容器..." `$DOCKER_COMPOSE down --timeout 30 2>/dev/null || true echo " ✓ 旧容器已停止" # 构建并启动应用 echo "[4/7] 构建并启动应用..." if [ -n "$buildFlag" ]; then echo " ⚠ 使用 --no-cache 模式重新构建" `$DOCKER_COMPOSE build $buildFlag app fi echo " ✓ 启动应用容器..." `$DOCKER_COMPOSE up -d app if [ `$? -ne 0 ]; then echo "错误: 容器启动失败" >&2 exit 1 fi # 等待容器就绪 echo "[5/7] 等待容器就绪..." for i in {1..30}; do if `$DOCKER_COMPOSE ps app | grep -q "Up"; then echo " ✓ 容器运行正常" break fi if [ $i -eq 30 ]; then echo "错误: 容器启动超时" >&2 exit 1 fi sleep 2 done # 数据库迁移 echo "[6/7] 检查数据库状态..." if [ -f prisma/schema.prisma ]; then echo " → 执行数据库迁移..." `$DOCKER_COMPOSE exec -T app npx prisma db push --accept-data-loss --schema=/app/prisma/schema.prisma 2>/dev/null || { echo " ⚠ 数据库迁移跳过或已存在" } fi # 健康检查 echo "[7/7] 执行健康检查..." sleep 5 max_attempts=10 attempt=0 while [ `$attempt -lt `$max_attempts ]; do if curl -f -s http://localhost:3000/api/health > /dev/null 2>&1; then echo " ✓ 应用健康检查通过" break fi attempt=`$((attempt + 1)) if [ $attempt -eq $max_attempts ]; then echo " ⚠ 健康检查超时,应用可能需要更多时间启动" fi sleep 3 done # 显示部署结果 echo "" echo "========================================" echo "部署完成!" echo "完成时间: $(date)" echo "========================================" echo "" echo "容器状态:" `$DOCKER_COMPOSE ps echo "" echo "最近日志:" `$DOCKER_COMPOSE logs app --tail=10 # 显示应用访问信息 echo "" echo "应用访问信息:" echo " 本地访问: http://localhost:3000" echo " 外网访问: http://$Server:4320" echo "" "@ [System.IO.File]::WriteAllText($localDeployScript, $deployScript, $utf8NoBom) # 上传并执行部署脚本 $remoteDeployScript = "/tmp/deploy-unified-$timestamp.sh" $uploadParams = @("-q") if ($useSshKey) { $uploadParams += "-i", $SshKeyPath, "-batch" } else { $uploadParams += "-pw", $Password } $uploadParams += $localDeployScript, "${User}@${Server}:$remoteDeployScript" Write-Host " → 上传部署脚本..." -ForegroundColor Yellow & $pscpPath @uploadParams if ($LASTEXITCODE -ne 0) { Write-Host "上传部署脚本失败" -ForegroundColor Red Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue exit 1 } Write-Host " → 执行远程部署..." -ForegroundColor Yellow if ($useSshKey) { & $plinkPath -ssh -i $SshKeyPath -batch "${User}@${Server}" "chmod +x $remoteDeployScript && bash $remoteDeployScript && rm -f $remoteDeployScript" } else { & $plinkPath -ssh -pw $Password "${User}@${Server}" "chmod +x $remoteDeployScript && bash $remoteDeployScript && rm -f $remoteDeployScript" } $deployExitCode = $LASTEXITCODE # 清理临时文件 Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue # 显示部署结果 Write-Host "[6/6] 部署结果..." -ForegroundColor Yellow if ($deployExitCode -eq 0) { Write-Host "" Write-Host "========================================" -ForegroundColor Green Write-Host " 部署成功完成!" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green Write-Host "" Write-Host "🎉 应用已成功部署到服务器" -ForegroundColor Green Write-Host "" Write-Host "📍 访问地址:" -ForegroundColor Cyan Write-Host " 本地: http://localhost:3000" -ForegroundColor White Write-Host " 外网: http://$Server:4320" -ForegroundColor White Write-Host "" Write-Host "🔧 使用方法:" -ForegroundColor Yellow if ($useSshKey) { Write-Host " - 使用SSH密钥认证 (推荐)" -ForegroundColor Green } else { Write-Host " - 使用密码认证" -ForegroundColor Yellow Write-Host " 建议配置SSH密钥以提高安全性" -ForegroundColor Gray } Write-Host "" } else { Write-Host "" Write-Host "========================================" -ForegroundColor Red Write-Host " 部署失败!" -ForegroundColor Red Write-Host " 退出码: $deployExitCode" -ForegroundColor Red Write-Host "========================================" -ForegroundColor Red Write-Host "" Write-Host "🔧 可能的解决方案:" -ForegroundColor Yellow Write-Host " 1. 检查网络连接" -ForegroundColor White Write-Host " 2. 验证服务器凭证" -ForegroundColor White Write-Host " 3. 查看服务器日志: docker-compose logs app" -ForegroundColor White Write-Host " 4. 重新运行脚本: .\$($MyInvocation.MyCommand.Name) -AuthMethod auto" -ForegroundColor White Write-Host "" } Write-Host "💡 提示:" -ForegroundColor Cyan Write-Host " - 使用 -NoCache 参数强制重新构建" -ForegroundColor Gray Write-Host " - 使用 -AuthMethod ssh-key 强制使用密钥认证" -ForegroundColor Gray Write-Host " - 使用 -AuthMethod password 强制使用密码认证" -ForegroundColor Gray Write-Host " - 使用 -SshKeyPath 指定自定义密钥路径" -ForegroundColor Gray Write-Host ""