flnxi-Wu/scripts/deploy/deploy-to-server.ps1
WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

447 lines
16 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ""