Files
live-hub-py/tests/test_huya_shop_flow_reuse.py
T
yml2213 9a0d59825c fix(huya): 开通商品只保留真正可购买的 spu,并清理历史死商品行
真机 2026-10-15(HAR 10.15-精英徽章两个-1 + 逐个 spu 探测):面板里的「精英徽章 hy-5872824」是积分中心「赠送指定礼物得积分」那排的礼物 spu —— getGoodsInfoV5 仍返回 200+价格,但 listPayChannelV5 返回 401「您的设备环境暂不支持购买该商品」,所以下单必报「当前支付渠道不可用」(与数量无关)。真能买的是 hy-8343549 精英徽章 ¥1 / hy-8343551 精英令 ¥10。

- 内置候选换成可购买的 4 个:hy-8328015 宝典 / hy-8327356 币 / hy-8343549 徽章 / hy-8343551 令
- 刷新商品增加「能买到」闸门:详情 200 后再校验 listPayChannelV5,非 200 或渠道为空即剔除并记录服务端原话
- 下单流程渠道失败时透出服务端消息,只有渠道真不在可用列表才说「当前支付渠道不可用(可用:…)」
- 商品列表只认「最近一次刷新」那一套:原先按账号并集返回,别的账号旧刷新留下的礼物 spu 会和最新一套叠成 7 个选项
- 刷新时清理历史死行:本账号旧行 + 全局默认行(account_id 为空)+ 其它账号里已不在候选中的 spu 行
- 测试:401 商品被剔除、渠道失败透传、列表只认最近一套、刷新清理别处死行
2026-10-15 21:01:23 +08:00

