From 2620023db469c57d72b3a51d2fd3368df8fcfeea Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 17 Oct 2026 16:18:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(huya):=20=E6=9F=A5=E8=AF=A2=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=A4=8D=E7=94=A8=E5=8D=8F=E4=BD=9C=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=20CK=20=E6=8E=A2=E9=92=88=E5=B9=B6=E6=A0=87=E8=AE=B0=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_huya_elite_query.py | 159 ++++++++++++++++++++++ web/backend/services/huya_runner_bind.py | 75 ++++++++-- web/backend/services/huya_runner_elite.py | 10 +- 3 files changed, 232 insertions(+), 12 deletions(-) diff --git a/tests/test_huya_elite_query.py b/tests/test_huya_elite_query.py index 8002669..0e3ed30 100644 --- a/tests/test_huya_elite_query.py +++ b/tests/test_huya_elite_query.py @@ -111,6 +111,90 @@ class _FakeBindStatus: return {"status": self.status, "msg": self.msg} +class _FakeGameAccount: + def __init__(self, nick: str): + self.nick = nick + + def to_dict(self) -> dict: + return {"nick": self.nick} + + +class _FakeGameRole: + def __init__(self, role_name: str, plat_name: str = "", area_name: str = ""): + self.roleName = role_name + self.platName = plat_name + self.areaName = area_name + + def to_dict(self) -> dict: + return { + "role_name": self.roleName, + "plat_name": self.platName, + "area_name": self.areaName, + } + + +class _FakeBindAccountData: + def __init__(self, nick: str, role_name: str): + self.gameAccount = _FakeGameAccount(nick) + self.gameRole = _FakeGameRole(role_name, "苹果(iOS)", "手Q") + self.isBindAcount = 1 + self.isBindRole = 1 + self.isNeedActCheck = 0 + self.changBindTime = 0 + + +class _FakeBindResponse: + def __init__( + self, + *, + status: int = 200, + msg: str = "", + nick: str = "微信用户", + role_name: str = "吃鸡199706", + ): + self.status = status + self.msg = msg + self.gameName = "和平精英" + self.changeBindDay = 7 + self.accountData = _FakeBindAccountData(nick, role_name) + + def to_dict(self) -> dict: + return { + "status": self.status, + "msg": self.msg, + "game_name": self.gameName, + "account_data": self.accountData.gameAccount.to_dict(), + "game_role": self.accountData.gameRole.to_dict(), + } + + +class _CkProbeClient: + def __init__(self, response): + self.response = response + self.calls: list[dict] = [] + + def check_user_bind_game_account(self, **kwargs): + self.calls.append(kwargs) + return self.response + + +def _query_game_runner(monkeypatch, client): + runner = HuyaBatchRunner( + db=None, batch_id="test-batch", task_type="query_game_name" + ) + marks: list[tuple[str, str, dict | None]] = [] + monkeypatch.setattr(runner, "_http_client", lambda: client, raising=False) + monkeypatch.setattr( + runner, + "_mark_task", + lambda _db, _task, status, message, result=None: marks.append( + (status, message, result) + ), + raising=False, + ) + return runner, marks + + class _StubClient: """积分中心通道的假客户端:可返回宝典状态/订单列表/赛季配置,并记录调用。""" @@ -667,6 +751,81 @@ def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch): assert "活动已下线" in message +def test_query_game_name_uses_new_probe_params_and_saves_role(monkeypatch): + """查询角色必须复用外部单协作的新探针:HTTP + outer_act_id + 15 秒超时。""" + client = _CkProbeClient(_FakeBindResponse()) + runner, marks = _query_game_runner(monkeypatch, client) + account = HuyaAccount(uid=str(UID), cookie=COOKIE) + + runner._execute_query_game_name( + None, + None, + account, + {"uid": UID, "cookie": COOKIE}, + {"bind_act_id": "9271", "outer_act_id": "17096"}, + ) + + status, message, result = marks[-1] + assert status == "success" + assert message == "角色: 吃鸡199706" + assert result["role_name"] == "吃鸡199706" + assert result["game_title"] == "和平精英" + assert account.status == "game_queried" + assert account.game_name == "吃鸡199706" + assert account.game_channel == "苹果(iOS) / 手Q" + assert client.calls == [ + { + "uid": UID, + "cookie": COOKIE, + "b_act_id": 9271, + "is_use_outer_act_id": 1, + "outer_act_id": "17096", + "timeout": 15.0, + } + ] + + +def test_query_game_name_marks_ck_expired_when_probe_has_no_response(monkeypatch): + client = _CkProbeClient(None) + runner, marks = _query_game_runner(monkeypatch, client) + account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported") + + runner._execute_query_game_name( + None, + None, + account, + {"uid": UID, "cookie": COOKIE}, + {"bind_act_id": "9271", "outer_act_id": "17096"}, + ) + + status, message, result = marks[-1] + assert status == "error" + assert "CK 已失效" in message + assert result["ck_expired"] is True + assert result["ck_expired_reason"] == "角色信息接口无响应" + assert account.status == "ck_expired" + + +def test_query_game_name_does_not_mark_business_error_as_ck_expired(monkeypatch): + client = _CkProbeClient(_FakeBindResponse(status=601, msg="活动已下线")) + runner, marks = _query_game_runner(monkeypatch, client) + account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported") + + runner._execute_query_game_name( + None, + None, + account, + {"uid": UID, "cookie": COOKIE}, + {"bind_act_id": "9271", "outer_act_id": "17096"}, + ) + + status, message, result = marks[-1] + assert status == "failed" + assert message == "活动已下线" + assert result.get("ck_expired") is None + assert account.status == "imported" + + def test_shop_client_falls_back_to_http_when_wss_init_fails(monkeypatch): """设备态齐全但积分中心端点不回包时(wsLaunch 超时)回退 HTTP,而不是判整批失败。""" cookie = ( diff --git a/web/backend/services/huya_runner_bind.py b/web/backend/services/huya_runner_bind.py index d4e6c28..e6dde3e 100644 --- a/web/backend/services/huya_runner_bind.py +++ b/web/backend/services/huya_runner_bind.py @@ -8,13 +8,16 @@ from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session +from ..huya_defaults import HUYA_DEFAULT_OUTER_ACT_ID from ..models import HuyaAccount, HuyaTask +from .huya_runner_core import HUYA_RECHARGE_SOURCE_ID, HuyaBatchRunnerCore HUYA_BIND_ROLE_POLL_SECONDS = 180 HUYA_BIND_ROLE_POLL_INTERVAL = 3 HUYA_BIND_ZT_UUID = "b02faae1" HUYA_BIND_ROOM_ID = "30596253" -from .huya_runner_core import HUYA_RECHARGE_SOURCE_ID, HuyaBatchRunnerCore +# 与外部单协作的新 CK 探针保持一致:HTTP cdnws + 外层活动参数 + 15 秒超时。 +HUYA_CK_PROBE_TIMEOUT_SECONDS = 15.0 if TYPE_CHECKING: from core.huya import HuyaHttpClient @@ -38,6 +41,7 @@ class BindMixin: def _mark_task(self, *args: Any, **kwargs: Any) -> None: ... def _update_task_progress(self, *args: Any, **kwargs: Any) -> None: ... def _activity_client(self, uid: int, cookie: str): ... + def _http_client(self): ... @staticmethod def _role_name(bind_status) -> str: @@ -103,14 +107,22 @@ class BindMixin: uid: int, cookie: str, b_act_id_int: int, + *, + outer_act_id: str = "", + timeout: float | None = None, ): """按活动页逻辑解析绑定状态,优先返回含角色的状态。""" - outer_status = client.check_user_bind_game_account( - uid=uid, - cookie=cookie, - b_act_id=b_act_id_int, - is_use_outer_act_id=1, - ) + probe_kwargs: dict[str, Any] = { + "uid": uid, + "cookie": cookie, + "b_act_id": b_act_id_int, + "is_use_outer_act_id": 1, + } + if outer_act_id: + probe_kwargs["outer_act_id"] = outer_act_id + if timeout is not None: + probe_kwargs["timeout"] = timeout + outer_status = client.check_user_bind_game_account(**probe_kwargs) if outer_status is None: return None, {} @@ -144,6 +156,29 @@ class BindMixin: query_result["bind_status"] = chosen_status.to_dict() return chosen_status, query_result + def _probe_bind_status( + self, + uid: int, + cookie: str, + b_act_id_int: int, + outer_act_id: str = "", + ): + """用外部单协作的新 CK 探针查角色。 + + 关键点(2026-10-16 真机验证):必须走 HTTP cdnws 只读通道,带上 + ``outer_act_id`` 和 15 秒超时;响应为 None 就说明这份 CK 已经不认了。 + 不能再用 WSS 业务会话的默认参数,否则过期 CK 只会表现为“接口无响应”, + 任务结果里没有可判定的失效标记。 + """ + return self._resolve_bind_status( + self._http_client(), + uid, + cookie, + b_act_id_int, + outer_act_id=outer_act_id or HUYA_DEFAULT_OUTER_ACT_ID, + timeout=HUYA_CK_PROBE_TIMEOUT_SECONDS, + ) + def _apply_role_to_account(self, account: HuyaAccount, bind_status, status: str): role_name = self._role_name(bind_status) account.status = status @@ -503,17 +538,35 @@ class BindMixin: self._mark_task(worker_db, task, "failed", "账号 Cookie 为空") return - client: Any = self._activity_client(uid, cookie) - bind_status, bind_query_result = self._resolve_bind_status( - client=client, + outer_act_id = str( + self.payload.get("outer_act_id") + or config_info.get("outer_act_id") + or HUYA_DEFAULT_OUTER_ACT_ID + ).strip() + bind_status, bind_query_result = self._probe_bind_status( uid=uid, cookie=cookie, b_act_id_int=b_act_id_int, + outer_act_id=outer_act_id, ) if bind_status is None: - self._mark_task(worker_db, task, "error", "虎牙角色信息接口无响应") + # 新探针没有回包 = 这份 CK 已经不认了,和外部单协作的判定一致。 + account.status = "ck_expired" + account.updated_at = datetime.now(UTC) + self._mark_task( + worker_db, + task, + "error", + "虎牙角色信息接口无响应(CK 已失效,请重新登录或重新导入 Cookie)", + { + "bind_act_id": b_act_id_int, + "ck_expired": True, + "ck_expired_reason": "角色信息接口无响应", + }, + ) return if bind_status.status != 200: + # 有回包、只是业务码非 200(例如活动已下线)不算 CK 失效,避免误标红。 self._mark_task( worker_db, task, diff --git a/web/backend/services/huya_runner_elite.py b/web/backend/services/huya_runner_elite.py index e9bb88b..8e499ba 100644 --- a/web/backend/services/huya_runner_elite.py +++ b/web/backend/services/huya_runner_elite.py @@ -58,6 +58,7 @@ class EliteMixin: def _resolve_uid(self, account_info: dict) -> int: ... def _shop_client(self, uid: int, cookie: str): ... def _activity_client(self, uid: int, cookie: str): ... + def _http_client(self): ... def _mark_task(self, *args: Any, **kwargs: Any) -> None: ... # 来自 BindMixin @@ -348,12 +349,19 @@ class EliteMixin: if not b_act_id_int: failures.append("未配置绑定 bActId") else: - bind_client: Any = self._activity_client(uid, cookie) + outer_act_id = str( + self.payload.get("outer_act_id") + or config_info.get("outer_act_id") + or "" + ).strip() + bind_client: Any = self._http_client() bind_status, bind_query_result = self._resolve_bind_status( client=bind_client, uid=uid, cookie=cookie, b_act_id_int=b_act_id_int, + outer_act_id=outer_act_id, + timeout=15.0, ) if bind_status is None: ck_expired = True