Files
live-hub-py/web/backend/services/huya_shop_flow.py
T
yml2213 f540dcfc29 feat(huya): 对齐积分中心开通链路(2026-10-13 抓包)
按 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 通过。
2026-10-15 14:56:30 +08:00

246 lines
7.7 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.
"""虎牙积分中心(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,
)