auto_trade_sys/backend/api/routes/accounts.py
薇薇安 9cd39c3655 1
2026-02-15 14:18:58 +08:00

302 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
账号管理 API多账号
说明:
- 这是“多账号第一步”的管理入口:创建/禁用/更新密钥
- 交易/配置/统计接口通过 X-Account-Id 头来选择账号(默认 1
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
import logging
from database.models import Account, UserAccountMembership
from api.auth_deps import get_current_user, get_admin_user, require_account_access, require_account_owner
from api.supervisor_account import (
ensure_account_program,
run_supervisorctl,
parse_supervisor_status,
program_name_for_account,
tail_supervisor,
tail_supervisord_log,
tail_trading_log_files,
)
logger = logging.getLogger(__name__)
router = APIRouter()
class AccountCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
api_key: Optional[str] = ""
api_secret: Optional[str] = ""
use_testnet: bool = False
status: str = Field("active", pattern="^(active|disabled)$")
class AccountUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
status: Optional[str] = Field(None, pattern="^(active|disabled)$")
use_testnet: Optional[bool] = None
class AccountCredentialsUpdate(BaseModel):
api_key: Optional[str] = None
api_secret: Optional[str] = None
use_testnet: Optional[bool] = None
@router.get("")
async def list_my_accounts(user: Dict[str, Any] = Depends(get_current_user)):
"""列出我有权访问的账号"""
try:
if user.get("role") == "admin":
accounts = Account.list_all()
else:
accounts = UserAccountMembership.get_user_accounts(user["id"])
# 补充一些运行时信息(可选),并处理敏感字段
for acc in accounts:
acc['has_api_key'] = bool(acc.get('api_key_enc'))
acc['has_api_secret'] = bool(acc.get('api_secret_enc'))
# 移除加密字段,不直接暴露给前端
acc.pop('api_key_enc', None)
acc.pop('api_secret_enc', None)
return accounts
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("")
async def create_account(
data: AccountCreate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""创建新账号(仅管理员或允许的用户)"""
# 暂时只允许 admin 创建
if user.get("role") != "admin":
raise HTTPException(status_code=403, detail="Only admin can create accounts")
try:
aid = Account.create(
name=data.name,
api_key=data.api_key,
api_secret=data.api_secret,
use_testnet=data.use_testnet,
status=data.status
)
# 自动将创建者关联为 owner
UserAccountMembership.add_membership(user["id"], aid, "owner")
return {"id": aid, "message": "Account created successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{account_id}")
async def get_account_detail(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""获取账号详情"""
require_account_access(account_id, user)
try:
acc = Account.get_by_id(account_id)
if not acc:
raise HTTPException(status_code=404, detail="Account not found")
return acc
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{account_id}")
async def update_account(
account_id: int,
data: AccountUpdate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""更新账号基本信息"""
require_account_owner(account_id, user)
try:
updates = {}
if data.name is not None:
updates['name'] = data.name
if data.status is not None:
updates['status'] = data.status
if data.use_testnet is not None:
updates['testnet'] = 1 if data.use_testnet else 0
if updates:
Account.update(account_id, **updates)
return {"message": "Account updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{account_id}/credentials")
async def update_credentials(
account_id: int,
data: AccountCredentialsUpdate,
user: Dict[str, Any] = Depends(get_current_user)
):
"""更新API密钥"""
require_account_owner(account_id, user)
try:
updates = {}
if data.api_key is not None:
updates['api_key'] = data.api_key
if data.api_secret is not None:
updates['api_secret'] = data.api_secret
if data.use_testnet is not None:
updates['testnet'] = 1 if data.use_testnet else 0
if updates:
Account.update(account_id, **updates)
return {"message": "Credentials updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# --- Service Management ---
@router.get("/{account_id}/trading/status")
@router.get("/{account_id}/service/status", include_in_schema=False) # 兼容旧路由
async def get_service_status(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""获取该账号关联的交易服务状态"""
# 手动调用权限检查,因为 Depends(require_account_access) 无法直接获取路径参数 account_id
require_account_access(account_id, user)
try:
program = program_name_for_account(account_id)
# status <program>
try:
out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(out)
return {
"program": program,
"running": running,
"pid": pid,
"state": state,
"raw": out
}
except RuntimeError as e:
# 可能进程不存在
return {
"program": program,
"running": False,
"pid": None,
"state": "UNKNOWN",
"raw": str(e),
"error": "Process likely not configured or supervisor error"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/start")
@router.post("/{account_id}/service/start", include_in_schema=False)
async def start_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""启动交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["start", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service start command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/stop")
@router.post("/{account_id}/service/stop", include_in_schema=False)
async def stop_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""停止交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["stop", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service stop command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/restart")
@router.post("/{account_id}/service/restart", include_in_schema=False)
async def restart_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""重启交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
out = run_supervisorctl(["restart", program])
# Check status again
status_out = run_supervisorctl(["status", program])
running, pid, state = parse_supervisor_status(status_out)
return {
"message": "Service restart command sent",
"output": out,
"status": {
"running": running,
"pid": pid,
"state": state
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{account_id}/trading/ensure-program")
async def ensure_trading_program(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
require_account_owner(int(account_id), user)
sup = ensure_account_program(int(account_id))
if not sup.ok:
raise HTTPException(status_code=500, detail=sup.error or "生成 supervisor 配置失败")
return {
"ok": True,
"program": sup.program,
"ini_path": sup.ini_path,
"program_dir": sup.program_dir,
"supervisor_conf": sup.supervisor_conf,
"reread": sup.reread,
"update": sup.update,
}