feat(nav): Enhance product API with historical NAV tracking and new charting features
- Updated `ready.py` to include a health check for the new `products_nav_history` endpoint, ensuring system readiness. - Enhanced the `ProductNavChartPanel` component to visualize historical NAV data with various charting options, including line and column charts. - Introduced new utility functions for filtering and aggregating NAV data, improving data handling in the frontend. - Updated tests to verify the inclusion of the new `products_nav_history` check in the API response. - Improved documentation to reflect recent changes and the new testing baseline of 833 passed tests, indicating enhanced stability. This update significantly improves the product API by providing access to historical NAV data and enhancing user insights through visualizations.
This commit is contained in:
+5112
-5098
File diff suppressed because it is too large
Load Diff
+143
-131
@@ -1,131 +1,143 @@
|
||||
"""为 core_product_nav 灌入近 365 个自然日历史(锚定 06-seed-nav 最新日)。␍
|
||||
␍
|
||||
仓库内 canonical 种子:`scripts/core/07-seed-nav-history.sql`(reset.ps1 在 06 之后执行)。␍
|
||||
␍
|
||||
python scripts/core/seed_nav_history.py # 直连 MySQL upsert(增量/修复)␍
|
||||
python scripts/core/seed_nav_history.py --export-sql # 重新生成 07-seed-nav-history.sql␍
|
||||
"""␍
|
||||
from __future__ import annotations␍
|
||||
␍
|
||||
import argparse␍
|
||||
import hashlib␍
|
||||
import sys␍
|
||||
from datetime import date, timedelta␍
|
||||
from pathlib import Path␍
|
||||
␍
|
||||
ROOT = Path(__file__).resolve().parents[2]␍
|
||||
if str(ROOT) not in sys.path:␍
|
||||
sys.path.insert(0, str(ROOT))␍
|
||||
␍
|
||||
from sqlalchemy import text␍
|
||||
␍
|
||||
from app.config.database import get_core_engine␍
|
||||
␍
|
||||
ANCHOR_DATE = date(2026, 9, 4)␍
|
||||
DAYS_BACK = 364 # 与锚定日合计约 365 个自然日␍
|
||||
SQL_OUT = Path(__file__).resolve().parent / "07-seed-nav-history.sql"␍
|
||||
␍
|
||||
# 与 scripts/core/06-seed-nav.sql 一致(锚定日净值)␍
|
||||
ANCHORS: list[tuple[str, float]] = [␍
|
||||
("PROD-110022", 1.0300),␍
|
||||
("PROD-110023", 1.0200),␍
|
||||
("PROD-005827", 0.9500),␍
|
||||
("PROD-005828", 1.0200),␍
|
||||
("PROD-161725", 1.0500),␍
|
||||
("PROD-161726", 1.0500),␍
|
||||
("PROD-003095", 0.9500),␍
|
||||
("PROD-510300", 1.0300),␍
|
||||
("PROD-510500", 0.9800),␍
|
||||
("PROD-XYZ999", 0.9000),␍
|
||||
("PROD-000001", 1.0020),␍
|
||||
("PROD-000002", 1.0050),␍
|
||||
("PROD-WMG001", 1.0150),␍
|
||||
("PROD-PRIV01", 1.0800),␍
|
||||
]␍
|
||||
␍
|
||||
␍
|
||||
def _daily_chg_pct(product_id: str, nav_date: date) -> float:␍
|
||||
h = hashlib.sha256(f"{product_id}:{nav_date.isoformat()}".encode()).hexdigest()␍
|
||||
n = int(h[:8], 16) / 0xFFFFFFFF␍
|
||||
return round((n - 0.5) * 3.0, 4) # roughly -1.5% .. +1.5%␍
|
||||
␍
|
||||
␍
|
||||
def build_history_rows() -> list[dict]:␍
|
||||
rows: list[dict] = []␍
|
||||
for product_id, anchor_nav in ANCHORS:␍
|
||||
nav = anchor_nav␍
|
||||
for offset in range(DAYS_BACK, 0, -1):␍
|
||||
d = ANCHOR_DATE - timedelta(days=offset)␍
|
||||
chg = _daily_chg_pct(product_id, d)␍
|
||||
nav = round(nav / (1 + chg / 100.0), 4)␍
|
||||
rows.append(␍
|
||||
{␍
|
||||
"product_id": product_id,␍
|
||||
"nav_date": d.isoformat(),␍
|
||||
"nav": nav,␍
|
||||
"daily_chg_pct": chg,␍
|
||||
}␍
|
||||
)␍
|
||||
return rows␍
|
||||
␍
|
||||
␍
|
||||
def export_sql(path: Path = SQL_OUT, *, batch_size: int = 500) -> int:␍
|
||||
rows = build_history_rows()␍
|
||||
lines = [␍
|
||||
"USE jinrong_core;",␍
|
||||
"",␍
|
||||
"-- 近 364 个自然日历史净值(锚定日 2026-09-04 由 06-seed-nav.sql 写入)",␍
|
||||
f"-- 生成:python scripts/core/seed_nav_history.py --export-sql · {len(rows)} 行 · {len(ANCHORS)} 产品",␍
|
||||
"",␍
|
||||
]␍
|
||||
for batch_start in range(0, len(rows), batch_size):␍
|
||||
chunk = rows[batch_start : batch_start + batch_size]␍
|
||||
lines.append(␍
|
||||
"INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) VALUES"␍
|
||||
)␍
|
||||
value_lines = [␍
|
||||
f"('{r['product_id']}', {r['nav']:.4f}, {r['daily_chg_pct']:.4f}, '{r['nav_date']}')"␍
|
||||
for r in chunk␍
|
||||
]␍
|
||||
lines.append(",\n".join(value_lines) + ";")␍
|
||||
lines.append("")␍
|
||||
path.write_text("\n".join(lines), encoding="utf-8")␍
|
||||
return len(rows)␍
|
||||
␍
|
||||
␍
|
||||
def upsert_mysql() -> int:␍
|
||||
rows = build_history_rows()␍
|
||||
sql = text(␍
|
||||
"""␍
|
||||
INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date)␍
|
||||
VALUES (:product_id, :nav, :daily_chg_pct, :nav_date)␍
|
||||
ON DUPLICATE KEY UPDATE nav = VALUES(nav), daily_chg_pct = VALUES(daily_chg_pct)␍
|
||||
"""␍
|
||||
)␍
|
||||
engine = get_core_engine()␍
|
||||
with engine.begin() as conn:␍
|
||||
for batch_start in range(0, len(rows), 500):␍
|
||||
chunk = rows[batch_start : batch_start + 500]␍
|
||||
conn.execute(sql, chunk)␍
|
||||
return len(rows)␍
|
||||
␍
|
||||
␍
|
||||
def main() -> None:␍
|
||||
parser = argparse.ArgumentParser(description="Core NAV history seed (364 days × 14 products)")␍
|
||||
parser.add_argument(␍
|
||||
"--export-sql",␍
|
||||
action="store_true",␍
|
||||
help=f"Write {SQL_OUT.name} instead of upserting MySQL",␍
|
||||
)␍
|
||||
args = parser.parse_args()␍
|
||||
if args.export_sql:␍
|
||||
n = export_sql()␍
|
||||
print(f"seed_nav_history: wrote {n} rows to {SQL_OUT}")␍
|
||||
return␍
|
||||
n = upsert_mysql()␍
|
||||
print(f"seed_nav_history: upserted {n} rows for {len(ANCHORS)} products")␍
|
||||
␍
|
||||
␍
|
||||
if __name__ == "__main__":␍
|
||||
main()␍
|
||||
␍
|
||||
"""为 core_product_nav 灌入近 365 个自然日历史(锚定 06-seed-nav 最新日)。
|
||||
|
||||
仓库内 canonical 种子:`scripts/core/07-seed-nav-history.sql`(reset.ps1 在 06 之后执行)。
|
||||
|
||||
python scripts/core/seed_nav_history.py # 直连 MySQL upsert(增量/修复)
|
||||
python scripts/core/seed_nav_history.py --export-sql # 重新生成 07-seed-nav-history.sql
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config.database import get_core_engine
|
||||
|
||||
ANCHOR_DATE = date(2026, 9, 4)
|
||||
DAYS_BACK = 364 # 与锚定日合计约 365 个自然日
|
||||
SQL_OUT = Path(__file__).resolve().parent / "07-seed-nav-history.sql"
|
||||
|
||||
# 与 scripts/core/06-seed-nav.sql 一致(锚定日净值)
|
||||
ANCHORS: list[tuple[str, float]] = [
|
||||
("PROD-110022", 1.0300),
|
||||
("PROD-110023", 1.0200),
|
||||
("PROD-005827", 0.9500),
|
||||
("PROD-005828", 1.0200),
|
||||
("PROD-161725", 1.0500),
|
||||
("PROD-161726", 1.0500),
|
||||
("PROD-003095", 0.9500),
|
||||
("PROD-510300", 1.0300),
|
||||
("PROD-510500", 0.9800),
|
||||
("PROD-XYZ999", 0.9000),
|
||||
("PROD-000001", 1.0020),
|
||||
("PROD-000002", 1.0050),
|
||||
("PROD-WMG001", 1.0150),
|
||||
("PROD-PRIV01", 1.0800),
|
||||
]
|
||||
|
||||
|
||||
def _daily_chg_pct(product_id: str, nav_date: date) -> float:
|
||||
h = hashlib.sha256(f"{product_id}:{nav_date.isoformat()}".encode()).hexdigest()
|
||||
n = int(h[:8], 16) / 0xFFFFFFFF
|
||||
return round((n - 0.5) * 3.0, 4) # roughly -1.5% .. +1.5%
|
||||
|
||||
|
||||
def build_history_rows() -> list[dict]:
|
||||
"""从锚定日净值向前反推首日,再按日涨跌正向生成至锚定日(与 06-seed 锚点一致、无末日断崖)。"""
|
||||
rows: list[dict] = []
|
||||
for product_id, anchor_nav in ANCHORS:
|
||||
days_list = [ANCHOR_DATE - timedelta(days=DAYS_BACK - i) for i in range(DAYS_BACK + 1)]
|
||||
nav = anchor_nav
|
||||
for d in reversed(days_list[1:]):
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
nav = nav / (1 + chg / 100.0)
|
||||
running = round(nav, 4)
|
||||
for i, d in enumerate(days_list):
|
||||
if i > 0:
|
||||
if d == ANCHOR_DATE:
|
||||
chg = round(((anchor_nav - running) / running) * 100, 4)
|
||||
running = anchor_nav
|
||||
else:
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
running = round(running * (1 + chg / 100.0), 4)
|
||||
else:
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
rows.append(
|
||||
{
|
||||
"product_id": product_id,
|
||||
"nav_date": d.isoformat(),
|
||||
"nav": running,
|
||||
"daily_chg_pct": chg,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def export_sql(path: Path = SQL_OUT, *, batch_size: int = 500) -> int:
|
||||
rows = build_history_rows()
|
||||
lines = [
|
||||
"USE jinrong_core;",
|
||||
"",
|
||||
"-- 近 365 个自然日(含锚定日 2026-09-04;与 06-seed-nav 锚点净值一致)",
|
||||
f"-- 生成:python scripts/core/seed_nav_history.py --export-sql · {len(rows)} 行 · {len(ANCHORS)} 产品 · 含锚定日",
|
||||
"",
|
||||
]
|
||||
for batch_start in range(0, len(rows), batch_size):
|
||||
chunk = rows[batch_start : batch_start + batch_size]
|
||||
lines.append(
|
||||
"INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) VALUES"
|
||||
)
|
||||
value_lines = [
|
||||
f"('{r['product_id']}', {r['nav']:.4f}, {r['daily_chg_pct']:.4f}, '{r['nav_date']}')"
|
||||
for r in chunk
|
||||
]
|
||||
lines.append(",\n".join(value_lines) + ";")
|
||||
lines.append("")
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
return len(rows)
|
||||
|
||||
|
||||
def upsert_mysql() -> int:
|
||||
rows = build_history_rows()
|
||||
sql = text(
|
||||
"""
|
||||
INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date)
|
||||
VALUES (:product_id, :nav, :daily_chg_pct, :nav_date)
|
||||
ON DUPLICATE KEY UPDATE nav = VALUES(nav), daily_chg_pct = VALUES(daily_chg_pct)
|
||||
"""
|
||||
)
|
||||
engine = get_core_engine()
|
||||
with engine.begin() as conn:
|
||||
for batch_start in range(0, len(rows), 500):
|
||||
chunk = rows[batch_start : batch_start + 500]
|
||||
conn.execute(sql, chunk)
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Core NAV history seed (364 days × 14 products)")
|
||||
parser.add_argument(
|
||||
"--export-sql",
|
||||
action="store_true",
|
||||
help=f"Write {SQL_OUT.name} instead of upserting MySQL",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.export_sql:
|
||||
n = export_sql()
|
||||
print(f"seed_nav_history: wrote {n} rows to {SQL_OUT}")
|
||||
return
|
||||
n = upsert_mysql()
|
||||
print(f"seed_nav_history: upserted {n} rows for {len(ANCHORS)} products")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0..\.."
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0restart-dev.ps1"
|
||||
if errorlevel 1 pause
|
||||
+121
-13
@@ -7,8 +7,8 @@
|
||||
.\scripts\dev\restart-dev.ps1 -Backend Local
|
||||
.\scripts\dev\restart-dev.ps1 -DockerContainer stu-system-app
|
||||
.NOTES
|
||||
后端 Auto:若 Docker 有容器映射 8000 → docker restart;否则停 8000 监听并新开窗口跑 uvicorn。
|
||||
前端:释放 5173–5175 后在新 PowerShell 窗口执行 npm run dev。
|
||||
后端 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(
|
||||
@@ -33,14 +33,93 @@ function Get-ListenerPids([int[]]$Ports) {
|
||||
$pids | Select-Object -Unique
|
||||
}
|
||||
|
||||
function Stop-PortListeners([int[]]$Ports) {
|
||||
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) {
|
||||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
||||
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
|
||||
@@ -49,10 +128,23 @@ function Get-DockerBackendName {
|
||||
return $null
|
||||
}
|
||||
$name = docker ps --filter "publish=8000" --format "{{.Names}}" 2>$null | Select-Object -First 1
|
||||
if ($name) { return $name.Trim() }
|
||||
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
|
||||
@@ -104,17 +196,33 @@ if (-not $SkipBackend) {
|
||||
Write-Host "后端就绪: http://127.0.0.1:8000/health" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
if ($container) {
|
||||
Write-Host "[WARN] 检测到 Docker 容器 '$container' 仍占用 8000;Local 模式请先停容器或改用 -Backend Docker" -ForegroundColor Yellow
|
||||
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
|
||||
Stop-PortListeners @(8000)
|
||||
Start-Sleep -Seconds 1
|
||||
$uvicornCmd = "Set-Location '$Root'; python -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000"
|
||||
$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)
|
||||
Start-Sleep -Seconds 4
|
||||
if (Test-HealthOk) {
|
||||
Write-Host "后端就绪: http://127.0.0.1:8000/health" -ForegroundColor Green
|
||||
$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
|
||||
}
|
||||
@@ -128,7 +236,7 @@ if (-not $SkipFrontend) {
|
||||
exit 1
|
||||
}
|
||||
Write-Host "重启前端 Vite (5173) ..." -ForegroundColor Cyan
|
||||
Stop-PortListeners @(5173, 5174, 5175)
|
||||
Stop-AllViteDev
|
||||
Start-Sleep -Seconds 1
|
||||
$viteCmd = "Set-Location '$Web'; npm run dev"
|
||||
Start-Process powershell -ArgumentList @("-NoExit", "-Command", $viteCmd)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
停止占用 8000 的 JinRong uvicorn(及非 jinrong Docker 容器)。
|
||||
#>
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Write-Host "Stopping uvicorn app.main on :8000 ..."
|
||||
Get-CimInstance Win32_Process -Filter "Name='python.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -match 'uvicorn' -and $_.CommandLine -match 'app\.main:app' -and $_.CommandLine -match ':8000' } |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
foreach ($procId in (Get-ListenerPids @(8000))) {
|
||||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||||
$name = docker ps --filter "publish=8000" --format "{{.Names}}" 2>$null | Select-Object -First 1
|
||||
if ($name -and ($name.Trim() -notmatch '^jinrong')) {
|
||||
Write-Host "Stopping Docker $name on 8000 ..."
|
||||
docker stop $name.Trim() | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
netstat -ano | findstr ":8000.*LISTENING"
|
||||
Write-Host "Done. Start backend: .\scripts\dev\restart-dev.ps1 -Backend Local -SkipFrontend"
|
||||
Reference in New Issue
Block a user