#Requires -Version 5.1 <# .SYNOPSIS 重启本机 JinRong 前后端开发服务(前端 Vite · 后端 Docker 或本机 uvicorn)。 .EXAMPLE .\scripts\dev\restart-dev.ps1 .\scripts\dev\restart-dev.ps1 -Backend Local .\scripts\dev\restart-dev.ps1 -DockerContainer stu-system-app .NOTES 后端 Auto:若 Docker 有 jinrong* 容器映射 8000 → docker restart;否则 taskkill /T 清掉所有 app.main:app uvicorn + 8000 监听,再开单实例 uvicorn。 前端:taskkill /T 清 vite/node(5173–5175)后在新 PowerShell 窗口执行 npm run dev。 Redis 不在此脚本内;需要时先 .\scripts\dev\start-redis.ps1 #> param( [ValidateSet("Auto", "Docker", "Local")] [string]$Backend = "Auto", [string]$DockerContainer = "", [switch]$SkipBackend, [switch]$SkipFrontend ) $ErrorActionPreference = "Stop" $Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $Web = Join-Path $Root "web" Set-Location $Root function Get-ListenerPids([int[]]$Ports) { $pids = @() foreach ($port in $Ports) { Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $pids += $_.OwningProcess } } $pids | Select-Object -Unique } function Stop-ProcessTree([int]$ProcId) { if (-not $ProcId -or $ProcId -le 0) { return } # /T 结束子进程(uvicorn --reload 会起父子两套 python,只杀 Listen PID 常会叠尸) $null = Start-Process -FilePath "taskkill.exe" -ArgumentList @("/PID", $ProcId, "/T", "/F") -Wait -WindowStyle Hidden -ErrorAction SilentlyContinue } function Stop-PortListeners([int[]]$Ports, [switch]$WithTree) { foreach ($procId in (Get-ListenerPids $Ports)) { if ($procId -and $procId -gt 0) { if ($WithTree) { Stop-ProcessTree $procId } else { Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue } } } } function Test-IsJinRongUvicornCommandLine([string]$CommandLine) { if (-not $CommandLine) { return $false } if ($CommandLine -notmatch '(?i)uvicorn') { return $false } if ($CommandLine -notmatch 'app\.main:app') { return $false } if ($CommandLine -match '(?i)--port\s+8000') { return $true } if ($CommandLine -match ':8000') { return $true } if ($CommandLine -match [regex]::Escape($Root)) { return $true } return $false } function Get-JinRongUvicornPids { $pids = @() foreach ($exe in @("python.exe", "python3.exe", "pythonw.exe")) { Get-CimInstance Win32_Process -Filter "Name='$exe'" -ErrorAction SilentlyContinue | Where-Object { Test-IsJinRongUvicornCommandLine $_.CommandLine } | ForEach-Object { $pids += $_.ProcessId } } $pids | Select-Object -Unique } function Stop-AllBackendOnPort8000 { Write-Host "清理 8000(Docker 外来容器 + 本机 uvicorn 进程树)..." -ForegroundColor Cyan Stop-ForeignPort8000Container for ($round = 1; $round -le 4; $round++) { $batch = @() $batch += Get-JinRongUvicornPids $batch += Get-ListenerPids @(8000) $batch = $batch | Where-Object { $_ -and $_ -gt 0 } | Select-Object -Unique if ($batch.Count -eq 0) { break } foreach ($procId in $batch) { Write-Host " [$round] taskkill /T PID $procId" -ForegroundColor DarkGray Stop-ProcessTree $procId } Start-Sleep -Milliseconds 700 } Stop-PortListeners @(8000) -WithTree Start-Sleep -Milliseconds 400 } function Stop-AllViteDev { $webEsc = [regex]::Escape($Web) Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | Where-Object { $cmd = $_.CommandLine $cmd -and ($cmd -match '(?i)\bvite\b') -and ($cmd -match $webEsc -or $cmd -match 'npm run dev') } | ForEach-Object { Write-Host " taskkill vite PID $($_.ProcessId)" -ForegroundColor DarkGray Stop-ProcessTree $_.ProcessId } Stop-PortListeners @(5173, 5174, 5175) -WithTree Start-Sleep -Milliseconds 400 } function Wait-Port8000Clear([int]$TimeoutSec = 15) { $deadline = (Get-Date).AddSeconds($TimeoutSec) while ((Get-Date) -lt $deadline) { $left = @(Get-ListenerPids @(8000)) if ($left.Count -eq 0) { return $true } Start-Sleep -Seconds 1 } return $false } function Test-BackendHasNavHistoryRoute { try { $paths = (Invoke-RestMethod -Uri "http://127.0.0.1:8000/openapi.json" -TimeoutSec 10).paths return [bool]$paths.'/api/products/{product_id}/nav/history' } catch { return $false } } function Get-DockerBackendName { if ($DockerContainer -ne "") { return $DockerContainer } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return $null } $name = docker ps --filter "publish=8000" --format "{{.Names}}" 2>$null | Select-Object -First 1 if (-not $name) { return $null } $name = $name.Trim() # 仅把 JinRong 自有容器当后端;其它项目(如 stu-system-app)占 8000 时走本机 uvicorn if ($name -match '^jinrong') { return $name } return $null } function Stop-ForeignPort8000Container { if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { return } $name = docker ps --filter "publish=8000" --format "{{.Names}}" 2>$null | Select-Object -First 1 if (-not $name) { return } $name = $name.Trim() if ($name -match '^jinrong') { return } Write-Host "[WARN] 容器 '$name' 占用 8000 且非 JinRong 后端,将 stop 以便加载本仓库 uvicorn(行情 nav/history 等路由)" -ForegroundColor Yellow docker stop $name | Out-Null } function Test-HttpOk([string]$Url, [int]$TimeoutSec = 12) { try { $null = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec $TimeoutSec return $true } catch { return $false } } function Test-HealthOk { try { $r = Invoke-RestMethod -Uri "http://127.0.0.1:8000/health" -TimeoutSec 15 return ($r.status -eq "ok") } catch { return $false } } # --- 后端 ----------------------------------------------------------------- if (-not $SkipBackend) { $useDocker = $false $container = Get-DockerBackendName switch ($Backend) { "Docker" { if (-not $container) { Write-Error "未找到映射 8000 的 Docker 容器;请 -DockerContainer 指定或改用 -Backend Local" exit 1 } $useDocker = $true } "Local" { $useDocker = $false } default { $useDocker = [bool]$container } } if ($useDocker) { Write-Host "重启后端 Docker: $container (8000) ..." -ForegroundColor Cyan docker restart $container | Out-Null $deadline = (Get-Date).AddSeconds(45) while ((Get-Date) -lt $deadline) { if (Test-HealthOk) { break } Start-Sleep -Seconds 2 } if (-not (Test-HealthOk)) { Write-Host "[WARN] /health 暂未就绪,请稍后 docker logs $container" -ForegroundColor Yellow } else { Write-Host "后端就绪: http://127.0.0.1:8000/health" -ForegroundColor Green } } else { Stop-AllBackendOnPort8000 $foreign = docker ps --filter "publish=8000" --format "{{.Names}}" 2>$null | Select-Object -First 1 if ($foreign -and ($foreign.Trim() -notmatch '^jinrong')) { Write-Host "[WARN] 8000 仍被 Docker 容器 '$($foreign.Trim())' 占用;脚本已尝试 stop,若仍存在请手动 docker stop" -ForegroundColor Yellow } if (-not (Wait-Port8000Clear)) { Write-Host "[WARN] 8000 仍被占用。将列出监听进程 — 请关掉所有「uvicorn」PowerShell 窗口后重跑本脚本。" -ForegroundColor Yellow netstat -ano | findstr ":8000" exit 1 } Write-Host "重启本机 uvicorn (8000) ..." -ForegroundColor Cyan Start-Sleep -Seconds 1 $uvicornCmd = "Set-Location '$Root'; `$env:PYTHONPATH='$Root'; python -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000" Start-Process powershell -ArgumentList @("-NoExit", "-Command", $uvicornCmd) $routeDeadline = (Get-Date).AddSeconds(30) $routesOk = $false while ((Get-Date) -lt $routeDeadline) { if ((Test-HealthOk) -and (Test-BackendHasNavHistoryRoute)) { $routesOk = $true break } Start-Sleep -Seconds 2 } if ($routesOk) { Write-Host "后端就绪: http://127.0.0.1:8000/health · nav/history 已挂载" -ForegroundColor Green } elseif (Test-HealthOk) { Write-Host "[WARN] /health 有响应但 OpenAPI 无 nav/history — 仍在跑旧代码,请关掉所有 uvicorn 窗口后重跑本脚本" -ForegroundColor Yellow } else { Write-Host "[WARN] uvicorn 窗口已打开,/health 尚未响应(可能仍在启动)" -ForegroundColor Yellow } } } # --- 前端 ----------------------------------------------------------------- if (-not $SkipFrontend) { if (-not (Test-Path (Join-Path $Web "package.json"))) { Write-Error "未找到 web/package.json" exit 1 } Write-Host "重启前端 Vite (5173) ..." -ForegroundColor Cyan Stop-AllViteDev Start-Sleep -Seconds 1 $viteCmd = "Set-Location '$Web'; npm run dev" Start-Process powershell -ArgumentList @("-NoExit", "-Command", $viteCmd) Start-Sleep -Seconds 4 if (Test-HttpOk "http://localhost:5173/") { Write-Host "前端就绪: http://localhost:5173/" -ForegroundColor Green } else { Write-Host "[WARN] Vite 窗口已打开,5173 暂未响应(可能落在 5174,请看新窗口日志)" -ForegroundColor Yellow } } Write-Host "`n完成。后端 Swagger: http://127.0.0.1:8000/docs" -ForegroundColor DarkGray