This commit is contained in:
薇薇安 2026-01-21 21:45:10 +08:00
parent e80fd1059b
commit 45a654f654
7 changed files with 107 additions and 46 deletions

View File

@ -55,6 +55,15 @@ def _mask(s: str) -> str:
return f"{s[:4]}...{s[-4:]}" 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("")
@router.get("/") @router.get("/")
async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> List[Dict[str, Any]]: 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: if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1") raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user) require_account_owner(int(account_id), user)
_ensure_account_active_for_start(int(account_id))
program = program_name_for_account(int(account_id)) program = program_name_for_account(int(account_id))
try: try:
out = run_supervisorctl(["start", program]) 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: if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1") raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user) require_account_owner(int(account_id), user)
_ensure_account_active_for_start(int(account_id))
program = program_name_for_account(int(account_id)) program = program_name_for_account(int(account_id))
try: try:
out = run_supervisorctl(["restart", program]) out = run_supervisorctl(["restart", program])

View File

@ -17,6 +17,7 @@ router = APIRouter(prefix="/api/system")
# 管理员鉴权JWT未启用登录时兼容 X-Admin-Token # 管理员鉴权JWT未启用登录时兼容 X-Admin-Token
from api.auth_deps import require_system_admin # noqa: E402 from api.auth_deps import require_system_admin # noqa: E402
from database.models import Account # noqa: E402
LOG_GROUPS = ("error", "warning", "info") LOG_GROUPS = ("error", "warning", "info")
@ -918,8 +919,22 @@ async def trading_restart_all(
names = _list_supervisor_process_names(status_all) names = _list_supervisor_process_names(status_all)
targets: list[str] = [] targets: list[str] = []
skipped_disabled: list[Dict[str, Any]] = []
for n in names: for n in names:
if n.startswith(prefix): 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) targets.append(n)
if include_default: if include_default:
@ -935,6 +950,7 @@ async def trading_restart_all(
"count": 0, "count": 0,
"targets": [], "targets": [],
"status_all": status_all, "status_all": status_all,
"skipped_disabled": skipped_disabled,
} }
reread_out = "" reread_out = ""
@ -982,6 +998,7 @@ async def trading_restart_all(
"update": update_out, "update": update_out,
"targets": targets, "targets": targets,
"results": results, "results": results,
"skipped_disabled": skipped_disabled,
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"批量重启失败: {e}") raise HTTPException(status_code=500, detail=f"批量重启失败: {e}")

View File

