真机 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 商品被剔除、渠道失败透传、列表只认最近一套、刷新清理别处死行
353 lines
11 KiB
Python
353 lines
11 KiB
Python
"""限购商品的旧订单复用:已有未支付订单时不该「已超过该商品的购买上限」直接失败。
|
||
|
||
生产反馈(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
|