refactor(huya): runner 按功能域拆分 Mixin
huya_runner.py (1664行) → core/bind/goods/recharge 四个 Mixin + 入口聚合: - huya_runner_core.py: 批次状态/日志/任务落库/注册表 + 共用常量 sourceId - huya_runner_bind.py: 绑定状态机/扫码确认绑定/轮询 - huya_runner_goods.py: 积分/兑换记录/商品刷新/兑换 - huya_runner_recharge.py: 充值商品/下单/到账轮询 - 对外 API 不变 (HuyaBatchRunner/huya_batch_registry), routers 零改动 - pyflakes 全净 (含 TYPE_CHECKING 注解/跨域常量导入修复), 99 单测通过
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""虎牙任务执行器:公共基础(由 huya_runner.py 按功能域拆分)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models import HuyaTask
|
||||
from .huya_service import cookie_value
|
||||
|
||||
# 绑定跳转与充值下单共用(bind 与 recharge 两个域都引用了 sourceId)
|
||||
HUYA_RECHARGE_SOURCE_ID = "yellowcarlist"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .huya_runner import HuyaBatchRunner
|
||||
|
||||
class HuyaBatchRunnerCore:
|
||||
"""虎牙任务执行器公共基础:批次状态、日志、任务落库。"""
|
||||
def __init__(
|
||||
self,
|
||||
db: Session,
|
||||
batch_id: str,
|
||||
task_type: str,
|
||||
payload: Optional[dict] = None,
|
||||
log_queue: Optional[asyncio.Queue] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
concurrency: int = 3,
|
||||
):
|
||||
self.db = db
|
||||
self.batch_id = batch_id
|
||||
self.task_type = task_type
|
||||
self.payload = payload or {}
|
||||
self.log_queue = log_queue
|
||||
self.loop = loop
|
||||
self.concurrency = max(1, min(concurrency, 10))
|
||||
self._stop = threading.Event()
|
||||
self._counter_lock = threading.Lock()
|
||||
self._started = 0
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
|
||||
def _push_log(self, level: str, message: str):
|
||||
if level == "result":
|
||||
try:
|
||||
huya_batch_registry.mark_finished(self.batch_id)
|
||||
except NameError:
|
||||
pass
|
||||
if level != "result" and message:
|
||||
log_func = getattr(logger, level, logger.info)
|
||||
log_func(f"[huya] {message}")
|
||||
if self.log_queue and self.loop:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.log_queue.put({"level": level, "message": message}),
|
||||
self.loop,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _account_name(account_info: dict) -> str:
|
||||
return (
|
||||
account_info.get("nickname")
|
||||
or account_info.get("username")
|
||||
or account_info.get("uid")
|
||||
or f"#{account_info.get('account_id')}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _to_int(value) -> int:
|
||||
text = str(value or "").strip()
|
||||
return int(text) if text.isdigit() else 0
|
||||
|
||||
def _resolve_uid(self, account_info: dict) -> int:
|
||||
cookie = account_info.get("cookie") or ""
|
||||
return (
|
||||
self._to_int(account_info.get("yyuid"))
|
||||
or self._to_int(account_info.get("uid"))
|
||||
or self._to_int(cookie_value(cookie, "yyuid"))
|
||||
or self._to_int(cookie_value(cookie, "udb_uid"))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_local_time(timestamp: int) -> str:
|
||||
if not timestamp:
|
||||
return ""
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@staticmethod
|
||||
def _parse_scheduled_time(value) -> datetime | None:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
normalized = text.replace("Z", "+00:00")
|
||||
dt = datetime.fromisoformat(normalized)
|
||||
except ValueError:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.astimezone()
|
||||
return dt
|
||||
|
||||
def _wait_until(self, when: datetime, uid: int) -> bool:
|
||||
target = when.timestamp()
|
||||
local_text = self._format_local_time(int(target))
|
||||
self._push_log("info", f"[{uid}] 定时兑换等待到 {local_text}")
|
||||
while not self._stop.is_set():
|
||||
remaining = target - time.time()
|
||||
if remaining <= 0:
|
||||
return True
|
||||
time.sleep(min(0.2, max(0.02, remaining)))
|
||||
return False
|
||||
|
||||
def _mark_task(
|
||||
self,
|
||||
worker_db: Session,
|
||||
task: HuyaTask,
|
||||
status: str,
|
||||
message: str,
|
||||
result: Optional[dict] = None,
|
||||
):
|
||||
task.status = status
|
||||
task.message = message
|
||||
task.result = result
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
worker_db.commit()
|
||||
|
||||
def _update_task_progress(
|
||||
self,
|
||||
worker_db: Session,
|
||||
task: HuyaTask,
|
||||
status: str,
|
||||
message: str,
|
||||
result: Optional[dict] = None,
|
||||
):
|
||||
task.status = status
|
||||
task.message = message
|
||||
if result is not None:
|
||||
task.result = result
|
||||
worker_db.commit()
|
||||
|
||||
|
||||
class HuyaBatchRegistry:
|
||||
"""管理运行中的虎牙批次。"""
|
||||
|
||||
def __init__(self):
|
||||
self._batches: dict[str, dict] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _cleanup_locked(self, ttl_seconds: int = 300):
|
||||
now = time.time()
|
||||
expired = [
|
||||
batch_id
|
||||
for batch_id, batch in self._batches.items()
|
||||
if batch.get("finished") and now - float(batch.get("finished_at") or now) > ttl_seconds
|
||||
]
|
||||
for batch_id in expired:
|
||||
self._batches.pop(batch_id, None)
|
||||
|
||||
def register(
|
||||
self,
|
||||
batch_id: str,
|
||||
log_queue: asyncio.Queue,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
runner: HuyaBatchRunner,
|
||||
):
|
||||
with self._lock:
|
||||
self._cleanup_locked()
|
||||
self._batches[batch_id] = {
|
||||
"log_queue": log_queue,
|
||||
"loop": loop,
|
||||
"runner": runner,
|
||||
"finished": False,
|
||||
"finished_at": None,
|
||||
}
|
||||
|
||||
def get(self, batch_id: str):
|
||||
with self._lock:
|
||||
self._cleanup_locked()
|
||||
return self._batches.get(batch_id)
|
||||
|
||||
def active_ids(self) -> set[str]:
|
||||
with self._lock:
|
||||
self._cleanup_locked()
|
||||
return {
|
||||
batch_id
|
||||
for batch_id, batch in self._batches.items()
|
||||
if not batch.get("finished")
|
||||
}
|
||||
|
||||
def mark_finished(self, batch_id: str):
|
||||
with self._lock:
|
||||
batch = self._batches.get(batch_id)
|
||||
if not batch:
|
||||
return
|
||||
batch["finished"] = True
|
||||
batch["finished_at"] = time.time()
|
||||
|
||||
def pop(self, batch_id: str):
|
||||
with self._lock:
|
||||
return self._batches.pop(batch_id, None)
|
||||
|
||||
|
||||
huya_batch_registry = HuyaBatchRegistry()
|
||||
|
||||
Reference in New Issue
Block a user