Files
live-hub-py/web/backend/routers/huya.py
T
2026-07-04 18:10:03 +08:00

303 lines
9.8 KiB
Python

"""虎牙基础管理路由"""
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 ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, require_permission
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaTask, User
from ..schemas import (
HuyaAccountOut,
HuyaConfigOut,
HuyaConfigUpdate,
HuyaCookieImport,
HuyaGoodsOut,
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,
)
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.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.updated_at.desc()).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)