feat(recommendations): 添加推荐服务管理API与前端控制功能
在后端API中新增推荐服务的状态检查、重启、停止和启动功能,确保能够有效管理推荐服务进程。同时,更新前端组件以支持推荐服务状态的显示与控制,提升用户体验。此改动为推荐服务的管理提供了更直观的操作界面与实时状态反馈。
This commit is contained in:
parent
24d01cba0d
commit
5f256daf27
|
|
@ -1353,3 +1353,108 @@ async def backend_stop(_admin: Dict[str, Any] = Depends(require_system_admin)) -
|
|||
"warning": "后端服务停止后,Web 界面将无法访问,请手动在服务器启动!",
|
||||
}
|
||||
|
||||
|
||||
def _recommendations_process_running() -> Tuple[bool, Optional[int]]:
|
||||
"""检查推荐服务进程是否运行,返回 (running, pid)"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "trading_system.recommendations_main"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pids = result.stdout.strip().split()
|
||||
pid = int(pids[0]) if pids else None
|
||||
return True, pid
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
return False, None
|
||||
|
||||
|
||||
@router.get("/recommendations/status")
|
||||
async def recommendations_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
|
||||
"""查看推荐服务状态(recommendations_main 进程)"""
|
||||
running, pid = _recommendations_process_running()
|
||||
return {
|
||||
"running": running,
|
||||
"pid": pid,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/recommendations/restart")
|
||||
async def recommendations_restart(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
|
||||
"""重启推荐服务(recommendations_main)"""
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
restart_script = backend_dir / "restart_recommendations.sh"
|
||||
if not restart_script.exists():
|
||||
raise HTTPException(status_code=500, detail=f"找不到重启脚本: {restart_script}")
|
||||
running_before, pid_before = _recommendations_process_running()
|
||||
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": pid_before,
|
||||
"running_before": running_before,
|
||||
"script": str(restart_script),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/recommendations/stop")
|
||||
async def recommendations_stop(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
|
||||
"""停止推荐服务"""
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
stop_script = backend_dir / "stop_recommendations.sh"
|
||||
if not stop_script.exists():
|
||||
raise HTTPException(status_code=500, detail=f"找不到停止脚本: {stop_script}")
|
||||
running_before, pid_before = _recommendations_process_running()
|
||||
cmd = ["bash", "-lc", f"sleep 1; '{stop_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": "已发起推荐服务停止",
|
||||
"pid_before": pid_before,
|
||||
"running_before": running_before,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/recommendations/start")
|
||||
async def recommendations_start(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
|
||||
"""启动推荐服务(若已运行则跳过)"""
|
||||
running, pid = _recommendations_process_running()
|
||||
if running:
|
||||
return {"message": "推荐服务已在运行中", "pid": pid, "skipped": True}
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
start_script = backend_dir / "start_recommendations.sh"
|
||||
if not start_script.exists():
|
||||
raise HTTPException(status_code=500, detail=f"找不到启动脚本: {start_script}")
|
||||
cmd = ["bash", "-lc", f"sleep 1; '{start_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 后执行)", "script": str(start_script)}
|
||||
|
||||
|
|
|
|||
22
backend/restart_recommendations.sh
Executable file
22
backend/restart_recommendations.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
# 重启推荐服务
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 查找 recommendations_main 进程
|
||||
PID=$(ps aux | grep "trading_system.recommendations_main" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -z "$PID" ]; then
|
||||
echo "未找到运行中的推荐服务,直接启动..."
|
||||
./start_recommendations.sh
|
||||
else
|
||||
echo "找到推荐服务,PID: $PID,正在重启..."
|
||||
kill $PID 2>/dev/null || true
|
||||
sleep 2
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
kill -9 $PID 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
echo "正在启动新服务..."
|
||||
./start_recommendations.sh
|
||||
fi
|
||||
35
backend/start_recommendations.sh
Executable file
35
backend/start_recommendations.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/bash
|
||||
# 启动推荐服务(recommendations_main)
|
||||
# 参考 backend/start.sh,使用相同 venv 与项目根目录
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
PROJECT_ROOT="$(cd .. && pwd)"
|
||||
|
||||
# 激活虚拟环境(与 backend/start.sh 一致)
|
||||
if [ -d ".venv" ]; then
|
||||
source .venv/bin/activate
|
||||
elif [ -d "../.venv" ]; then
|
||||
source ../.venv/bin/activate
|
||||
else
|
||||
echo "错误: 找不到虚拟环境(.venv 或 ../.venv)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 设置环境变量(与 backend/start.sh 类似)
|
||||
export PYTHONPATH="${PROJECT_ROOT}"
|
||||
export DB_HOST=${DB_HOST:-localhost}
|
||||
export DB_PORT=${DB_PORT:-3306}
|
||||
export DB_USER=${DB_USER:-autosys}
|
||||
export DB_PASSWORD=${DB_PASSWORD:-}
|
||||
export DB_NAME=${DB_NAME:-auto_trade_sys}
|
||||
export LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "${PROJECT_ROOT}/logs"
|
||||
|
||||
# 启动推荐服务(后台运行)
|
||||
cd "${PROJECT_ROOT}"
|
||||
nohup python -m trading_system.recommendations_main > logs/recommendations.log 2>&1 &
|
||||
PID=$!
|
||||
echo "推荐服务已启动,PID: $PID"
|
||||
echo "日志: tail -f ${PROJECT_ROOT}/logs/recommendations.log"
|
||||
21
backend/stop_recommendations.sh
Executable file
21
backend/stop_recommendations.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
# 停止推荐服务(recommendations_main)
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 查找 recommendations_main 进程
|
||||
PID=$(ps aux | grep "trading_system.recommendations_main" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -z "$PID" ]; then
|
||||
echo "未找到运行中的推荐服务"
|
||||
else
|
||||
echo "找到推荐服务,PID: $PID"
|
||||
echo "正在停止..."
|
||||
kill $PID 2>/dev/null || true
|
||||
sleep 2
|
||||
if ps -p $PID > /dev/null 2>&1; then
|
||||
echo "尝试强制停止..."
|
||||
kill -9 $PID 2>/dev/null || true
|
||||
fi
|
||||
echo "推荐服务已停止"
|
||||
fi
|
||||
|
|
@ -199,6 +199,7 @@ const GlobalConfig = () => {
|
|||
// 系统控制相关
|
||||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
const [backendStatus, setBackendStatus] = useState(null)
|
||||
const [recommendationsStatus, setRecommendationsStatus] = useState(null)
|
||||
const [servicesSummary, setServicesSummary] = useState(null)
|
||||
const [systemBusy, setSystemBusy] = useState(false)
|
||||
|
||||
|
|
@ -555,6 +556,15 @@ const GlobalConfig = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const loadRecommendationsStatus = async () => {
|
||||
try {
|
||||
const res = await api.getRecommendationsStatus()
|
||||
setRecommendationsStatus(res)
|
||||
} catch (error) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
if (isAdmin) {
|
||||
|
|
@ -562,7 +572,8 @@ const GlobalConfig = () => {
|
|||
await Promise.allSettled([
|
||||
loadConfigs(),
|
||||
loadSystemStatus(),
|
||||
loadBackendStatus()
|
||||
loadBackendStatus(),
|
||||
loadRecommendationsStatus()
|
||||
])
|
||||
}
|
||||
setLoading(false)
|
||||
|
|
@ -574,6 +585,7 @@ const GlobalConfig = () => {
|
|||
const timer = setInterval(() => {
|
||||
loadSystemStatus().catch(() => {})
|
||||
loadBackendStatus().catch(() => {})
|
||||
loadRecommendationsStatus().catch(() => {})
|
||||
}, 3000)
|
||||
return () => clearInterval(timer)
|
||||
}
|
||||
|
|
@ -628,6 +640,49 @@ const GlobalConfig = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleRestartRecommendations = async () => {
|
||||
setSystemBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const res = await api.restartRecommendations()
|
||||
setMessage(res.message || '已发起推荐服务重启')
|
||||
setTimeout(() => loadRecommendationsStatus(), 4000)
|
||||
} catch (error) {
|
||||
setMessage('重启推荐服务失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
setSystemBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopRecommendations = async () => {
|
||||
if (!window.confirm('确定要停止推荐服务吗?停止后需在此处点击「启动推荐服务」或手动在服务器启动。')) return
|
||||
setSystemBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const res = await api.stopRecommendations()
|
||||
setMessage(res.message || '已发起推荐服务停止')
|
||||
setTimeout(() => loadRecommendationsStatus(), 3000)
|
||||
} catch (error) {
|
||||
setMessage('停止推荐服务失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
setSystemBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartRecommendations = async () => {
|
||||
setSystemBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const res = await api.startRecommendations()
|
||||
setMessage(res.message || (res.skipped ? '推荐服务已在运行中' : '已发起推荐服务启动'))
|
||||
setTimeout(() => loadRecommendationsStatus(), 3000)
|
||||
} catch (error) {
|
||||
setMessage('启动推荐服务失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
setSystemBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestartAllTrading = async () => {
|
||||
if (!window.confirm('确定要重启【所有账号】的交易进程吗?这会让所有用户的交易服务短暂中断(约 3-10 秒),用于升级代码后统一生效。')) return
|
||||
setSystemBusy(true)
|
||||
|
|
@ -1110,6 +1165,10 @@ const GlobalConfig = () => {
|
|||
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: backendStatus?.running ? '#4caf50' : '#f44336', display: 'inline-block' }}></span>
|
||||
<span style={{ fontWeight: 500 }}>后端: {backendStatus?.running ? '运行中' : '停止'}</span>
|
||||
</div>
|
||||
<div className="status-item" style={{ display: 'flex', alignItems: 'center', gap: '6px', borderLeft: '1px solid #ddd', paddingLeft: '15px' }}>
|
||||
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: recommendationsStatus?.running ? '#4caf50' : '#f44336', display: 'inline-block' }}></span>
|
||||
<span style={{ fontWeight: 500 }}>推荐: {recommendationsStatus?.running ? '运行中' : '停止'}</span>
|
||||
</div>
|
||||
{servicesSummary && (
|
||||
<div className="status-item" style={{ display: 'flex', alignItems: 'center', gap: '6px', borderLeft: '1px solid #ddd', paddingLeft: '15px' }}>
|
||||
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#2196f3', display: 'inline-block' }}></span>
|
||||
|
|
@ -1148,6 +1207,39 @@ const GlobalConfig = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-control-group" style={{ marginTop: '15px' }}>
|
||||
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>推荐服务管理</h4>
|
||||
<div className="system-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn primary"
|
||||
onClick={handleRestartRecommendations}
|
||||
disabled={systemBusy}
|
||||
title="通过 backend/restart_recommendations.sh 重启推荐服务(recommendations_main)"
|
||||
>
|
||||
重启推荐服务
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn danger"
|
||||
onClick={handleStopRecommendations}
|
||||
disabled={systemBusy}
|
||||
title="停止推荐服务"
|
||||
>
|
||||
停止推荐服务
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="system-btn primary"
|
||||
onClick={handleStartRecommendations}
|
||||
disabled={systemBusy}
|
||||
title="启动推荐服务(若已运行则跳过)"
|
||||
>
|
||||
启动推荐服务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-control-group" style={{ marginTop: '15px' }}>
|
||||
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>系统数据维护</h4>
|
||||
<div className="system-actions">
|
||||
|
|
|
|||
|
|
@ -774,6 +774,52 @@ export const api = {
|
|||
return response.json();
|
||||
},
|
||||
|
||||
// 推荐服务控制(recommendations_main)
|
||||
getRecommendationsStatus: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/recommendations/status'), { headers: withAccountHeaders() });
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '获取推荐服务状态失败' }));
|
||||
throw new Error(error.detail || '获取推荐服务状态失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
restartRecommendations: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/recommendations/restart'), {
|
||||
method: 'POST',
|
||||
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '重启推荐服务失败' }));
|
||||
throw new Error(error.detail || '重启推荐服务失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
stopRecommendations: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/recommendations/stop'), {
|
||||
method: 'POST',
|
||||
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: '停止推荐服务失败' }));
|
||||
throw new Error(error.detail || '停止推荐服务失败');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
startRecommendations: async () => {
|
||||
const response = await fetch(buildUrl('/api/system/recommendations/start'), {
|
||||
method: 'POST',
|
||||
headers: withAccountHeaders({ '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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user