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 单测通过
210 lines
6.1 KiB
Python
210 lines
6.1 KiB
Python
"""虎牙任务执行器:公共基础(由 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()
|
||
|