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 re
import subprocess import subprocess
import json import json
import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
@ -16,6 +17,41 @@ router = APIRouter(prefix="/api/system")
LOG_GROUPS = ("error", "warning", "info") 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 异常刷屏(前端可能自动刷新) # 避免 Redis 异常刷屏(前端可能自动刷新)
_last_logs_redis_err_ts: float = 0.0 _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: try:
raw, resolved_name, status_all = _status_with_fallback(program) raw, resolved_name, status_all = _status_with_fallback(program)
running, pid, state = _parse_supervisor_status(raw) running, pid, state = _parse_supervisor_status(raw)
meta = _system_meta_read("trading:last_restart") or {}
return { return {
"mode": "supervisor", "mode": "supervisor",
"program": program, "program": program,
@ -723,6 +760,7 @@ async def trading_status(x_admin_token: Optional[str] = Header(default=None, ali
"state": state, "state": state,
"raw": raw, "raw": raw,
"status_all": status_all, "status_all": status_all,
"meta": meta,
} }
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
@ -791,9 +829,22 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
program = _get_program_name() program = _get_program_name()
try: try:
requested_at = _beijing_time_str()
requested_at_ms = int(time.time() * 1000)
out, resolved_name, status_all = _action_with_fallback("restart", program) out, resolved_name, status_all = _action_with_fallback("restart", program)
raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program) raw, resolved_name2, status_all2 = _status_with_fallback(resolved_name or program)
running, pid, state = _parse_supervisor_status(raw) 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 { return {
"message": "交易系统已重启supervisor", "message": "交易系统已重启supervisor",
"output": out, "output": out,
@ -807,7 +858,89 @@ async def trading_restart(x_admin_token: Optional[str] = Header(default=None, al
"raw": raw, "raw": raw,
"status_all": status_all2 or status_all, "status_all": status_all2 or status_all,
}, },
"meta": {
"requested_at": requested_at,
"requested_at_ms": requested_at_ms,
},
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {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 [feasibilityCheck, setFeasibilityCheck] = useState(null)
const [checkingFeasibility, setCheckingFeasibility] = useState(false) const [checkingFeasibility, setCheckingFeasibility] = useState(false)
const [systemStatus, setSystemStatus] = useState(null) const [systemStatus, setSystemStatus] = useState(null)
const [backendStatus, setBackendStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(false) 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 () => { const handleClearCache = async () => {
setSystemBusy(true) setSystemBusy(true)
setMessage('') 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(() => { useEffect(() => {
loadConfigs() loadConfigs()
checkFeasibility() checkFeasibility()
loadSystemStatus() loadSystemStatus()
loadBackendStatus()
const timer = setInterval(() => { const timer = setInterval(() => {
loadSystemStatus() loadSystemStatus()
loadBackendStatus()
}, 3000) }, 3000)
return () => clearInterval(timer) return () => clearInterval(timer)
@ -495,6 +525,7 @@ const ConfigPanel = () => {
</span> </span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null} {systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</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> </div>
<div className="system-actions"> <div className="system-actions">
@ -534,6 +565,22 @@ const ConfigPanel = () => {
> >
重启交易系统 重启交易系统
</button> </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>
<div className="system-hint"> <div className="system-hint">
建议流程先更新配置里的 Key 点击清除缓存 点击重启交易系统确保不再使用旧账号下单 建议流程先更新配置里的 Key 点击清除缓存 点击重启交易系统确保不再使用旧账号下单

View File

@ -298,6 +298,28 @@ export const api = {
return response.json(); 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 // 日志监控Redis List
getSystemLogs: async (params = {}) => { getSystemLogs: async (params = {}) => {
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();