feat(recommendations): 添加推荐服务管理API与前端控制功能

在后端API中新增推荐服务的状态检查、重启、停止和启动功能,确保能够有效管理推荐服务进程。同时,更新前端组件以支持推荐服务状态的显示与控制,提升用户体验。此改动为推荐服务的管理提供了更直观的操作界面与实时状态反馈。
This commit is contained in:
薇薇安 2026-02-23 18:02:53 +08:00
parent 24d01cba0d
commit 5f256daf27
6 changed files with 322 additions and 1 deletions

View File

@ -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)}

View 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

View 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
View 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

View File

@ -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">

View File

@ -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();