Files
Neo-ZQYY/scripts/ops/start-admin.ps1
Neo 2a7a5d68aa feat: 2026-04-15~04-20 累积变更基线 — 多主线合流
主线 1: rns1-customer-coach-api + 04-miniapp-core-business 后端实施
  - 新增 GET /xcx/coaches/{id}/banner 轻量接口
  - performance/records 加 coach_id 参数 + view_board_coach 权限分流
  - coach/customer/performance/board/task 服务层重构
  - fdw_queries 结算单粒度聚合 + consumption_summary 视图统一
  - task_generator 回访宽限 72h + UPSERT 替代策略 + Step 5 保底清理
  - recall_detector settle_type=3 双重限制 + 门店级 resolved

主线 2: 小程序权限分流 + 新增 coach-service-records 管理者视角业绩明细页
  - perf-progress 共享模块去重 task-list/coach-detail 动画逻辑
  - isScattered 散客标记端到端
  - foodDetail/phoneFull/creator* 字段透传

主线 3: P19 指数回测框架 Phase 1+2
  - 3 个指数表 stat_date 日快照模式
  - 新增 DWS_INDEX_BACKFILL / DWS_TASK_SIMULATION 工具任务
  - task_engine 升级 HTTP 实时 + 推演回测双模式

主线 4: Core 维度层启用
  - 新增 CORE_DIM_SYNC 任务(DWD → core 4 维度表)
  - 修复 app 视图空查询问题

主线 5: member_project_tag 改为 LAST_30_VISITS 消费次数窗口

主线 6: 2 个迁移 SQL 已执行(stat_date + member_project_tag 新窗口)
  - schema 基线与 DDL 快照同步

主线 7: 开发机路径迁移 C:\NeoZQYY → C:\Project\NeoZQYY(约 95% 改动量)

附带: 新建运维脚本(churned_customer_report / simulate_historical_tasks /
      backfill_index_snapshots)+ tools/task-analysis/ 任务分析工具

合计 157 文件。未包含中间产物(tmp/ .playwright-mcp/ inspect-* excel/sheet 分析 txt)。
审计记录见下一个 commit。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 06:32:07 +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:\Project\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")