<#> .SYNOPSIS 云升数码 - Windows 一键部署脚本 .DESCRIPTION 在 Windows Server / Windows 10/11 上部署云升数码展示页面 包含 PocketBase 后端、Nginx 反向代理、SSL 证书配置 .NOTES 需要管理员权限运行 支持 Windows 10/11, Windows Server 2019/2022 #> param( [Parameter(Mandatory=$false)] [string]$Domain = "", [Parameter(Mandatory=$false)] [string]$Email = "", [Parameter(Mandatory=$false)] [switch]$SkipNginx = $false, [Parameter(Mandatory=$false)] [switch]$SkipSSL = $false ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # 颜色输出函数 function Write-Log { param([string]$Message, [string]$Level = "INFO") $colors = @{ INFO = "Cyan" SUCCESS = "Green" WARN = "Yellow" ERROR = "Red" } $color = $colors[$Level] Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" -ForegroundColor $color } # 检查管理员权限 function Check-Admin { $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Log "请以管理员身份运行 PowerShell" "ERROR" exit 1 } } # 检测架构 function Get-Architecture { $arch = [Environment]::Is64BitOperatingSystem if ($arch) { return "amd64" } else { return "386" } } # 下载文件 function Download-File { param([string]$Url, [string]$OutputPath) Write-Log "下载: $Url" try { Invoke-WebRequest -Uri $Url -OutFile $OutputPath -UseBasicParsing Write-Log "下载完成: $OutputPath" "SUCCESS" } catch { Write-Log "下载失败: $_" "ERROR" throw } } # 安装 PocketBase function Install-PocketBase { $version = "0.22.0" $arch = Get-Architecture $url = "https://github.com/pocketbase/pocketbase/releases/download/v${version}/pocketbase_${version}_windows_${arch}.zip" $installDir = "C:\pocketbase" $dataDir = "$installDir\pb_data" $migrationsDir = "$installDir\pb_migrations" $publicDir = "$installDir\pb_public" Write-Log "安装 PocketBase v$version ($arch)..." # 创建目录 New-Item -ItemType Directory -Force -Path $installDir, $dataDir, $migrationsDir, $publicDir | Out-Null # 下载 $zipPath = "$env:TEMP\pocketbase.zip" Download-File $url $zipPath # 解压 Write-Log "解压..." Expand-Archive -Path $zipPath -DestinationPath $installDir -Force Remove-Item $zipPath -Force # 复制迁移文件 $projectMigrations = "$PSScriptRoot\..\apps\pb_migrations" if (Test-Path $projectMigrations) { Copy-Item -Path "$projectMigrations\*" -Destination $migrationsDir -Recurse -Force Write-Log "迁移文件已复制" "SUCCESS" } # 生成加密密钥 $encryptionKey = [Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 })) Write-Log "加密密钥: $encryptionKey" "WARN" Write-Log "请妥善保存加密密钥!" "WARN" # 创建启动脚本 $startScript = @" @echo off cd /d "$installDir" set PB_ENCRYPTION_KEY=$encryptionKey pocketbase.exe serve --http=127.0.0.1:8090 --dir="$dataDir" --publicDir="$publicDir" --migrationsDir="$migrationsDir" "@ Set-Content -Path "$installDir\start.bat" -Value $startScript -Encoding UTF8 # 创建系统服务 Write-Log "创建 Windows 服务..." $serviceName = "PocketBase" $serviceDisplayName = "云升数码 PocketBase 服务" $serviceDescription = "PocketBase 后端服务 for 云升数码" # 使用 NSSM (Non-Sucking Service Manager) 创建服务 # 这里使用 sc.exe 创建简单服务 $servicePath = "C:\Windows\System32\cmd.exe" $serviceArgs = "/c `"$installDir\start.bat`"" # 检查是否已安装 NSSM $nssmPath = "C:\pocketbase\nssm.exe" if (-not (Test-Path $nssmPath)) { Write-Log "下载 NSSM..." $nssmUrl = "https://github.com/nssm/nssm/releases/download/v2.24/nssm-2.24.zip" $nssmZip = "$env:TEMP\nssm.zip" Download-File $nssmUrl $nssmZip Expand-Archive -Path $nssmZip -DestinationPath "$env:TEMP\nssm" -Force Copy-Item "$env:TEMP\nssm\win64\nssm.exe" -Destination $nssmPath -Force Remove-Item $nssmZip -Force Remove-Item "$env:TEMP\nssm" -Recurse -Force } # 安装服务 & $nssmPath install $serviceName $servicePath $serviceArgs & $nssmPath set $serviceName DisplayName $serviceDisplayName & $nssmPath set $serviceName Description $serviceDescription & $nssmPath set $serviceName AppDirectory $installDir & $nssmPath set $serviceName AppEnvironmentExtra "PB_ENCRYPTION_KEY=$encryptionKey" & $nssmPath set $serviceName Start SERVICE_AUTO_START & $nssmPath set $serviceName AppStdout "$installDir\logs\stdout.log" & $nssmPath set $serviceName AppStderr "$installDir\logs\stderr.log" & $nssmPath set $serviceName AppRotateFiles 1 & $nssmPath set $serviceName AppRotateBytes 10485760 New-Item -ItemType Directory -Force -Path "$installDir\logs" | Out-Null Write-Log "PocketBase 服务创建完成" "SUCCESS" return @{ InstallDir = $installDir DataDir = $dataDir PublicDir = $publicDir EncryptionKey = $encryptionKey } } # 安装 Nginx function Install-Nginx { param([string]$Domain) if ($SkipNginx) { Write-Log "跳过 Nginx 安装" "WARN" return } Write-Log "安装 Nginx..." $nginxVersion = "1.25.3" $url = "https://nginx.org/download/nginx-$nginxVersion.zip" $installDir = "C:\nginx" $zipPath = "$env:TEMP\nginx.zip" Download-File $url $zipPath Expand-Archive -Path $zipPath -DestinationPath $installDir -Force Remove-Item $zipPath -Force # 找到解压后的实际目录 $extractedDir = Get-ChildItem "$installDir" -Directory | Select-Object -First 1 if ($extractedDir.FullName -ne $installDir) { Move-Item -Path "$extractedDir\*" -Destination $installDir -Force Remove-Item $extractedDir.FullName -Recurse -Force } # 配置 Nginx $nginxConf = @" # 云升数码 - Nginx 配置 worker_processes auto; error_log logs/error.log warn; pid logs/nginx.pid; events { worker_connections 1024; use iocp; } http { include mime.types; default_type application/octet-stream; log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' '\$status \$body_bytes_sent "\$http_referer" ' '"\$http_user_agent" "\$http_x_forwarded_for"'; access_log logs/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 100M; gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # HTTP 重定向到 HTTPS server { listen 80; server_name $Domain www.$Domain; location /.well-known/acme-challenge/ { root C:\nginx\acme; try_files \$uri =404; } location / { return 301 https://\$host\$request_uri; } } # HTTPS 主站点 server { listen 443 ssl http2; server_name $Domain www.$Domain; # SSL 证书路径 (Certbot 会自动配置) # ssl_certificate C:/Certbot/live/$Domain/fullchain.pem; # ssl_certificate_key C:/Certbot/live/$Domain/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; root $env:USERPROFILE\yunsheng-digital\apps\web\dist; index index.html; # 静态文件缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; try_files \$uri =404; } # SPA 路由 location / { try_files \$uri \$uri/ /index.html; } # API 代理 location /api/ { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; client_max_body_size 100M; } # WebSocket location /api/realtime { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host \$host; proxy_read_timeout 86400; } # Admin UI location /_/ { proxy_pass http://127.0.0.1:8090; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # 禁止隐藏文件 location ~ /\. { deny all; } } } "@ Set-Content -Path "$installDir\conf\nginx.conf" -Value $nginxConf -Encoding UTF8 # 创建 ACME 目录 New-Item -ItemType Directory -Force -Path "C:\nginx\acme" | Out-Null # 测试配置 & "$installDir\nginx.exe" -t Write-Log "Nginx 配置测试通过" "SUCCESS" # 创建服务 $serviceName = "nginx" $nssmPath = "C:\pocketbase\nssm.exe" & $nssmPath install $serviceName "$installDir\nginx.exe" "" & $nssmPath set $serviceName DisplayName "Nginx Web Server" & $nssmPath set $serviceName Description "Nginx 反向代理 for 云升数码" & $nssmPath set $serviceName AppDirectory $installDir & $nssmPath set $serviceName Start SERVICE_AUTO_START Write-Log "Nginx 安装完成" "SUCCESS" } # 安装 Certbot (Windows) function Install-Certbot { param([string]$Domain, [string]$Email) if ($SkipSSL) { Write-Log "跳过 SSL 证书申请" "WARN" return } if (-not $Domain -or -not $Email) { Write-Log "域名或邮箱为空,跳过 SSL 证书申请" "WARN" return } Write-Log "安装 Certbot..." # 使用 Chocolatey 安装 if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { Write-Log "安装 Chocolatey..." Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) } choco install certbot -y --no-progress # 申请证书 Write-Log "申请 SSL 证书..." certbot certonly --standalone -d $Domain -d "www.$Domain" ` --non-interactive --agree-tos --email $Email ` --preferred-challenges http Write-Log "SSL 证书申请完成" "SUCCESS" } # 创建维护脚本 function Create-MaintenanceScripts { $scriptsDir = "C:\pocketbase\scripts" New-Item -ItemType Directory -Force -Path $scriptsDir | Out-Null # 备份脚本 $backupScript = @" @echo off set BACKUP_DIR=C:\backup\yunsheng-digital set DATE=%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2% set DATE=%DATE: =0% mkdir "%BACKUP_DIR%" 2>nul echo [INFO] 开始备份... REM 备份 PocketBase 数据 C:\pocketbase\pocketbase.exe dump --dir=C:\pocketbase\pb_data --output="%BACKUP_DIR%\pocketbase_%DATE%.zip" REM 备份 Nginx 配置 copy C:\nginx\conf\nginx.conf "%BACKUP_DIR%\nginx_%DATE%.conf" REM 清理 30 天前的备份 forfiles /p "%BACKUP_DIR%" /s /m *.* /d -30 /c "cmd /c del @path" echo [SUCCESS] 备份完成 "@ Set-Content -Path "$scriptsDir\backup.bat" -Value $backupScript -Encoding UTF8 # 更新脚本 $updateScript = @" @echo off cd /d "%USERPROFILE%\yunsheng-digital" echo [INFO] 拉取最新代码... git pull origin master echo [INFO] 安装依赖... cd apps\web pnpm install --frozen-lockfile echo [INFO] 构建前端... pnpm build echo [INFO] 部署构建产物... xcopy /E /Y /I dist\* C:\pocketbase\pb_public\ echo [INFO] 重启服务... net stop nginx net stop PocketBase timeout /t 3 net start PocketBase timeout /t 3 net start nginx echo [SUCCESS] 更新完成 "@ Set-Content -Path "$scriptsDir\update.bat" -Value $updateScript -Encoding UTF8 # 状态脚本 $statusScript = @" @echo off echo === 云升数码 服务状态 === echo. echo --- PocketBase --- sc query PocketBase echo. echo --- Nginx --- sc query nginx echo. echo --- 磁盘使用 --- wmic logicaldisk get size,freespace,caption echo. echo --- 网络连接 --- netstat -an | findstr "80 443 8090" "@ Set-Content -Path "$scriptsDir\status.bat" -Value $statusScript -Encoding UTF8 Write-Log "维护脚本创建完成" "SUCCESS" } # 配置防火墙 function Configure-Firewall { Write-Log "配置 Windows 防火墙..." # 允许 HTTP/HTTPS New-NetFirewallRule -DisplayName "云升数码 HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "云升数码 HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "云升数码 PocketBase" -Direction Inbound -Protocol TCP -LocalPort 8090 -Action Allow -Profile Private -ErrorAction SilentlyContinue Write-Log "防火墙配置完成" "SUCCESS" } # 创建计划任务 function Create-ScheduledTasks { Write-Log "创建计划任务..." # 每天备份 $action = New-ScheduledTaskAction -Execute "C:\pocketbase\scripts\backup.bat" $trigger = New-ScheduledTaskTrigger -Daily -At 3am $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd Register-ScheduledTask -TaskName "云升数码-每日备份" -Action $action -Trigger $trigger -Settings $settings -Force -ErrorAction SilentlyContinue # 每周清理日志 $action2 = New-ScheduledTaskAction -Execute "wevtutil.exe" -Argument "cl System /bu:C:\backup\system_logs\system_%date%.evtx" $trigger2 = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 4am Register-ScheduledTask -TaskName "云升数码-周日志清理" -Action $action2 -Trigger $trigger2 -Settings $settings -Force -ErrorAction SilentlyContinue Write-Log "计划任务创建完成" "SUCCESS" } # 主函数 function Main { Write-Host "==========================================" -ForegroundColor Cyan Write-Host " 云升数码 - Windows 一键部署脚本 v1.0" -ForegroundColor Cyan Write-Host "==========================================" -ForegroundColor Cyan Write-Host "" Check-Admin # 获取参数 if (-not $Domain) { $Domain = Read-Host "请输入域名 (例: yunsheng.digital)" } if (-not $Email) { $Email = Read-Host "请输入邮箱 (用于 SSL 证书)" } Write-Log "开始部署..." Write-Log "域名: $Domain" Write-Log "邮箱: $Email" # 克隆/更新代码 $projectDir = "$env:USERPROFILE\yunsheng-digital" if (Test-Path $projectDir) { Write-Log "更新现有代码..." cd $projectDir git pull origin master } else { Write-Log "克隆仓库..." git clone https://git.grxiao.cn/yuns/plerr-open.git $projectDir } # 安装 Node.js (如果需要) if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Log "安装 Node.js..." choco install nodejs-lts -y --no-progress refreshenv } # 安装 pnpm if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { Write-Log "安装 pnpm..." npm install -g pnpm } # 构建前端 Write-Log "构建前端..." cd "$projectDir\apps\web" pnpm install --frozen-lockfile pnpm build # 复制构建产物到 PocketBase public 目录 $pbPublic = "C:\pocketbase\pb_public" New-Item -ItemType Directory -Force -Path $pbPublic | Out-Null Copy-Item -Path "dist\*" -Destination $pbPublic -Recurse -Force Write-Log "前端构建产物已部署" "SUCCESS" # 安装后端服务 $pbInfo = Install-PocketBase # 安装 Nginx Install-Nginx -Domain $Domain # 配置防火墙 Configure-Firewall # 创建维护脚本 Create-MaintenanceScripts # 创建计划任务 Create-ScheduledTasks # SSL 证书 Install-Certbot -Domain $Domain -Email $Email # 启动服务 Write-Log "启动服务..." net start PocketBase Start-Sleep -Seconds 3 net start nginx Write-Host "" Write-Host "==========================================" -ForegroundColor Green Write-Host " 部署完成!" -ForegroundColor Green Write-Host "==========================================" -ForegroundColor Green Write-Host "" Write-Host "📋 重要信息:" -ForegroundColor Cyan Write-Host " - 网站地址: https://$Domain" Write-Host " - 后台管理: https://$Domain/_/" Write-Host " - 管理员账号: admin@yunsheng.digital" Write-Host " - 管理员密码: YunSheng@2024!Admin" Write-Host " - PocketBase 目录: C:\pocketbase" Write-Host " - 前端构建目录: C:\pocketbase\pb_public" Write-Host "" Write-Host "🔐 安全提醒:" -ForegroundColor Yellow Write-Host " 1. 请立即登录后台修改默认管理员密码" Write-Host " 2. 加密密钥已保存在服务配置中" Write-Host " 3. 建议配置 RDP 限制、启用 BitLocker" Write-Host "" Write-Host "🛠️ 常用命令:" -ForegroundColor Cyan Write-Host " - 查看状态: C:\pocketbase\scripts\status.bat" Write-Host " - 备份数据: C:\pocketbase\scripts\backup.bat" Write-Host " - 更新代码: C:\pocketbase\scripts\update.bat" Write-Host " - 查看日志: C:\pocketbase\logs\" Write-Host "" } Main