diff --git a/web/backend/services/douyu_runner_bind.py b/web/backend/services/douyu_runner_bind.py index ce2c9e4..74a8b9a 100644 --- a/web/backend/services/douyu_runner_bind.py +++ b/web/backend/services/douyu_runner_bind.py @@ -3,19 +3,40 @@ from __future__ import annotations import time from datetime import datetime, timezone +from typing import Any, TYPE_CHECKING from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient, DouyuActivityError from ..models import Account, DouyuTask +if TYPE_CHECKING: + from .douyu_runner import DouyuBatchRunner + DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX" DOUYU_BIND_ROLE_POLL_SECONDS = 65 DOUYU_BIND_ROLE_POLL_INTERVAL = 5 DOUYU_CONFIRM_EFFECT_POLL_TIMES = 3 DOUYU_CONFIRM_EFFECT_POLL_INTERVAL = 3 + class BindMixin: """游戏角色绑定域:绑定状态机、扫码/确认绑定、换绑冷却。""" + + if TYPE_CHECKING: + _stop: Any + payload: dict + + @staticmethod + def _to_int(value: Any) -> int | None: ... + + @staticmethod + def _format_wait_time(seconds: int | None) -> str: ... + + def _push_log(self, level: str, message: str) -> None: ... + def _mark_task(self, *args: Any, **kwargs: Any) -> None: ... + def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ... + def _client(self, cookie: str) -> DouyuActivityClient: ... + @staticmethod def _action_act_alias(config: dict, key: str) -> str: """动作类接口用的活动 alias,排除只用于查询最新角色的 legacy alias。""" @@ -30,7 +51,9 @@ class BindMixin: @classmethod def _bind_qr_act_alias(cls, config: dict) -> str: """生成绑定二维码用的活动 alias。""" - return cls._action_act_alias(config, "bind_act_alias") or cls._action_act_alias(config, "confirm_act_alias") + return cls._action_act_alias(config, "bind_act_alias") or cls._action_act_alias( + config, "confirm_act_alias" + ) @staticmethod def _query_bind_act_aliases(config: dict) -> list[str]: @@ -53,7 +76,9 @@ class BindMixin: @classmethod def _confirm_act_alias(cls, config: dict) -> str: """确认绑定接口用的活动 alias。""" - return cls._action_act_alias(config, "confirm_act_alias") or cls._action_act_alias(config, "bind_act_alias") + return cls._action_act_alias( + config, "confirm_act_alias" + ) or cls._action_act_alias(config, "bind_act_alias") # 兼容旧调用名 @classmethod @@ -73,7 +98,9 @@ class BindMixin: @staticmethod def _role_channel(bind_info: dict) -> str: return " / ".join( - part for part in [bind_info.get("area_name"), bind_info.get("plat_name")] if part + part + for part in [bind_info.get("area_name"), bind_info.get("plat_name")] + if part ) @staticmethod @@ -177,7 +204,9 @@ class BindMixin: "bind_info": info, } - def _format_bind_summary(self, bind_info: dict | None, *, pending: bool | None = None) -> str: + def _format_bind_summary( + self, bind_info: dict | None, *, pending: bool | None = None + ) -> str: snap = self._bind_snapshot(bind_info) role = snap["role_name"] or "-" area = snap["area_name"] or "-" @@ -197,20 +226,30 @@ class BindMixin: f"{pending_text}" ) - def _apply_bind_info_to_account(self, account: Account, bind_info: dict, status: str) -> None: + def _apply_bind_info_to_account( + self, account: Account, bind_info: dict, status: str + ) -> None: role_name = str(bind_info.get("role_name") or "") account.game_name = role_name or account.game_name account.game_channel = self._role_channel(bind_info) or account.game_channel - account.change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time")) + account.change_role_wait_time = self._to_int( + bind_info.get("change_role_wait_time") + ) account.bind_status = status account.updated_at = datetime.now(timezone.utc) - def _apply_esports_bind_info_to_account(self, account: Account, bind_info: dict, status: str) -> None: + def _apply_esports_bind_info_to_account( + self, account: Account, bind_info: dict, status: str + ) -> None: """将电竞手册角色状态写入专属字段,避免覆盖精英宝典数据。""" role_name = str(bind_info.get("role_name") or "") account.esports_game_name = role_name or account.esports_game_name - account.esports_game_channel = self._role_channel(bind_info) or account.esports_game_channel - account.esports_change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time")) + account.esports_game_channel = ( + self._role_channel(bind_info) or account.esports_game_channel + ) + account.esports_change_role_wait_time = self._to_int( + bind_info.get("change_role_wait_time") + ) account.esports_can_change_time = self._to_int(bind_info.get("can_change_time")) account.esports_bind_status = status account.updated_at = datetime.now(timezone.utc) @@ -235,7 +274,9 @@ class BindMixin: "has_selected_role": has_selected_role, "bind_ready_for_confirm": has_selected_role and not esports_bound, "bind_confirmed": esports_bound, - "bind_phase": "confirmed" if esports_bound else ("role_ready" if has_selected_role else "waiting_role"), + "bind_phase": "confirmed" + if esports_bound + else ("role_ready" if has_selected_role else "waiting_role"), "activity_bind_info": activity_info, "activity_bind_snapshot": activity_snapshot, "role_source": "activity", @@ -302,7 +343,9 @@ class BindMixin: except ValueError: return len(prefer_aliases) + 1 - ranked = sorted(enumerate(candidates), key=lambda item: (_alias_rank(item[1]), item[0])) + ranked = sorted( + enumerate(candidates), key=lambda item: (_alias_rank(item[1]), item[0]) + ) ordered = [item[1] for item in ranked] if prefer_pending: @@ -337,12 +380,15 @@ class BindMixin: bound = [ info for info in candidates - if self._is_bound_act(info) and str(info.get("role_name") or "").strip() + if self._is_bound_act(info) + and str(info.get("role_name") or "").strip() and str(info.get("act_alias") or "") in prefer ] if not bound: return None - return sorted(bound, key=lambda info: prefer.index(str(info.get("act_alias") or "")))[0] + return sorted( + bound, key=lambda info: prefer.index(str(info.get("act_alias") or "")) + )[0] def _pick_baseline_bind_info( self, @@ -385,7 +431,9 @@ class BindMixin: f"aliases={','.join(query_aliases) or '-'} " f"baseline={baseline_role_name or '-'} bound={1 if baseline_is_bound_act else 0}", ) - self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result) + self._update_task_progress( + db, task, "running", "已生成绑定二维码,等待扫码绑定", result + ) poll_count = 0 last_summary = "" @@ -396,17 +444,22 @@ class BindMixin: candidates = self._fetch_bind_info_candidates(client, query_aliases) if not candidates: result["bind_poll_error"] = "所有 actAlias 查询绑定信息失败" - self._update_task_progress(db, task, "running", "等待绑定角色同步: 查询失败", result) + self._update_task_progress( + db, task, "running", "等待绑定角色同步: 查询失败", result + ) continue poll_count += 1 - bind_info = self._pick_bind_info( - candidates, - baseline_role_name=baseline_role_name, - baseline_is_bound_act=baseline_is_bound_act, - prefer_pending=True, - prefer_aliases=query_aliases, - ) or candidates[0] + bind_info = ( + self._pick_bind_info( + candidates, + baseline_role_name=baseline_role_name, + baseline_is_bound_act=baseline_is_bound_act, + prefer_pending=True, + prefer_aliases=query_aliases, + ) + or candidates[0] + ) snapshot = self._bind_snapshot(bind_info) is_pending_role = self._is_pending_role( bind_info, @@ -426,30 +479,34 @@ class BindMixin: # 注意:未识别到新角色时,不要把当前已绑定角色写进 role_name, # 否则前端会把旧角色误当成“待确认角色/查询结果”。 if is_pending_role: - result.update({ - **snapshot, - "act_alias": query_alias, - "query_act_alias": query_alias, - "bind_ready_for_confirm": True, - "bind_confirmed": False, - "bind_phase": "role_ready", - "bind_polling": False, - "poll_count": poll_count, - "bind_summary": summary, - "bind_candidates": [ - { - "act_alias": item.get("act_alias"), - "role_name": item.get("role_name"), - "is_bound_act": self._is_bound_act(item), - } - for item in candidates - ], - }) + result.update( + { + **snapshot, + "act_alias": query_alias, + "query_act_alias": query_alias, + "bind_ready_for_confirm": True, + "bind_confirmed": False, + "bind_phase": "role_ready", + "bind_polling": False, + "poll_count": poll_count, + "bind_summary": summary, + "bind_candidates": [ + { + "act_alias": item.get("act_alias"), + "role_name": item.get("role_name"), + "is_bound_act": self._is_bound_act(item), + } + for item in candidates + ], + } + ) role_name = snapshot["role_name"] # 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新” account.bind_status = "game_queried" account.updated_at = datetime.now(timezone.utc) - self._push_log("success", f"识别到待确认角色: {role_name} (act={query_alias})") + self._push_log( + "success", f"识别到待确认角色: {role_name} (act={query_alias})" + ) self._update_task_progress( db, task, @@ -464,28 +521,32 @@ class BindMixin: result["current_area_name"] = snapshot["area_name"] result["current_plat_name"] = snapshot["plat_name"] - result.update({ - "bind_info": bind_info, - "act_alias": query_alias, - "query_act_alias": query_alias, - "is_bound_act": snapshot["is_bound_act"], - "is_bound_role": snapshot["is_bound_role"], - "is_bound_account": snapshot["is_bound_account"], - "need_bind_act": snapshot["need_bind_act"], - "need_bind_role": snapshot["need_bind_role"], - "change_role_wait_time": snapshot["change_role_wait_time"], - "can_change_role": snapshot["can_change_role"], - "role_name": "", - "area_name": "", - "plat_name": "", - "bind_ready_for_confirm": False, - "bind_confirmed": False, - "bind_phase": "waiting_scan", - "bind_polling": True, - "poll_count": poll_count, - "bind_summary": summary, - }) - self._update_task_progress(db, task, "running", f"等待扫码绑定 ({summary})", result) + result.update( + { + "bind_info": bind_info, + "act_alias": query_alias, + "query_act_alias": query_alias, + "is_bound_act": snapshot["is_bound_act"], + "is_bound_role": snapshot["is_bound_role"], + "is_bound_account": snapshot["is_bound_account"], + "need_bind_act": snapshot["need_bind_act"], + "need_bind_role": snapshot["need_bind_role"], + "change_role_wait_time": snapshot["change_role_wait_time"], + "can_change_role": snapshot["can_change_role"], + "role_name": "", + "area_name": "", + "plat_name": "", + "bind_ready_for_confirm": False, + "bind_confirmed": False, + "bind_phase": "waiting_scan", + "bind_polling": True, + "poll_count": poll_count, + "bind_summary": summary, + } + ) + self._update_task_progress( + db, task, "running", f"等待扫码绑定 ({summary})", result + ) result["bind_polling"] = False result["bind_ready_for_confirm"] = False @@ -499,7 +560,9 @@ class BindMixin: ) return "", result - def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): + def _execute_get_bind_qr( + self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict + ): client = self._client(cookie) qr_act_alias = self._bind_qr_act_alias(config) query_aliases = self._query_bind_act_aliases(config) @@ -511,19 +574,26 @@ class BindMixin: before_candidates = self._fetch_bind_info_candidates(client, query_aliases) if not before_candidates: - self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") + self._mark_task( + db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias" + ) return # baseline 用活动 alias 的“当前已绑定”;pending 检测优先 cjm 的换绑最新态 before = self._pick_baseline_bind_info(before_candidates, config) or {} # 换绑冷却必须看活动当前绑定(QYOOB),不要用 cjm cooldown_info = self._pick_change_wait_bind_info(before_candidates, config) - pending_before = self._pick_bind_info( - before_candidates, - baseline_role_name=str(before.get("role_name") or ""), - baseline_is_bound_act=self._is_bound_act(before), - prefer_pending=True, - prefer_aliases=query_aliases, - ) or before or before_candidates[0] + cooldown = cooldown_info or {} + pending_before = ( + self._pick_bind_info( + before_candidates, + baseline_role_name=str(before.get("role_name") or ""), + baseline_is_bound_act=self._is_bound_act(before), + prefer_pending=True, + prefer_aliases=query_aliases, + ) + or before + or before_candidates[0] + ) before_snapshot = self._bind_snapshot(before) cooldown_snapshot = self._bind_snapshot(cooldown_info) current_role_name = before_snapshot["role_name"] or ( @@ -535,7 +605,7 @@ class BindMixin: "绑定前状态 " f"qr_act={qr_act_alias or '-'} query={','.join(query_aliases)} " f"baseline_hit={before.get('act_alias') or '-'} {self._format_bind_summary(before)} " - f"cooldown_hit={cooldown_info.get('act_alias') or '-'} " + f"cooldown_hit={cooldown.get('act_alias') or '-'} " f"{self._format_bind_summary(cooldown_info)} " f"pending_hit={pending_before.get('act_alias') or '-'} " f"{self._format_bind_summary(pending_before)}", @@ -543,33 +613,43 @@ class BindMixin: # 生成二维码前强制检查换绑冷却:冷却中直接失败,绝不发码 if self._is_change_cooling(cooldown_info): - role_label = current_role_name or cooldown_snapshot["role_name"] or "当前角色" + role_label = ( + current_role_name or cooldown_snapshot["role_name"] or "当前角色" + ) wait_text = self._format_wait_time(wait_time) or "冷却中" self._push_log( "warning", f"换绑冷却中,跳过生成二维码 role={role_label} wait={wait_time if wait_time is not None else '-'} " - f"can={cooldown_info.get('can_change_role')}", + f"can={cooldown.get('can_change_role')}", ) result = { - "act_alias": qr_act_alias or cooldown_info.get("act_alias") or before.get("act_alias"), - "query_act_alias": cooldown_info.get("act_alias") or before.get("act_alias"), + "act_alias": qr_act_alias + or cooldown.get("act_alias") + or before.get("act_alias"), + "query_act_alias": cooldown.get("act_alias") or before.get("act_alias"), "query_act_aliases": query_aliases, "before_bind_info": before, "cooldown_bind_info": cooldown_info, **cooldown_snapshot, - "current_role_name": role_label if role_label != "当前角色" else current_role_name, - "current_area_name": cooldown_snapshot["area_name"] or before_snapshot["area_name"], - "current_plat_name": cooldown_snapshot["plat_name"] or before_snapshot["plat_name"], + "current_role_name": role_label + if role_label != "当前角色" + else current_role_name, + "current_area_name": cooldown_snapshot["area_name"] + or before_snapshot["area_name"], + "current_plat_name": cooldown_snapshot["plat_name"] + or before_snapshot["plat_name"], "change_role_wait_time": wait_time, "change_role_wait_text": self._format_wait_time(wait_time), "bind_ready_for_confirm": False, - "bind_confirmed": bool(cooldown_snapshot["is_bound_act"] or before_snapshot["is_bound_act"]), + "bind_confirmed": bool( + cooldown_snapshot["is_bound_act"] or before_snapshot["is_bound_act"] + ), "bind_phase": "change_waiting", "bind_polling": False, } self._apply_bind_info_to_account( account, - cooldown_info if cooldown_snapshot["role_name"] else before, + cooldown if cooldown_snapshot["role_name"] else before, "bind_confirmed" if result["bind_confirmed"] else "game_queried", ) self._mark_task( @@ -610,7 +690,9 @@ class BindMixin: account.bind_status = "bind_qr_generated" account.updated_at = datetime.now(timezone.utc) # 关键:先把二维码 progress 出去,前端 running 期间即可弹窗扫码。 - self._update_task_progress(db, task, "running", "已生成绑定二维码,等待扫码绑定", result) + self._update_task_progress( + db, task, "running", "已生成绑定二维码,等待扫码绑定", result + ) role_name, result = self._wait_bind_role_result( client, @@ -623,7 +705,9 @@ class BindMixin: baseline_is_bound_act=before_snapshot["is_bound_act"], ) if role_name: - self._mark_task(db, task, "success", f"已识别角色: {role_name},待确认绑定", result) + self._mark_task( + db, task, "success", f"已识别角色: {role_name},待确认绑定", result + ) return if result.get("bind_phase") == "stopped": self._mark_task(db, task, "stopped", "任务已停止", result) @@ -636,7 +720,9 @@ class BindMixin: timeout_msg = f"{timeout_msg} | {last_summary}" self._mark_task(db, task, "success", timeout_msg, result) - def _execute_confirm_bind(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): + def _execute_confirm_bind( + self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict + ): client = self._client(cookie) confirm_alias = self._confirm_act_alias(config) query_aliases = self._query_bind_act_aliases(config) @@ -651,15 +737,20 @@ class BindMixin: before_candidates = self._fetch_bind_info_candidates(client, query_aliases) if not before_candidates: - self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") + self._mark_task( + db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias" + ) return - before = self._pick_bind_info( - before_candidates, - baseline_role_name="", - baseline_is_bound_act=False, - prefer_pending=True, - prefer_aliases=query_aliases, - ) or before_candidates[0] + before = ( + self._pick_bind_info( + before_candidates, + baseline_role_name="", + baseline_is_bound_act=False, + prefer_pending=True, + prefer_aliases=query_aliases, + ) + or before_candidates[0] + ) before_snapshot = self._bind_snapshot(before) role_name = before_snapshot["role_name"] query_alias = str(before.get("act_alias") or "") @@ -695,10 +786,11 @@ class BindMixin: before_is_current_bound = ( before_bound is not None - and str(before.get("act_alias") or "") == str(before_bound.get("act_alias") or "") + and str(before.get("act_alias") or "") + == str(before_bound.get("act_alias") or "") and role_name == str(before_bound.get("role_name") or "") ) - if before_is_current_bound: + if before_is_current_bound and before_bound is not None: self._apply_bind_info_to_account(account, before_bound, "bind_confirmed") self._mark_task( db, @@ -755,6 +847,7 @@ class BindMixin: f"确认绑定接口返回 act={use_confirm_alias} " f"error={confirm_raw.get('error')} msg={confirm_raw.get('msg') or '-'}", ) + def _pick_bound_after(candidates: list[dict]) -> dict | None: """确认后回查:只认活动 alias 上的已生效绑定(bound_act=1)。""" return self._pick_current_bound_info( @@ -773,7 +866,9 @@ class BindMixin: for _ in range(DOUYU_CONFIRM_EFFECT_POLL_TIMES): if self._stop.wait(DOUYU_CONFIRM_EFFECT_POLL_INTERVAL): break - after_candidates = self._fetch_bind_info_candidates(client, query_aliases) + after_candidates = self._fetch_bind_info_candidates( + client, query_aliases + ) if not after_candidates: break after = _pick_bound_after(after_candidates) @@ -782,7 +877,9 @@ class BindMixin: if after is None: # 已生效绑定始终未出现:确认未生效(或同步延迟超时),保留原绑定 if before_bound is not None: - self._apply_bind_info_to_account(account, before_bound, "game_queried") + self._apply_bind_info_to_account( + account, before_bound, "game_queried" + ) else: account.bind_status = "game_queried" account.updated_at = datetime.now(timezone.utc) @@ -815,7 +912,9 @@ class BindMixin: # 查询接口异常:确认接口已成功时按成功处理,但保留错误信息。 # 写库优先确认前已生效绑定,避免待确认角色被误写入。 if before_bound is not None: - self._apply_bind_info_to_account(account, before_bound, "bind_confirmed") + self._apply_bind_info_to_account( + account, before_bound, "bind_confirmed" + ) else: account.bind_status = "bind_confirmed" account.updated_at = datetime.now(timezone.utc) @@ -882,7 +981,9 @@ class BindMixin: self._apply_esports_bind_info_to_account( account, state, - "esports_bound" if esports_bound else ("esports_bind_ready" if role_text else "game_not_bound"), + "esports_bound" + if esports_bound + else ("esports_bind_ready" if role_text else "game_not_bound"), ) # 二维码用于重新选择角色,不应因为已有角色或换绑冷却而隐藏。 # 冷却是否允许最终由 actBind 返回结果决定。 @@ -970,10 +1071,14 @@ class BindMixin: result = { **after_state, "confirm": confirm_result, - "bind_phase": "confirmed" if after_state["esports_bound"] else "confirm_failed", + "bind_phase": "confirmed" + if after_state["esports_bound"] + else "confirm_failed", } if not after_state["esports_bound"]: - self._apply_esports_bind_info_to_account(account, after_state, "esports_bind_ready") + self._apply_esports_bind_info_to_account( + account, after_state, "esports_bind_ready" + ) self._mark_task( db, task, @@ -1013,7 +1118,9 @@ class BindMixin: self._apply_esports_bind_info_to_account( account, state, - "esports_bound" if is_bound else ("esports_bind_ready" if role_name else "game_not_bound"), + "esports_bound" + if is_bound + else ("esports_bind_ready" if role_name else "game_not_bound"), ) result = { **state, @@ -1026,7 +1133,9 @@ class BindMixin: message = "未查询到游戏角色,请先切换角色" self._mark_task(db, task, "success", message, result) - def _execute_query_game_name(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): + def _execute_query_game_name( + self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict + ): client = self._client(cookie) query_aliases = self._query_bind_act_aliases(config) if not query_aliases: @@ -1034,24 +1143,33 @@ class BindMixin: return candidates = self._fetch_bind_info_candidates(client, query_aliases) if not candidates: - self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") + self._mark_task( + db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias" + ) return # 1) 优先“已生效绑定”角色(bound_act=1 且有角色名,通常是活动 alias)。 # 避免把扫码后未确认的新角色当成当前绑定结果。 bound_info = self._pick_current_bound_info(candidates, config) # 2) 待确认角色(cjm 扫码后未确认;无已绑定时也用于首次绑定展示) - pending_info = self._pick_bind_info( - candidates, - prefer_pending=True, - prefer_aliases=query_aliases, - ) or candidates[0] + pending_info = ( + self._pick_bind_info( + candidates, + prefer_pending=True, + prefer_aliases=query_aliases, + ) + or candidates[0] + ) pending_role = str(pending_info.get("role_name") or "").strip() bound_role = str(bound_info.get("role_name") or "") if bound_info else "" - has_pending = bool(pending_role) and (pending_role != bound_role or not bound_info) + has_pending = bool(pending_role) and ( + pending_role != bound_role or not bound_info + ) # 查询结果角色 = 已生效绑定;首次绑定(无绑定)时回退待确认角色 - bind_info = bound_info if bound_info else (pending_info if has_pending else None) + bind_info = ( + bound_info if bound_info else (pending_info if has_pending else None) + ) snapshot = self._bind_snapshot(bind_info) if bind_info else {} role_name = snapshot.get("role_name") or "" is_bound = snapshot.get("is_bound_act", False) @@ -1082,7 +1200,8 @@ class BindMixin: "bind_ready_for_confirm": has_pending, "bind_confirmed": is_bound, "bind_phase": ( - "confirmed" if is_bound + "confirmed" + if is_bound else ("role_ready" if role_name else "waiting_role") ), "bind_summary": summary, @@ -1104,14 +1223,18 @@ class BindMixin: message = f"未获取到游戏名 | {summary}" elif is_bound: if has_pending and pending_role != bound_role: - message = f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}" + message = ( + f"当前已绑定: {bound_role},待确认: {pending_role} | {summary}" + ) else: message = f"当前已绑定: {bound_role} | {summary}" else: message = f"待确认角色: {role_name} | {summary}" self._mark_task(db, task, "success" if role_name else "failed", message, result) - def _pick_change_wait_bind_info(self, candidates: list[dict], config: dict) -> dict | None: + def _pick_change_wait_bind_info( + self, candidates: list[dict], config: dict + ) -> dict | None: """换绑倒计时优先看活动当前绑定(QYOOB),不是 cjm 换绑最新态。""" if not candidates: return None @@ -1121,12 +1244,18 @@ class BindMixin: return current_bound action_aliases = self._action_act_aliases(config) return self._pick_bind_info( - [info for info in candidates if str(info.get("act_alias") or "") in action_aliases], + [ + info + for info in candidates + if str(info.get("act_alias") or "") in action_aliases + ], prefer_pending=False, prefer_aliases=action_aliases, ) - def _execute_query_change_bind_time(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): + def _execute_query_change_bind_time( + self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict + ): client = self._client(cookie) query_aliases = self._query_bind_act_aliases(config) if not query_aliases: @@ -1134,7 +1263,9 @@ class BindMixin: return candidates = self._fetch_bind_info_candidates(client, query_aliases) if not candidates: - self._mark_task(db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias") + self._mark_task( + db, task, "failed", "查询绑定信息失败,请检查 Cookie 或 actAlias" + ) return for item in candidates: self._push_log( @@ -1143,7 +1274,9 @@ class BindMixin: f"{self._format_bind_summary(item)}", ) # 换绑时间看“当前已绑定”活动态,不要优先 cjm(cjm 常无 changeRoleWaitTime) - bind_info = self._pick_change_wait_bind_info(candidates, config) or candidates[0] + bind_info = ( + self._pick_change_wait_bind_info(candidates, config) or candidates[0] + ) snapshot = self._bind_snapshot(bind_info) wait_time = snapshot["change_role_wait_time"] account.change_role_wait_time = wait_time @@ -1171,7 +1304,8 @@ class BindMixin: "query_act_aliases": query_aliases, **snapshot, "change_role_wait_text": wait_text, - "bind_ready_for_confirm": bool(snapshot["role_name"]) and not snapshot["is_bound_act"], + "bind_ready_for_confirm": bool(snapshot["role_name"]) + and not snapshot["is_bound_act"], "bind_confirmed": snapshot["is_bound_act"], "bind_summary": f"act={source_alias or '-'} {self._format_bind_summary(bind_info)}", "bind_candidates": [ @@ -1179,7 +1313,9 @@ class BindMixin: "act_alias": item.get("act_alias"), "role_name": item.get("role_name"), "is_bound_act": self._is_bound_act(item), - "change_role_wait_time": self._to_int(item.get("change_role_wait_time")), + "change_role_wait_time": self._to_int( + item.get("change_role_wait_time") + ), "can_change_role": item.get("can_change_role"), } for item in candidates @@ -1197,4 +1333,3 @@ class BindMixin: f"{role_name} {status_text} | act={source_alias or '-'}", result, ) - diff --git a/web/backend/services/huya_runner_goods.py b/web/backend/services/huya_runner_goods.py index 90e560c..44abb17 100644 --- a/web/backend/services/huya_runner_goods.py +++ b/web/backend/services/huya_runner_goods.py @@ -3,14 +3,35 @@ from __future__ import annotations import time from datetime import datetime, timezone +from typing import Any, TYPE_CHECKING from sqlalchemy.orm import Session from core.huya import HuyaHttpClient from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask +if TYPE_CHECKING: + from .huya_runner import HuyaBatchRunner + + class GoodsMixin: """积分与商城域:积分/兑换记录/商品刷新/兑换。""" + + if TYPE_CHECKING: + payload: dict + + @staticmethod + def _to_int(value: Any) -> int: ... + + @staticmethod + def _format_local_time(timestamp: int) -> str: ... + + def _resolve_uid(self, account_info: dict) -> int: ... + def _push_log(self, level: str, message: str) -> None: ... + def _mark_task(self, *args: Any, **kwargs: Any) -> None: ... + def _wait_until(self, when: datetime, uid: int) -> bool: ... + def _parse_scheduled_time(self, value: Any) -> datetime | None: ... + def _execute_query_points( self, worker_db: Session, @@ -39,7 +60,9 @@ class GoodsMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) + client: Any = 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", "虎牙积分接口无响应") @@ -91,7 +114,9 @@ class GoodsMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) + client: Any = HuyaHttpClient( + logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") + ) response = client.get_user_prize_records(uid=uid, cookie=cookie, sid=sid_int) if response is None: self._mark_task(worker_db, task, "error", "虎牙兑换记录接口无响应") @@ -112,7 +137,9 @@ class GoodsMixin: records = result.get("records", []) for index, item in enumerate(records, start=1): item["index"] = index - item["exchange_time_text"] = self._format_local_time(int(item.get("exchange_time") or 0)) + item["exchange_time_text"] = self._format_local_time( + int(item.get("exchange_time") or 0) + ) if item.get("score") is not None: item["score_text"] = f"{int(item.get('score') or 0)}积分" @@ -150,7 +177,9 @@ class GoodsMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) + client: Any = HuyaHttpClient( + logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") + ) response = client.get_act_prize_list(uid=uid, cookie=cookie, sid=sid_int) if response is None: self._mark_task(worker_db, task, "error", "虎牙商品列表接口无响应") @@ -169,20 +198,23 @@ class GoodsMixin: return goods = [ - item for item in result.get("goods", []) + item + for item in result.get("goods", []) if item.get("product_id") and item.get("name") ] now = datetime.now(timezone.utc) worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False) for item in goods: - worker_db.add(HuyaGoodsSnapshot( - product_id=item["product_id"], - name=item["name"], - price=item["price"], - remain_text=item["remain_text"], - raw=item, - updated_at=now, - )) + worker_db.add( + HuyaGoodsSnapshot( + product_id=item["product_id"], + name=item["name"], + price=item["price"], + remain_text=item["remain_text"], + raw=item, + updated_at=now, + ) + ) account.status = "goods_refreshed" account.updated_at = now @@ -222,10 +254,16 @@ class GoodsMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - snapshot = worker_db.query(HuyaGoodsSnapshot).filter( - HuyaGoodsSnapshot.product_id == str(product_id) - ).first() - product_name = str(self.payload.get("product_name") or (snapshot.name if snapshot else "") or product_id) + snapshot = ( + worker_db.query(HuyaGoodsSnapshot) + .filter(HuyaGoodsSnapshot.product_id == str(product_id)) + .first() + ) + product_name = str( + self.payload.get("product_name") + or (snapshot.name if snapshot else "") + or product_id + ) scheduled_at = self._parse_scheduled_time(self.payload.get("scheduled_at")) if self.payload.get("scheduled_at") and scheduled_at is None: self._mark_task(worker_db, task, "failed", "定时兑换时间格式无效") @@ -235,21 +273,27 @@ class GoodsMixin: self._mark_task(worker_db, task, "stopped", "兑换任务已停止") return - client = HuyaHttpClient(logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")) - response = client.score_exchange_prize(uid=uid, cookie=cookie, sid=sid_int, pid=product_id) + client: Any = HuyaHttpClient( + logger=lambda msg: self._push_log("info", f"[{uid}] {msg}") + ) + response = client.score_exchange_prize( + uid=uid, cookie=cookie, sid=sid_int, pid=product_id + ) if response is None: self._mark_task(worker_db, task, "error", "虎牙兑换接口无响应") return result = response.to_dict() - result.update({ - "sid": sid_int, - "product_id": str(product_id), - "product_name": product_name, - "scheduled_at": scheduled_at.isoformat() if scheduled_at else "", - "executed_at": datetime.now(timezone.utc).isoformat(), - "goods": snapshot.raw if snapshot else None, - }) + result.update( + { + "sid": sid_int, + "product_id": str(product_id), + "product_name": product_name, + "scheduled_at": scheduled_at.isoformat() if scheduled_at else "", + "executed_at": datetime.now(timezone.utc).isoformat(), + "goods": snapshot.raw if snapshot else None, + } + ) if response.status != 200: self._mark_task( worker_db, @@ -264,4 +308,3 @@ class GoodsMixin: account.updated_at = datetime.now(timezone.utc) message = response.msg or f"兑换成功: {product_name}" self._mark_task(worker_db, task, "success", message, result) -