This commit is contained in:
薇薇安 2026-01-19 12:59:41 +08:00
parent 82fd5e3f58
commit c348e3acbb
3 changed files with 202 additions and 0 deletions

View File

@ -2,6 +2,7 @@ import os
import re
import subprocess
import json
import time
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
@ -16,6 +17,41 @@ router = APIRouter(prefix="/api/system")
LOG_GROUPS = ("error", "warning", "info")
# 后端服务启动时间(用于前端展示“运行多久/是否已重启”)
_BACKEND_STARTED_AT_MS: int = int(time.time() * 1000)
# 系统元信息存储(优先 Redis用于记录重启时间等
def _system_meta_prefix() -> str:
return (os.getenv("SYSTEM_META_PREFIX", "ats:system").strip() or "ats:system")
def _system_meta_key(name: str) -> str:
name = (name or "").strip()
return f"{_system_meta_prefix()}:{name}"
def _system_meta_read(name: str) -> Optional[Dict[str, Any]]:
client = _get_redis_client_for_logs()
if client is None:
return None
try:
raw = client.get(_system_meta_key(name))
if not raw:
return None
return json.loads(raw)
except Exception:
return None
def _system_meta_write(name: str, payload: Dict[str, Any], ttl_sec: int = 30 * 24 * 3600) -> None:
client = _get_redis_client_for_logs()
if client is None:
return
try:
client.setex(_system_meta_key(name), int(ttl_sec), json.dumps(payload, ensure_ascii=False))
except Exception:
return
# 避免 Redis 异常刷屏(前端可能自动刷新)
_last_logs_redis_err_ts: float = 0.0
@ -714,6 +750,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
try:
raw, resolved_name, status_all = _status_with_fallback(program)
running, pid, state = _parse_supervisor_status(raw)
meta = _system_meta_read("trading:last_restart") or {}
return {
"mode": "supervisor",
"program": program,
@ -723,6 +760,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
"state": state,
"raw": raw,
"status_all": status_all,
"meta": meta,
}
except Exception as e:
raise HTTPException(
@ -791,9 +829,22 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
program = _get_program_name()
try:
requested_at = _beijing_time_str()
requested_at_ms = int(time.time() * 1000)
out, resolved_name, status_all = _action_with_fallback("restart", program)
raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program)
running, pid, state = _parse_supervisor_status(raw)
# 记录交易系统重启时间(用于前端展示)
_system_meta_write(
"trading:last_restart",
{
"requested_at": requested_at,
"requested_at_ms": requested_at_ms,
"pid": pid,
"program": resolved_name2 or resolved_name or program,
},
)
return {
"message": "交易系统已重启supervisor",
"output": out,
@ -807,7 +858,89 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
"raw": raw,
"status_all": status_all2 or status_all,
},
"meta": {
"requested_at": requested_at,
"requested_at_ms": requested_at_ms,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}")
@router.get("/backend/status")
async def backend_status(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
"""
查看后端服务状态当前 uvicorn 进程
说明
- pid 使用 os.getpid()当前 FastAPI 进程
- last_restart Redis 读取若可用
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
meta = _system_meta_read("backend:last_restart") or {}
return {
"running": True,
"pid": os.getpid(),
"started_at_ms": _BACKEND_STARTED_AT_MS,
"started_at": _beijing_time_str(),
"meta": meta,
}
@router.post("/backend/restart")
async def backend_restart(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
"""
重启后端服务uvicorn
实现方式
- 后端启动脚本为 nohup uvicorn ... &
- 这里通过后台启动 backend/restart.sh 来完成
1) grep 找到 uvicorn api.main:app 进程并 kill
2) 再执行 backend/start.sh 拉起新进程
注意
- 为了让接口能先返回这里会延迟 1s 再执行 restart.sh
"""
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
backend_dir = Path(__file__).parent.parent.parent # backend/
restart_script = backend_dir / "restart.sh"
if not restart_script.exists():
raise HTTPException(status_code=500, detail=f"找不到重启脚本: {restart_script}")
requested_at = _beijing_time_str()
requested_at_ms = int(time.time() * 1000)
cur_pid = os.getpid()
_system_meta_write(
"backend:last_restart",
{
"requested_at": requested_at,
"requested_at_ms": requested_at_ms,
"pid_before": cur_pid,
"script": str(restart_script),
},
)
# 后台执行sleep 1 后再重启,保证当前请求可以返回
cmd = ["bash", "-lc", f"sleep 1; '{restart_script}'"]
try:
subprocess.Popen(
cmd,
cwd=str(backend_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"启动重启脚本失败: {e}")
return {
"message": "已发起后端重启1s 后执行)",
"pid_before": cur_pid,
"requested_at": requested_at,
"requested_at_ms": requested_at_ms,
"script": str(restart_script),
"note": "重启期间接口可能短暂不可用,页面可等待 3-5 秒后刷新状态。",
}

View File

@ -11,6 +11,7 @@ const ConfigPanel = () => {
const [feasibilityCheck, setFeasibilityCheck] = useState(null)
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
const [systemStatus, setSystemStatus] = useState(null)
const [backendStatus, setBackendStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(false)
// /
@ -99,6 +100,15 @@ const ConfigPanel = () => {
}
}
const loadBackendStatus = async () => {
try {
const res = await api.getBackendStatus()
setBackendStatus(res)
} catch (error) {
// 使
}
}
const handleClearCache = async () => {
setSystemBusy(true)
setMessage('')
@ -156,13 +166,33 @@ const ConfigPanel = () => {
}
}
const handleRestartBackend = async () => {
if (!window.confirm('确定要重启后端服务吗?重启期间页面接口会短暂不可用(约 3-10 秒)。')) return
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartBackend()
setMessage(res.message || '已发起后端重启')
//
setTimeout(() => {
loadBackendStatus()
}, 4000)
} catch (error) {
setMessage('重启后端失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
useEffect(() => {
loadConfigs()
checkFeasibility()
loadSystemStatus()
loadBackendStatus()
const timer = setInterval(() => {
loadSystemStatus()
loadBackendStatus()
}, 3000)
return () => clearInterval(timer)
@ -495,6 +525,7 @@ const ConfigPanel = () => {
</span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null}
</div>
</div>
<div className="system-actions">
@ -534,6 +565,22 @@ const ConfigPanel = () => {
>
重启交易系统
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartBackend}
disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。"
>
重启后端服务
</button>
</div>
<div className="system-status" style={{ marginTop: '10px' }}>
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
后端 {backendStatus?.running ? '运行中' : '未知'}
</span>
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
</div>
<div className="system-hint">
建议流程先更新配置里的 Key 点击清除缓存 点击重启交易系统确保不再使用旧账号下单

View File

@ -298,6 +298,28 @@ export const api = {
return response.json();
},
// 后端控制uvicorn
getBackendStatus: async () => {
const response = await fetch(buildUrl('/api/system/backend/status'));
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '获取后端状态失败' }));
throw new Error(error.detail || '获取后端状态失败');
}
return response.json();
},
restartBackend: async () => {
const response = await fetch(buildUrl('/api/system/backend/restart'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '重启后端失败' }));
throw new Error(error.detail || '重启后端失败');
}
return response.json();
},
// 日志监控Redis List
getSystemLogs: async (params = {}) => {
const query = new URLSearchParams(params).toString();