Files
live-hub-py/tests/test_huya_pay_link.py
T
yml2213 0b9a01ab3f feat(huya): 开通支付码复用未支付旧单 + 二维码改用支付宝收银台直达地址
- 限购商品(精英宝典 buy_limit=1)挂着未支付旧单时,真正先失败的是购买权限
  校验 checkUserBuyAuth(真机 703「已超过该商品的购买上限,下单失败~」),
  createOrderV5 根本不会执行;现在 checkUserBuyAuth 与 createOrderV5 两处失败
  都会回退 find_reusable_order,拿同一商品的未支付旧单继续 payOrderSubmitV5。
- 新增支付宝收银台直达地址解析(resolve_pay_entry_url):跟随网关 302 后的
  excashier.alipay.com/standard/auth.htm?payOrderId=…(93 字符,原始网关地址
  1616 字符)写入结果 pay_cashier_url;解析失败回落本站短链
  /api/huya/pay/<task_id>?k=<hmac>,后端跑在 localhost 时客户也扫得开。
- 前端 QRCode 显式 color="#000" bgColor="#fff":antd v6 暗色主题默认
  color=token.colorText、bgColor=transparent,白码落在白底容器里根本看不见。
- 支付成功后列表「二维码」列不再展示二维码(qrDoneLabel → 已支付/已开通)。

测试:pytest 767 passed / 6 skipped;typecheck、eslint、ruff 通过。
2026-10-15 19:28:26 +08:00

182 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""开通支付码二维码内容:1600+ 字符的支付地址 → 手机扫得动、打得开的地址。
生产实测(2026-10-15):`payOrderSubmitV5` 返回的支付宝网关地址有 **1626** 字符
(`https://openapi.alipay.com/gateway.do?...&sign=...`)。直接把它编成二维码是
version 29+(140+ 个模块),在 150px 的画面上每个模块只有约 1 像素 —— 手机扫不上。
现在的优先级:
1. **支付宝收银台直达地址**(`resolve_pay_entry_url`):网关地址 302 两次后落到
`https://excashier.alipay.com/standard/auth.htm?payOrderId=...`(约 74 字符),
公网域名,后端跑在 localhost 时客户也扫得开;
2. **本站短链** `/api/huya/pay/<task_id>?k=<hmac>`(约 38 字符):后端 302 跳到网关地址,
但要求本页域名手机能访问到。
"""
import pytest
from fastapi import HTTPException
from web.backend.models import HuyaAccount, HuyaTask, User
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,
is_alipay_url,
pay_qr_path,
pay_token,
resolve_pay_entry_url,
verify_pay_token,
)
# 真机取到的支付宝网关地址形态(长度同量级)
ALIPAY_LONG_URL = (
"https://openapi.alipay.com/gateway.do"
"?alipay_sdk=alipay-sdk-java-4.40.272.ALL&app_id=2017123456789012"
"&method=alipay.trade.page.pay&sign=" + "a" * 1500
)
# 真机实测:上面的网关地址 302 两次后的落点(任务 191 原样取回)
CASHIER_URL = (
"https://excashier.alipay.com/standard/auth.htm"
"?payOrderId=6cb86f1d259642ac843522b85dab8a52.25"
)
def _create_pay_task(db, pay_url: str) -> HuyaTask:
user = User(username="pay-link-test", password_hash="x", role="support")
db.add(user)
db.commit()
account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="")
db.add(account)
db.commit()
task = HuyaTask(
batch_id="test-batch",
account_id=account.id,
task_type="create_recharge_order",
handbook_scope="elite",
status="running",
created_by=user.id,
result={"pay_url": pay_url} if pay_url else {},
)
db.add(task)
db.commit()
return task
def test_pay_token_round_trip_and_rejects_others():
token = pay_token(587)
assert len(token) == 16
assert verify_pay_token(587, token) is True
# 大小写不同就是另一个签名(hex 严格比对)
assert verify_pay_token(587, token.upper()) is False
assert verify_pay_token(587, "") is False
assert verify_pay_token(588, token) is False
def test_pay_qr_path_is_short_enough_to_scan():
path = pay_qr_path(587)
assert path.startswith(f"{PAY_PATH_PREFIX}/587?k=")
# 短链必须足够短:36 字符 → 二维码约 30 个模块(长链是 140+ 个模块)
assert len(path) <= 48
# 原始支付地址长度对比(就是它导致扫不上)
assert len(ALIPAY_LONG_URL) > 1000
def test_pay_redirect_route_returns_pay_url(db):
task = _create_pay_task(db, ALIPAY_LONG_URL)
response = redirect_huya_pay(task.id, pay_token(task.id), db)
assert response.status_code == 302
assert response.headers["location"] == ALIPAY_LONG_URL
def test_pay_redirect_route_rejects_bad_token(db):
task = _create_pay_task(db, ALIPAY_LONG_URL)
with pytest.raises(HTTPException) as excinfo:
redirect_huya_pay(task.id, "deadbeefdeadbeef", db)
assert excinfo.value.status_code == 404
def test_pay_redirect_route_rejects_task_without_pay_url(db):
task = _create_pay_task(db, "")
with pytest.raises(HTTPException) as excinfo:
redirect_huya_pay(task.id, pay_token(task.id), db)
assert excinfo.value.status_code == 404
class _FakeResponse:
"""假的 requests 响应:只需要 ``url`` 和 ``close()``。"""
def __init__(self, url: str):
self.url = url
self.closed = False
def close(self) -> None:
self.closed = True
def _patch_get(monkeypatch, *, final_url: str | None = None, 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 "")
monkeypatch.setattr(huya_pay_link.requests, "get", fake_get)
return calls
def test_resolve_pay_entry_url_follows_redirect_to_cashier(monkeypatch):
calls = _patch_get(monkeypatch, final_url=CASHIER_URL)
assert resolve_pay_entry_url(ALIPAY_LONG_URL) == CASHIER_URL
assert calls[0]["url"] == ALIPAY_LONG_URL
# 必须跟随 302,否则拿到的是网关页自己
assert calls[0]["allow_redirects"] is True
assert "Mozilla" in calls[0]["headers"]["User-Agent"]
def test_resolve_pay_entry_url_rejects_non_alipay_target(monkeypatch):
"""落到非支付宝域名就当没解析出来(调用方回落到本站短链)。"""
_patch_get(monkeypatch, final_url="https://evil.example.com/cashier")
assert resolve_pay_entry_url(ALIPAY_LONG_URL) == ""
def test_resolve_pay_entry_url_swallows_errors(monkeypatch):
_patch_get(monkeypatch, error=RuntimeError("network down"))
assert resolve_pay_entry_url(ALIPAY_LONG_URL) == ""
def test_resolve_pay_entry_url_ignores_blank_or_relative():
assert resolve_pay_entry_url("") == ""
assert resolve_pay_entry_url("/api/huya/pay/191?k=x") == ""
def test_resolve_pay_entry_url_is_shorter_than_raw_url(monkeypatch):
_patch_get(monkeypatch, final_url=CASHIER_URL)
resolved = resolve_pay_entry_url(ALIPAY_LONG_URL)
# 收银台直达地址约 74 字符 → 二维码模块数远低于原始地址
assert len(resolved) < 120
assert len(resolved) < len(ALIPAY_LONG_URL)
def test_is_alipay_url_matches_only_alipay_domains():
assert is_alipay_url(CASHIER_URL) 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
assert is_alipay_url("https://excashier.alipay.com.evil.com/") is False
assert is_alipay_url("") is False