1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<#
  一键打包部署包(含"去密自检")—— 2026-09-27 新增
  用法:
    powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -OutRoot "E:\试验" -SkipFrontend
  做四件事:
    1) mvn clean package 构建后端 jar
    2) 解压 jar 检查内置 application.yml:必须全是占位符(禁止 sk- 密钥 / 明文口令),不合格直接中止
    3) npm run build 构建前端 dist(可 -SkipFrontend)
    4) 组装 deploy 目录结构(backend / frontend / db / README-部署说明.txt),输出到 <OutRoot>\traffic-audit-deploy-<yyyyMMdd>
#>
param(
  [string]$OutRoot = 'E:\试验',
  [switch]$SkipFrontend
)
 
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$tplDir = Join-Path $root 'deploy-templates'                  # 打包模板件(已入库,2026-09-27 起)
$jarRel = 'traffic-audit-server\target\traffic-audit-server-1.0.0-SNAPSHOT.jar'
$jar = Join-Path $root $jarRel
 
Write-Host '[1/5] 构建后端(mvn clean package -DskipTests)...'
Push-Location $root
cmd /c "mvn.cmd -q clean package -DskipTests -f traffic-audit-server\pom.xml > pack-build.log 2>&1"
$rc = $LASTEXITCODE
Pop-Location
if ($rc -ne 0) { throw '后端构建失败,详见 pack-build.log' }
if (-not (Test-Path $jar)) { throw "未找到构建产物:$jar" }
Write-Host ("      产物:{0:N1} MB" -f ((Get-Item $jar).Length / 1MB))
 
Write-Host '[2/5] 去密自检:解压 jar 检查 BOOT-INF/classes/application.yml ...'
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($jar)
try {
  $entry = $zip.Entries | Where-Object { $_.FullName -eq 'BOOT-INF/classes/application.yml' }
  if (-not $entry) { throw 'jar 内缺少 BOOT-INF/classes/application.yml' }
  $sr = New-Object System.IO.StreamReader($entry.Open())
  $text = $sr.ReadToEnd()
  $sr.Close()
} finally { $zip.Dispose() }
 
$bad = @()
if ($text -match 'sk-[A-Za-z0-9]{16,}') { $bad += '疑似明文 API key(sk-…)' }
foreach ($line in ($text -split "`n")) {
  if ($line -match '^\s*(password|api-key|secret)\s*:\s*(.+)$') {
    $v = $matches[2].Trim().Trim('"')
    if ($v -and -not $v.StartsWith('${')) { $bad += ("$($matches[1]) 不是占位符") }
  }
}
if ($bad.Count -gt 0) {
  throw ('去密自检未通过:' + ($bad -join ';') + '。请检查 traffic-audit-server\src\main\resources\application.yml')
}
Write-Host '      通过:jar 内置配置全部为占位符,无明文密钥/口令。'
 
Write-Host '[3/5] 构建前端(npm run build,可 -SkipFrontend 跳过)...'
if (-not $SkipFrontend) {
  Push-Location (Join-Path $root 'traffic-audit-web')
  cmd /c "npm.cmd run build > ..\pack-front.log 2>&1"
  $rc2 = $LASTEXITCODE
  Pop-Location
  if ($rc2 -ne 0) { throw '前端构建失败,详见 pack-front.log' }
  Write-Host '      前端 dist 已重建'
}
 
Write-Host '[4/5] 组装部署目录...'
$stamp = Get-Date -Format 'yyyyMMdd'
$dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp)
$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
 
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 (Join-Path $root 'traffic-audit-web\dist') (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
 
# docs:运行时模板 + 用户文档;排除开发过程文档与缓存/备份(减小体积、避免把内部清单带出去)
$docsDest = Join-Path $dest 'backend\docs'
New-Item -ItemType Directory -Force -Path $docsDest | Out-Null
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') $docsDest -Force -ErrorAction SilentlyContinue
 
# README:用旧包模板并替换日期占位
$readme = Get-Content (Join-Path $tplDir 'README-部署说明.txt') -Raw -Encoding UTF8
$readme = $readme.Replace('{stamp}', $stamp)
Set-Content -LiteralPath (Join-Path $dest 'README-部署说明.txt') -Value $readme -Encoding UTF8
 
Write-Host '[5/5] 汇总...'
$files = Get-ChildItem $dest -Recurse -File
Write-Host ("      输出目录:{0}" -f $dest)
Write-Host ("      文件数:{0} 合计:{1:N2} MB" -f $files.Count, (($files | Measure-Object Length -Sum).Sum / 1MB))
Write-Host '      提醒:把包拷到部署机后,只需改 backend\application.yml 里的数据库三处 + 需要时填 deepseek.api-key。'