From 5f256daf272fed3539678ca9eaacea4fcbdaced0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 23 Feb 2026 18:02:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(recommendations):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=8E=A8=E8=8D=90=E6=9C=8D=E5=8A=A1=E7=AE=A1=E7=90=86API?= =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AF=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在后端API中新增推荐服务的状态检查、重启、停止和启动功能,确保能够有效管理推荐服务进程。同时,更新前端组件以支持推荐服务状态的显示与控制,提升用户体验。此改动为推荐服务的管理提供了更直观的操作界面与实时状态反馈。 --- backend/api/routes/system.py | 105 +++++++++++++++++++++++ backend/restart_recommendations.sh | 22 +++++ backend/start_recommendations.sh | 35 ++++++++ backend/stop_recommendations.sh | 21 +++++ frontend/src/components/GlobalConfig.jsx | 94 +++++++++++++++++++- frontend/src/services/api.js | 46 ++++++++++ 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100755 backend/restart_recommendations.sh create mode 100755 backend/start_recommendations.sh create mode 100755 backend/stop_recommendations.sh diff --git a/backend/api/routes/system.py b/backend/api/routes/system.py index e9032c9..6535c4f 100644 --- a/backend/api/routes/system.py +++ b/backend/api/routes/system.py @@ -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)} + diff --git a/backend/restart_recommendations.sh b/backend/restart_recommendations.sh new file mode 100755 index 0000000..3e2fd30 --- /dev/null +++ b/backend/restart_recommendations.sh @@ -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 diff --git a/backend/start_recommendations.sh b/backend/start_recommendations.sh new file mode 100755 index 0000000..ff14127 --- /dev/null +++ b/backend/start_recommendations.sh @@ -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" diff --git a/backend/stop_recommendations.sh b/backend/stop_recommendations.sh new file mode 100755 index 0000000..9addb1f --- /dev/null +++ b/backend/stop_recommendations.sh @@ -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 diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index b052e0b..fa9b6e9 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -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 = () => { 后端: {backendStatus?.running ? '运行中' : '停止'} +
+ + 推荐: {recommendationsStatus?.running ? '运行中' : '停止'} +
{servicesSummary && (
@@ -1148,6 +1207,39 @@ const GlobalConfig = () => {
+
+

推荐服务管理

+
+ + + +
+
+

系统数据维护

diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 8946d17..3453c26 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -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();