# Deploy Script - Windows PowerShell # Upload source code directly to server, build with Docker on server # No need to test and build locally param( [string]$Server = "45.249.208.118", [string]$User = "root", [string]$Password = "", [string]$ProjectPath = "/root/youyidian-3d", [switch]$SkipBackup = $false, [switch]$SkipCheck = $false ) $ErrorActionPreference = "Stop" # UTF-8 Encoding [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 if ($PSVersionTable.PSVersion.Major -ge 6) { $PSDefaultParameterValues['*:Encoding'] = 'utf8' } # 函数:获取服务器密钥指纹(用于 plink/pscp 的 -hostkey 参数) function Get-ServerHostKey { param([string]$Server) $hostKey = $null try { # 方法1: 先从 known_hosts 文件中读取(如果已经连接过服务器) $knownHostsPath = Join-Path $env:USERPROFILE ".ssh\known_hosts" if (Test-Path $knownHostsPath) { $knownHostsContent = Get-Content $knownHostsPath -ErrorAction SilentlyContinue if ($knownHostsContent) { $matchingLines = $knownHostsContent | Where-Object { $_ -notmatch '^\s*#' -and $_ -match '\s+(ssh-\w+|ecdsa-sha2-\S+)\s+' -and ($_ -match [regex]::Escape($Server) -or $_ -match "^$Server,") } if ($matchingLines) { $keyLine = $matchingLines | Where-Object { $_ -match 'ssh-ed25519' } | Select-Object -First 1 if (-not $keyLine) { $keyLine = $matchingLines | Where-Object { $_ -match 'ecdsa-sha2' } | Select-Object -First 1 } if (-not $keyLine) { $keyLine = $matchingLines | Where-Object { $_ -match 'ssh-rsa' } | Select-Object -First 1 } if ($keyLine -and $keyLine -match '\s+(ssh-\w+|ecdsa-sha2-\S+)\s+(\S+)') { $keyType = $matches[1] $keyData = $matches[2] $tempKeyFile = [System.IO.Path]::GetTempFileName() try { "$Server $keyType $keyData" | Out-File -FilePath $tempKeyFile -Encoding ASCII -NoNewline $fingerprintOutput = & ssh-keygen -lf $tempKeyFile -E sha256 2>$null if ($fingerprintOutput) { $fingerprint = ($fingerprintOutput -split '\s+')[1] if ($fingerprint) { # plink/pscp 的 -hostkey 格式:仅使用 SHA256:指纹(不需要算法名) # fingerprint 已经包含 SHA256: 前缀 $hostKey = $fingerprint return $hostKey } } } finally { Remove-Item -Path $tempKeyFile -ErrorAction SilentlyContinue } } } } } # 方法2: 使用 ssh-keyscan 获取服务器公钥 $keyscanOutput = ssh-keyscan -t ed25519,ecdsa,rsa $Server 2>$null if ($keyscanOutput) { $keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ssh-ed25519' } | Select-Object -First 1 if (-not $keyLine) { $keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ecdsa-sha2' } | Select-Object -First 1 } if (-not $keyLine) { $keyLine = $keyscanOutput | Where-Object { $_ -notmatch '^\s*#' -and $_ -match 'ssh-rsa' } | Select-Object -First 1 } if ($keyLine) { $keyLine = $keyLine.ToString().Trim() if ($keyLine -match '^\S+\s+(ssh-\w+)\s+(\S+)') { $keyType = $matches[1] $keyData = $matches[2] $tempKeyFile = [System.IO.Path]::GetTempFileName() try { "$keyLine" | Out-File -FilePath $tempKeyFile -Encoding ASCII -NoNewline $fingerprintOutput = & ssh-keygen -lf $tempKeyFile -E sha256 2>$null if ($fingerprintOutput) { $fingerprint = ($fingerprintOutput -split '\s+')[1] if ($fingerprint) { # plink/pscp 的 -hostkey 格式:仅使用 SHA256:指纹(不需要算法名) # fingerprint 已经包含 SHA256: 前缀 $hostKey = $fingerprint } } } finally { Remove-Item -Path $tempKeyFile -ErrorAction SilentlyContinue } } } } } catch { # 如果获取密钥失败,返回 null } return $hostKey } Write-Host "========================================" -ForegroundColor Cyan Write-Host "安全部署脚本 - 直接上传源代码" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # 检查当前目录 if (-not (Test-Path "package.json")) { Write-Host "错误: 请在项目根目录运行此脚本" -ForegroundColor Red exit 1 } # 步骤 1: 部署前检查(已移除Git检查,不需要上传到git库) if (-not $SkipCheck) { Write-Host "[1/5] 执行部署前检查..." -ForegroundColor Yellow # 只检查必要文件是否存在 if (-not (Test-Path "package.json")) { Write-Host " ✗ 未找到 package.json 文件" -ForegroundColor Red exit 1 } if (-not (Test-Path "prisma/schema.prisma")) { Write-Host " ⚠ 未找到 prisma/schema.prisma 文件" -ForegroundColor Yellow } Write-Host " ✓ 部署前检查通过" -ForegroundColor Green Write-Host "" } # 步骤 2: 打包项目文件(只包含功能性代码,排除非功能性文件) Write-Host "[2/5] 打包项目文件..." -ForegroundColor Yellow Write-Host " 注意: 只打包功能性代码,排除文档、脚本、临时文件等" -ForegroundColor Gray Write-Host " 排除内容: docs/, scripts/, *.md, uploads/, backups/, .env.local 等" -ForegroundColor Gray $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $tempFile = Join-Path $env:TEMP "project-deploy-$timestamp.tar.gz" # 排除列表(与 .dockerignore 保持一致,但适配 tar 命令) $excludes = @( # 构建产物和依赖 "node_modules", ".next", "out", "build", "dist", ".pnp", ".pnp.js", # 测试文件 "coverage", "*.test.ts", "*.test.tsx", "*.spec.ts", "*.spec.tsx", "test", "tests", "__tests__", # 日志和调试 "*.log", "logs", "*.log.*", "npm-debug.log*", "yarn-debug.log*", "yarn-error.log*", # 环境变量 ".env.local", ".env.development.local", ".env.test.local", ".env.production.local", ".env", ".env.example", ".env.template", ".env*.local", # 版本控制 ".git", ".gitignore", ".gitattributes", ".gitmodules", # IDE 和编辑器 ".vscode", ".idea", ".cursor", "*.swp", "*.swo", "*.sublime-project", "*.sublime-workspace", ".editorconfig", ".eslintcache", # 文档文件 "docs", "*.md", "README.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md", # 注意:不排除 *.txt,因为 VERSION.txt 需要被包含 # 其他不需要的 .txt 文件会在文档排除中处理 # 脚本文件 "scripts", "*.sh", "*.ps1", "*.bat", "*.py", "*.js", "!next.config.js", "!postcss.config.js", "!tailwind.config.ts", # 临时文件和缓存 "uploads", "public/uploads", "backups", "*.tmp", "*.temp", ".cache", ".tmp", "tmp", "temp", "*.cache", ".DS_Store", "Thumbs.db", # 压缩包和发布包 "*.tar.gz", "*.zip", "*.rar", "*.7z", "*.tar", "*.tgz", "release-*", "dist-*", # 数据库备份文件 "*.sql", "*.db", "*.sqlite", "*.sqlite3", # 其他文件 "*.rpm", "*.deb", "*.pem", "*.key", "*.cert", "*.crt", "frp_*", "docker-daemon-config.json" ) # 构建 tar 排除参数 $excludeArgs = $excludes | ForEach-Object { "--exclude=$_" } # 打包文件 Write-Host " 正在打包..." -ForegroundColor Gray & tar -czf $tempFile $excludeArgs * if (-not (Test-Path $tempFile)) { Write-Host " ✗ 打包失败" -ForegroundColor Red exit 1 } $fileSize = (Get-Item $tempFile).Length / 1MB Write-Host " ✓ 打包完成: $([math]::Round($fileSize, 2)) MB" -ForegroundColor Green Write-Host "" # 步骤 3: 上传到服务器 Write-Host "[3/5] 上传文件到服务器..." -ForegroundColor Yellow Write-Host " 服务器: $Server" -ForegroundColor Gray Write-Host " 用户: $User" -ForegroundColor Gray Write-Host " 项目路径: $ProjectPath" -ForegroundColor Gray Write-Host "" # 检查是否有 plink(PuTTY 工具) $plinkPath = "C:\Program Files\PuTTY\plink.exe" $pscpPath = "C:\Program Files\PuTTY\pscp.exe" $usePlink = (Test-Path $plinkPath) -and (Test-Path $pscpPath) -and ($Password -ne "") # 检查 SSH 连接 Write-Host " 检查 SSH 连接..." -ForegroundColor Gray if ($usePlink) { Write-Host " 使用 plink 进行连接(已提供密码)" -ForegroundColor Gray # 获取服务器密钥 $hostKey = Get-ServerHostKey -Server $Server if ($hostKey) { & $plinkPath -ssh -pw $Password -batch -hostkey $hostKey "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null } else { & $plinkPath -ssh -pw $Password -batch "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null } } else { Write-Host " 提示: 如果需要,请输入 SSH 密码" -ForegroundColor Yellow ssh -o ConnectTimeout=10 "${User}@${Server}" "echo SSH连接成功" 2>&1 | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Host " ✗ SSH 连接失败,请检查连接配置" -ForegroundColor Red if (-not $usePlink) { Write-Host " 提示: 请确保已配置 SSH 密钥或密码正确" -ForegroundColor Yellow } exit 1 } Write-Host " ✓ SSH 连接成功" -ForegroundColor Green Write-Host "" # 上传压缩包 Write-Host " 上传压缩包..." -ForegroundColor Gray $remoteFileName = "project-deploy.tar.gz" if ($usePlink) { # 获取服务器密钥(如果之前没有获取) if (-not $hostKey) { $hostKey = Get-ServerHostKey -Server $Server } if ($hostKey) { & $pscpPath -pw $Password -batch -hostkey $hostKey $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null } else { & $pscpPath -pw $Password -batch $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null } } else { scp $tempFile "${User}@${Server}:${ProjectPath}/$remoteFileName" 2>&1 | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Host " ✗ 上传失败" -ForegroundColor Red Remove-Item $tempFile -Force -ErrorAction SilentlyContinue exit 1 } Write-Host " ✓ 上传成功" -ForegroundColor Green Write-Host "" # 步骤 4: 在服务器上部署 Write-Host "[4/5] 在服务器上部署..." -ForegroundColor Yellow # 创建本地临时部署脚本文件 $localDeployScript = Join-Path $env:TEMP "deploy-$timestamp.sh" # 从模板文件读取 Bash 脚本内容 # 模板文件应该在脚本目录中,优先使用项目根目录的相对路径 $projectRoot = Get-Location $templatePath = Join-Path $projectRoot "scripts\deploy\deploy-remote.sh.template" # 如果找不到,尝试从脚本路径获取 if (-not (Test-Path $templatePath)) { $scriptPath = $MyInvocation.MyCommand.Path if ($scriptPath -and (Test-Path $scriptPath)) { $scriptDir = Split-Path -Parent $scriptPath $templatePath = Join-Path $scriptDir "deploy-remote.sh.template" } } if (Test-Path $templatePath) { $bashScriptTemplate = Get-Content -Path $templatePath -Raw -Encoding UTF8 } else { Write-Host " ✗ 未找到部署脚本模板: $templatePath" -ForegroundColor Red exit 1 } # 替换占位符 $bashScript = $bashScriptTemplate -replace "PROJECT_PATH_PLACEHOLDER", $ProjectPath $bashScript = $bashScript -replace "REMOTE_FILE_NAME_PLACEHOLDER", $remoteFileName # 确保使用 Unix 换行符(移除所有 \r,只保留 \n) $bashScript = $bashScript -replace "`r`n", "`n" -replace "`r", "`n" # 写入本地临时文件(使用 UTF8 无 BOM 编码) $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($localDeployScript, $bashScript, $utf8NoBom) # 上传部署脚本到服务器 $remoteDeployScript = "/tmp/deploy-$timestamp.sh" if ($usePlink) { # 获取服务器密钥(如果之前没有获取) if (-not $hostKey) { $hostKey = Get-ServerHostKey -Server $Server } if ($hostKey) { & $pscpPath -pw $Password -batch -hostkey $hostKey $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null } else { & $pscpPath -pw $Password -batch $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null } } else { scp $localDeployScript "${User}@${Server}:$remoteDeployScript" 2>&1 | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Host " ✗ 上传部署脚本失败" -ForegroundColor Red Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue exit 1 } # 在服务器上执行部署脚本 if ($usePlink) { # 获取服务器密钥(如果之前没有获取) if (-not $hostKey) { $hostKey = Get-ServerHostKey -Server $Server } if ($hostKey) { & $plinkPath -ssh -pw $Password -batch -hostkey $hostKey "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1 } else { & $plinkPath -ssh -pw $Password -batch "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1 } } else { ssh "${User}@${Server}" "chmod +x $remoteDeployScript; bash $remoteDeployScript; rm -f $remoteDeployScript" 2>&1 } if ($LASTEXITCODE -ne 0) { Write-Host " ✗ 服务器部署失败" -ForegroundColor Red Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue Remove-Item $tempFile -Force -ErrorAction SilentlyContinue exit 1 } # 清理本地临时文件 Remove-Item $localDeployScript -Force -ErrorAction SilentlyContinue Write-Host " ✓ 服务器部署成功" -ForegroundColor Green Write-Host "" # 步骤 5: 清理本地文件 Write-Host "[5/5] 清理本地文件..." -ForegroundColor Yellow Remove-Item $tempFile -Force -ErrorAction SilentlyContinue Write-Host " ✓ 清理完成" -ForegroundColor Green Write-Host "" # 部署完成 Write-Host "========================================" -ForegroundColor Cyan Write-Host "部署完成!" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Cyan Write-Host "" Write-Host "下一步:" -ForegroundColor Yellow Write-Host " 1. 访问网站检查是否正常" -ForegroundColor White Write-Host " 2. 测试关键功能" -ForegroundColor White Write-Host " 3. 检查错误日志" -ForegroundColor White $logCheckCmd = "ssh ${User}@${Server} 'cd ${ProjectPath}; docker compose logs app --tail=100'" Write-Host " $logCheckCmd" -ForegroundColor Gray Write-Host ""