"""虎牙基础管理路由""" import asyncio import threading from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect from sqlalchemy.orm import Session, joinedload from core.huya import HuyaCredentialError, HuyaLoginError, login_huya_password from ..database import SessionLocal, get_db from ..deps import authenticate_websocket, require_permission from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User from ..schemas import ( HuyaAccountOut, HuyaConfigOut, HuyaConfigUpdate, HuyaCookieImport, HuyaGoodsOut, HuyaPasswordLoginRequest, HuyaRechargeGoodsOut, HuyaTaskBatchRequest, HuyaTaskOut, ) from ..services.huya_service import ( HUYA_CONFIG_FIELDS, SUPPORTED_TASK_TYPES, apply_huya_config_defaults, create_planned_tasks, ensure_huya_config, huya_config_value, import_huya_cookies, upsert_huya_cookie, ) from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry router = APIRouter(prefix="/api/huya", tags=["虎牙"]) def _fmt_cookie_preview(cookie: str) -> str: if not cookie: return "" return cookie[:50] + "..." if len(cookie) > 50 else cookie def _account_out(account: HuyaAccount) -> HuyaAccountOut: cookie = account.cookie or "" return HuyaAccountOut( id=account.id, uid=account.uid or "", yyuid=account.yyuid or "", username=account.username or "", nickname=account.nickname or "", cookie=cookie, cookie_preview=_fmt_cookie_preview(cookie), tag=account.tag or "", remark=account.remark or "", status=account.status or "", points=account.points, game_name=account.game_name or "", game_channel=account.game_channel or "", game_phone=account.game_phone or "", assigned_to=account.assigned_to, assigned_username=account.assigned_user.username if account.assigned_user else None, created_at=account.created_at, updated_at=account.updated_at, ) def _task_out(task: HuyaTask) -> HuyaTaskOut: account = task.account return HuyaTaskOut( id=task.id, batch_id=task.batch_id, account_id=task.account_id, account_uid=account.uid if account else "", account_nickname=account.nickname if account else "", task_type=task.task_type, status=task.status or "", message=task.message or "", result=task.result, created_by=task.created_by, created_at=task.created_at, finished_at=task.finished_at, ) def _config_out(config: HuyaConfig) -> HuyaConfigOut: return HuyaConfigOut( room_pid=huya_config_value("room_pid", config.room_pid), sid=huya_config_value("sid", config.sid), outer_act_id=huya_config_value("outer_act_id", config.outer_act_id), bind_act_id=huya_config_value("bind_act_id", config.bind_act_id), pay_channel=huya_config_value("pay_channel", config.pay_channel), updated_at=config.updated_at, ) @router.get("/task-types") def task_types(current: User = Depends(require_permission("huya:task"))): """返回当前规划的虎牙任务类型。""" return SUPPORTED_TASK_TYPES @router.get("/accounts", response_model=list[HuyaAccountOut]) def list_accounts( tag: str | None = Query(None), db: Session = Depends(get_db), current: User = Depends(require_permission("huya:account")), ): """查看虎牙 CK 账号。""" query = db.query(HuyaAccount).options(joinedload(HuyaAccount.assigned_user)) if tag: query = query.filter(HuyaAccount.tag == tag) accounts = query.order_by(HuyaAccount.id.desc()).all() return [_account_out(account) for account in accounts] @router.post("/accounts/import-cookies") def import_cookies( req: HuyaCookieImport, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:account")), ): """粘贴并导入虎牙 Cookie。""" count, skipped = import_huya_cookies(db, req.text, req.tag) return { "message": f"导入/更新 {count} 条,跳过 {skipped} 条", "success": True, "count": count, "skipped": skipped, } @router.post("/accounts/password-login") def password_login_account( req: HuyaPasswordLoginRequest, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:account")), ): """使用账号密码登录虎牙,成功后保存 Cookie。""" try: result = login_huya_password( username=req.username.strip(), password=req.password, cookie=req.cookie.strip() or None, ) except HuyaCredentialError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc except HuyaLoginError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=502, detail=f"虎牙密码登录失败: {exc}") from exc if not result.success or not result.cookie: raise HTTPException(status_code=502, detail=result.message or "虎牙密码登录失败") try: account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint=req.username) except ValueError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc return { "message": "登录成功,Cookie 已保存", "success": True, "account": _account_out(account), "sdid": result.sdid, } @router.delete("/accounts/batch") def delete_accounts_batch( account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"), db: Session = Depends(get_db), current: User = Depends(require_permission("huya:account")), ): """批量删除虎牙 CK 账号及任务记录。""" ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()] if not ids: raise HTTPException(status_code=400, detail="无效的账号ID") db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False) deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False) db.commit() return {"message": f"已删除 {deleted} 个虎牙账号", "deleted": deleted, "success": True} @router.delete("/accounts/{account_id}") def delete_account( account_id: int, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:account")), ): """删除单个虎牙 CK 账号。""" account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first() if not account: raise HTTPException(status_code=404, detail="账号不存在") db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(synchronize_session=False) db.delete(account) db.commit() return {"message": "已删除", "success": True} @router.get("/config", response_model=HuyaConfigOut) def get_config( db: Session = Depends(get_db), current: User = Depends(require_permission("huya:config")), ): """获取虎牙配置。""" config = ensure_huya_config(db) return _config_out(config) @router.put("/config", response_model=HuyaConfigOut) def update_config( req: HuyaConfigUpdate, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:config")), ): """更新虎牙配置。""" config = ensure_huya_config(db) for field in HUYA_CONFIG_FIELDS: value = getattr(req, field) if value is not None: setattr(config, field, value.strip()) apply_huya_config_defaults(config) config.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(config) return _config_out(config) @router.get("/goods", response_model=list[HuyaGoodsOut]) def list_goods( db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): """查看已缓存的虎牙商品快照。""" rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.id.asc()).all() return rows @router.get("/recharge-goods", response_model=list[HuyaRechargeGoodsOut]) def list_recharge_goods( db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): """查看已缓存的虎牙充值商品快照。""" rows = db.query(HuyaRechargeGoodsSnapshot).order_by(HuyaRechargeGoodsSnapshot.id.asc()).all() return rows @router.post("/tasks/batch") async def create_task_batch( req: HuyaTaskBatchRequest, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): """创建虎牙任务记录并启动后台执行器。""" if not req.account_ids: raise HTTPException(status_code=400, detail="请选择虎牙账号") try: batch_id, count = create_planned_tasks( db, req.account_ids, req.task_type, current.id, req.payload, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc if count == 0: raise HTTPException(status_code=400, detail="没有有效的虎牙账号") log_queue = asyncio.Queue() loop = asyncio.get_running_loop() thread_db = SessionLocal() runner = HuyaBatchRunner( db=thread_db, batch_id=batch_id, task_type=req.task_type, payload=req.payload, log_queue=log_queue, loop=loop, concurrency=req.concurrency, ) huya_batch_registry.register(batch_id, log_queue, loop, runner) thread = threading.Thread(target=runner.run, daemon=True) thread.start() return {"batch_id": batch_id, "count": count, "success": True} @router.get("/tasks", response_model=list[HuyaTaskOut]) def list_tasks( batch_id: str | None = None, db: Session = Depends(get_db), current: User = Depends(require_permission("huya:task")), ): """查看虎牙任务记录。""" query = db.query(HuyaTask).options(joinedload(HuyaTask.account)) if batch_id: query = query.filter(HuyaTask.batch_id == batch_id) tasks = query.order_by(HuyaTask.id.desc()).limit(300).all() return [_task_out(task) for task in tasks] @router.post("/stop/{batch_id}") def stop_batch( batch_id: str, current: User = Depends(require_permission("huya:task")), ): """停止正在运行的虎牙批次。""" batch = huya_batch_registry.get(batch_id) if batch: batch["runner"].stop() return {"message": "已发送停止信号", "success": True} raise HTTPException(status_code=404, detail="批次不存在或已结束") @router.websocket("/ws/{batch_id}") async def ws_huya_logs(websocket: WebSocket, batch_id: str): """虎牙实时日志推送通道。""" user = authenticate_websocket(websocket) if not user: await websocket.close(code=1008, reason="未授权") return await websocket.accept() batch = huya_batch_registry.get(batch_id) if not batch: await websocket.send_json({"level": "error", "message": "批次不存在或已结束"}) await websocket.close() return log_queue: asyncio.Queue = batch["log_queue"] try: while True: try: msg = await asyncio.wait_for(log_queue.get(), timeout=30) await websocket.send_json(msg) if msg.get("level") == "result": await asyncio.sleep(0.1) break except asyncio.TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: pass finally: huya_batch_registry.pop(batch_id)