|
|
@@ -1,22 +1,247 @@
|
|
|
-$ErrorActionPreference = "Stop"
|
|
|
+[CmdletBinding()]
|
|
|
+param(
|
|
|
+ # 仅用于自动化验证;正常启动时保持 0,由 Ctrl+C 结束。
|
|
|
+ [ValidateRange(0, 3600)]
|
|
|
+ [int]$RunForSeconds = 0
|
|
|
+)
|
|
|
+
|
|
|
+$ErrorActionPreference = "Stop"
|
|
|
+
|
|
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
|
|
$FrontendPath = Join-Path $ProjectRoot "frontend"
|
|
|
-$FrontendJob = Start-Job -ScriptBlock {
|
|
|
- param($WorkingDirectory)
|
|
|
- Set-Location $WorkingDirectory
|
|
|
- pnpm dev
|
|
|
-} -ArgumentList $FrontendPath
|
|
|
+$BackendPath = Join-Path $ProjectRoot "backend"
|
|
|
+$ManagedProcessIds = [System.Collections.Generic.HashSet[int]]::new()
|
|
|
+$StartedProcesses = @()
|
|
|
+
|
|
|
+function Get-DevPortListeners {
|
|
|
+ param([int]$Port)
|
|
|
+
|
|
|
+ return @(
|
|
|
+ Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function Test-DevPortListening {
|
|
|
+ param([int]$Port)
|
|
|
+
|
|
|
+ $Endpoints = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
|
|
|
+ return $null -ne ($Endpoints | Where-Object { $_.Port -eq $Port } | Select-Object -First 1)
|
|
|
+}
|
|
|
+
|
|
|
+function Assert-DevPortAvailable {
|
|
|
+ param(
|
|
|
+ [int]$Port,
|
|
|
+ [string]$ApplicationName
|
|
|
+ )
|
|
|
+
|
|
|
+ if (-not (Test-DevPortListening -Port $Port)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ # 只有端口确实被占用时才调用较慢的 Windows 连接查询,以便展示 PID。
|
|
|
+ $Listeners = Get-DevPortListeners -Port $Port
|
|
|
+
|
|
|
+ $Owners = foreach ($Listener in $Listeners) {
|
|
|
+ $Process = Get-CimInstance Win32_Process `
|
|
|
+ -Filter "ProcessId=$($Listener.OwningProcess)" `
|
|
|
+ -ErrorAction SilentlyContinue
|
|
|
+ if ($null -ne $Process) {
|
|
|
+ "PID $($Process.ProcessId):$($Process.CommandLine)"
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ "PID $($Listener.OwningProcess)"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw "$ApplicationName 端口 $Port 已被占用。请先结束以下进程后重试:`n$($Owners -join "`n")"
|
|
|
+}
|
|
|
+
|
|
|
+function Update-ManagedProcessTree {
|
|
|
+ # Uvicorn reload、pnpm 和 Vite 都会派生子进程。持续记录完整进程树,
|
|
|
+ # 即使某个中间父进程先退出,最终清理时仍能找到它已经创建的子进程。
|
|
|
+ # Win32_Process 单次查询在部分 Windows 机器上约需半秒,逐个父 PID
|
|
|
+ # 查询会明显拖慢启动。因此每轮只取一次快照,在内存中展开进程树。
|
|
|
+ $AllProcesses = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
|
|
|
+ $AddedProcess = $true
|
|
|
+ while ($AddedProcess) {
|
|
|
+ $AddedProcess = $false
|
|
|
+ foreach ($Child in $AllProcesses) {
|
|
|
+ if (-not $ManagedProcessIds.Contains([int]$Child.ParentProcessId)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ $ChildId = [int]$Child.ProcessId
|
|
|
+ if ($ManagedProcessIds.Add($ChildId)) {
|
|
|
+ $AddedProcess = $true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Test-ProcessRunning {
|
|
|
+ param([System.Diagnostics.Process]$Process)
|
|
|
+
|
|
|
+ try {
|
|
|
+ $Process.Refresh()
|
|
|
+ return -not $Process.HasExited
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return $false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Test-HttpEndpoint {
|
|
|
+ param([string]$Uri)
|
|
|
+
|
|
|
+ try {
|
|
|
+ $Response = Invoke-WebRequest `
|
|
|
+ -Uri $Uri `
|
|
|
+ -UseBasicParsing `
|
|
|
+ -TimeoutSec 2 `
|
|
|
+ -ErrorAction Stop
|
|
|
+ return $Response.StatusCode -ge 200 -and $Response.StatusCode -lt 500
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return $false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Wait-ForDevelopmentServices {
|
|
|
+ param(
|
|
|
+ [System.Diagnostics.Process]$FrontendProcess,
|
|
|
+ [System.Diagnostics.Process]$BackendProcess,
|
|
|
+ [int]$TimeoutSeconds = 45
|
|
|
+ )
|
|
|
+
|
|
|
+ $Deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
|
+ while ((Get-Date) -lt $Deadline) {
|
|
|
+ Update-ManagedProcessTree
|
|
|
+
|
|
|
+ if (-not (Test-ProcessRunning -Process $FrontendProcess)) {
|
|
|
+ throw "前端开发服务器启动失败,请检查上方 pnpm 输出。"
|
|
|
+ }
|
|
|
+ if (-not (Test-ProcessRunning -Process $BackendProcess)) {
|
|
|
+ throw "后端开发服务器启动失败,请检查上方 API 与退款 Worker 输出。"
|
|
|
+ }
|
|
|
+
|
|
|
+ $H5Ready = Test-HttpEndpoint -Uri "http://127.0.0.1:5173/"
|
|
|
+ $AdminReady = Test-HttpEndpoint -Uri "http://127.0.0.1:5174/"
|
|
|
+ $BackendReady = Test-HttpEndpoint -Uri "http://127.0.0.1:8000/docs"
|
|
|
+ if ($H5Ready -and $AdminReady -and $BackendReady) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ Start-Sleep -Milliseconds 300
|
|
|
+ }
|
|
|
+
|
|
|
+ throw "开发服务在 $TimeoutSeconds 秒内未全部就绪,请检查上方启动日志。"
|
|
|
+}
|
|
|
+
|
|
|
+function Stop-ManagedProcesses {
|
|
|
+ Write-Host "`n正在停止智保通开发服务..." -ForegroundColor Yellow
|
|
|
+
|
|
|
+ # 多刷新几次,覆盖清理开始前恰好生成的 reload/Vite 子进程。
|
|
|
+ 1..3 | ForEach-Object {
|
|
|
+ Update-ManagedProcessTree
|
|
|
+ Start-Sleep -Milliseconds 100
|
|
|
+ }
|
|
|
+
|
|
|
+ $Processes = @(
|
|
|
+ Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
|
+ Where-Object {
|
|
|
+ $_.ProcessId -ne $PID -and
|
|
|
+ $ManagedProcessIds.Contains([int]$_.ProcessId)
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ # 子进程通常晚于父进程创建;倒序停止可避免父进程退出后留下孤儿。
|
|
|
+ $Processes |
|
|
|
+ Sort-Object CreationDate -Descending |
|
|
|
+ ForEach-Object {
|
|
|
+ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
|
+ }
|
|
|
+
|
|
|
+ # 确认本次脚本启动的三个监听端口已经释放。
|
|
|
+ $Deadline = (Get-Date).AddSeconds(5)
|
|
|
+ while ((Get-Date) -lt $Deadline) {
|
|
|
+ $AnyPortListening = $false
|
|
|
+ foreach ($Port in 5173, 5174, 8000) {
|
|
|
+ if (Test-DevPortListening -Port $Port) {
|
|
|
+ $AnyPortListening = $true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (-not $AnyPortListening) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ Start-Sleep -Milliseconds 200
|
|
|
+ }
|
|
|
+
|
|
|
+ Write-Host "智保通开发服务已停止。" -ForegroundColor Green
|
|
|
+}
|
|
|
+
|
|
|
+foreach ($Service in @(
|
|
|
+ @{ Port = 5173; Name = "H5" },
|
|
|
+ @{ Port = 5174; Name = "管理后台" },
|
|
|
+ @{ Port = 8000; Name = "后端 API" }
|
|
|
+)) {
|
|
|
+ Assert-DevPortAvailable -Port $Service.Port -ApplicationName $Service.Name
|
|
|
+}
|
|
|
+
|
|
|
+$PnpmPath = (Get-Command pnpm.cmd -ErrorAction Stop).Source
|
|
|
+$UvPath = (Get-Command uv.exe -ErrorAction Stop).Source
|
|
|
|
|
|
-Push-Location (Join-Path $ProjectRoot "backend")
|
|
|
try {
|
|
|
+ Write-Host "正在启动 H5、管理后台和后端 API..." -ForegroundColor Cyan
|
|
|
+
|
|
|
+ $FrontendProcess = Start-Process `
|
|
|
+ -FilePath $PnpmPath `
|
|
|
+ -ArgumentList "dev" `
|
|
|
+ -WorkingDirectory $FrontendPath `
|
|
|
+ -NoNewWindow `
|
|
|
+ -PassThru
|
|
|
+ $StartedProcesses += $FrontendProcess
|
|
|
+ [void]$ManagedProcessIds.Add($FrontendProcess.Id)
|
|
|
+
|
|
|
+ $BackendProcess = Start-Process `
|
|
|
+ -FilePath $UvPath `
|
|
|
+ -ArgumentList @(
|
|
|
+ "run", "zbt-serve"
|
|
|
+ ) `
|
|
|
+ -WorkingDirectory $BackendPath `
|
|
|
+ -NoNewWindow `
|
|
|
+ -PassThru
|
|
|
+ $StartedProcesses += $BackendProcess
|
|
|
+ [void]$ManagedProcessIds.Add($BackendProcess.Id)
|
|
|
+
|
|
|
+ Wait-ForDevelopmentServices `
|
|
|
+ -FrontendProcess $FrontendProcess `
|
|
|
+ -BackendProcess $BackendProcess
|
|
|
+
|
|
|
+ Write-Host ""
|
|
|
+ Write-Host "智保通开发服务已全部启动:" -ForegroundColor Green
|
|
|
Write-Host "H5:http://127.0.0.1:5173"
|
|
|
Write-Host "管理后台:http://127.0.0.1:5174"
|
|
|
- Write-Host "后端API:http://127.0.0.1:8000/docs"
|
|
|
- Write-Host "后端进程:API 与退款 Worker 由统一主管启动"
|
|
|
- uv run zbt-serve
|
|
|
+ Write-Host "后端 API:http://127.0.0.1:8000/docs"
|
|
|
+ Write-Host "按 Ctrl+C 可一次性停止全部服务。" -ForegroundColor DarkGray
|
|
|
+
|
|
|
+ $ReadyAt = Get-Date
|
|
|
+ while ($true) {
|
|
|
+ Update-ManagedProcessTree
|
|
|
+
|
|
|
+ foreach ($StartedProcess in $StartedProcesses) {
|
|
|
+ if (-not (Test-ProcessRunning -Process $StartedProcess)) {
|
|
|
+ throw "开发服务进程 PID $($StartedProcess.Id) 意外退出,请检查上方日志。"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($RunForSeconds -gt 0 -and
|
|
|
+ ((Get-Date) - $ReadyAt).TotalSeconds -ge $RunForSeconds) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ Start-Sleep -Milliseconds 500
|
|
|
+ }
|
|
|
}
|
|
|
finally {
|
|
|
- Pop-Location
|
|
|
- Stop-Job -Job $FrontendJob -ErrorAction SilentlyContinue
|
|
|
- Remove-Job -Job $FrontendJob -Force -ErrorAction SilentlyContinue
|
|
|
+ Stop-ManagedProcesses
|
|
|
}
|