diff --git a/tests/test_huya_points_query.py b/tests/test_huya_points_query.py index dbb08c0..300eadd 100644 --- a/tests/test_huya_points_query.py +++ b/tests/test_huya_points_query.py @@ -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 == [] diff --git a/web/backend/services/huya_runner_goods.py b/web/backend/services/huya_runner_goods.py index a3f3380..ad5180d 100644 --- a/web/backend/services/huya_runner_goods.py +++ b/web/backend/services/huya_runner_goods.py @@ -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,