353 lines
11 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.
"""限购商品的旧订单复用:已有未支付订单时不该「已超过该商品的购买上限」直接失败。
生产反馈(2026-10-15):账号上挂着上一张没付掉的宝典订单时,先是**购买权限校验**
`checkUserBuyAuth` 返回 703
已超过该商品的购买上限,下单失败~(如有未支付订单,请及时完成支付)
(后端日志 18:34 / 18:48 / 19:00 三批都停在 checkUserBuyAuth,createOrderV5 根本没发),
下单接口 `createOrderV5` 也可能返回同一句。限购 1 件的商品,旧单没付掉就再也下不了
新单 —— 所以这两种失败都要复用那张旧单去发起支付(官方页面同样是复用旧单继续付)。
"""
from types import SimpleNamespace
from web.backend.services.huya_shop_flow import find_reusable_order, run_open_flow
BUY_LIMIT_MESSAGE = (
"已超过该商品的购买上限,下单失败~(如有未支付订单,请及时完成支付)"
)
SPU_ID = "hy-8328015"
UNPAID_ELITE_ORDER = {
"order_id": "11346649",
"item_name": "精英宝典",
"order_status": 10,
"pay_time": 0,
"goods_detail": {"spu_id": SPU_ID, "sku_id": 7756811},
}
PAID_ELITE_ORDER = {
"order_id": "11346648",
"item_name": "精英宝典",
"order_status": 60,
"pay_time": 1791895396000,
"goods_detail": {"spu_id": SPU_ID, "sku_id": 7756811},
}
OTHER_GOODS_ORDER = {
"order_id": "11346647",
"item_name": "精英币",
"order_status": 10,
"pay_time": 0,
"goods_detail": {"spu_id": "hy-8327356", "sku_id": 7759407},
}
#: 真机 2026-10-15:账号 66 的旧单被用户取消 → order_status=90、pay_time=0
CANCELLED_ELITE_ORDER = {
"order_id": "11357769",
"item_name": "精英宝典",
"order_status": 90,
"pay_time": 0,
"goods_detail": {"spu_id": SPU_ID, "sku_id": 7756811},
}
class _Order(SimpleNamespace):
def to_dict(self) -> dict:
return dict(self.__dict__)
class _OrderList(SimpleNamespace):
pass
class _Client:
"""商城链路假客户端:只造流程需要的那几个返回。"""
def __init__(
self,
*,
create_code: int,
create_message: str = "",
orders=None,
auth_code: int = 200,
auth_message: str = "",
channel_code: int = 200,
channel_message: str = "",
channels=("Zfb",),
):
self.create_code = create_code
self.create_message = create_message
self.orders = orders or []
self.auth_code = auth_code
self.auth_message = auth_message
self.channel_code = channel_code
self.channel_message = channel_message
self.channels = list(channels)
self.pay_order_ids: list[int] = []
self.order_list_calls = 0
self.create_calls = 0
def get_goods_info(self, **kwargs):
return SimpleNamespace(
code=200,
message="",
to_dict=lambda: {
"sku_id": 7756811,
"name": "精英宝典",
"price": 3000,
},
)
def get_supplier_info(self, **kwargs):
resp = SimpleNamespace(code=200, message="", payload={})
resp.value = lambda *path: "和平精英官方小店"
return resp
def list_pay_channels(self, **kwargs):
return SimpleNamespace(
code=self.channel_code, message=self.channel_message, channels=self.channels
)
def check_hy_protocol(self, **kwargs):
return SimpleNamespace(code=200, message="")
def play_mall_get_my_promotion(self, **kwargs):
return SimpleNamespace(code=200, message="", to_dict=lambda: {"code": 200})
def calc_order_promotion(self, **kwargs):
return SimpleNamespace(code=200, message="", to_dict=lambda: {"code": 200})
def accept_hy_protocol(self, **kwargs):
return SimpleNamespace(code=200, message="")
def check_user_buy_auth(self, **kwargs):
return SimpleNamespace(code=self.auth_code, message=self.auth_message)
def create_order(self, **kwargs):
self.create_calls += 1
if self.create_code == 200:
return SimpleNamespace(
code=200,
message="",
orderId=11346649,
to_dict=lambda: {"order_id": 11346649, "code": 200},
)
return SimpleNamespace(
code=self.create_code,
message=self.create_message,
orderId=0,
to_dict=lambda: {"code": self.create_code, "message": self.create_message},
)
def query_user_order_list(self, **kwargs):
self.order_list_calls += 1
return _OrderList(
totalCount=len(self.orders),
orders=[_Order(**item) for item in self.orders],
)
def pay_order_submit(self, **kwargs):
self.pay_order_ids.append(int(kwargs.get("order_id") or 0))
return SimpleNamespace(
code=200,
message="",
payUrl="https://openapi.alipay.com/gateway.do?probe=1",
appOrderId="app-1",
payOrderId="pay-1",
amount=3000,
to_dict=lambda: {
"code": 200,
"pay_url": "https://openapi.alipay.com/gateway.do?probe=1",
},
)
def _run(client: _Client):
return run_open_flow(
client,
uid=1199665170519,
cookie="yyuid=1199665170519",
pid=1199650619883,
spu_id=SPU_ID,
sku_id=7756811,
count=1,
pay_channel="Zfb",
)
def test_reuse_unpaid_order_when_create_hits_buy_limit():
client = _Client(
create_code=703, create_message=BUY_LIMIT_MESSAGE, orders=[UNPAID_ELITE_ORDER]
)
outcome = _run(client)
assert outcome.ok is True
assert outcome.order_id == 11346649 # 复用旧单
assert outcome.message == "已复用未支付订单 #11346649"
assert outcome.pay_url.startswith("https://openapi.alipay.com/gateway.do")
assert client.pay_order_ids == [11346649] # 支付发给的是旧单
assert client.create_calls == 1 # 先试过下单,失败后才复用
assert outcome.reused_order["order_id"] == "11346649"
def test_auth_buy_limit_reuses_unpaid_order_without_creating():
"""真机 2026-10-15:限购校验先报 703,这时复用旧单且不该再发下单请求。"""
client = _Client(
create_code=200,
orders=[UNPAID_ELITE_ORDER],
auth_code=703,
auth_message=BUY_LIMIT_MESSAGE,
)
outcome = _run(client)
assert outcome.ok is True
assert outcome.order_id == 11346649
assert outcome.message == "已复用未支付订单 #11346649"
assert client.pay_order_ids == [11346649]
assert client.create_calls == 0
assert outcome.reused_order["order_status"] == 10
def test_auth_buy_limit_without_unpaid_order_keeps_original_error():
"""没有可复用的旧单时保持原样:报限购错误、不下单、不起支付。"""
client = _Client(
create_code=200,
orders=[],
auth_code=703,
auth_message=BUY_LIMIT_MESSAGE,
)
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == BUY_LIMIT_MESSAGE
assert client.pay_order_ids == []
assert client.create_calls == 0
def test_auth_buy_limit_ignores_paid_and_cancelled_orders():
client = _Client(
create_code=200,
orders=[CANCELLED_ELITE_ORDER, PAID_ELITE_ORDER],
auth_code=703,
auth_message=BUY_LIMIT_MESSAGE,
)
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == BUY_LIMIT_MESSAGE
assert client.pay_order_ids == []
def test_auth_no_response_fails_without_touching_orders():
client = _Client(create_code=200, orders=[UNPAID_ELITE_ORDER], auth_code=200)
client.check_user_buy_auth = lambda **kwargs: None
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == "购买权限校验接口无响应"
assert client.create_calls == 0
assert client.order_list_calls == 0
def test_unbuyable_goods_surfaces_channel_message():
"""能看不能买的商品(listPayChannelV5 401):把服务端原话透出来。
真机 2026-10-15:hy-5872824「精英徽章」是积分中心的礼物 spu,getGoodsInfoV5
返回 200 + ¥1,但支付渠道接口返回 401「您的设备环境暂不支持购买该商品」。
以前一律报「当前支付渠道不可用」,看不出是商品不能买。
"""
client = _Client(
create_code=200,
channel_code=401,
channel_message="您的设备环境暂不支持购买该商品",
channels=(),
)
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == "您的设备环境暂不支持购买该商品"
# 渠道校验没过就不该下单、更不该起支付
assert client.create_calls == 0
assert client.pay_order_ids == []
def test_unavailable_pay_channel_lists_what_is_available():
client = _Client(create_code=200, channels=("Weixin",))
outcome = _run(client) # 默认走 Zfb
assert outcome.ok is False
assert "当前支付渠道不可用" in outcome.message
assert "Weixin" in outcome.message
assert client.create_calls == 0
def test_paid_order_is_not_reused():
client = _Client(
create_code=703, create_message=BUY_LIMIT_MESSAGE, orders=[PAID_ELITE_ORDER]
)
outcome = _run(client)
assert outcome.ok is False
assert outcome.message == BUY_LIMIT_MESSAGE
assert client.pay_order_ids == []
def test_cancelled_order_is_not_reused():
"""已取消(90)的旧单不能复用(真机:用户取消后状态变 90)。"""
client = _Client(
create_code=703,
create_message=BUY_LIMIT_MESSAGE,
orders=[CANCELLED_ELITE_ORDER],
)
outcome = _run(client)
assert outcome.ok is False
assert client.pay_order_ids == []
def test_other_goods_order_is_not_reused():
client = _Client(
create_code=703, create_message=BUY_LIMIT_MESSAGE, orders=[OTHER_GOODS_ORDER]
)
outcome = _run(client)
assert outcome.ok is False
assert client.pay_order_ids == []
def test_create_success_does_not_look_up_old_orders():
client = _Client(create_code=200)
outcome = _run(client)
assert outcome.ok is True
assert outcome.message == "已生成支付二维码"
assert outcome.reused_order == {}
# 下单成功就不该多打一次订单列表
assert client.order_list_calls == 0
def test_find_reusable_order_skips_to_next_page():
class _PagedClient(_Client):
def query_user_order_list(self, **kwargs):
self.order_list_calls += 1
offset = int(kwargs.get("offset") or 0)
page = [OTHER_GOODS_ORDER] * 20 if offset == 0 else [UNPAID_ELITE_ORDER]
return _OrderList(totalCount=21, orders=[_Order(**item) for item in page])
client = _PagedClient(create_code=200)
reused = find_reusable_order(client, uid=1, cookie="c", spu_id=SPU_ID)
assert reused["order_id"] == "11346649"
assert client.order_list_calls == 2