"""虎牙积分中心(m-shop.huya.com/points.html)开通 / 买币下单链路。 2026-10-13「开通精英宝典 / 购买幸运币」抓包实证的帧顺序: getGoodsInfoV5 → getSupplierInfoV5 → listPayChannelV5 → checkHyProtocolV5 → PlayMallUI.getMyPromotion → calcOrderPromotion → acceptHyProtocolV5 → checkUserBuyAuth → createOrderV5 → payOrderSubmitV5 → orderDetailV5(轮询) 本模块只负责「准备 → 下单 → 起支付」;支付到账轮询由调用方决定(任务侧会轮询, 外部 CK 批量开通只出支付码)。任务执行器与外部 CK 批量开通共用这里,避免链路 在两处各写一遍。 """ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass, field from typing import Any #: 小黄车供应商(和平精英官方小店),抓包实测。 SUPPLIER_ID_YELLOW_CAR = 87401 #: 订单列表里「已支付/已交付」的状态值(与任务侧 _is_huya_order_paid 对齐)。 PAID_ORDER_STATUSES = frozenset({50, 55, 60, 70, 80}) #: 只有这两种状态能被继续支付:10=待支付、1000=尾款待支付 #: (core.huya.shop_structs.OrderStatus;90=已取消、60=已关闭都不能复用, #: 真机 2026-10-15:账号 66 的旧单被用户取消后 order_status 变成 90) REUSABLE_ORDER_STATUSES = frozenset({10, 1000}) #: 复用旧单时最多翻几页订单。 REUSE_ORDER_PAGE_SIZE = 20 REUSE_ORDER_MAX_PAGES = 3 @dataclass class ShopOrderOutcome: """开通链路的结果(ok=False 时 message 是失败原因)。""" ok: bool message: str detail: dict = field(default_factory=dict) supplier_name: str = "" promotion: dict | None = None promotion_info: dict | None = None order_raw: dict = field(default_factory=dict) pay_raw: dict = field(default_factory=dict) order_id: int = 0 app_order_id: str = "" pay_order_id: str = "" pay_url: str = "" amount: int = 0 pay_channel: str = "Zfb" sku_id: int = 0 unit_price: int = 0 count: int = 1 product_name: str = "" #: 复用了未支付的旧订单时为那张单的原始信息(限购商品没付掉就下不了新单) reused_order: dict = field(default_factory=dict) def find_reusable_order( client: Any, *, uid: int, cookie: str, spu_id: str, page_size: int = REUSE_ORDER_PAGE_SIZE, max_pages: int = REUSE_ORDER_MAX_PAGES, ) -> dict: """找这个账号**同一商品**的未支付订单(用于复用旧单继续支付)。 真机(2026-10-15):已经有一张未支付的精英宝典订单时,**限购校验** `checkUserBuyAuth` 会先返回 703 「已超过该商品的购买上限,下单失败~(如有未支付订单,请及时完成支付)」, `createOrderV5` 根本不会执行(下单接口有时也会返回同一句)。限购 1 件的 商品旧单没付掉就再也下不了新单,这时应该拿旧单去发起支付,而不是把任务 判失败(官方页面同样是复用旧单继续付)。 真机核对(账号 hy_300546644):未支付单 #11353073 order_status=10、 pay_time=0、goods_detail.spu_id=hy-8328015;已取消单是 order_status=90。 查不到 / 接口不可用都返回空 dict。 """ offset = 0 for _page in range(max(1, max_pages)): resp = client.query_user_order_list( uid=uid, guid="", cookie=cookie, offset=offset, page_size=page_size, order_type=1, status=0, ) if resp is None: return {} orders = list(getattr(resp, "orders", None) or []) for order in orders: item = order.to_dict() if hasattr(order, "to_dict") else dict(order) goods = item.get("goods_detail") or {} if str(goods.get("spu_id") or "") != str(spu_id or ""): continue if int(item.get("pay_time") or 0) > 0: continue # 只认「待支付」:已取消(90)/已关闭(60) 的旧单付不了 if int(item.get("order_status") or 0) not in REUSABLE_ORDER_STATUSES: continue return item total = int(getattr(resp, "totalCount", 0) or 0) if len(orders) < page_size or offset + len(orders) >= total: break offset += len(orders) return {} def run_open_flow( client: Any, *, uid: int, cookie: str, pid: int, spu_id: str, sku_id: int = 0, count: int = 1, pay_channel: str = "Zfb", supplier_id: int = SUPPLIER_ID_YELLOW_CAR, snapshot_price: int = 0, log: Callable[[str], None] | None = None, stopped: Callable[[], bool] | None = None, ) -> ShopOrderOutcome: """执行抓包链路;成功时返回「已生成支付二维码」的订单信息。""" def emit(message: str) -> None: if log is not None: log(message) def fail(message: str, **extra: Any) -> ShopOrderOutcome: return ShopOrderOutcome(ok=False, message=message, **extra) # 1) 商品详情(拿 skuId / 价格 / 商品名) detail_resp = client.get_goods_info( uid=uid, guid="", cookie=cookie, pid=pid, spu_id=spu_id, sku_id=sku_id or 0, game_id="", ) if detail_resp is None: return fail("虎牙充值商品详情接口无响应") detail = detail_resp.to_dict() if detail_resp.code != 200: return fail( detail_resp.message or f"虎牙充值商品详情获取失败: {detail_resp.code}", detail=detail, ) sku_id = int(detail.get("sku_id") or sku_id or 0) product_name = str(detail.get("name") or spu_id) unit_price = int(detail.get("price") or snapshot_price or 0) if not sku_id: return fail("充值商品缺少 SKU,请先刷新充值商品列表", detail=detail) # 2) 供应商(小店)信息 supplier = client.get_supplier_info(uid=uid, guid="", cookie=cookie) supplier_name = "" if supplier is not None: raw_name = supplier.value("tag2", "tag0") supplier_name = raw_name if isinstance(raw_name, str) else "" emit(f"[{uid}] 小店:{supplier_name or '未知'}(supplierId={supplier_id})") # 3) 支付渠道 # # 真机 2026-10-15:积分中心那批「礼物」spu(hy-5872824 精英徽章、hy-5879346 # 精英令…)在这里返回 401「您的设备环境暂不支持购买该商品」,而商品详情接口 # 仍是 200 + 价格 —— 「详情拿得到」不代表「能买」。失败时把服务端原话透出来, # 不要再糊一句「当前支付渠道不可用」让人以为是渠道配置问题。 channels = client.list_pay_channels( uid=uid, guid="", cookie=cookie, spu_id=spu_id, sku_id=0 ) if channels is None: return fail("支付渠道接口无响应", detail=detail, supplier_name=supplier_name) if channels.code != 200: return fail( channels.message or f"该商品当前不可购买(支付渠道 {channels.code})", detail=detail, supplier_name=supplier_name, ) available_channels = set(getattr(channels, "channels", []) or []) if pay_channel not in available_channels: channel_text = "/".join(sorted(available_channels)) or "无" return fail( f"当前支付渠道不可用(可用:{channel_text})", detail=detail, supplier_name=supplier_name, ) # 4) 商城协议校验 protocol = client.check_hy_protocol(uid=uid, guid="", cookie=cookie) if protocol is None or protocol.code != 200: return fail( getattr(protocol, "message", "") or "商城协议校验失败", detail=detail, supplier_name=supplier_name, ) # 5) 返利/促销查询 + 下单试算 promotion_info = client.play_mall_get_my_promotion( uid=uid, guid="", cookie=cookie, spu_id=spu_id, sku_id=sku_id, price=unit_price, ) promotion = client.calc_order_promotion( uid=uid, guid="", cookie=cookie, pid=pid, spu_id=spu_id, sku_id=sku_id, item_count=count, ) promotion_dict = promotion.to_dict() if promotion is not None else None if promotion is not None and promotion.code not in (0, 200): emit(f"[{uid}] 促销试算返回 {promotion.code}:{promotion.message}") # 6) 同意商城协议(抓包:这一步在 checkUserBuyAuth 之前) accept = client.accept_hy_protocol(uid=uid, guid="", cookie=cookie) if accept is None or accept.code != 200: return fail( getattr(accept, "message", "") or "虎牙商城协议确认失败", detail=detail, supplier_name=supplier_name, promotion=promotion_dict, promotion_info=( promotion_info.to_dict() if promotion_info is not None else None ), ) if stopped is not None and stopped(): return fail("已停止", detail=detail, supplier_name=supplier_name) common = { "detail": detail, "supplier_name": supplier_name, "promotion": promotion_dict, "promotion_info": ( promotion_info.to_dict() if promotion_info is not None else None ), "sku_id": sku_id, "unit_price": unit_price, "count": count, "product_name": product_name, "pay_channel": pay_channel, } order_id = 0 order_raw: dict = {} reused_order: dict = {} # 7) 购买权限 # # 限购商品(精英宝典 buy_limit=1)已经挂着未支付旧单时,**先失败的是这一步**: # 真机 2026-10-15 实测 checkUserBuyAuth 直接返回 703「已超过该商品的购买上限, # 下单失败~(如有未支付订单,请及时完成支付)」,createOrderV5 根本不会执行。 # 所以这里失败也要先找同商品的未支付旧单接着付,找不到才判失败。 auth = client.check_user_buy_auth( uid=uid, guid="", cookie=cookie, spu_id=spu_id, sku_id=sku_id ) if auth is None: return fail("购买权限校验接口无响应", **common) if auth.code != 200: auth_message = auth.message or f"购买权限校验失败: {auth.code}" reused_order = find_reusable_order( client, uid=uid, cookie=cookie, spu_id=spu_id ) if not reused_order or not reused_order.get("order_id"): return fail(auth_message, **common) order_id = int(reused_order["order_id"]) order_raw = dict(reused_order) emit(f"[{uid}] {auth_message};改为复用未支付订单 #{order_id} 继续支付") # 8) 下单(限购商品若已有未支付的旧单,就复用旧单发起支付,而不是判失败) if not order_id: order_resp = client.create_order( uid=uid, guid="", cookie=cookie, pid=pid, spu_id=spu_id, sku_id=sku_id, item_count=count, game_id="0", order_type=6, ) if order_resp is not None and order_resp.code == 200 and order_resp.orderId: order_id = int(order_resp.orderId) order_raw = order_resp.to_dict() else: create_message = ( order_resp.message if order_resp is not None else "虎牙下单接口无响应" ) or f"虎牙下单失败: {getattr(order_resp, 'code', 0)}" reused_order = find_reusable_order( client, uid=uid, cookie=cookie, spu_id=spu_id ) if not reused_order or not reused_order.get("order_id"): return fail(create_message, **common) order_id = int(reused_order["order_id"]) order_raw = dict(reused_order) emit(f"[{uid}] {create_message};改为复用未支付订单 #{order_id} 继续支付") common["order_raw"] = order_raw common["reused_order"] = reused_order # 9) 起支付(出支付宝/微信二维码) pay_resp = client.pay_order_submit( uid=uid, guid="", cookie=cookie, order_id=order_id, pay_type=pay_channel, pid=pid, item_count=count, ) if pay_resp is None: return fail("虎牙支付接口无响应", **common) pay_raw = pay_resp.to_dict() common["pay_raw"] = pay_raw if pay_resp.code != 200 or not pay_resp.payUrl: return fail( pay_resp.message or f"虎牙支付二维码生成失败: {pay_resp.code}", **common, ) amount = int(pay_resp.amount or unit_price * count or 0) return ShopOrderOutcome( ok=True, message=( f"已复用未支付订单 #{order_id}" if reused_order else "已生成支付二维码" ), order_id=order_id, app_order_id=pay_resp.appOrderId, pay_order_id=pay_resp.payOrderId, pay_url=pay_resp.payUrl, amount=amount, **common, )