第一阶段迁移已完成。
已实现: ASP.NET Core 10 分层解决方案:[Eis.slnx](C:/Users/BI/Documents/EIS-dotnet/Eis.slnx) ASP.NET Core 统一入口与健康检查:[Program.cs](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Web/Program.cs) 原前端静态资源无修改托管 未迁移 API 自动转发至旧 Node 服务,兼容 JSON、Cookie、请求体和文件下载 可重复执行的端到端测试:[smoke-dotnet-migration.ps1](C:/Users/BI/Documents/EIS-dotnet/scripts/smoke-dotnet-migration.ps1) 迁移进度与运行说明:[MIGRATION.md](C:/Users/BI/Documents/EIS-dotnet/MIGRATION.md) 验证结果:
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
$testDirectory = Join-Path $temporaryRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N'))
|
||||
$nodeProcess = $null
|
||||
$dotnetProcess = $null
|
||||
|
||||
function Get-AvailableTcpPort {
|
||||
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||
try {
|
||||
$listener.Start()
|
||||
return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port
|
||||
}
|
||||
finally {
|
||||
$listener.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
function Start-TestProcess {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $FileName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string[]] $ArgumentList,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[hashtable] $Environment
|
||||
)
|
||||
|
||||
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $FileName
|
||||
$startInfo.WorkingDirectory = $repositoryRoot
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
foreach ($argument in $ArgumentList) {
|
||||
$startInfo.ArgumentList.Add($argument)
|
||||
}
|
||||
foreach ($entry in $Environment.GetEnumerator()) {
|
||||
$startInfo.Environment[$entry.Key] = [string] $entry.Value
|
||||
}
|
||||
|
||||
return [System.Diagnostics.Process]::Start($startInfo)
|
||||
}
|
||||
|
||||
function Wait-ForUrl {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[uri] $Uri,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[System.Diagnostics.Process[]] $Processes
|
||||
)
|
||||
|
||||
$deadline = [DateTimeOffset]::UtcNow.AddSeconds(25)
|
||||
while ([DateTimeOffset]::UtcNow -lt $deadline) {
|
||||
foreach ($process in $Processes) {
|
||||
if ($process.HasExited) {
|
||||
$standardOutput = $process.StandardOutput.ReadToEnd()
|
||||
$standardError = $process.StandardError.ReadToEnd()
|
||||
throw "Process $($process.Id) exited with code $($process.ExitCode) before $Uri became ready`n$standardOutput`n$standardError"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Uri -TimeoutSec 2
|
||||
if ($response.StatusCode -eq 200) {
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
}
|
||||
|
||||
throw "Timed out waiting for $Uri"
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $testDirectory | Out-Null
|
||||
|
||||
$legacyPort = Get-AvailableTcpPort
|
||||
$webPort = Get-AvailableTcpPort
|
||||
$legacyBaseUrl = "http://127.0.0.1:$legacyPort"
|
||||
$webBaseUrl = "http://127.0.0.1:$webPort"
|
||||
|
||||
$nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source
|
||||
$dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source
|
||||
$nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment @{
|
||||
NODE_ENV = 'test'
|
||||
HOST = '127.0.0.1'
|
||||
PORT = [string] $legacyPort
|
||||
DATABASE_CLIENT = 'sqlite'
|
||||
SQLITE_PATH = (Join-Path $testDirectory 'smoke.sqlite')
|
||||
INITIAL_ADMIN_USERNAME = 'migration_admin'
|
||||
INITIAL_ADMIN_PASSWORD = 'Migration123!'
|
||||
INITIAL_ADMIN_DISPLAY_NAME = '迁移测试管理员'
|
||||
REDIS_URL = ''
|
||||
REDIS_SESSION_URL = ''
|
||||
TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum'
|
||||
DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum'
|
||||
}
|
||||
|
||||
$projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj'
|
||||
$dotnetProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @(
|
||||
'run',
|
||||
'--project', $projectPath,
|
||||
'--configuration', 'Release',
|
||||
'--no-build',
|
||||
'--no-launch-profile',
|
||||
'--',
|
||||
'--urls', $webBaseUrl
|
||||
) -Environment @{
|
||||
ASPNETCORE_ENVIRONMENT = 'Development'
|
||||
LegacyNode__Enabled = 'true'
|
||||
LegacyNode__BaseUrl = $legacyBaseUrl
|
||||
}
|
||||
|
||||
Wait-ForUrl -Uri "$webBaseUrl/health/live" -Processes @($nodeProcess, $dotnetProcess)
|
||||
Wait-ForUrl -Uri "$webBaseUrl/health/migration" -Processes @($nodeProcess, $dotnetProcess)
|
||||
|
||||
$index = Invoke-WebRequest -Uri "$webBaseUrl/"
|
||||
if ($index.Content -notmatch '衡准') {
|
||||
throw 'ASP.NET Core did not serve the existing index.html'
|
||||
}
|
||||
|
||||
$clientModule = Invoke-WebRequest -Uri "$webBaseUrl/src/client/api.mjs"
|
||||
if ($clientModule.Content -notmatch 'export function api') {
|
||||
throw 'ASP.NET Core did not serve the existing client module'
|
||||
}
|
||||
|
||||
$homePayload = Invoke-RestMethod -Uri "$webBaseUrl/api/public/home"
|
||||
if ($homePayload.ok -ne $true) {
|
||||
throw 'Legacy public API proxy did not preserve the JSON response'
|
||||
}
|
||||
|
||||
$session = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
|
||||
$loginBody = @{ username = 'migration_admin'; password = 'Migration123!' } | ConvertTo-Json -Compress
|
||||
$login = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $session
|
||||
if ($login.ok -ne $true -or $login.user.username -ne 'migration_admin') {
|
||||
throw 'Legacy login API proxy did not preserve the response'
|
||||
}
|
||||
|
||||
$currentUser = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/me" -WebSession $session
|
||||
if ($currentUser.user.username -ne 'migration_admin') {
|
||||
throw 'Legacy API proxy did not preserve the session cookie'
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
AspNetCoreHost = 'passed'
|
||||
StaticAssets = 'passed'
|
||||
JsonProxy = 'passed'
|
||||
SessionCookie = 'passed'
|
||||
} | Format-List
|
||||
}
|
||||
finally {
|
||||
foreach ($process in @($dotnetProcess, $nodeProcess)) {
|
||||
if ($null -ne $process -and -not $process.HasExited) {
|
||||
$process.Kill($true)
|
||||
$process.WaitForExit()
|
||||
}
|
||||
if ($null -ne $process) {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $testDirectory) {
|
||||
$resolvedTestDirectory = [System.IO.Path]::GetFullPath($testDirectory)
|
||||
if (-not $resolvedTestDirectory.StartsWith($temporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($resolvedTestDirectory)).StartsWith('eis-migration-smoke-', [StringComparison]::Ordinal)) {
|
||||
throw "Refusing to remove unexpected smoke-test directory: $resolvedTestDirectory"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolvedTestDirectory -Recurse -Force
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user