This commit is contained in:
薇薇安 2026-01-21 17:40:25 +08:00
parent 3b0ff0227e
commit fe60f12ee0
4 changed files with 147 additions and 12 deletions

View File

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

View File

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

View File

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

View File

@ -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 }) => {
<div className="system-hint">
提示若按钮报无权限请让管理员在用户授权里把该账号分配为 owner若报 supervisor 相关错误请检查后端对 `/www/server/panel/plugin/supervisor` 的写权限与 supervisorctl 可执行权限
</div>
{accountTradingErr ? (
<div className="system-hint" style={{ color: '#b00020' }}>
{accountTradingErr}
</div>
) : null}
{accountTradingStatus?.raw ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>查看原始状态输出</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.raw || '')}</pre>
</details>
) : null}
{accountTradingStatus?.stderr_tail ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>查看最近错误日志stderr</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
</details>
) : null}
</div>
{/* 账号密钥当前账号owner/admin 可改) */}
<div className="system-section">
<div className="system-header">
<h3>账号密钥当前账号</h3>
<div className="system-status">
<span className="system-status-meta">说明为安全起见页面不会回显 Secret填入后保存即可</span>
</div>
</div>
<div className="accounts-form">
<label>
API KEY留空=不改
<input
type="password"
value={credForm.api_key}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
placeholder="粘贴你的 Binance API KEY"
/>
</label>
<label>
API SECRET留空=不改
<input
type="password"
value={credForm.api_secret}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
placeholder="粘贴你的 Binance API SECRET"
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!credForm.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
/>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn primary"
disabled={systemBusy}
onClick={async () => {
setSystemBusy(true)
setMessage('')
try {
const payload = {}
if (credForm.api_key) payload.api_key = credForm.api_key
if (credForm.api_secret) payload.api_secret = credForm.api_secret
payload.use_testnet = !!credForm.use_testnet
await api.updateAccountCredentials(accountId, payload)
setMessage('密钥已更新(建议重启该账号交易进程)')
setCredForm({ api_key: '', api_secret: '', use_testnet: !!credForm.use_testnet })
} catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}}
>
保存密钥
</button>
</div>
</div>
</div>
{/* 系统控制:清缓存 / 启停 / 重启supervisor */}