""" 账号管理 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"]) # 补充一些运行时信息(可选) 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(require_account_access)): """获取账号详情""" 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(require_account_owner) ): """更新账号基本信息""" try: # TODO: Implement Account.update() in models # For now, manually update allowed fields 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(require_account_owner) ): """更新API密钥""" 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(require_account_access)): """获取该账号关联的交易服务状态""" try: program = program_name_for_account(account_id) # status 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}/service/start") async def start_service(account_id: int, user: Dict[str, Any] = Depends(require_account_owner)): """启动交易服务""" 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}/service/stop") async def stop_service(account_id: int, user: Dict[str, Any] = Depends(require_account_owner)): """停止交易服务""" 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}/service/restart") async def restart_service(account_id: int, user: Dict[str, Any] = Depends(require_account_owner)): """重启交易服务""" 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") # 允许管理员或该账号 owner 执行(owner 用于“我重建配置再启动”) if (user.get("role") or "user") != "admin": 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, }