fix(huya): 查询积分先验 CK 并改用 HTTP 快速通道
This commit is contained in:
@@ -9,8 +9,8 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
from core.huya.activity_structs import GetUserScoreResp
|
||||
from core.huya.frame_decoder import _decode_wup_body, decode_struct_bytes
|
||||
from core.huya.http_client import HuyaHttpClient
|
||||
from core.huya.shop_structs import ShopRawRsp
|
||||
@@ -80,27 +80,19 @@ class _StubClient:
|
||||
return self.grow
|
||||
|
||||
|
||||
class _LegacyClient:
|
||||
"""旧活动接口:真机实测任何 sid 都返回 603「活动已结束」。"""
|
||||
|
||||
def query_user_score(self, uid: int, cookie: str, sid: int):
|
||||
response = GetUserScoreResp()
|
||||
response.status = 603
|
||||
response.msg = "活动已结束"
|
||||
response.score = 0
|
||||
return response
|
||||
|
||||
|
||||
def _runner(monkeypatch, client, legacy=None) -> tuple[HuyaBatchRunner, list]:
|
||||
def _runner(
|
||||
monkeypatch, client, *, ck_valid: bool = True
|
||||
) -> tuple[HuyaBatchRunner, list]:
|
||||
runner = HuyaBatchRunner(db=None, batch_id="test-batch", task_type="query_points")
|
||||
marks: list[tuple[str, str, dict | None]] = []
|
||||
monkeypatch.setattr(runner, "_http_client", lambda: client, raising=False)
|
||||
probe_status = SimpleNamespace(status=200, msg="") if ck_valid else None
|
||||
monkeypatch.setattr(
|
||||
runner, "_shop_client", lambda uid, cookie: client, raising=False
|
||||
runner,
|
||||
"_probe_bind_status",
|
||||
lambda **_kwargs: (probe_status, {}),
|
||||
raising=False,
|
||||
)
|
||||
if legacy is not None:
|
||||
monkeypatch.setattr(
|
||||
runner, "_activity_client", lambda uid, cookie: legacy, raising=False
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_mark_task",
|
||||
@@ -222,6 +214,52 @@ def test_query_points_reads_points_from_points_center(monkeypatch):
|
||||
assert result["nickname"] == "软踏日潺湲"
|
||||
|
||||
|
||||
def test_query_points_uses_http_and_prechecks_ck_with_new_probe(monkeypatch):
|
||||
client = _StubClient()
|
||||
runner, marks = _runner(monkeypatch, client)
|
||||
probe_calls: list[dict] = []
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_probe_bind_status",
|
||||
lambda **kwargs: (
|
||||
probe_calls.append(kwargs) or SimpleNamespace(status=200, msg=""),
|
||||
{},
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_shop_client",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("查询积分不应再建 WSS 商城会话")
|
||||
),
|
||||
raising=False,
|
||||
)
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
|
||||
|
||||
runner._execute_query_points(
|
||||
None,
|
||||
None,
|
||||
account,
|
||||
{"uid": UID, "cookie": COOKIE},
|
||||
{"bind_act_id": "9271", "outer_act_id": "17096"},
|
||||
)
|
||||
|
||||
assert probe_calls == [
|
||||
{
|
||||
"uid": UID,
|
||||
"cookie": COOKIE,
|
||||
"b_act_id_int": 9271,
|
||||
"outer_act_id": "17096",
|
||||
}
|
||||
]
|
||||
assert marks[-1][0] == "success"
|
||||
assert [name for name, _kwargs in client.calls] == [
|
||||
"get_redeem_need_goods",
|
||||
"get_grow_level_user_info",
|
||||
]
|
||||
|
||||
|
||||
def test_query_points_keeps_working_when_grow_level_fails(monkeypatch):
|
||||
client = _StubClient(grow=False)
|
||||
runner, marks = _runner(monkeypatch, client)
|
||||
@@ -265,31 +303,46 @@ def test_query_points_reports_server_error_of_points_center(monkeypatch):
|
||||
assert result["goods_num"] == 8315982
|
||||
|
||||
|
||||
def test_query_points_fallback_reports_legacy_interface_offline(monkeypatch):
|
||||
def test_query_points_reports_points_center_offline_without_legacy_fallback(
|
||||
monkeypatch,
|
||||
):
|
||||
client = _StubClient(redeem=False)
|
||||
runner, marks = _runner(monkeypatch, client, legacy=_LegacyClient())
|
||||
runner, marks = _runner(monkeypatch, client)
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
|
||||
|
||||
runner._execute_query_points(
|
||||
None, None, account, {"uid": UID, "cookie": COOKIE}, {"sid": "2203"}
|
||||
None,
|
||||
None,
|
||||
account,
|
||||
{"uid": UID, "cookie": COOKIE},
|
||||
{"bind_act_id": "9271", "outer_act_id": "17096"},
|
||||
)
|
||||
|
||||
status, message, _result = marks[-1]
|
||||
assert status == "failed"
|
||||
# 旧接口的真实返回必须如实暴露,而不是只报「活动已结束」
|
||||
assert "旧活动接口(sid=2203)返回 603 活动已结束" in message
|
||||
assert "积分中心接口无响应" in message
|
||||
|
||||
|
||||
def test_query_points_without_sid_reports_points_center_offline(monkeypatch):
|
||||
client = _StubClient(redeem=False)
|
||||
runner, marks = _runner(monkeypatch, client, legacy=_LegacyClient())
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
|
||||
|
||||
runner._execute_query_points(
|
||||
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
|
||||
)
|
||||
|
||||
status, message, _result = marks[-1]
|
||||
status, message, result = marks[-1]
|
||||
assert status == "error"
|
||||
assert "积分中心接口无响应" in message
|
||||
assert result["ck_valid"] is True
|
||||
# 旧接口已下线,不能再让它多等一个 15 秒超时。
|
||||
assert [name for name, _kwargs in client.calls] == ["get_redeem_need_goods"]
|
||||
|
||||
|
||||
def test_query_points_marks_ck_expired_before_calling_points_center(monkeypatch):
|
||||
client = _StubClient()
|
||||
runner, marks = _runner(monkeypatch, client, ck_valid=False)
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported")
|
||||
|
||||
runner._execute_query_points(
|
||||
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"
|
||||
assert client.calls == []
|
||||
|
||||
@@ -10,8 +10,12 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.huya.wss_client import POINTS_REDEEM_GOODS_NUM
|
||||
|
||||
from ..huya_defaults import HUYA_DEFAULT_OUTER_ACT_ID
|
||||
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
|
||||
|
||||
# 积分查询超时:HTTP 通道正常几百毫秒,10 秒足够;失败时别让整批任务长时间挂住。
|
||||
HUYA_POINTS_QUERY_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
|
||||
class GoodsMixin:
|
||||
"""积分与商城域:积分/兑换记录/商品刷新/兑换。"""
|
||||
@@ -29,6 +33,10 @@ class GoodsMixin:
|
||||
def _push_log(self, level: str, message: str) -> None: ...
|
||||
def _activity_client(self, uid: int, cookie: str): ...
|
||||
def _shop_client(self, uid: int, cookie: str): ...
|
||||
def _http_client(self): ...
|
||||
def _probe_bind_status(
|
||||
self, uid: int, cookie: str, b_act_id_int: int, outer_act_id: str = ""
|
||||
): ...
|
||||
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: ...
|
||||
@@ -93,12 +101,15 @@ class GoodsMixin:
|
||||
account_info: dict,
|
||||
config_info: dict,
|
||||
):
|
||||
"""查询积分:主源是积分中心的 shopMiddleUI.getRedeemNeedGoods。
|
||||
"""查询积分:先验 CK,再走积分中心 HTTP 接口。
|
||||
|
||||
2026-10-15 真机实测:旧活动接口 webActUI.getUserScore 对任何 sid 都返回
|
||||
603「活动已结束」(该接口已下线,与配置无关);积分中心接口返回
|
||||
``tag2``=当前积分、``tag3``=兑换门槛积分,另可顺手取 GameBizUI
|
||||
的成长值/成长等级。
|
||||
2026-10-15 真机实测:旧活动接口 ``webActUI.getUserScore`` 对任何 sid 都返回
|
||||
603「活动已结束」(该接口已下线,与配置无关),不再作为回退——否则积分中心
|
||||
超时后还要再等一次 15 秒。积分中心接口返回 ``tag2``=当前积分、
|
||||
``tag3``=兑换门槛积分,另可顺手取 GameBizUI 的成长值/成长等级。
|
||||
|
||||
CK 判定复用外部单协作的新探针(HTTP + outer_act_id + 15 秒):积分中心对
|
||||
失效 CK 仍可能返回 200/0 分,不能拿积分结果判断凭据是否有效。
|
||||
"""
|
||||
uid = self._resolve_uid(account_info)
|
||||
if not uid:
|
||||
@@ -110,16 +121,63 @@ class GoodsMixin:
|
||||
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
|
||||
return
|
||||
|
||||
# ---- 先验 CK:没有回包就立即判失效,不再空跑积分与旧接口 ----
|
||||
ck_valid = False
|
||||
b_act_id_int = self._to_int(
|
||||
self.payload.get("bind_act_id") or config_info.get("bind_act_id")
|
||||
)
|
||||
if b_act_id_int:
|
||||
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:
|
||||
account.status = "ck_expired"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"error",
|
||||
"虎牙角色信息接口无响应(CK 已失效,请重新登录或重新导入 Cookie)",
|
||||
{
|
||||
"ck_expired": True,
|
||||
"ck_expired_reason": "角色信息接口无响应",
|
||||
},
|
||||
)
|
||||
return
|
||||
ck_valid = int(getattr(bind_status, "status", 0) or 0) == 200
|
||||
|
||||
goods_num = (
|
||||
self._to_int(self.payload.get("goods_num")) or POINTS_REDEEM_GOODS_NUM
|
||||
)
|
||||
client: Any = self._shop_client(uid, cookie)
|
||||
# 直接走 HTTP:积分中心不需要 WSS 会话,省掉每个账号建连/初始化的几秒等待。
|
||||
client: Any = self._http_client()
|
||||
redeem = client.get_redeem_need_goods(
|
||||
uid=uid, guid="", cookie=cookie, goods_num=goods_num
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
goods_num=goods_num,
|
||||
timeout=HUYA_POINTS_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
if redeem is None:
|
||||
self._query_points_legacy(
|
||||
worker_db, task, account, uid, cookie, config_info
|
||||
offline_result: dict[str, Any] = {"goods_num": goods_num}
|
||||
if ck_valid:
|
||||
offline_result["ck_valid"] = True
|
||||
self._mark_task(
|
||||
worker_db,
|
||||
task,
|
||||
"error",
|
||||
"虎牙积分中心接口无响应(CK 探针正常,请稍后重试)"
|
||||
if ck_valid
|
||||
else "虎牙积分中心接口无响应",
|
||||
offline_result,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -138,7 +196,12 @@ class GoodsMixin:
|
||||
points = self._to_int(redeem.value("tag2"))
|
||||
result["points"] = points
|
||||
result["redeem_threshold"] = self._to_int(redeem.value("tag3"))
|
||||
growth = client.get_grow_level_user_info(uid=uid, guid="", cookie=cookie)
|
||||
growth = client.get_grow_level_user_info(
|
||||
uid=uid,
|
||||
guid="",
|
||||
cookie=cookie,
|
||||
timeout=HUYA_POINTS_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
if growth is not None and growth.code == 0:
|
||||
result["grow_value"] = self._to_int(growth.value("tag1", "tag0"))
|
||||
result["grow_level"] = self._to_int(growth.value("tag1", "tag1"))
|
||||
@@ -149,60 +212,6 @@ class GoodsMixin:
|
||||
account.updated_at = datetime.now(UTC)
|
||||
self._mark_task(worker_db, task, "success", f"积分: {points}", result)
|
||||
|
||||
def _query_points_legacy(
|
||||
self,
|
||||
worker_db: Session,
|
||||
task: HuyaTask,
|
||||
account: HuyaAccount,
|
||||
uid: int,
|
||||
cookie: str,
|
||||
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, "error", "积分中心接口无响应,且未配置可回退的活动 SID"
|
||||
)
|
||||
return
|
||||
|
||||
sid_int = self._to_int(sid)
|
||||
if not sid_int:
|
||||
self._mark_task(worker_db, task, "failed", f"虎牙活动 SID 无效: {sid}")
|
||||
return
|
||||
|
||||
client: Any = self._activity_client(uid, cookie)
|
||||
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",
|
||||
f"积分中心接口无响应;旧活动接口(sid={sid_int})返回 "
|
||||
f"{response.status} {response.msg}".strip(),
|
||||
result,
|
||||
)
|
||||
return
|
||||
|
||||
points = response.available_score
|
||||
account.points = points
|
||||
account.status = "points_queried"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
self._mark_task(
|
||||
worker_db, task, "success", f"积分: {points}(旧活动接口)", result
|
||||
)
|
||||
|
||||
def _execute_query_exchange_records(
|
||||
self,
|
||||
worker_db: Session,
|
||||
|
||||
Reference in New Issue
Block a user