diff --git a/backend/api/routes/accounts.py b/backend/api/routes/accounts.py index f85bc2c..441ab9b 100644 --- a/backend/api/routes/accounts.py +++ b/backend/api/routes/accounts.py @@ -81,7 +81,8 @@ async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> Lis return out memberships = UserAccountMembership.list_for_user(int(user["id"])) - account_ids = [int(m.get("account_id")) for m in (memberships or []) if m.get("account_id") is not None] + membership_map = {int(m.get("account_id")): (m.get("role") or "viewer") for m in (memberships or []) if m.get("account_id") is not None} + account_ids = list(membership_map.keys()) for aid in account_ids: r = Account.get(int(aid)) if not r: @@ -94,6 +95,7 @@ async def list_accounts(user: Dict[str, Any] = Depends(get_current_user)) -> Lis "name": r.get("name") or "", "status": r.get("status") or "active", "use_testnet": bool(use_testnet), + "role": membership_map.get(int(aid), "viewer"), } ) return out @@ -167,8 +169,10 @@ async def update_account(account_id: int, payload: AccountUpdate, _admin: Dict[s @router.put("/{account_id}/credentials") -async def update_credentials(account_id: int, payload: AccountCredentialsUpdate, _admin: Dict[str, Any] = Depends(get_admin_user)): +async def update_credentials(account_id: int, payload: AccountCredentialsUpdate, user: Dict[str, Any] = Depends(get_current_user)): try: + if (user.get("role") or "user") != "admin": + require_account_owner(int(account_id), user) row = Account.get(int(account_id)) if not row: raise HTTPException(status_code=404, detail="账号不存在") @@ -213,11 +217,44 @@ async def trading_status_for_account(account_id: int, user: Dict[str, Any] = Dep try: raw = run_supervisorctl(["status", program]) running, pid, state = parse_supervisor_status(raw) - return {"program": program, "running": running, "pid": pid, "state": state, "raw": raw} + # 仅 owner/admin 可看 tail(便于自助排障) + stderr_tail = "" + try: + is_admin = (user.get("role") or "user") == "admin" + role = UserAccountMembership.get_role(int(user["id"]), int(account_id)) if not is_admin else "admin" + if is_admin or role == "owner": + if state in {"FATAL", "EXITED", "BACKOFF"}: + stderr_tail = tail_supervisor(program, "stderr", 120) + except Exception: + stderr_tail = "" + + resp = {"program": program, "running": running, "pid": pid, "state": state, "raw": raw} + if stderr_tail: + resp["stderr_tail"] = stderr_tail + return resp except Exception as e: raise HTTPException(status_code=500, detail=f"读取交易进程状态失败: {e}") +@router.get("/{account_id}/trading/tail") +async def trading_tail_for_account( + account_id: int, + stream: str = "stderr", + lines: int = 200, + user: Dict[str, Any] = Depends(get_current_user), +): + """ + 读取该账号交易进程日志尾部(用于排障)。仅 owner/admin 可读。 + """ + require_account_owner(int(account_id), user) + program = program_name_for_account(int(account_id)) + try: + out = tail_supervisor(program, stream=stream, lines=lines) + return {"program": program, "stream": stream, "lines": lines, "tail": out} + except Exception as e: + raise HTTPException(status_code=500, detail=f"读取交易进程日志失败: {e}") + + @router.post("/{account_id}/trading/start") async def trading_start_for_account(account_id: int, user: Dict[str, Any] = Depends(get_current_user)): require_account_owner(int(account_id), user) diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 1b8834f..ca5462d 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -15,7 +15,7 @@ sys.path.insert(0, str(project_root / 'backend')) sys.path.insert(0, str(project_root / 'trading_system')) from database.models import TradingConfig, Account -from api.auth_deps import get_current_user, get_account_id, require_admin +from api.auth_deps import get_current_user, get_account_id, require_admin, require_account_owner logger = logging.getLogger(__name__) router = APIRouter() @@ -140,13 +140,13 @@ async def get_all_configs( "value": _mask(api_key or ""), "type": "string", "category": "api", - "description": "币安API密钥(账号私有,仅脱敏展示;仅管理员可修改)", + "description": "币安API密钥(账号私有,仅脱敏展示;账号 owner/admin 可修改)", } result["BINANCE_API_SECRET"] = { "value": "", "type": "string", "category": "api", - "description": "币安API密钥Secret(账号私有,不回传明文;仅管理员可修改)", + "description": "币安API密钥Secret(账号私有,不回传明文;账号 owner/admin 可修改)", } result["USE_TESTNET"] = { "value": bool(use_testnet), @@ -502,9 +502,14 @@ async def update_config( ): """更新配置""" try: + # 非管理员:必须是该账号 owner 才允许修改配置 + if (user.get("role") or "user") != "admin": + require_account_owner(account_id, user) + # API Key/Secret/Testnet:写入 accounts 表(账号私有) if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}: - require_admin(user) + if (user.get("role") or "user") != "admin": + require_account_owner(account_id, user) try: if key == "BINANCE_API_KEY": Account.update_credentials(account_id, api_key=str(item.value or "")) @@ -594,13 +599,17 @@ async def update_configs_batch( ): """批量更新配置""" try: + # 非管理员:必须是该账号 owner 才允许修改配置 + if (user.get("role") or "user") != "admin": + require_account_owner(account_id, user) updated_count = 0 errors = [] for item in configs: try: if item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}: - require_admin(user) + if (user.get("role") or "user") != "admin": + require_account_owner(account_id, user) # 验证配置值 if item.type == 'number': try: diff --git a/backend/database/models.py b/backend/database/models.py index bd039c6..7db8f20 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -105,9 +105,13 @@ class Account: api_key = decrypt_str(row.get("api_key_enc") or "") api_secret = decrypt_str(row.get("api_secret_enc") or "") except Exception: - # 兼容:无 cryptography 或未配 master key 时,先按明文兜底 - api_key = row.get("api_key_enc") or "" - api_secret = row.get("api_secret_enc") or "" + # 兼容:无 cryptography 或未配 master key 时: + # - 若库里是明文,仍可工作 + # - 若库里是 enc:v1 密文但未配 ATS_MASTER_KEY,则不能解密,也不能把密文当作 Key 使用 + api_key_raw = row.get("api_key_enc") or "" + api_secret_raw = row.get("api_secret_enc") or "" + api_key = "" if str(api_key_raw).startswith("enc:v1:") else str(api_key_raw) + api_secret = "" if str(api_secret_raw).startswith("enc:v1:") else str(api_secret_raw) use_testnet = bool(row.get("use_testnet") or False) return api_key, api_secret, use_testnet diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index 1bdf42e..02b1ec0 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -14,6 +14,7 @@ const ConfigPanel = ({ currentUser }) => { const [backendStatus, setBackendStatus] = useState(null) const [systemBusy, setSystemBusy] = useState(false) const [accountTradingStatus, setAccountTradingStatus] = useState(null) + const [accountTradingErr, setAccountTradingErr] = useState('') // 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航) const [accountId, setAccountId] = useState(getCurrentAccountId()) @@ -212,8 +213,10 @@ const ConfigPanel = ({ currentUser }) => { try { const res = await api.getAccountTradingStatus(accountId) setAccountTradingStatus(res) + setAccountTradingErr('') } catch (error) { - // 非 owner/未配置 supervisor 时可能会失败,静默即可 + setAccountTradingStatus(null) + setAccountTradingErr(error?.message || '获取交易进程状态失败') } } @@ -401,6 +404,7 @@ const ConfigPanel = ({ currentUser }) => { useEffect(() => { const timer = setInterval(() => { const cur = getCurrentAccountId() + if (cur !== accountId) setAccountId(cur) }, 1000) return () => clearInterval(timer) @@ -795,6 +799,87 @@ const ConfigPanel = ({ currentUser }) => {
{String(accountTradingStatus.raw || '')}
+ {String(accountTradingStatus.stderr_tail || '')}
+