fix(huya): 查询角色复用协作台账 CK 探针并标记失效

This commit is contained in:
yml2213
2026-10-17 16:18:28 +08:00
parent fe1f959648
commit 2620023db4
3 changed files with 232 additions and 12 deletions
+159
View File
@@ -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 = (
+64 -11
View File
@@ -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,
+9 -1
View File
@@ -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