fix(huya): 查询角色复用协作台账 CK 探针并标记失效
This commit is contained in:
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user