[CmdletBinding()] param() # 仅做离线检查,不启动 Java、Maven 或 Docker,不连接数据库和云服务。 $ErrorActionPreference = 'Stop' $launcher = Join-Path $PSScriptRoot 'Start-Local.ps1' $tokens = $null $parseErrors = $null [System.Management.Automation.Language.Parser]::ParseFile($launcher, [ref]$tokens, [ref]$parseErrors) | Out-Null if ($parseErrors.Count) { throw 'Start-Local.ps1 语法检查失败。' } $fixtureDirectory = Join-Path ([IO.Path]::GetTempPath()) ('qinghe-deployment-check-' + [Guid]::NewGuid().ToString('N')) [void](New-Item -ItemType Directory -Path $fixtureDirectory) $fixtureFile = Join-Path $fixtureDirectory '.env' $utf8Bom = New-Object System.Text.UTF8Encoding($true) $baseFixture = @' DB_PASSWORD=test-db-value MYSQL_ROOT_PASSWORD=test-root-value REDIS_PASSWORD=test-redis-value RABBITMQ_PASSWORD=test-rabbit-value JWT_SECRET=test-only-secret-with-at-least-32-bytes-000 '@ $passedCases = 0 function Assert-ConfigurationCase { param([string]$Name, [string]$Content, [bool]$ShouldPass, [string]$ExpectedMessage) [IO.File]::WriteAllText($fixtureFile, $Content, $utf8Bom) $didPass = $true $failureMessage = '' try { & $launcher -EnvFile $fixtureFile -CheckOnly } catch { $didPass = $false $failureMessage = $_.Exception.Message } if ($didPass -ne $ShouldPass) { throw "用例失败:$Name,结果不符合预期。" } if (-not $ShouldPass -and -not $failureMessage.Contains($ExpectedMessage)) { throw "用例失败:$Name,未得到预期的配置错误提示。" } Write-Host "PASS: $Name" } try { Assert-ConfigurationCase '合法配置' $baseFixture $true '' $passedCases++ Assert-ConfigurationCase '拒绝短 JWT secret' ($baseFixture.Replace('test-only-secret-with-at-least-32-bytes-000', 'short')) $false '至少需要 32 字节' $passedCases++ Assert-ConfigurationCase '拒绝未修改的示例密码' ($baseFixture.Replace('test-db-value', 'replace-me-db-password')) $false 'DB_PASSWORD' $passedCases++ Assert-ConfigurationCase '拒绝缺失密码' ($baseFixture.Replace('REDIS_PASSWORD=test-redis-value', 'REDIS_PASSWORD=')) $false 'REDIS_PASSWORD' $passedCases++ Assert-ConfigurationCase '拒绝重复变量' ($baseFixture + "`nDB_PASSWORD=second-value") $false '重复配置名' $passedCases++ Assert-ConfigurationCase '拒绝未知变量' ($baseFixture + "`nPATH=unknown") $false '未知配置名' $passedCases++ Assert-ConfigurationCase '拒绝脚本表达式' ($baseFixture + "`nOSS_DOMAIN=" + '$(Get-Process)') $false '不能包含变量引用' $passedCases++ Assert-ConfigurationCase '拒绝带引号的值' ($baseFixture + "`nOSS_DOMAIN=" + '"localhost"') $false '未加引号' $passedCases++ $examplePath = Join-Path $PSScriptRoot '.env.example' $exampleContent = [IO.File]::ReadAllText($examplePath) Assert-ConfigurationCase '原始示例必须先填写' $exampleContent $false 'DB_PASSWORD' $passedCases++ # 用局部 Java 替身检查启动参数及环境恢复,不启动 JVM 或访问任何服务。 if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'target/portable/campus-server-1.0.0.jar')) { $javaObservation = @{ Arguments = @(); DatabasePassword = $null } function java { $javaObservation.Arguments = @($args) $javaObservation.DatabasePassword = [Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') $global:LASTEXITCODE = 0 } $previousPassword = [Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') try { [Environment]::SetEnvironmentVariable('DB_PASSWORD', 'outer-test-sentinel', 'Process') [IO.File]::WriteAllText($fixtureFile, $baseFixture, $utf8Bom) & $launcher -EnvFile $fixtureFile -SkipBuild if ($javaObservation.DatabasePassword -ne 'test-db-value') { throw '启动时未正确注入本地变量。' } $locationArguments = @($javaObservation.Arguments | Where-Object { $_ -like '--spring.config.location=file:*application.example.yml' }) if ($locationArguments.Count -ne 1) { throw '未显式指定独立模板。' } if ([Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') -ne 'outer-test-sentinel') { throw '退出后没有恢复原进程环境变量。' } Write-Host 'PASS: 独立配置参数、环境注入及退出恢复(Java 替身,无外部连接)' $passedCases++ } finally { if ($null -eq $previousPassword) { Remove-Item -LiteralPath Env:DB_PASSWORD -ErrorAction SilentlyContinue } else { [Environment]::SetEnvironmentVariable('DB_PASSWORD', $previousPassword, 'Process') } Remove-Item -LiteralPath Function:\java } } $jarPath = Join-Path $PSScriptRoot 'target/portable/campus-server-1.0.0.jar' if (Test-Path -LiteralPath $jarPath -PathType Leaf) { Add-Type -AssemblyName System.IO.Compression.FileSystem $archive = [IO.Compression.ZipFile]::OpenRead($jarPath) try { $privateEntries = @($archive.Entries | Where-Object { $_.FullName -match '^BOOT-INF/classes/(.*/)?(application[^/]*\.(yml|yaml|properties)|\.env[^/]*)$' }) if ($privateEntries.Count) { throw '部署 JAR 混入应用配置或 .env 文件,禁止分发。' } if (-not $archive.GetEntry('BOOT-INF/classes/com/campus/CampusApplication.class')) { throw '部署 JAR 缺少应用主类。' } } finally { $archive.Dispose() } Write-Host 'PASS: 部署 JAR 不含约定的私有应用配置,且存在主类' $passedCases++ } else { Write-Host 'SKIP: 未找到 portable JAR,打包内容尚未检查。' } Write-Host "离线检查通过:$passedCases 项。未连接外部服务。" } finally { # 只清理本脚本刚创建、路径已核对的临时夹;不触及用户环境文件。 $resolvedFixture = [IO.Path]::GetFullPath($fixtureDirectory) $resolvedTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\' if (-not $resolvedFixture.StartsWith($resolvedTemp, [StringComparison]::OrdinalIgnoreCase) -or -not ([IO.Path]::GetFileName($resolvedFixture)).StartsWith('qinghe-deployment-check-')) { throw '临时检查目录超出预期路径,未清理。' } Remove-Item -LiteralPath $resolvedFixture -Recurse -Force }