"""虎牙任务批次执行器。""" import asyncio import threading from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timezone from typing import Optional from loguru import logger from sqlalchemy.orm import Session, joinedload from core.huya import HuyaHttpClient from ..database import SessionLocal from ..models import HuyaAccount, HuyaTask from .huya_service import HUYA_CONFIG_FIELDS, cookie_value, ensure_huya_config, huya_config_value class HuyaBatchRunner: """批量执行虎牙任务,通过队列推送实时日志。""" 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" 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 _role_name(bind_status) -> str: account_data = bind_status.accountData return account_data.gameRole.roleName or account_data.gameAccount.nick or "" @staticmethod def _bind_role_result(bind_status) -> dict: account_data = bind_status.accountData game_account = account_data.gameAccount game_role = account_data.gameRole return { "game_title": bind_status.gameName, "role_name": HuyaBatchRunner._role_name(bind_status), "change_bind_day": bind_status.changeBindDay, "is_bind_account": account_data.isBindAcount, "is_bind_role": account_data.isBindRole, "is_need_act_check": account_data.isNeedActCheck, "change_bind_time": account_data.changBindTime, "game_account": game_account.to_dict(), "game_role": game_role.to_dict(), } @staticmethod def _role_channel(bind_status) -> str: game_role = bind_status.accountData.gameRole parts = [bind_status.gameName, game_role.areaName, game_role.platName] return " / ".join(part for part in parts if part) @staticmethod def _format_local_time(timestamp: int) -> str: if not timestamp: return "" return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") @classmethod def _bind_change_state(cls, bind_status) -> dict: account_data = bind_status.accountData is_bound = bool(account_data.isBindAcount and account_data.isBindRole) change_time = int(account_data.changBindTime or 0) now = int(datetime.now(timezone.utc).timestamp()) can_change = not is_bound or not change_time or change_time <= now return { "is_bound": is_bound, "can_change_bind": can_change, "change_bind_time": change_time, "change_available_at": cls._format_local_time(change_time), "change_bind_day": int(bind_status.changeBindDay or 0), } def _apply_role_to_account(self, account: HuyaAccount, bind_status, status: str): role_name = self._role_name(bind_status) account.status = status account.game_name = role_name or bind_status.gameName or account.game_name account.game_channel = self._role_channel(bind_status) or account.game_channel account.updated_at = datetime.now(timezone.utc) 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 _execute_query_points( self, worker_db: Session, task: HuyaTask, account: HuyaAccount, account_info: dict, config_info: dict, ): sid = str(self.payload.get("sid") or config_info.get("sid") or "").strip() if not sid: self._mark_task(worker_db, task, "failed", "请先配置虎牙活动 SID") return sid_int = self._to_int(sid) if not sid_int: self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}") return uid = self._resolve_uid(account_info) if not uid: self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid") return cookie = account_info.get("cookie") or "" if not cookie: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) response = client.query_user_score(uid=uid, cookie=cookie, sid=sid_int) if response is None: self._mark_task(worker_db, task, "error", "虎牙积分接口无响应") return result = response.to_dict() result["sid"] = sid_int if response.status != 200: self._mark_task( worker_db, task, "failed", response.msg or f"虎牙积分查询失败: {response.status}", result, ) return points = response.available_score account.points = points account.status = "points_queried" account.updated_at = datetime.now(timezone.utc) self._mark_task(worker_db, task, "success", f"积分: {points}", result) def _execute_get_bind_qr( self, worker_db: Session, task: HuyaTask, account: HuyaAccount, account_info: dict, config_info: dict, ): b_act_id = str(self.payload.get("bind_act_id") or config_info.get("bind_act_id") or "").strip() if not b_act_id: self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId") return b_act_id_int = self._to_int(b_act_id) if not b_act_id_int: self._mark_task(worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}") return uid = self._resolve_uid(account_info) if not uid: self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid") return cookie = account_info.get("cookie") or "" if not cookie: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) bind_status = client.check_user_bind_game_account( uid=uid, cookie=cookie, b_act_id=b_act_id_int, is_use_outer_act_id=1, ) if bind_status is None: self._mark_task(worker_db, task, "error", "虎牙绑定状态接口无响应") return if bind_status.status != 200: self._mark_task( worker_db, task, "failed", bind_status.msg or f"虎牙绑定状态查询失败: {bind_status.status}", bind_status.to_dict(), ) return role_info = self._bind_role_result(bind_status) change_state = self._bind_change_state(bind_status) if role_info["role_name"]: self._apply_role_to_account(account, bind_status, account.status or "imported") if not change_state["can_change_bind"]: role_name = role_info["role_name"] or "当前角色" available_at = change_state["change_available_at"] result = { "bind_act_id": b_act_id_int, **role_info, **change_state, "bind_status": bind_status.to_dict(), } self._mark_task( worker_db, task, "failed", f"{role_name} 暂不能更换,{available_at} 后可更换", result, ) return live_link = client.get_live_link_param( uid=uid, cookie=cookie, b_act_id=b_act_id_int, game_auth_scene=bind_status.gameAuthScene, ) if live_link is None: self._mark_task(worker_db, task, "error", "虎牙绑定二维码参数接口无响应") return if live_link.status != 200: self._mark_task( worker_db, task, "failed", live_link.msg or f"虎牙绑定二维码参数获取失败: {live_link.status}", live_link.to_log_dict(), ) return profile_nick = account_info.get("nickname") or account_info.get("username") or "" profile_avatar = "" profile_resp = client.get_user_profile_batch(uid=uid, cookie=cookie, target_uids=[uid]) if profile_resp is not None and profile_resp.profiles: profile = profile_resp.profiles[0] profile_nick = profile.nick or profile.passport or profile_nick profile_avatar = profile.avatar or "" urls = client.build_bind_urls( live_link.livelinkParam, b_act_id_int, game_auth_scene=bind_status.gameAuthScene, nick_name=profile_nick, face_url=profile_avatar, ) mini_qrcode = client.get_livelink_mini_qrcode(urls["qr_url"]) if not mini_qrcode: result = { "bind_act_id": b_act_id_int, "profile": { "nick": profile_nick, "avatar": profile_avatar, }, } self._mark_task(worker_db, task, "failed", "绑定小程序码获取失败", result) return result = { "bind_act_id": b_act_id_int, "mini_qrcode_image": mini_qrcode["mini_qrcode_image"], "qrcode_token": mini_qrcode["qrcode_token"], "bind_status": bind_status.to_dict(), **role_info, **change_state, "profile": { "nick": profile_nick, "avatar": profile_avatar, }, } account.status = "bind_qr_generated" account.game_name = role_info["role_name"] or bind_status.gameName or account.game_name account.nickname = profile_nick or account.nickname account.updated_at = datetime.now(timezone.utc) self._mark_task(worker_db, task, "success", "已生成绑定小程序码", result) def _execute_query_game_name( self, worker_db: Session, task: HuyaTask, account: HuyaAccount, account_info: dict, config_info: dict, ): b_act_id = str(self.payload.get("bind_act_id") or config_info.get("bind_act_id") or "").strip() if not b_act_id: self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId") return b_act_id_int = self._to_int(b_act_id) if not b_act_id_int: self._mark_task(worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}") return uid = self._resolve_uid(account_info) if not uid: self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid") return cookie = account_info.get("cookie") or "" if not cookie: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) bind_status = client.check_user_bind_game_account( uid=uid, cookie=cookie, b_act_id=b_act_id_int, is_use_outer_act_id=1, ) if bind_status is None: self._mark_task(worker_db, task, "error", "虎牙角色信息接口无响应") return if bind_status.status != 200: self._mark_task( worker_db, task, "failed", bind_status.msg or f"虎牙角色信息查询失败: {bind_status.status}", {"bind_act_id": b_act_id_int, "bind_status": bind_status.to_dict()}, ) return role_info = self._bind_role_result(bind_status) change_state = self._bind_change_state(bind_status) result = { "bind_act_id": b_act_id_int, **role_info, **change_state, "bind_status": bind_status.to_dict(), } role_name = role_info["role_name"] if role_name: self._apply_role_to_account(account, bind_status, "game_queried") self._mark_task(worker_db, task, "success", f"角色: {role_name}", result) return account.status = "game_not_bound" account.updated_at = datetime.now(timezone.utc) self._mark_task(worker_db, task, "success", "未绑定游戏角色", result) def _execute_confirm_bind( self, worker_db: Session, task: HuyaTask, account: HuyaAccount, account_info: dict, config_info: dict, ): b_act_id = str(self.payload.get("bind_act_id") or config_info.get("bind_act_id") or "").strip() if not b_act_id: self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId") return b_act_id_int = self._to_int(b_act_id) if not b_act_id_int: self._mark_task(worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}") return uid = self._resolve_uid(account_info) if not uid: self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid") return cookie = account_info.get("cookie") or "" if not cookie: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) role_status = client.check_user_bind_game_account( uid=uid, cookie=cookie, b_act_id=b_act_id_int, is_use_outer_act_id=0, ) if role_status is None: self._mark_task(worker_db, task, "error", "虎牙绑定角色查询接口无响应") return if role_status.status != 200: self._mark_task( worker_db, task, "failed", role_status.msg or f"虎牙绑定角色查询失败: {role_status.status}", {"bind_act_id": b_act_id_int, "bind_status": role_status.to_dict()}, ) return if not role_status.accountData.isBindAcount or not role_status.accountData.isBindRole: result = { "bind_act_id": b_act_id_int, "bind_confirmed": False, "bind_status": role_status.to_dict(), } self._mark_task(worker_db, task, "failed", "尚未绑定游戏角色,请先扫码完成绑定", result) return confirm_resp = client.confirm_bind_act_account( uid=uid, cookie=cookie, b_act_id=b_act_id_int, ) if confirm_resp is None: self._mark_task(worker_db, task, "error", "虎牙确认绑定接口无响应") return if confirm_resp.status != 200: result = { "bind_act_id": b_act_id_int, "bind_confirmed": False, "confirm_result": confirm_resp.to_dict(), "before_bind_status": role_status.to_dict(), } self._mark_task( worker_db, task, "failed", confirm_resp.msg or f"虎牙确认绑定失败: {confirm_resp.status}", result, ) return refreshed_status = client.check_user_bind_game_account( uid=uid, cookie=cookie, b_act_id=b_act_id_int, is_use_outer_act_id=1, ) if refreshed_status is None: result = { "bind_act_id": b_act_id_int, "bind_confirmed": False, "confirm_result": confirm_resp.to_dict(), "before_bind_status": role_status.to_dict(), } self._mark_task(worker_db, task, "error", "虎牙活动绑定状态刷新无响应", result) return if refreshed_status.status != 200: result = { "bind_act_id": b_act_id_int, "bind_confirmed": False, "confirm_result": confirm_resp.to_dict(), "before_bind_status": role_status.to_dict(), "bind_status": refreshed_status.to_dict(), } self._mark_task( worker_db, task, "failed", refreshed_status.msg or f"虎牙活动绑定状态刷新失败: {refreshed_status.status}", result, ) return bind_confirmed = bool( refreshed_status.accountData.isBindAcount and refreshed_status.accountData.isBindRole ) role_info = self._bind_role_result(refreshed_status) result = { "bind_act_id": b_act_id_int, "bind_confirmed": bind_confirmed, "confirm_result": confirm_resp.to_dict(), "before_bind_status": role_status.to_dict(), "bind_status": refreshed_status.to_dict(), **role_info, } if not bind_confirmed: self._mark_task(worker_db, task, "failed", "确认后仍未检测到活动绑定角色", result) return self._apply_role_to_account(account, refreshed_status, "bind_confirmed") role_name = self._role_name(refreshed_status) or "已绑定" self._mark_task(worker_db, task, "success", f"确认绑定: {role_name}", result) def _execute_one(self, task_id: int, account_info: dict, config_info: dict, total: int): worker_db = SessionLocal() try: task = worker_db.query(HuyaTask).filter(HuyaTask.id == task_id).first() account = worker_db.query(HuyaAccount).filter(HuyaAccount.id == account_info["account_id"]).first() if not task or not account: return if self._stop.is_set(): self._mark_task(worker_db, task, "failed", "任务已停止") return task.status = "running" task.message = "执行中" task.finished_at = None worker_db.commit() with self._counter_lock: self._started += 1 current = self._started name = self._account_name(account_info) self._push_log("info", f"[{current}/{total}] 开始虎牙任务: {name}") if self.task_type not in {"query_points", "get_bind_qr", "confirm_bind", "query_game_name"}: self._mark_task(worker_db, task, "failed", "该虎牙任务执行器暂未实现") self._push_log("warning", f"[{current}] {name} 暂未实现: {self.task_type}") return try: if self.task_type == "query_points": self._execute_query_points(worker_db, task, account, account_info, config_info) elif self.task_type == "get_bind_qr": self._execute_get_bind_qr(worker_db, task, account, account_info, config_info) elif self.task_type == "confirm_bind": self._execute_confirm_bind(worker_db, task, account, account_info, config_info) elif self.task_type == "query_game_name": self._execute_query_game_name(worker_db, task, account, account_info, config_info) worker_db.refresh(task) if task.status == "success": self._push_log("success", f"[{current}] {name} {task.message}") else: self._push_log("error", f"[{current}] {name} {task.message}") except Exception as exc: self._mark_task(worker_db, task, "error", f"执行异常: {exc}") self._push_log("error", f"[{current}] {name} 执行异常: {exc}") finally: worker_db.close() def run(self): """在线程中执行虎牙批次任务。""" self._push_log( "info", f"虎牙批次 {self.batch_id} 开始,共执行 {self.task_type},并发数: {self.concurrency}", ) try: config = ensure_huya_config(self.db) config_info = { field: huya_config_value(field, getattr(config, field, None)) for field in HUYA_CONFIG_FIELDS } tasks = ( self.db.query(HuyaTask) .options(joinedload(HuyaTask.account)) .filter(HuyaTask.batch_id == self.batch_id) .order_by(HuyaTask.id.asc()) .all() ) task_infos = [] for task in tasks: account = task.account if not account: task.status = "error" task.message = "账号不存在" task.finished_at = datetime.now(timezone.utc) continue task.status = "pending" task.message = "等待执行" task.finished_at = None task_infos.append({ "task_id": task.id, "account_info": { "account_id": account.id, "uid": account.uid or "", "yyuid": account.yyuid or "", "username": account.username or "", "nickname": account.nickname or "", "cookie": account.cookie or "", }, }) self.db.commit() total = len(task_infos) if total == 0: self._push_log("warning", "没有可执行的虎牙任务") self._push_log("result", "") return with ThreadPoolExecutor(max_workers=self.concurrency) as executor: futures = [] for item in task_infos: if self._stop.is_set(): self._push_log("warning", "任务已停止,跳过剩余账号") break futures.append(executor.submit( self._execute_one, item["task_id"], item["account_info"], config_info, total, )) for future in as_completed(futures): try: future.result() except Exception as exc: self._push_log("error", f"虎牙 Worker 异常: {exc}") self._push_log("info", f"虎牙批次 {self.batch_id} 完成") self._push_log("result", "") except Exception as exc: self._push_log("error", f"虎牙批次执行异常: {exc}") self._push_log("result", "") finally: self.db.close() class HuyaBatchRegistry: """管理运行中的虎牙批次。""" def __init__(self): self._batches: dict[str, dict] = {} def register( self, batch_id: str, log_queue: asyncio.Queue, loop: asyncio.AbstractEventLoop, runner: HuyaBatchRunner, ): self._batches[batch_id] = { "log_queue": log_queue, "loop": loop, "runner": runner, } def get(self, batch_id: str): return self._batches.get(batch_id) def pop(self, batch_id: str): return self._batches.pop(batch_id, None) huya_batch_registry = HuyaBatchRegistry()