@ -8,7 +8,7 @@ import Recommendations from './components/Recommendations'
import LogMonitor from './components/LogMonitor' import LogMonitor from './components/LogMonitor'
import AccountSelector from './components/AccountSelector' import AccountSelector from './components/AccountSelector'
import Login from './components/Login' 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' import './App.css'
function App() { function App() {
@ -20,21 +20,29 @@ function App() {
const u = await api.me() const u = await api.me()
setMe(u) setMe(u)
// // admin/沿 accountId
// account_id == user.id
try { try {
if ((u?.role || '') !== 'admin') {
const list = await api.getAccounts() const list = await api.getAccounts()
const accounts = Array.isArray(list) ? list : [] 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 uid = parseInt(String(u?.id || ''), 10)
const match = accounts.find((a) => parseInt(String(a?.id || ''), 10) === uid) const match = active.find((a) => parseInt(String(a?.id || ''), 10) === uid)
const target = match?.id || accounts[0]?.id target = match?.id || active[0]?.id || accounts[0]?.id
}
if (target) { if (target) {
const cur = getCurrentAccountId() const cur = getCurrentAccountId()
const next = parseInt(String(target), 10) const next = parseInt(String(target), 10)
if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next) if (Number.isFinite(next) && next > 0 && cur !== next) setCurrentAccountId(next)
} }
}
} catch (e) { } catch (e) {
// ignore // ignore
} }
@ -91,6 +99,7 @@ function App() {
className="nav-logout" className="nav-logout"
onClick={() => { onClick={() => {
clearAuthToken() clearAuthToken()
clearCurrentAccountId()
setMe(null) setMe(null)
setChecking(false) setChecking(false)
}} }}

View File

@ -38,7 +38,8 @@ const AccountSelector = ({ onChanged }) => {
useEffect(() => { useEffect(() => {
if (!options.length) return if (!options.length) return
if (options.some((a) => a.id === accountId)) 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [optionsKey, accountId]) }, [optionsKey, accountId])
@ -57,6 +58,7 @@ const AccountSelector = ({ onChanged }) => {
{options.map((a) => ( {options.map((a) => (
<option key={a.id} value={a.id}> <option key={a.id} value={a.id}>
#{a.id} {a.name || 'account'} #{a.id} {a.name || 'account'}
{String(a?.status || 'active') === 'disabled' ? '(已禁用)' : ''}
</option> </option>
))} ))}
</select> </select>

View File

@ -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 withAuthHeaders = (headers = {}) => {
const token = getAuthToken(); const token = getAuthToken();
if (!token) return { ...headers }; if (!token) return { ...headers };

View File

@ -6,23 +6,31 @@ import logging
import sys import sys
from pathlib import Path from pathlib import Path
# 启动方式兼容: # 启动方式兼容(更鲁棒):
# - python trading_system/main.py__package__ 为空,需从同目录导入) # - supervisor 推荐python -m trading_system.main相对导入
# - python -m trading_system.main__package__='trading_system',必须用相对导入) # - 手动调试python trading_system/main.py同目录导入
if __package__ in (None, ""): # - 其它非常规启动方式:尽量通过补齐 sys.path 避免本地模块找不到
from binance_client import BinanceClient try:
from market_scanner import MarketScanner from .binance_client import BinanceClient # type: ignore
from risk_manager import RiskManager from .market_scanner import MarketScanner # type: ignore
from position_manager import PositionManager from .risk_manager import RiskManager # type: ignore
from strategy import TradingStrategy from .position_manager import PositionManager # type: ignore
import config from .strategy import TradingStrategy # type: ignore
else: from . import config # type: ignore
from .binance_client import BinanceClient except Exception:
from .market_scanner import MarketScanner _here = Path(__file__).resolve().parent
from .risk_manager import RiskManager _root = _here.parent
from .position_manager import PositionManager # 某些 supervisor/启动脚本可能会导致 sys.path 没包含 trading_system 目录
from .strategy import TradingStrategy if str(_here) not in sys.path:
from . import config 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 log_file = config.LOG_FILE

View File

@ -14,21 +14,27 @@ from pathlib import Path
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
# 启动方式兼容: # 启动方式兼容(更鲁棒):
# - python trading_system/recommendations_main.py__package__ 为空,需从同目录导入) # - supervisor 推荐python -m trading_system.recommendations_main相对导入
# - python -m trading_system.recommendations_main__package__='trading_system',必须用相对导入) # - 手动调试python trading_system/recommendations_main.py同目录导入
if __package__ in (None, ""): try:
from binance_client import BinanceClient from .binance_client import BinanceClient # type: ignore
from market_scanner import MarketScanner from .market_scanner import MarketScanner # type: ignore
from risk_manager import RiskManager from .risk_manager import RiskManager # type: ignore
from trade_recommender import TradeRecommender from .trade_recommender import TradeRecommender # type: ignore
import config from . import config # type: ignore
else: except Exception:
from .binance_client import BinanceClient _here = Path(__file__).resolve().parent
from .market_scanner import MarketScanner _root = _here.parent
from .risk_manager import RiskManager if str(_here) not in sys.path:
from .trade_recommender import TradeRecommender sys.path.insert(0, str(_here))
from . import config 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): class BeijingTimeFormatter(logging.Formatter):