From 45a654f6549619c9384cd952721895eb5113f933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Wed, 21 Jan 2026 21:45:10 +0800 Subject: [PATCH] a --- backend/api/routes/accounts.py | 11 ++++++ backend/api/routes/system.py | 17 +++++++++ frontend/src/App.jsx | 35 ++++++++++------- frontend/src/components/AccountSelector.jsx | 4 +- frontend/src/services/api.js | 8 ++++ trading_system/main.py | 42 ++++++++++++--------- trading_system/recommendations_main.py | 36 ++++++++++-------- 7 files changed, 107 insertions(+), 46 deletions(-) diff --git a/backend/api/routes/accounts.py b/backend/api/routes/accounts.py index 6b87e3f..50331a0 100644 --- a/backend/api/routes/accounts.py +++ b/backend/api/routes/accounts.py @@ -55,6 +55,15 @@ def _mask(s: str) -> str: return f"{s[:4]}...{s[-4:]}" +def _ensure_account_active_for_start(account_id: int): + row = Account.get(int(account_id)) + if not row: + raise HTTPException(status_code=404, detail="账号不存在") + status = (row.get("status") or "active").strip().lower() + if status != "active": + raise HTTPException(status_code=400, detail="账号已禁用,不能启动/重启交易进程") + + @router.get("") @router.get("/") async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> List[Dict[str, Any]]: @@ -268,6 +277,7 @@ async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depe if int(account_id) <= 0: raise HTTPException(status_code=400, detail="account_id 必须 >= 1") require_account_owner(int(account_id), user) + _ensure_account_active_for_start(int(account_id)) program = program_name_for_account(int(account_id)) try: out = run_supervisorctl(["start", program]) @@ -311,6 +321,7 @@ async def trading_restart_for_account(account_id: int, user: Dict[str, Any] = De if int(account_id) <= 0: raise HTTPException(status_code=400, detail="account_id 必须 >= 1") require_account_owner(int(account_id), user) + _ensure_account_active_for_start(int(account_id)) program = program_name_for_account(int(account_id)) try: out = run_supervisorctl(["restart", program]) diff --git a/backend/api/routes/system.py b/backend/api/routes/system.py index 0388d12..3bcbb7c 100644 --- a/backend/api/routes/system.py +++ b/backend/api/routes/system.py @@ -17,6 +17,7 @@ router = APIRouter(prefix="/api/system") # 管理员鉴权(JWT;未启用登录时兼容 X-Admin-Token) from api.auth_deps import require_system_admin # noqa: E402 +from database.models import Account # noqa: E402 LOG_GROUPS = ("error", "warning", "info") @@ -918,8 +919,22 @@ async def trading_restart_all( names = _list_supervisor_process_names(status_all) targets: list[str] = [] + skipped_disabled: list[Dict[str, Any]] = [] for n in names: if n.startswith(prefix): + # 若能解析出 account_id,则跳过 disabled 的账号 + try: + m = re.match(rf"^{re.escape(prefix)}(\d+)$", n) + if m: + aid = int(m.group(1)) + row = Account.get(aid) + st = (row.get("status") if isinstance(row, dict) else None) or "active" + if str(st).strip().lower() != "active": + skipped_disabled.append({"program": n, "account_id": aid, "status": st}) + continue + except Exception: + # 解析失败/查库失败:不影响批量重启流程 + pass targets.append(n) if include_default: @@ -935,6 +950,7 @@ async def trading_restart_all( "count": 0, "targets": [], "status_all": status_all, + "skipped_disabled": skipped_disabled, } reread_out = "" @@ -982,6 +998,7 @@ async def trading_restart_all( "update": update_out, "targets": targets, "results": results, + "skipped_disabled": skipped_disabled, } except Exception as e: raise HTTPException(status_code=500, detail=f"批量重启失败: {e}") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8495801..57e358a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,7 +8,7 @@ import Recommendations from './components/Recommendations' import LogMonitor from './components/LogMonitor' import AccountSelector from './components/AccountSelector' import Login from './components/Login' -import { api, clearAuthToken, setCurrentAccountId, getCurrentAccountId } from './services/api' +import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api' import './App.css' function App() { @@ -20,20 +20,28 @@ function App() { const u = await api.me() setMe(u) - // 普通用户:登录后默认选择“自己的账号” - // 规则:若可见账号列表里存在 account_id == user.id,则优先选它;否则选第一个可见账号。 + // 登录后默认选择账号(避免 admin/用户切换时沿用旧 accountId) try { - if ((u?.role || '') !== 'admin') { - const list = await api.getAccounts() - const accounts = Array.isArray(list) ? list : [] + const list = await api.getAccounts() + const accounts = Array.isArray(list) ? list : [] + const active = accounts.filter((a) => String(a?.status || 'active') === 'active') + const isAdmin = (u?.role || '') === 'admin' + + let target = null + if (isAdmin) { + // 管理员:默认选第一个 active 账号(避免沿用旧值导致“看起来还是上个账号”) + target = active[0]?.id || accounts[0]?.id + } else { + // 普通用户:优先选“自己的账号”(且必须 active),否则选第一个 active const uid = parseInt(String(u?.id || ''), 10) - const match = accounts.find((a) => parseInt(String(a?.id || ''), 10) === uid) - const target = match?.id || accounts[0]?.id - if (target) { - const cur = getCurrentAccountId() - const next = parseInt(String(target), 10) - if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next) - } + const match = active.find((a) => parseInt(String(a?.id || ''), 10) === uid) + target = match?.id || active[0]?.id || accounts[0]?.id + } + + if (target) { + const cur = getCurrentAccountId() + const next = parseInt(String(target), 10) + if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next) } } catch (e) { // ignore @@ -91,6 +99,7 @@ function App() { className="nav-logout" onClick={() => { clearAuthToken() + clearCurrentAccountId() setMe(null) setChecking(false) }} diff --git a/frontend/src/components/AccountSelector.jsx b/frontend/src/components/AccountSelector.jsx index 3d9c76f..29a1244 100644 --- a/frontend/src/components/AccountSelector.jsx +++ b/frontend/src/components/AccountSelector.jsx @@ -38,7 +38,8 @@ const AccountSelector = ({ onChanged }) => { useEffect(() => { if (!options.length) return if (options.some((a) => a.id === accountId)) return - setAccountId(options[0].id) + const firstActive = options.find((a) => String(a?.status || 'active') === 'active') || options[0] + setAccountId(firstActive.id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [optionsKey, accountId]) @@ -57,6 +58,7 @@ const AccountSelector = ({ onChanged }) => { {options.map((a) => ( ))} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index c4110bc..7dab9c3 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -47,6 +47,14 @@ export const setCurrentAccountId = (accountId) => { } }; +export const clearCurrentAccountId = () => { + try { + localStorage.removeItem(ACCOUNT_ID_STORAGE_KEY); + } catch (e) { + // ignore + } +}; + const withAuthHeaders = (headers = {}) => { const token = getAuthToken(); if (!token) return { ...headers }; diff --git a/trading_system/main.py b/trading_system/main.py index 1e0b1bd..ade2c6f 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -6,23 +6,31 @@ import logging import sys from pathlib import Path -# 启动方式兼容: -# - python trading_system/main.py(__package__ 为空,需从同目录导入) -# - python -m trading_system.main(__package__='trading_system',必须用相对导入) -if __package__ in (None, ""): - from binance_client import BinanceClient - from market_scanner import MarketScanner - from risk_manager import RiskManager - from position_manager import PositionManager - from strategy import TradingStrategy - import config -else: - from .binance_client import BinanceClient - from .market_scanner import MarketScanner - from .risk_manager import RiskManager - from .position_manager import PositionManager - from .strategy import TradingStrategy - from . import config +# 启动方式兼容(更鲁棒): +# - supervisor 推荐:python -m trading_system.main(相对导入) +# - 手动调试:python trading_system/main.py(同目录导入) +# - 其它非常规启动方式:尽量通过补齐 sys.path 避免本地模块找不到 +try: + from .binance_client import BinanceClient # type: ignore + from .market_scanner import MarketScanner # type: ignore + from .risk_manager import RiskManager # type: ignore + from .position_manager import PositionManager # type: ignore + from .strategy import TradingStrategy # type: ignore + from . import config # type: ignore +except Exception: + _here = Path(__file__).resolve().parent + _root = _here.parent + # 某些 supervisor/启动脚本可能会导致 sys.path 没包含 trading_system 目录 + if str(_here) not in sys.path: + sys.path.insert(0, str(_here)) + if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + from binance_client import BinanceClient # type: ignore + from market_scanner import MarketScanner # type: ignore + from risk_manager import RiskManager # type: ignore + from position_manager import PositionManager # type: ignore + from strategy import TradingStrategy # type: ignore + import config # type: ignore # 配置日志(支持相对路径) log_file = config.LOG_FILE diff --git a/trading_system/recommendations_main.py b/trading_system/recommendations_main.py index 49323ac..50c01e8 100644 --- a/trading_system/recommendations_main.py +++ b/trading_system/recommendations_main.py @@ -14,21 +14,27 @@ from pathlib import Path from datetime import datetime, timezone, timedelta -# 启动方式兼容: -# - python trading_system/recommendations_main.py(__package__ 为空,需从同目录导入) -# - python -m trading_system.recommendations_main(__package__='trading_system',必须用相对导入) -if __package__ in (None, ""): - from binance_client import BinanceClient - from market_scanner import MarketScanner - from risk_manager import RiskManager - from trade_recommender import TradeRecommender - import config -else: - from .binance_client import BinanceClient - from .market_scanner import MarketScanner - from .risk_manager import RiskManager - from .trade_recommender import TradeRecommender - from . import config +# 启动方式兼容(更鲁棒): +# - supervisor 推荐:python -m trading_system.recommendations_main(相对导入) +# - 手动调试:python trading_system/recommendations_main.py(同目录导入) +try: + from .binance_client import BinanceClient # type: ignore + from .market_scanner import MarketScanner # type: ignore + from .risk_manager import RiskManager # type: ignore + from .trade_recommender import TradeRecommender # type: ignore + from . import config # type: ignore +except Exception: + _here = Path(__file__).resolve().parent + _root = _here.parent + if str(_here) not in sys.path: + sys.path.insert(0, str(_here)) + if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + from binance_client import BinanceClient # type: ignore + from market_scanner import MarketScanner # type: ignore + from risk_manager import RiskManager # type: ignore + from trade_recommender import TradeRecommender # type: ignore + import config # type: ignore class BeijingTimeFormatter(logging.Formatter):