fix(huya): 支付二维码改用收银台页面里的扫码内容 qr.alipay.com

真机复现(2026-10-15):上一版把二维码内容放成了**收银台页面地址**
(excashier.alipay.com/standard/auth.htm?payOrderId=…),那是 PC 版页面
(页面里写死 qrcodeWidth=150)。有些安卓机是用系统相机/浏览器打开的,就停在
这张 PC 页上——只显示左上角一个很小的二维码,不触发付款。

收银台页面里真正的扫码内容是隐藏字段 `J_qrCode`:

    <input name="qrCode" value="https://qr.alipay.com/upx03110ghym9vm37sx7255c" id="J_qrCode"/>

- huya_pay_link 新增 PayTargets / resolve_pay_targets() / extract_pay_qr_code():
  跟随网关 302 取收银台页面并解析出 qr.alipay.com 扫码码(约 40 字符)。
- 出码时写入 pay_qr_code,保留 pay_cashier_url 兜底;拿不到扫码码会打 warning。
- 前端二维码优先级:pay_qr_code → pay_cashier_url → 本站短链 → 原始地址。

验证:全量 pytest 772 passed / 6 skipped;用真机那张收银台页面跑解析得到
https://qr.alipay.com/upx03110ghym9vm37sx7255c;typecheck / eslint / ruff 通过。
This commit is contained in:
yml2213
2026-10-15 19:51:51 +08:00
parent 0b9a01ab3f
commit bf1e2e9361
5 changed files with 170 additions and 45 deletions
+9 -6
View File
@@ -16,7 +16,7 @@ from core.huya.frame_decoder import decode_struct_bytes
from core.huya.http_client import HuyaHttpClient
from core.huya.shop_structs import ShopRawRsp
from web.backend.models import HuyaAccount
from web.backend.services.huya_pay_link import pay_qr_path
from web.backend.services.huya_pay_link import PayTargets, pay_qr_path
from web.backend.services.huya_runner import HuyaBatchRunner
UID = 1199668595203
@@ -382,11 +382,12 @@ def test_create_recharge_order_confirms_elite_open_after_payment(db, monkeypatch
"web.backend.services.huya_runner_recharge.run_open_flow",
lambda *args, **kwargs: outcome,
)
# 二维码内容优先用支付宝收银台直达地址(真机:网关 302 后的落点)
# 二维码内容优先用收银台页面里的扫码内容(真机:支付宝扫码支付码)
monkeypatch.setattr(
"web.backend.services.huya_runner_recharge.resolve_pay_entry_url",
lambda _pay_url, **_kwargs: (
"https://excashier.alipay.com/standard/auth.htm?payOrderId=x.25"
"web.backend.services.huya_runner_recharge.resolve_pay_targets",
lambda _pay_url, **_kwargs: PayTargets(
cashier_url="https://excashier.alipay.com/standard/auth.htm?payOrderId=x.25",
qr_code="https://qr.alipay.com/upx03110ghym9vm37sx7255c",
),
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
@@ -410,7 +411,9 @@ def test_create_recharge_order_confirms_elite_open_after_payment(db, monkeypatch
assert result["post_payment_points"] == 280
# 二维码内容给前端两条短地址(原始支付地址 1626 字符,扫码扫不动)
assert result["pay_qr_path"] == pay_qr_path(587)
# 优先用支付宝收银台直达地址(公网域名,localhost 下客户也扫得开)
# 优先用收银台页面里的扫码内容(支付宝 App 扫码即付款)
assert result["pay_qr_code"] == "https://qr.alipay.com/upx03110ghym9vm37sx7255c"
# 收银台直达地址作为兜底保留(PC 打开就是这个页面)
assert result["pay_cashier_url"].startswith("https://excashier.alipay.com/")
assert account.elite_opened is True
assert account.elite_expire_time == 1798732799
+67 -5
View File
@@ -21,10 +21,13 @@ from web.backend.routers.huya.pay import redirect_huya_pay
from web.backend.services import huya_pay_link
from web.backend.services.huya_pay_link import (
PAY_PATH_PREFIX,
PayTargets,
extract_pay_qr_code,
is_alipay_url,
pay_qr_path,
pay_token,
resolve_pay_entry_url,
resolve_pay_targets,
verify_pay_token,
)
@@ -39,6 +42,17 @@ CASHIER_URL = (
"https://excashier.alipay.com/standard/auth.htm"
"?payOrderId=6cb86f1d259642ac843522b85dab8a52.25"
)
# 收银台页面里真正的扫码内容(真机页面原样片段)
QR_CODE_LITERAL = "https://qr.alipay.com/upx03110ghym9vm37sx7255c"
CASHIER_HTML = (
'<input name="timeoutUrl" type="hidden" value="" id="J_timeoutUrl"/>\n'
f'<input name="qrCode" type="hidden" value="{QR_CODE_LITERAL}" id="J_qrCode"/>\n'
'<input name="qrImgUrl" type="hidden" '
'value="https://mobilecodec.alipay.com/show.htm?code=upx03110ghym9vm37sx7255c" '
'id="J_qrImgUrl"/>\n'
f'<input name="qrRenewalURL" type="hidden" value="{CASHIER_URL}" id="J_qrRenewalURL"/>\n'
'<input type="hidden" name="qrcodeWidth" id="J_qrcodeWidth" value="150"/>'
)
def _create_pay_task(db, pay_url: str) -> HuyaTask:
@@ -111,24 +125,27 @@ def test_pay_redirect_route_rejects_task_without_pay_url(db):
class _FakeResponse:
"""假的 requests 响应:只需要 ``url`` 和 ``close()``。"""
"""假的 requests 响应:只需要 ``url`` / ``text`` / ``close()``。"""
def __init__(self, url: str):
def __init__(self, url: str, text: str = ""):
self.url = url
self.text = text
self.closed = False
def close(self) -> None:
self.closed = True
def _patch_get(monkeypatch, *, final_url: str | None = None, error=None):
def _patch_get(
monkeypatch, *, final_url: str | None = None, body: str = "", error=None
):
calls: list[dict] = []
def fake_get(url, **kwargs):
calls.append({"url": url, **kwargs})
if error is not None:
raise error
return _FakeResponse(final_url or "")
return _FakeResponse(final_url or "", body)
monkeypatch.setattr(huya_pay_link.requests, "get", fake_get)
return calls
@@ -167,13 +184,58 @@ def test_resolve_pay_entry_url_is_shorter_than_raw_url(monkeypatch):
resolved = resolve_pay_entry_url(ALIPAY_LONG_URL)
# 收银台直达地址约 74 字符 → 二维码模块数远低于原始地址
# 收银台直达地址约 93 字符 → 二维码模块数远低于原始地址
assert len(resolved) < 120
assert len(resolved) < len(ALIPAY_LONG_URL)
def test_extract_pay_qr_code_from_cashier_html():
"""收银台页面里的 J_qrCode 才是手机该扫的内容(真机抓到的原样片段)。"""
assert extract_pay_qr_code(CASHIER_HTML) == QR_CODE_LITERAL
def test_extract_pay_qr_code_ignores_other_alipay_urls():
"""只有 qr.alipay.com 才算扫码内容:qrImgUrl(mobilecodec)不算。"""
html = (
'<input name="qrImgUrl" type="hidden" '
'value="https://mobilecodec.alipay.com/show.htm?code=upx03110ghym9vm37sx7255c" '
'id="J_qrImgUrl"/>'
)
assert extract_pay_qr_code(html) == ""
assert extract_pay_qr_code("") == ""
def test_resolve_pay_targets_returns_cashier_and_qr_code(monkeypatch):
_patch_get(monkeypatch, final_url=CASHIER_URL, body=CASHIER_HTML)
targets = resolve_pay_targets(ALIPAY_LONG_URL)
assert targets.cashier_url == CASHIER_URL
assert targets.qr_code == QR_CODE_LITERAL
def test_resolve_pay_targets_keeps_cashier_when_qr_code_missing(monkeypatch):
"""拿不到 J_qrCode(比如停在中间页)时只回落到收银台地址。"""
_patch_get(monkeypatch, final_url=CASHIER_URL, body="<html>no qr here</html>")
targets = resolve_pay_targets(ALIPAY_LONG_URL)
assert targets.cashier_url == CASHIER_URL
assert targets.qr_code == ""
def test_resolve_pay_targets_empty_on_error(monkeypatch):
_patch_get(monkeypatch, error=RuntimeError("boom"))
targets = resolve_pay_targets(ALIPAY_LONG_URL)
assert targets == PayTargets()
def test_is_alipay_url_matches_only_alipay_domains():
assert is_alipay_url(CASHIER_URL) is True
assert is_alipay_url(QR_CODE_LITERAL) is True
assert is_alipay_url("https://openapi.alipay.com/gateway.do?x=1") is True
assert is_alipay_url("https://alipay.com/") is True
assert is_alipay_url("https://alipay.com.evil.com/") is False