| | |
| | | 用法: |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -OutRoot "E:\试验" -SkipFrontend |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -NoFrontendDist -SkipServiceWrapper |
| | | —— 单端口发布:不生成独立的 frontend\dist 目录(仍构建并同步 backend\web,单端口必需); |
| | | 服务器已装服务时配合 -SkipServiceWrapper 去掉约 17 MB 的 WinSW,可显著减小包体积。 |
| | | |
| | | 做五件事: |
| | | 1) 用 deploy-templates\jar-application.yml(全占位符)临时替换源码 application.yml, |
| | | 执行 mvn clean package;无论成败都还原开发机真实配置 |
| | | 2) 解压 jar 检查内置 application.yml:必须全是占位符(禁止 sk- 密钥 / 明文口令),不合格直接中止 |
| | | 3) npm run build 重建前端 dist(可 -SkipFrontend) |
| | | 4) 组装完整部署包:backend(jar + application.yml + 启动脚本 + docs + web) / frontend/dist / db / README |
| | | 4) 组装完整部署包:backend(jar + application.yml + 启动脚本 + 服务包装器 + docs + web) / frontend/dist(可选,-NoFrontendDist 不打) / db / README |
| | | Windows 服务包装器 WinSW:优先用 deploy-templates\tools\winsw.exe 缓存(校验 SHA-256), |
| | | 缺失时自动下载 v2.12.0 x64 到该缓存目录;随包输出为 backend\trafficAudit.exe |
| | | 5) 对组装后的整包再扫一遍明文密钥 |
| | | |
| | | 修订背景(2026-10-03):2026-10-02 手工产出的包缺 application.yml / docs / db / 启动脚本, |
| | |
| | | param( |
| | | [string]$OutRoot = '', |
| | | [switch]$SkipFrontend, |
| | | [switch]$SkipBackend # 复用已存在的 jar(不重编译、不动源码配置),仅用于补全/重组发布包 |
| | | [switch]$NoFrontendDist, # 单端口发布:不打独立的 frontend\dist 目录,但仍构建/同步 backend\web(单端口必需) |
| | | [switch]$SkipBackend, # 复用已存在的 jar(不重编译、不动源码配置),仅用于补全/重组发布包 |
| | | [switch]$SkipServiceWrapper # 不把 WinSW 服务包装器打进包(即不提供“注册成 Windows 服务”的开机自启) |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | $root = Split-Path -Parent $MyInvocation.MyCommand.Path |
| | | $pomPath = Join-Path $root 'traffic-audit-server\pom.xml' |
| | | if (-not (Test-Path $pomPath)) { throw "Missing Maven project: $pomPath" } |
| | | [xml]$pom = Get-Content -LiteralPath $pomPath -Raw -Encoding UTF8 |
| | | $productVersion = [string]$pom.project.version |
| | | if (-not $productVersion) { throw 'Missing <version> in traffic-audit-server\pom.xml' } |
| | | if ($productVersion -match '(?i)-SNAPSHOT$') { throw "Release builds must not use a SNAPSHOT version: $productVersion" } |
| | | if (-not $OutRoot) { $OutRoot = Join-Path $root 'deploy' } # 默认输出到仓库 deploy\(已 gitignore) |
| | | $tplDir = Join-Path $root 'deploy-templates' # 打包模板件(已入库) |
| | | $tplJarCfg = Join-Path $tplDir 'jar-application.yml' # 进 jar 的占位符配置 |
| | | $srcCfg = Join-Path $root 'traffic-audit-server\src\main\resources\application.yml' |
| | | $jarRel = 'traffic-audit-server\target\traffic-audit-server-1.0.0-SNAPSHOT.jar' |
| | | $jarRel = 'traffic-audit-server\target\traffic-audit-server.jar' |
| | | $jar = Join-Path $root $jarRel |
| | | $webDist = Join-Path $root 'traffic-audit-web\dist' |
| | | |
| | |
| | | $sr = New-Object System.IO.StreamReader($entry.Open()) |
| | | $text = $sr.ReadToEnd() |
| | | $sr.Close() |
| | | |
| | | $versionEntry = $zip.Entries | Where-Object { $_.FullName -eq 'BOOT-INF/classes/app-version.properties' } |
| | | if (-not $versionEntry) { throw 'jar 内缺少 BOOT-INF/classes/app-version.properties' } |
| | | $vr = New-Object System.IO.StreamReader($versionEntry.Open()) |
| | | $versionText = $vr.ReadToEnd() |
| | | $vr.Close() |
| | | |
| | | $manifestEntry = $zip.Entries | Where-Object { $_.FullName -eq 'META-INF/MANIFEST.MF' } |
| | | if (-not $manifestEntry) { throw 'jar 内缺少 META-INF/MANIFEST.MF' } |
| | | $mr = New-Object System.IO.StreamReader($manifestEntry.Open()) |
| | | $manifestText = $mr.ReadToEnd() |
| | | $mr.Close() |
| | | } finally { $zip.Dispose() } |
| | | |
| | | function Get-SecretHits([string]$ymlText) { |
| | |
| | | } |
| | | Write-Host ' 通过:jar 内置配置全部为占位符,无明文密钥/口令。' |
| | | |
| | | $builtVersion = '' |
| | | if ($versionText -match '(?m)^\s*version\s*=\s*(.+?)\s*$') { $builtVersion = $matches[1].Trim() } |
| | | if ($builtVersion -ne $productVersion) { |
| | | throw "版本不一致:pom.xml=$productVersion, app-version.properties=$builtVersion" |
| | | } |
| | | |
| | | $manifestVersion = '' |
| | | if ($manifestText -match '(?m)^Implementation-Version:\s*(.+?)\s*$') { $manifestVersion = $matches[1].Trim() } |
| | | if ($manifestVersion -ne $productVersion) { |
| | | throw "版本不一致:pom.xml=$productVersion, MANIFEST.MF=$manifestVersion" |
| | | } |
| | | Write-Host " 版本自检通过:$productVersion (pom / app-version.properties / MANIFEST.MF)" |
| | | |
| | | # ---------- [3/5] 前端构建 ---------- |
| | | # 单端口部署必须有 backend\web,所以即使 -NoFrontendDist 也需要 dist;只有 -SkipFrontend 才复用现有 dist。 |
| | | Write-Host '[3/5] 构建前端(npm run build,可 -SkipFrontend 跳过)...' |
| | | if (-not $SkipFrontend) { |
| | | Push-Location (Join-Path $root 'traffic-audit-web') |
| | |
| | | } |
| | | if (-not (Test-Path $webDist)) { throw "未找到前端产物:$webDist(去掉 -SkipFrontend 重新构建)" } |
| | | |
| | | # ---------- WinSW(Windows 服务包装器)准备:本地缓存优先,缺失时从官方发布页下载并校验 SHA-256 ---------- |
| | | # WinSW v2.12.0 官方产物未做 Authenticode 签名,所以以固定 SHA-256 作为完整性校验。 |
| | | $winswUrl = 'https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe' |
| | | $winswSha256 = '05B82D46AD331CC16BDC00DE5C6332C1EF818DF8CEEFCD49C726553209B3A0DA' |
| | | $winswSrc = Join-Path $tplDir 'tools\winsw.exe' |
| | | |
| | | function Test-WinswCache { |
| | | param([string]$Path) |
| | | if (-not (Test-Path -LiteralPath $Path)) { return $false } |
| | | $hash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash |
| | | if ($hash -ne $winswSha256) { |
| | | Write-Host (' 警告:WinSW 缓存 SHA-256 不匹配,将重新下载(期望 {0}...,实际 {1}...)' -f $winswSha256.Substring(0, 12), $hash.Substring(0, 12)) |
| | | return $false |
| | | } |
| | | return $true |
| | | } |
| | | |
| | | if ($SkipServiceWrapper) { |
| | | Write-Host ' 已按 -SkipServiceWrapper 跳过:包内不含 Windows 服务方式开机自启' |
| | | } else { |
| | | if (-not (Test-WinswCache $winswSrc)) { |
| | | New-Item -ItemType Directory -Force -Path (Split-Path -Parent $winswSrc) | Out-Null |
| | | Write-Host ' 下载 WinSW v2.12.0 x64 ...' |
| | | try { |
| | | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 |
| | | Invoke-WebRequest -Uri $winswUrl -OutFile $winswSrc -UseBasicParsing |
| | | } catch { |
| | | throw ("下载 WinSW 失败:{0}。`n请手动下载 {1} 存为 {2} 后重试,或用 -SkipServiceWrapper 跳过。" -f $_.Exception.Message, $winswUrl, $winswSrc) |
| | | } |
| | | if (-not (Test-WinswCache $winswSrc)) { throw "WinSW 校验失败,已中止打包:$winswSrc" } |
| | | } |
| | | Write-Host (' WinSW 就绪(SHA-256 已校验):{0:N1} MB' -f ((Get-Item -LiteralPath $winswSrc).Length / 1MB)) |
| | | } |
| | | |
| | | # ---------- [4/5] 组装完整部署目录 ---------- |
| | | Write-Host '[4/5] 组装部署目录...' |
| | | $stamp = Get-Date -Format 'yyyyMMdd' |
| | | $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp) |
| | | $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-v" + $productVersion) |
| | | $n = 2 |
| | | while (Test-Path $dest) { $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-" + $n); $n++ } |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'frontend'), (Join-Path $dest 'db') | Out-Null |
| | | while (Test-Path $dest) { $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-v" + $productVersion + "-" + $n); $n++ } |
| | | if ($NoFrontendDist) { |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'db') | Out-Null |
| | | } else { |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'frontend'), (Join-Path $dest 'db') | Out-Null |
| | | } |
| | | |
| | | Copy-Item $jar (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\application.yml') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend.cmd') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend.sh') (Join-Path $dest 'backend') -Force |
| | | Copy-Item $webDist (Join-Path $dest 'frontend') -Recurse -Force |
| | | Copy-Item (Join-Path $tplDir 'frontend\nginx.conf.example') (Join-Path $dest 'frontend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend-service.cmd') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-autostart.ps1') (Join-Path $dest 'backend') -Force |
| | | if (-not $SkipServiceWrapper) { |
| | | # WinSW 要求配置文件名与 exe 同名:trafficAudit.exe 读同目录的 trafficAudit.xml |
| | | Copy-Item -LiteralPath $winswSrc (Join-Path $dest 'backend\trafficAudit.exe') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\trafficAudit.xml') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-service.ps1') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-service.cmd') (Join-Path $dest 'backend') -Force |
| | | } |
| | | if (-not $NoFrontendDist) { |
| | | Copy-Item $webDist (Join-Path $dest 'frontend') -Recurse -Force |
| | | Copy-Item (Join-Path $tplDir 'frontend\nginx.conf.example') (Join-Path $dest 'frontend') -Force |
| | | } |
| | | Copy-Item (Join-Path $tplDir 'db\*.sql') (Join-Path $dest 'db') -Force |
| | | |
| | | # 单端口 8090 模式:后端从 jar 同级 web\ 读前端静态文件,必须和 dist 内容一致 |
| | | # 单端口 8090 模式:后端从 jar 同级 web\ 读前端静态文件,必须和 dist 内容一致。 |
| | | # 单端口发布(-NoFrontendDist)同样必须同步,否则页面 404。 |
| | | Copy-Item $webDist (Join-Path $dest 'backend\web') -Recurse -Force |
| | | |
| | | # docs:运行时模板 + 用户文档;排除开发过程文档与缓存/备份(减小体积、避免把内部清单带出去) |
| | |
| | | cmd /c "robocopy `"$root\docs`" `"$docsDest`" /E /NFL /NDL /NJH /NJS /NP /XD _cache 工作日结 功能测试报告 问题汇总 /XF 生成_*.xlsx _bak_* >nul" |
| | | if ($LASTEXITCODE -ge 8) { throw "docs 复制失败(robocopy 返回 $LASTEXITCODE)" } |
| | | # db 脚本也放一份到 docs(与旧包一致:docs/init.sql 等) |
| | | Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_previous_day_migration.sql'), (Join-Path $tplDir 'db\sql_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | |
| | | # README:用模板并替换日期占位 |
| | | $readme = Get-Content (Join-Path $tplDir 'README-部署说明.txt') -Raw -Encoding UTF8 |
| | | $readme = $readme.Replace('{stamp}', $stamp) |
| | | $readme = $readme.Replace('{stamp}', $stamp).Replace('{version}', $productVersion) |
| | | Set-Content -LiteralPath (Join-Path $dest 'README-部署说明.txt') -Value $readme -Encoding UTF8 |
| | | |
| | | # ---------- [5/5] 整包明文密钥扫描 + 汇总 ---------- |