302 lines
9.8 KiB
Python
302 lines
9.8 KiB
Python
"""
|
||
账号管理 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,
|
||
}
|