Files
Neo-ZQYY/scripts/ops/start-admin.ps1
Neo 779b2f6d52 chore: v1 整理 — 清理历史文件、DDL 合并、文档归档
- 清理 1155 个已删除的历史文件(废弃 prompt_logs、tmp、旧 ops 脚本)
- export/ 数据文件从 git 移除(已在 .gitignore)
- demo-miniprogram 从 tmp/ 移入 apps/,添加 CLAUDE.md 注解
- DDL 合并:完整 schema 定义填充到 db/*/schemas/(从 docs/database/ddl/ 复制)
- 39 个 v1 迁移脚本归档到 db/_archived/migrations_v1_merged/
- 4 个迁移变更类 BD_Manual 文档归档到 docs/database/_archived/
- .gitignore 补充 .vite/ 和 apps/*.zip
- settings.json 添加 effortLevel 默认配置
- scripts/ops/ 新增运维脚本入库

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:39:27 +08:00

396 lines
20 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# 启动管理后台(后端 + 前端)
# 服务成功启动后自动打开浏览器
$ErrorActionPreference = "Stop"
try {
# CHANGE 2026-03-07 | 定位项目根目录:从 bat 启动目录推算,不穿透 junction
# 背景C:\NeoZQYY 是 junction → D:\NeoZQYY\...\repo
# $MyInvocation.MyCommand.Path 和 Split-Path 都会穿透 junction 解析到 D 盘,
# 导致后端 CWD、.venv python、.env 全部指向 D 盘副本。
# 解决:优先用环境变量 NEOZQYY_ROOT其次用 bat 传入的 %~dp0不穿透 junction
# 最后才回退到脚本路径推算。
if ($env:NEOZQYY_ROOT -and (Test-Path $env:NEOZQYY_ROOT)) {
$ProjectRoot = $env:NEOZQYY_ROOT
} elseif ($env:NEOZQYY_LAUNCH_DIR -and (Test-Path $env:NEOZQYY_LAUNCH_DIR)) {
# 由 start-admin.bat 通过 %~dp0 注入,不穿透 junction
$ProjectRoot = $env:NEOZQYY_LAUNCH_DIR.TrimEnd('\')
} elseif ($MyInvocation.MyCommand.Path) {
$ProjectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path))
} else {
$ProjectRoot = $PWD.Path
}
# CHANGE 2026-03-07 | 将 ProjectRoot 注入环境变量 NEOZQYY_ROOT
# 使 config.py 的 _find_project_root() 策略 1 直接命中,
# 不再依赖 __file__ 推算__file__ 会穿透 junction 到 D 盘)。
# 这个环境变量会被子进程uvicorn继承。
$env:NEOZQYY_ROOT = $ProjectRoot
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " NeoZQYY 管理后台启动脚本" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "项目根目录: $ProjectRoot"
Write-Host "NEOZQYY_ROOT=$env:NEOZQYY_ROOT"
Write-Host ""
$backendDir = Join-Path $ProjectRoot "apps\backend"
$frontendDir = Join-Path $ProjectRoot "apps\admin-web"
$tenantDir = Join-Path $ProjectRoot "apps\tenant-admin"
if (-not (Test-Path $backendDir)) { throw "后端目录不存在: $backendDir" }
if (-not (Test-Path $frontendDir)) { throw "前端目录不存在: $frontendDir" }
if (-not (Test-Path $tenantDir)) { throw "租户管理后台目录不存在: $tenantDir" }
# ── 端口冲突检测:确保 8000/5173/5174 未被旧进程占用 ──
foreach ($port in @(8000, 5173, 5174)) {
$listeners = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if ($listeners) {
foreach ($l in $listeners) {
$procId = $l.OwningProcess
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
$procPath = if ($proc) { $proc.Path } else { "未知" }
Write-Host "端口 $port 被占用: PID=$procId ($procPath)" -ForegroundColor Yellow
# 尝试多种方式终止Stop-Process → taskkill /T /F
Write-Host " 正在终止旧进程..." -ForegroundColor Yellow
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
# 如果 Stop-Process 不够,用 taskkill 连子进程一起杀
# taskkill 非零退出码不应终止脚本
$ErrorActionPreference = "Continue"
taskkill /PID $procId /T /F 2>$null | Out-Null
$ErrorActionPreference = "Stop"
}
# 等待端口释放(最多 30 秒)
$waitMax = 30
$waited = 0
while ($waited -lt $waitMax) {
Start-Sleep -Seconds 2
$waited += 2
$still = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if (-not $still) {
Write-Host " 端口 $port 已释放(等待 ${waited}s" -ForegroundColor Green
break
}
# 父进程可能已死但子进程仍占端口,扫描并杀子进程
$zombiePid = $still.OwningProcess
$children = Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $zombiePid }
if ($children) {
foreach ($child in $children) {
Write-Host " 发现残留子进程: PID=$($child.ProcessId) ($($child.ExecutablePath))" -ForegroundColor Yellow
# taskkill 非零退出码不应终止脚本
$ErrorActionPreference = "Continue"
taskkill /PID $child.ProcessId /T /F 2>$null | Out-Null
$ErrorActionPreference = "Stop"
}
}
Write-Host " 端口 $port 仍被占用,继续等待... (${waited}/${waitMax}s)" -ForegroundColor Yellow
}
if ($waited -ge $waitMax) {
$still2 = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
if ($still2) {
Write-Host ""
Write-Host " !! 端口 $port 无法释放(僵尸进程 PID=$($still2.OwningProcess)" -ForegroundColor Red
Write-Host " !! 请重启电脑或手动执行: netsh int ipv4 reset" -ForegroundColor Red
Write-Host " !! 或等待几分钟后重试" -ForegroundColor Red
throw "端口 $port 被僵尸进程占用,无法启动"
}
}
}
}
# 前端日志文件(每次用唯一文件名,避免上次进程锁定旧文件)
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$frontendLog = Join-Path $env:TEMP "neozqyy_fe_${ts}.log"
$tenantLog = Join-Path $env:TEMP "neozqyy_ta_${ts}.log"
# 选择 PowerShell 可执行文件:优先 pwsh (7+),回退 powershell (5.1)
$psExe = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
# ── 生成临时启动脚本(带时间戳,避免旧脚本残留) ──
$beTmp = Join-Path $env:TEMP "neozqyy_start_be_${ts}.ps1"
$feTmp = Join-Path $env:TEMP "neozqyy_start_fe_${ts}.ps1"
$taTmp = Join-Path $env:TEMP "neozqyy_start_ta_${ts}.ps1"
$q = [char]39
# CHANGE 2026-03-07 | 显式使用 .venv 的 python避免 uv run 解析到 miniconda
# 背景uv run uvicorn 在此机器上解析到 C:\ProgramData\miniconda3\python.exe
# 导致 config.py 的 Path(__file__).resolve() 指向错误的代码副本D 盘)
$venvPython = Join-Path $ProjectRoot ".venv\Scripts\python.exe"
if (-not (Test-Path $venvPython)) {
throw ".venv Python 不存在: $venvPython — 请先运行 uv sync"
}
# CHANGE 2026-03-07 | 临时脚本中显式注入 NEOZQYY_ROOT确保 uvicorn 子进程
# 无论通过何种方式启动都能拿到正确的项目根目录
# CHANGE 2026-03-07 | 解决 ANSI 转义码乱码:
# PowerShell 5.1 + 旧版 conhost 不解析 ANSI 转义序列uvicorn 彩色日志
# 显示为 [32m、[0m 等乱码。设置 NO_COLOR=1 禁用颜色输出。
# pwsh 7+ 和 Windows Terminal 原生支持 VT不受影响。
# CHANGE 2026-04-01 | 修复三个显示问题:
# 1. Write-Host 用单引号包裹导致 $env:NEOZQYY_ROOT 不展开 → 改用双引号
# 2. NO_COLOR 对 uvicorn 不生效 → 加 --no-color 参数
# 3. PS 5.1 传统控制台 UTF-8 箭头乱码 → 所有临时脚本开头 chcp 65001
# 并统一设 NO_COLOR=1 禁用 ANSI 转义码
$beLines = @(
"`$env:NEOZQYY_ROOT = ${q}${ProjectRoot}${q}"
""
"# PS 5.1 + 传统控制台:启用 UTF-8 代码页 + 禁用 ANSI 颜色"
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
" Write-Host ${q}[提示] PS 5.1 + 传统控制台,已启用 UTF-8 并禁用 ANSI 颜色${q} -ForegroundColor Yellow"
"}"
""
"Set-Location -LiteralPath ${q}${backendDir}${q}"
"Write-Host ${q}=== 后端 FastAPI ===${q} -ForegroundColor Green"
"Write-Host `"NEOZQYY_ROOT=`$env:NEOZQYY_ROOT`""
"& ${q}${venvPython}${q} -m uvicorn app.main:app --reload --port 8000 --no-use-colors"
"Write-Host ${q}后端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$beLines | Set-Content -Path $beTmp -Encoding UTF8
$feLines = @(
"# PS 5.1 + 传统控制台:启用 UTF-8 + 禁用 ANSI"
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
"}"
""
"Set-Location -LiteralPath ${q}${frontendDir}${q}"
"Write-Host ${q}=== 前端 Vite (admin-web) ===${q} -ForegroundColor Green"
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${frontendLog}${q}"
"Write-Host ${q}前端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$feLines | Set-Content -Path $feTmp -Encoding UTF8
$taLines = @(
"# PS 5.1 + 传统控制台:启用 UTF-8 + 禁用 ANSI"
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
"}"
""
"Set-Location -LiteralPath ${q}${tenantDir}${q}"
"Write-Host ${q}=== 租户管理后台 Vite (tenant-admin) ===${q} -ForegroundColor Green"
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${tenantLog}${q}"
"Write-Host ${q}租户管理后台已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$taLines | Set-Content -Path $taTmp -Encoding UTF8
# ── 启动服务函数 ──
function Start-AllServices {
param([ref]$BeProc, [ref]$FeProc, [ref]$TaProc)
Write-Host "[1/3] 启动后端 FastAPI (http://localhost:8000) ..." -ForegroundColor Yellow
$BeProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $beTmp -PassThru
Start-Sleep -Seconds 2
Write-Host "[2/3] 启动前端 Vite admin-web (http://localhost:5173) ..." -ForegroundColor Yellow
$FeProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $feTmp -PassThru
Start-Sleep -Seconds 1
Write-Host "[3/3] 启动租户管理后台 Vite tenant-admin (http://localhost:5174) ..." -ForegroundColor Yellow
$TaProc.Value = Start-Process $psExe -ArgumentList "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $taTmp -PassThru
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host " 三个服务已在新窗口中启动" -ForegroundColor Green
Write-Host " 后端: http://localhost:8000" -ForegroundColor Green
Write-Host " 管理后台: http://localhost:5173" -ForegroundColor Green
Write-Host " 租户管理后台: http://localhost:5174" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
}
# ── 终止服务函数 ──
function Stop-AllServices {
param($BeProc, $FeProc, $TaProc)
Write-Host ""
Write-Host "正在终止所有服务..." -ForegroundColor Yellow
$ErrorActionPreference = "Continue"
foreach ($info in @(
@{ Name = "后端 FastAPI"; Proc = $BeProc; Port = 8000 },
@{ Name = "前端 admin-web"; Proc = $FeProc; Port = 5173 },
@{ Name = "租户 tenant-admin"; Proc = $TaProc; Port = 5174 }
)) {
$p = $info.Proc
if ($p -and -not $p.HasExited) {
Write-Host " 终止 $($info.Name) (PID=$($p.Id))..." -ForegroundColor Yellow
taskkill /PID $p.Id /T /F 2>$null | Out-Null
}
# 同时按端口清理(防止子进程残留)
$listeners = Get-NetTCPConnection -LocalPort $info.Port -State Listen -ErrorAction SilentlyContinue
foreach ($l in $listeners) {
taskkill /PID $l.OwningProcess /T /F 2>$null | Out-Null
}
}
$ErrorActionPreference = "Stop"
Start-Sleep -Seconds 1
Write-Host " 所有服务已终止" -ForegroundColor Green
}
# ── 等待前端就绪 + 打开浏览器 ──
function Wait-AndOpenBrowser {
Write-Host "等待前端 Vite 就绪..." -ForegroundColor Yellow
$timeout = 45
$elapsed = 0
$feReady = $false
$taReady = $false
while ($elapsed -lt $timeout -and (-not $feReady -or -not $taReady)) {
Start-Sleep -Seconds 1
$elapsed++
if (-not $feReady -and (Test-Path $frontendLog)) {
$raw = Get-Content $frontendLog -Raw -ErrorAction SilentlyContinue
if ($raw) {
$clean = $raw -replace '\x1b\[[0-9;]*m', ''
if ($clean -match "localhost:5173" -or $clean -match "ready in") {
$feReady = $true
Write-Host " admin-web 已就绪(${elapsed}s" -ForegroundColor Green
}
}
}
if (-not $taReady -and (Test-Path $tenantLog)) {
$raw = Get-Content $tenantLog -Raw -ErrorAction SilentlyContinue
if ($raw) {
$clean = $raw -replace '\x1b\[[0-9;]*m', ''
if ($clean -match "localhost:5174" -or $clean -match "ready in") {
$taReady = $true
Write-Host " tenant-admin 已就绪(${elapsed}s" -ForegroundColor Green
}
}
}
}
if ($feReady) { Start-Process "http://localhost:5173" }
else { Write-Host "admin-web 等待超时,请手动打开 http://localhost:5173" -ForegroundColor Red }
if ($taReady) { Start-Process "http://localhost:5174" }
else { Write-Host "tenant-admin 等待超时,请手动打开 http://localhost:5174" -ForegroundColor Red }
}
# ── 倒计时函数 ──
function Show-Countdown {
param([int]$Seconds)
for ($i = $Seconds; $i -ge 1; $i--) {
Write-Host "`r 重启倒计时: ${i}s ... " -NoNewline -ForegroundColor Cyan
Start-Sleep -Seconds 1
}
Write-Host "`r 重启倒计时: 0s — 开始启动! " -ForegroundColor Green
Write-Host ""
}
# ── 首次启动 ──
$beProc = $null; $feProc = $null; $taProc = $null
Start-AllServices -BeProc ([ref]$beProc) -FeProc ([ref]$feProc) -TaProc ([ref]$taProc)
Wait-AndOpenBrowser
# ── 交互菜单循环 ──
$running = $true
while ($running) {
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 操作菜单" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " [1] 终止所有服务" -ForegroundColor Yellow
Write-Host " [2] 重启所有服务(间隔 5 秒倒计时)" -ForegroundColor Yellow
Write-Host " [3] 退出(终止服务并关闭窗口)" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Cyan
$choice = Read-Host "请输入选项 (1/2/3)"
switch ($choice) {
"1" {
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
$beProc = $null; $feProc = $null; $taProc = $null
}
"2" {
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
Show-Countdown -Seconds 5
# 重新生成日志文件名(避免旧文件锁定)
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$frontendLog = Join-Path $env:TEMP "neozqyy_fe_${ts}.log"
$tenantLog = Join-Path $env:TEMP "neozqyy_ta_${ts}.log"
# 重新生成临时启动脚本
$beLines = @(
"`$env:NEOZQYY_ROOT = ${q}${ProjectRoot}${q}"
""
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
" Write-Host ${q}[提示] PS 5.1 + 传统控制台,已启用 UTF-8 并禁用 ANSI 颜色${q} -ForegroundColor Yellow"
"}"
""
"Set-Location -LiteralPath ${q}${backendDir}${q}"
"Write-Host ${q}=== 后端 FastAPI ===${q} -ForegroundColor Green"
"Write-Host `"NEOZQYY_ROOT=`$env:NEOZQYY_ROOT`""
"& ${q}${venvPython}${q} -m uvicorn app.main:app --reload --port 8000 --no-use-colors"
"Write-Host ${q}后端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$beLines | Set-Content -Path $beTmp -Encoding UTF8
$feLines = @(
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
"}"
""
"Set-Location -LiteralPath ${q}${frontendDir}${q}"
"Write-Host ${q}=== 前端 Vite (admin-web) ===${q} -ForegroundColor Green"
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${frontendLog}${q}"
"Write-Host ${q}前端已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$feLines | Set-Content -Path $feTmp -Encoding UTF8
$taLines = @(
"if (`$PSVersionTable.PSVersion.Major -lt 7 -and -not `$env:WT_SESSION) {"
" chcp 65001 | Out-Null"
" `$env:NO_COLOR = ${q}1${q}"
" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
"}"
""
"Set-Location -LiteralPath ${q}${tenantDir}${q}"
"Write-Host ${q}=== 租户管理后台 Vite (tenant-admin) ===${q} -ForegroundColor Green"
"cmd /c ${q}pnpm dev 2>&1${q} | Tee-Object -FilePath ${q}${tenantLog}${q}"
"Write-Host ${q}租户管理后台已退出,按任意键关闭...${q} -ForegroundColor Red"
"`$null = `$Host.UI.RawUI.ReadKey(${q}NoEcho,IncludeKeyDown${q})"
)
$taLines | Set-Content -Path $taTmp -Encoding UTF8
$beProc = $null; $feProc = $null; $taProc = $null
Start-AllServices -BeProc ([ref]$beProc) -FeProc ([ref]$feProc) -TaProc ([ref]$taProc)
Wait-AndOpenBrowser
}
"3" {
Stop-AllServices -BeProc $beProc -FeProc $feProc -TaProc $taProc
$running = $false
}
default {
Write-Host " 无效选项,请输入 1、2 或 3" -ForegroundColor Red
}
}
}
} catch {
Write-Host ""
Write-Host "启动失败: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor DarkRed
}
Write-Host ""
Write-Host "按任意键关闭此窗口..." -ForegroundColor DarkGray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")