diff --git a/tests/test_huya_elite_query.py b/tests/test_huya_elite_query.py index 0e3ed30..dfff5db 100644 --- a/tests/test_huya_elite_query.py +++ b/tests/test_huya_elite_query.py @@ -230,9 +230,7 @@ def _runner(monkeypatch, shop_client, activity_client=None): runner = HuyaBatchRunner(db=None, batch_id="test-batch", task_type="query_all") marks: list[tuple[str, str, dict | None]] = [] logs: list[tuple[str, str]] = [] - monkeypatch.setattr( - runner, "_shop_client", lambda uid, cookie: shop_client, raising=False - ) + monkeypatch.setattr(runner, "_http_client", lambda: shop_client, raising=False) if activity_client is not None: monkeypatch.setattr( runner, @@ -324,7 +322,10 @@ def test_query_all_fills_expire_time_from_season(monkeypatch): ) runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( - runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False + runner, + "_probe_bind_status", + lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}), + raising=False, ) account = HuyaAccount(uid=str(UID), cookie=COOKIE) @@ -606,7 +607,7 @@ def test_query_all_combines_points_elite_and_role(monkeypatch): runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( runner, - "_resolve_bind_status", + "_probe_bind_status", lambda **kwargs: (_FakeBindStatus(), {"bind_status_source": "outer"}), raising=False, ) @@ -660,7 +661,10 @@ def test_query_all_reports_not_opened_without_order_lookup(monkeypatch): shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX)) runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( - runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False + runner, + "_probe_bind_status", + lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}), + raising=False, ) account = HuyaAccount(uid=str(UID), cookie=COOKIE) @@ -669,13 +673,13 @@ def test_query_all_reports_not_opened_without_order_lookup(monkeypatch): ) status, message, result = marks[-1] - # 角色接口挂了不影响宝典/积分结论 + # 角色接口业务失败不影响宝典/积分结论 assert status == "success" assert result["elite_opened"] is False assert account.elite_opened is False assert account.elite_open_time is None assert "未开通宝典" in message - assert "角色信息接口无响应" in message + assert "活动已下线" in message # 未开通不查订单(省一次请求) assert [name for name, _kwargs in shop_client.calls] == ["get_redeem_need_goods"] @@ -684,9 +688,12 @@ def test_query_all_fails_when_points_center_unreachable(monkeypatch): shop_client = _StubClient(redeem=None) runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( - runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False + runner, + "_probe_bind_status", + lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}), + raising=False, ) - account = HuyaAccount(uid=str(UID), cookie=COOKIE) + account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported") runner._execute_query_all( None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"} @@ -695,9 +702,9 @@ def test_query_all_fails_when_points_center_unreachable(monkeypatch): status, message, result = marks[-1] assert status == "failed" assert "积分中心接口无响应" in message - # 两条通道都没回包:CK 失效标记跟着失败结果一起落库(前端整行标红) - assert result["ck_expired"] is True - assert account.status == "ck_expired" + assert result.get("ck_expired") is None + # 没有查到任何东西时不能把账号标成 all_queried + assert account.status == "imported" def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch): @@ -705,13 +712,16 @@ def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch 真机日志(logs/app-2026-10-15.log,hy_297838517)里 ``webActUI.checkUserBindGameAccount`` 的响应只有 ``newdata keys: ['']``(没有 tRsp), - ``_resolve_bind_status`` 因此返回 None;积分中心走另一条通道却还能返回 200, + ``_probe_bind_status`` 因此返回 None;积分中心走另一条通道却还能返回 200, 所以不能凭「积分查到了」就当成功,必须打 ck_expired 让页面标红。 + + 2026-10-17 起一键查询也改成先验 CK:判定失效后立即停止, + 不再白跑积分中心和宝典接口。 """ shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX)) runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( - runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False + runner, "_probe_bind_status", lambda **kwargs: (None, {}), raising=False ) account = HuyaAccount(uid=str(UID), cookie=COOKIE) @@ -720,13 +730,12 @@ def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch ) status, message, result = marks[-1] - # 宝典/积分查到了,任务本身不算失败(结果照样展示),但 CK 失效标记必须带上 - assert status == "success" + assert status == "error" assert result["ck_expired"] is True assert result["ck_expired_reason"] == "角色信息接口无响应" assert account.status == "ck_expired" - assert "角色信息接口无响应" in message - assert "疑似 CK 已失效" in message + assert "CK 已失效" in message + assert shop_client.calls == [] def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch): @@ -735,7 +744,7 @@ def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch): runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object()) monkeypatch.setattr( runner, - "_resolve_bind_status", + "_probe_bind_status", lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}), raising=False, ) diff --git a/web/backend/services/huya_runner_elite.py b/web/backend/services/huya_runner_elite.py index 8e499ba..5fd1a5f 100644 --- a/web/backend/services/huya_runner_elite.py +++ b/web/backend/services/huya_runner_elite.py @@ -263,7 +263,12 @@ class EliteMixin: account_info: dict, config_info: dict, ): - """一键查询:角色绑定 + 精英宝典开通 + 积分(勾选账号批量跑)。""" + """一键查询:先验 CK,再查角色绑定 + 精英宝典开通 + 积分。 + + 与「查询角色 / 查询积分」保持同一套最新链路: + - CK 走 HTTP 探针 + outer_act_id + 15 秒;无回包立即判失效并返回; + - 积分/宝典直接走 HTTP,不再为一次查询建 WSS 商城会话。 + """ uid = self._resolve_uid(account_info) if not uid: self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid") @@ -277,14 +282,86 @@ class EliteMixin: result: dict[str, Any] = {"uid": uid} failures: list[str] = [] message_parts: list[str] = [] + role_name = "" - # ---- 积分 + 宝典开通(积分中心同一个接口) ---- - shop_client: Any = self._shop_client(uid, cookie) + # ---- 角色绑定 / CK 有效性(先做,失效就不空跑后面两个接口) ---- + b_act_id_int = self._to_int( + self.payload.get("bind_act_id") or config_info.get("bind_act_id") + ) + if not b_act_id_int: + failures.append("未配置绑定 bActId") + result["ck_valid"] = False + else: + outer_act_id = str( + self.payload.get("outer_act_id") + or config_info.get("outer_act_id") + or "" + ).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) + result.update( + { + "bind_act_id": b_act_id_int, + "failures": failures + ["角色信息接口无响应"], + "ck_expired": True, + "ck_expired_reason": "角色信息接口无响应", + } + ) + self._mark_task( + worker_db, + task, + "error", + "虎牙角色信息接口无响应(CK 已失效,请重新登录或重新导入 Cookie)", + result, + ) + return + result.update( + { + "bind_act_id": b_act_id_int, + "ck_valid": int(getattr(bind_status, "status", 0) or 0) == 200, + } + ) + if bind_status.status != 200: + failures.append( + bind_status.msg or f"角色信息查询失败: {bind_status.status}" + ) + else: + role_info = self._bind_role_result(bind_status) + role_name = role_info.get("role_name") or "" + result.update( + { + **role_info, + **self._bind_change_state(bind_status), + **bind_query_result, + } + ) + if role_name: + account.game_name = role_name + account.game_channel = ( + self._role_channel(bind_status) or account.game_channel + ) + message_parts.append(f"角色 {role_name}") + else: + message_parts.append("未绑定角色") + + # ---- 积分 + 宝典开通(积分中心同一个接口,直接 HTTP) ---- + shop_client: Any = self._http_client() goods_num = ( self._to_int(self.payload.get("goods_num")) or POINTS_REDEEM_GOODS_NUM ) redeem = shop_client.get_redeem_need_goods( - uid=uid, guid="", cookie=cookie, goods_num=goods_num + uid=uid, + guid="", + cookie=cookie, + goods_num=goods_num, + timeout=10.0, ) points: int | None = None opened: bool | None = None @@ -338,68 +415,8 @@ class EliteMixin: opened_text = "未开通宝典" message_parts.append(opened_text) - # ---- 角色绑定(活动通道) ---- - b_act_id_int = self._to_int( - self.payload.get("bind_act_id") or config_info.get("bind_act_id") - ) - role_name = "" - # 角色查不到 = 这个 CK 已经不认了(模块头里有真机依据),整行要标红提醒 - ck_expired = False - ck_expired_reason = "" - if not b_act_id_int: - failures.append("未配置绑定 bActId") - else: - 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 - ck_expired_reason = "角色信息接口无响应" - failures.append(ck_expired_reason) - elif bind_status.status != 200: - failures.append( - bind_status.msg or f"角色信息查询失败: {bind_status.status}" - ) - else: - role_info = self._bind_role_result(bind_status) - role_name = role_info.get("role_name") or "" - result.update( - { - "bind_act_id": b_act_id_int, - **role_info, - **self._bind_change_state(bind_status), - **bind_query_result, - } - ) - if role_name: - account.game_name = role_name - account.game_channel = ( - self._role_channel(bind_status) or account.game_channel - ) - message_parts.append(f"角色 {role_name}") - else: - message_parts.append("未绑定角色") - account.points = points if points is not None else account.points - # CK 失效时账号状态单独标出来(前端「状态」列据此显示红色「CK 失效」) - account.status = "ck_expired" if ck_expired else "all_queried" - account.updated_at = datetime.now(UTC) result["failures"] = failures - if ck_expired: - result["ck_expired"] = True - result["ck_expired_reason"] = ck_expired_reason - ck_hint = ";疑似 CK 已失效,请重新登录或重新导入 Cookie" if ck_expired else "" fetched_anything = points is not None or role_name or opened is not None if not fetched_anything: @@ -407,12 +424,14 @@ class EliteMixin: worker_db, task, "failed", - (";".join(failures) or "一键查询全部失败") + ck_hint, + ";".join(failures) or "一键查询全部失败", result, ) return + account.status = "all_queried" + account.updated_at = datetime.now(UTC) message = "|".join(message_parts) or "一键查询完成" if failures: message += f";部分失败: {';'.join(failures)}" - self._mark_task(worker_db, task, "success", message + ck_hint, result) + self._mark_task(worker_db, task, "success", message, result)