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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user