按 10.13「开通精英宝典/购买幸运币」抓包(m-shop.huya.com/points.html)对齐商城链路: - WSS host 改为 launch.queryHttpDns 动态解析(失败回退抓包实测 host);握手 baseinfo 只带 uid/guid/页面 UA/exp,Origin 与页面同源 - wsLaunch 改为带真实 lUid/sGuid;初始化补上 AUTH(0x0A) → getConfig → UpdateUserExps(0x17),并把 getConfig 下发的版本 map 原样回传(不回传则业务 RPC 无响应) - 补齐新 RPC:getSupplierInfoV5 / acceptHyProtocolV5 / calcOrderPromotion / PlayMallUI.getMyPromotion|queryAllocation / wupui.* / huyauserui.getUserProfile - 下单参数对齐:shopAppInfo.scene=7 与订单 src/orderScene=4 分离、orderType=6、 bizType=5、gameCategoryId=507、extraParam.bizParams 标记 jifencenter、env 仅空点击 序列、支付回调指向 m-shop.huya.com/points.html - 开通链路抽到 services/huya_shop_flow.py,充值任务改为调用它 验证:新增 tests/test_huya_points_shop_protocol.py 与抓包逐字节比对(心跳、版本回传、 wsLaunch/getConfig/下单/支付帧与新 RPC 参数,共 10 条);按 AGENTS.md 做了反向验证 (把 jifencenter 改成 0 后该用例变红,还原后恢复)。ruff format --check 与全量 pytest 通过。
246 lines
7.7 KiB
Python
246 lines
7.7 KiB
Python
"""虎牙积分中心(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
|
||
|
||
|
||
@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 = ""
|
||
|
||
|
||
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) 支付渠道
|
||
channels = client.list_pay_channels(
|
||
uid=uid, guid="", cookie=cookie, spu_id=spu_id, sku_id=0
|
||
)
|
||
available_channels = set(getattr(channels, "channels", []) or [])
|
||
if (
|
||
channels is None
|
||
or channels.code != 200
|
||
or pay_channel not in available_channels
|
||
):
|
||
return fail(
|
||
"当前支付渠道不可用",
|
||
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)
|
||
|
||
# 7) 购买权限
|
||
auth = client.check_user_buy_auth(
|
||
uid=uid, guid="", cookie=cookie, spu_id=spu_id, sku_id=sku_id
|
||
)
|
||
if auth is None or auth.code != 200:
|
||
return fail(
|
||
getattr(auth, "message", "") or "购买权限校验失败",
|
||
detail=detail,
|
||
supplier_name=supplier_name,
|
||
promotion=promotion_dict,
|
||
)
|
||
|
||
# 8) 下单
|
||
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,
|
||
)
|
||
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,
|
||
}
|
||
if order_resp is None:
|
||
return fail("虎牙下单接口无响应", **common)
|
||
order_raw = order_resp.to_dict()
|
||
common["order_raw"] = order_raw
|
||
if order_resp.code != 200 or not order_resp.orderId:
|
||
return fail(
|
||
order_resp.message or f"虎牙下单失败: {order_resp.code}",
|
||
**common,
|
||
)
|
||
|
||
# 9) 起支付(出支付宝/微信二维码)
|
||
pay_resp = client.pay_order_submit(
|
||
uid=uid,
|
||
guid="",
|
||
cookie=cookie,
|
||
order_id=order_resp.orderId,
|
||
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="已生成支付二维码",
|
||
order_id=int(order_resp.orderId),
|
||
app_order_id=pay_resp.appOrderId,
|
||
pay_order_id=pay_resp.payOrderId,
|
||
pay_url=pay_resp.payUrl,
|
||
amount=amount,
|
||
**common,
|
||
)
|