真机现象(2026-10-15,外部 CK 导入后查询角色):checkUserBindGameAccount 响应结构 尾部多了 tag6(ZERO 类型 0x6c),共享的 read_struct 读到非 STRUCT_END 直接抛 「期望 STRUCT_END, 实际 0x0c」,整个 RPC 返回 None —— 工作台「查询角色」永远失败。 - taf_protocol:read_struct 收尾改为按 TAF 语义跳过未建模的尾部字段(上限 32 个, 防止解析错位时无限吞字节);新增 skip_to_struct_end() - frame_decoder / shop_structs:抽出 decode_struct_bytes 与 ShopRawRsp.from_payload, WSS 与 HTTP 两条通道共用一份通用解码逻辑(wss_client 同步改用) 验证:tests/test_huya_struct_parse_resilience.py 用真机响应字节回归(含 tag6=ZERO 的 响应能解析出角色「泪雨全是谎言」);按 AGENTS.md 反向验证 —— 注掉 skip_to_struct_end() 即复现线上报错「期望 STRUCT_END, 实际 0x0c」,还原后恢复。 全量 pytest 通过。
1407 lines
47 KiB
Python
1407 lines
47 KiB
Python
"""
|
||
虎牙充值相关 JCE 结构定义
|
||
字段顺序 = TAF tag 顺序(从 JS 逆向所得)
|
||
|
||
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
|
||
|
||
|
||
class OrderType:
|
||
HUYA_PHYSICAL = 1
|
||
TENCENT_VIRTUAL = 2
|
||
MAGIC_BOX = 3
|
||
MAGIC_BOX_DELIVERY = 4
|
||
PAID_COURSE = 5
|
||
HUYA_VIRTUAL = 6
|
||
LOTTERY_PICKUP = 7
|
||
DELIVERY_WORKBENCH = 11
|
||
GAME_ACCOUNT = 12
|
||
SHELF_GOODS = 17
|
||
|
||
|
||
class OrderStatus:
|
||
DEPOSIT_WAIT_PAY = 10
|
||
DEPOSIT_PAID = 20
|
||
WAIT_DELIVER = 30
|
||
WAIT_RECEIVE = 40
|
||
FINISHED = 50
|
||
FINISHED_CLOSED = 60
|
||
CANCELLED = 90
|
||
BALANCE_WAIT_PAY = 1000
|
||
CANCELLED_BALANCE_EXPIRED = 1001
|
||
|
||
|
||
def _opt_str(os: TafOutputStream, tag: int, val: str):
|
||
"""非空才写字符串(与浏览器一致,省默认值)"""
|
||
if val:
|
||
os.write_string(tag, val)
|
||
|
||
|
||
def _opt_int(os: TafOutputStream, tag: int, val):
|
||
"""非 0 才写整数"""
|
||
if val:
|
||
os.write_int64(tag, val)
|
||
|
||
|
||
def _opt_struct(os: TafOutputStream, tag: int, val):
|
||
"""非 None 才写结构体"""
|
||
if val is not None:
|
||
os.write_struct(tag, val)
|
||
|
||
|
||
def _opt_list(os: TafOutputStream, tag: int, val):
|
||
"""非空才写 list"""
|
||
if val:
|
||
os.write_list(tag, val)
|
||
|
||
|
||
def _opt_map(os: TafOutputStream, tag: int, val):
|
||
"""非空才写 map"""
|
||
if val:
|
||
os.write_map(tag, val)
|
||
|
||
|
||
def _skip_to_struct_end(ins: TafInputStream):
|
||
"""跳过当前结构里未解析的尾部字段,停在 STRUCT_END 前。"""
|
||
while True:
|
||
pos = ins.buf.tell()
|
||
_tag, dtype = ins.read_head()
|
||
if dtype == TafType.STRUCT_END:
|
||
ins.buf.seek(pos)
|
||
return
|
||
ins.skip_field(dtype)
|
||
|
||
|
||
# ============================================================
|
||
# 基础结构
|
||
# ============================================================
|
||
|
||
|
||
class UserId(TafStruct):
|
||
"""用户标识(cookie 在这里)"""
|
||
|
||
def __init__(self):
|
||
self.lUid: int = 0 # tag 0
|
||
self.sGuid: str = "" # tag 1
|
||
self.sToken: str = "" # tag 2
|
||
self.sHuYaUA: str = "" # tag 3 "webh5&0.0.1&websocket&&diypc_52775"
|
||
self.sCookie: str = "" # tag 4 完整 cookie
|
||
self.iTokenType: int = 0 # tag 5
|
||
self.sDeviceInfo: str = "" # tag 6
|
||
self.sQIMEI: str = "" # tag 7
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR2 实证: 浏览器不优化空串/0值, 全写字段
|
||
os.write_int64(0, self.lUid)
|
||
os.write_string(1, self.sGuid)
|
||
os.write_string(2, self.sToken)
|
||
os.write_string(3, self.sHuYaUA)
|
||
os.write_string(4, self.sCookie)
|
||
os.write_int32(5, self.iTokenType)
|
||
os.write_string(6, self.sDeviceInfo)
|
||
os.write_string(7, self.sQIMEI)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.lUid = ins.read_int64(0, default=self.lUid)
|
||
self.sGuid = ins.read_string(1, default=self.sGuid)
|
||
self.sToken = ins.read_string(2, default=self.sToken)
|
||
self.sHuYaUA = ins.read_string(3, default=self.sHuYaUA)
|
||
self.sCookie = ins.read_string(4, default=self.sCookie)
|
||
self.iTokenType = ins.read_int32(5, default=self.iTokenType)
|
||
self.sDeviceInfo = ins.read_string(6, default=self.sDeviceInfo)
|
||
self.sQIMEI = ins.read_string(7, default=self.sQIMEI)
|
||
|
||
|
||
class ShopAppInfo(TafStruct):
|
||
"""应用信息(HAR2 实证字段顺序: tag0 sAppId, tag1 sBizType, tag4 scene, tag5 sourceId)"""
|
||
|
||
def __init__(self):
|
||
self.sAppId: str = "huya" # tag 0
|
||
self.sBizType: str = "" # tag 1
|
||
self.scene: int = 0 # tag 4
|
||
self.sourceId: str = "" # tag 5
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR2 实证: 全写字段(含空串/0)
|
||
os.write_string(0, self.sAppId)
|
||
os.write_string(1, self.sBizType)
|
||
os.write_int32(4, self.scene)
|
||
os.write_string(5, self.sourceId)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.sAppId = ins.read_string(0, default=self.sAppId)
|
||
self.sBizType = ins.read_string(1, default=self.sBizType)
|
||
self.scene = ins.read_int32(4, default=self.scene)
|
||
self.sourceId = ins.read_string(5, default=self.sourceId)
|
||
|
||
|
||
# ============================================================
|
||
# wsLaunch 初始化
|
||
# ============================================================
|
||
|
||
|
||
class WsLaunchSubStruct(TafStruct):
|
||
"""wsLaunch tag4 子结构(5个空字符串字段,浏览器强制写)"""
|
||
|
||
def __init__(self):
|
||
self.s0: str = "" # tag 0
|
||
self.s1: str = "" # tag 1
|
||
self.s2: str = "" # tag 2
|
||
self.s3: str = "" # tag 3
|
||
self.s4: str = "" # tag 4
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# 浏览器写空字符串(STRING1 length0),这里强制写以精确匹配
|
||
os.write_string(0, self.s0)
|
||
os.write_string(1, self.s1)
|
||
os.write_string(2, self.s2)
|
||
os.write_string(3, self.s3)
|
||
os.write_string(4, self.s4)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class WsLaunchReq(TafStruct):
|
||
"""launch.wsLaunch 请求(tReq 是 STRUCT,不是 list)
|
||
|
||
从 HAR 逆向:
|
||
tag0: lUid (int64)
|
||
tag1: string ""
|
||
tag2: sHuYaUA "webh5&1.0.0&huya"
|
||
tag3: appSrc "HUYA&ZH&2052"
|
||
tag4: 子struct (5个空字符串)
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.lUid: int = 0 # tag 0
|
||
self.s1: str = "" # tag 1
|
||
self.sHuYaUA: str = "webh5&1.0.0&huya" # tag 2
|
||
self.appSrc: str = "HUYA&ZH&2052" # tag 3
|
||
self.sub: WsLaunchSubStruct = WsLaunchSubStruct() # tag 4
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# 注意: 浏览器不优化空字符串,tag1 强制写(HAR 实证)
|
||
_opt_int(os, 0, self.lUid)
|
||
os.write_string(1, self.s1) # 强制写空字符串
|
||
_opt_str(os, 2, self.sHuYaUA)
|
||
_opt_str(os, 3, self.appSrc)
|
||
_opt_struct(os, 4, self.sub)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
# ============================================================
|
||
# 商品查询
|
||
# ============================================================
|
||
|
||
|
||
class GetGoodsInfoReqV5(TafStruct):
|
||
"""商品查询请求 (shopMiddleUI.getGoodsInfoV5)"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.shopAppInfo = ShopAppInfo() # tag 1
|
||
self.pid: int = 0 # tag 2
|
||
self.gameId: str = "" # tag 3
|
||
self.spuId: str = "" # tag 4
|
||
self.channelStockCode: str = "" # tag 5
|
||
self.skuId: int = 0 # tag 6
|
||
self.inviterUid: int = 0 # tag 7
|
||
self.userModifyPriceId: int = 0 # tag 8
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR2 实证: 全写字段
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.pid)
|
||
os.write_string(3, self.gameId)
|
||
os.write_string(4, self.spuId)
|
||
os.write_string(5, self.channelStockCode)
|
||
os.write_int64(6, self.skuId)
|
||
os.write_int64(7, self.inviterUid)
|
||
os.write_int64(8, self.userModifyPriceId)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
# 请求结构体一般不需要读,但保留
|
||
pass
|
||
|
||
|
||
class GoodsInfoRsp(TafStruct):
|
||
"""商品查询响应。"""
|
||
|
||
def __init__(self):
|
||
self.code: int = 0 # tag 0
|
||
self.message: str = "" # tag 1
|
||
self.goodsInfo = None # tag 2
|
||
self.selfGoods: int = 0 # tag 3
|
||
self.marketStatus: int = 0 # tag 5
|
||
self.timestamp: int = 0 # tag 6
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
self.goodsInfo = ins.read_struct(2, GoodsInfoDetail)
|
||
self.selfGoods = ins.read_int32(3, default=self.selfGoods)
|
||
self.marketStatus = ins.read_int32(5, default=self.marketStatus)
|
||
self.timestamp = ins.read_int64(6, default=self.timestamp)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
@property
|
||
def spu_id(self) -> str:
|
||
return self.goodsInfo.baseInfo.spuId if self.goodsInfo else ""
|
||
|
||
@property
|
||
def sku_id(self) -> int:
|
||
return self.goodsInfo.priceInfo.first_sku_id if self.goodsInfo else 0
|
||
|
||
@property
|
||
def name(self) -> str:
|
||
return self.goodsInfo.baseInfo.name if self.goodsInfo else ""
|
||
|
||
@property
|
||
def price(self) -> int:
|
||
return self.goodsInfo.priceInfo.price if self.goodsInfo else 0
|
||
|
||
@property
|
||
def stock(self) -> int:
|
||
return self.goodsInfo.priceInfo.stock if self.goodsInfo else 0
|
||
|
||
@property
|
||
def buy_limit(self) -> int:
|
||
return self.goodsInfo.priceInfo.buyLimit if self.goodsInfo else 0
|
||
|
||
def to_dict(self) -> dict:
|
||
goods = self.goodsInfo.to_dict() if self.goodsInfo else {}
|
||
return {
|
||
"code": self.code,
|
||
"message": self.message,
|
||
"spu_id": goods.get("spu_id", ""),
|
||
"sku_id": goods.get("sku_id", 0),
|
||
"name": goods.get("name", ""),
|
||
"description": goods.get("description", ""),
|
||
"icon": goods.get("icon", ""),
|
||
"detail_url": goods.get("detail_url", ""),
|
||
"price": goods.get("price", 0),
|
||
"min_price": goods.get("min_price", 0),
|
||
"max_price": goods.get("max_price", 0),
|
||
"stock": goods.get("stock", 0),
|
||
"buy_limit": goods.get("buy_limit", 0),
|
||
"sku_list": goods.get("sku_list", []),
|
||
"self_goods": self.selfGoods,
|
||
"market_status": self.marketStatus,
|
||
"timestamp": self.timestamp,
|
||
"goods": goods,
|
||
}
|
||
|
||
|
||
class GoodsBaseInfo(TafStruct):
|
||
"""商品基础信息(getGoodsInfoV5 tag2.tag0)。"""
|
||
|
||
def __init__(self):
|
||
self.spuId: str = ""
|
||
self.appId: str = ""
|
||
self.name: str = ""
|
||
self.description: str = ""
|
||
self.icon: str = ""
|
||
self.detailUrl: str = ""
|
||
self.detailHtml: str = ""
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.spuId = ins.read_string(0, default=self.spuId)
|
||
self.appId = ins.read_string(1, default=self.appId)
|
||
self.name = ins.read_string(2, default=self.name)
|
||
self.description = ins.read_string(4, default=self.description)
|
||
self.icon = ins.read_string(5, default=self.icon)
|
||
self.detailUrl = ins.read_string(6, default=self.detailUrl)
|
||
self.detailHtml = ins.read_string(7, default=self.detailHtml)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"spu_id": self.spuId,
|
||
"app_id": self.appId,
|
||
"name": self.name,
|
||
"description": self.description,
|
||
"icon": self.icon,
|
||
"detail_url": self.detailUrl,
|
||
}
|
||
|
||
|
||
class GoodsSkuItem(TafStruct):
|
||
"""商品 SKU 信息(getGoodsInfoV5 tag2.tag4.tag3 map value)。"""
|
||
|
||
def __init__(self):
|
||
self.skuId: int = 0
|
||
self.spuId: str = ""
|
||
self.price: int = 0
|
||
self.stock: int = 0
|
||
self.status: int = 0
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.skuId = ins.read_int64(0, default=self.skuId)
|
||
self.spuId = ins.read_string(1, default=self.spuId)
|
||
self.price = ins.read_int64(2, default=self.price)
|
||
self.stock = ins.read_int64(3, default=self.stock)
|
||
self.status = ins.read_int32(4, default=self.status)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
@staticmethod
|
||
def read_map_value(ins: TafInputStream, dtype: int):
|
||
if dtype != TafType.STRUCT_BEGIN:
|
||
ins.skip_field(dtype)
|
||
return None
|
||
item = GoodsSkuItem()
|
||
item.read_from(ins)
|
||
_end_tag, end_type = ins.read_head()
|
||
if end_type != TafType.STRUCT_END:
|
||
raise ValueError(f"期望 SKU STRUCT_END,实际 0x{end_type:02x}")
|
||
return item
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"sku_id": self.skuId,
|
||
"spu_id": self.spuId,
|
||
"price": self.price,
|
||
"stock": self.stock,
|
||
"status": self.status,
|
||
}
|
||
|
||
|
||
class GoodsPriceInfo(TafStruct):
|
||
"""商品价格与 SKU 信息(getGoodsInfoV5 tag2.tag4)。"""
|
||
|
||
def __init__(self):
|
||
self.spuId: str = ""
|
||
self.minPrice: int = 0
|
||
self.maxPrice: int = 0
|
||
self.skuMap: dict[int, GoodsSkuItem] = {}
|
||
self.stock: int = 0
|
||
self.buyLimit: int = 0
|
||
self.price: int = 0
|
||
|
||
def _read_sku_map(self, ins: TafInputStream):
|
||
found = ins._find_tag(3, required=False)
|
||
if not found:
|
||
return {}
|
||
if found[1] != TafType.MAP:
|
||
ins.skip_field(found[1])
|
||
return {}
|
||
count = ins._read_int_len()
|
||
result = {}
|
||
for _ in range(count):
|
||
_, key_type = ins.read_head()
|
||
key = ins._read_int_value(key_type)
|
||
_, value_type = ins.read_head()
|
||
item = GoodsSkuItem.read_map_value(ins, value_type)
|
||
if item is not None:
|
||
result[int(key)] = item
|
||
return result
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.spuId = ins.read_string(0, default=self.spuId)
|
||
self.minPrice = ins.read_int64(1, default=self.minPrice)
|
||
self.maxPrice = ins.read_int64(2, default=self.maxPrice)
|
||
self.skuMap = self._read_sku_map(ins)
|
||
self.stock = ins.read_int64(4, default=self.stock)
|
||
self.buyLimit = ins.read_int64(5, default=self.buyLimit)
|
||
self.price = ins.read_int64(7, default=self.price)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
@property
|
||
def first_sku_id(self) -> int:
|
||
if not self.skuMap:
|
||
return 0
|
||
return min(self.skuMap.keys())
|
||
|
||
@property
|
||
def sku_list(self) -> list[dict]:
|
||
return [self.skuMap[key].to_dict() for key in sorted(self.skuMap.keys())]
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"spu_id": self.spuId,
|
||
"sku_id": self.first_sku_id,
|
||
"price": self.price or self.minPrice,
|
||
"min_price": self.minPrice,
|
||
"max_price": self.maxPrice,
|
||
"stock": self.stock,
|
||
"buy_limit": self.buyLimit,
|
||
"sku_list": self.sku_list,
|
||
}
|
||
|
||
|
||
class GoodsInfoDetail(TafStruct):
|
||
"""getGoodsInfoV5 响应里的 goodsInfo 主体。"""
|
||
|
||
def __init__(self):
|
||
self.baseInfo = GoodsBaseInfo()
|
||
self.priceInfo = GoodsPriceInfo()
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.baseInfo = ins.read_struct(0, GoodsBaseInfo) or self.baseInfo
|
||
self.priceInfo = ins.read_struct(4, GoodsPriceInfo) or self.priceInfo
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
data = self.baseInfo.to_dict()
|
||
data.update(self.priceInfo.to_dict())
|
||
return data
|
||
|
||
|
||
# ============================================================
|
||
# 订单历史
|
||
# ============================================================
|
||
|
||
|
||
class OrderListShopInfo(TafStruct):
|
||
"""订单明细里的店铺信息(只取展示需要的字段)"""
|
||
|
||
def __init__(self):
|
||
self.shopName: str = "" # tag 0
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.shopName = ins.read_string(0, default=self.shopName)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"shop_name": self.shopName,
|
||
}
|
||
|
||
|
||
class OrderListGoodsDetail(TafStruct):
|
||
"""订单明细(queryUserOrderList 响应 tag16)"""
|
||
|
||
def __init__(self):
|
||
self.spuId: str = "" # tag 4
|
||
self.skuId: int = 0 # tag 16
|
||
self.buyerUid: int = 0 # tag 18
|
||
self.virtualType: int = 0 # tag 19
|
||
self.quantity: int = 0 # tag 20
|
||
self.shopInfo: OrderListShopInfo | None = None # tag 21
|
||
self.points: int = 0 # tag 23
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.spuId = ins.read_string(4, default=self.spuId)
|
||
self.skuId = ins.read_int64(16, default=self.skuId)
|
||
self.buyerUid = ins.read_int64(18, default=self.buyerUid)
|
||
self.virtualType = ins.read_int32(19, default=self.virtualType)
|
||
self.quantity = ins.read_int64(20, default=self.quantity)
|
||
self.shopInfo = ins.read_struct(21, OrderListShopInfo)
|
||
self.points = ins.read_int64(23, default=self.points)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"spu_id": self.spuId,
|
||
"sku_id": self.skuId,
|
||
"buyer_uid": self.buyerUid,
|
||
"virtual_type": self.virtualType,
|
||
"quantity": self.quantity,
|
||
"shop_info": self.shopInfo.to_dict() if self.shopInfo else None,
|
||
"points": self.points,
|
||
}
|
||
|
||
|
||
class OrderListItem(TafStruct):
|
||
"""订单列表项(queryUserOrderList 响应 tag3 的 list 元素)"""
|
||
|
||
def __init__(self):
|
||
self.bizOrderId: str = "" # tag 0 shop10148750
|
||
self.appId: str = "" # tag 1 shop
|
||
self.orderId: str = "" # tag 2
|
||
self.pid: int = 0 # tag 3
|
||
self.shopName: str = "" # tag 4
|
||
self.orderStatus: int = 0 # tag 5
|
||
self.itemName: str = "" # tag 8
|
||
self.unitPrice: int = 0 # tag 9 分
|
||
self.quantity: int = 0 # tag 10
|
||
self.totalPrice: int = 0 # tag 12 分
|
||
self.createTime: int = 0 # tag 14 毫秒时间戳
|
||
self.payTime: int = 0 # tag 15 毫秒时间戳
|
||
self.goodsDetail: OrderListGoodsDetail | None = None # tag 16
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
|
||
self.appId = ins.read_string(1, default=self.appId)
|
||
self.orderId = ins.read_string(2, default=self.orderId)
|
||
self.pid = ins.read_int64(3, default=self.pid)
|
||
self.shopName = ins.read_string(4, default=self.shopName)
|
||
self.orderStatus = ins.read_int32(5, default=self.orderStatus)
|
||
self.itemName = ins.read_string(8, default=self.itemName)
|
||
self.unitPrice = ins.read_int64(9, default=self.unitPrice)
|
||
self.quantity = ins.read_int64(10, default=self.quantity)
|
||
self.totalPrice = ins.read_int64(12, default=self.totalPrice)
|
||
self.createTime = ins.read_int64(14, default=self.createTime)
|
||
self.payTime = ins.read_int64(15, default=self.payTime)
|
||
self.goodsDetail = ins.read_struct(16, OrderListGoodsDetail)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"biz_order_id": self.bizOrderId,
|
||
"app_id": self.appId,
|
||
"order_id": self.orderId,
|
||
"pid": self.pid,
|
||
"shop_name": self.shopName,
|
||
"order_status": self.orderStatus,
|
||
"item_name": self.itemName,
|
||
"unit_price": self.unitPrice,
|
||
"quantity": self.quantity,
|
||
"total_price": self.totalPrice,
|
||
"create_time": self.createTime,
|
||
"pay_time": self.payTime,
|
||
"goods_detail": self.goodsDetail.to_dict() if self.goodsDetail else None,
|
||
}
|
||
|
||
|
||
class QueryUserOrderListReq(TafStruct):
|
||
"""查询购买历史订单 (revenueWebUI.queryUserOrderList)"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.offset: int = 0 # tag 1
|
||
self.orderType: int = 1 # tag 2
|
||
self.pageSize: int = 10 # tag 3
|
||
self.status: int = 0 # tag 4
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_int32(1, self.offset)
|
||
os.write_int32(2, self.orderType)
|
||
os.write_int32(3, self.pageSize)
|
||
os.write_int32(4, self.status)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class QueryUserOrderListRsp(TafStruct):
|
||
"""购买历史订单响应"""
|
||
|
||
def __init__(self):
|
||
self.code: int = 0
|
||
self.message: str = ""
|
||
self.totalCount: int = 0
|
||
self.orders: list[OrderListItem] = []
|
||
|
||
@staticmethod
|
||
def _read_order_item(ins: TafInputStream, _tag: int):
|
||
item = OrderListItem()
|
||
item.read_from(ins)
|
||
_end_tag, dtype = ins.read_head()
|
||
if dtype != TafType.STRUCT_END:
|
||
raise ValueError(f"期望订单 STRUCT_END,实际 0x{dtype:02x}")
|
||
return item
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
self.totalCount = ins.read_int64(2, default=self.totalCount)
|
||
self.orders = ins.read_list(3, item_reader=self._read_order_item)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"code": self.code,
|
||
"message": self.message,
|
||
"total_count": self.totalCount,
|
||
"orders": [item.to_dict() for item in self.orders],
|
||
}
|
||
|
||
|
||
class ShopAppRequest(TafStruct):
|
||
"""商城前置 RPC 的公共请求字段。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId()
|
||
self.shopAppInfo = ShopAppInfo()
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.userId = ins.read_struct(0, UserId) or self.userId
|
||
self.shopAppInfo = ins.read_struct(1, ShopAppInfo) or self.shopAppInfo
|
||
|
||
|
||
class ListPayChannelReq(ShopAppRequest):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.supplierId = 0
|
||
self.bizType = 5
|
||
self.gameCategoryId = 507
|
||
self.spuId = ""
|
||
self.skuId = 0
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.supplierId)
|
||
os.write_int32(3, self.bizType)
|
||
os.write_int32(4, self.gameCategoryId)
|
||
os.write_string(5, self.spuId)
|
||
os.write_int64(6, self.skuId)
|
||
|
||
|
||
class CheckHyProtocolReq(ShopAppRequest):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.protocolVersion = 1
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int32(2, self.protocolVersion)
|
||
|
||
|
||
class CheckUserBuyAuthReq(ShopAppRequest):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.spuId = ""
|
||
self.skuId = 0
|
||
self.checkType = 3
|
||
self.payChannel = ""
|
||
self.itemCount = 0
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_string(2, self.spuId)
|
||
os.write_int64(3, self.skuId)
|
||
os.write_int32(4, self.checkType)
|
||
os.write_string(5, self.payChannel)
|
||
os.write_int32(6, self.itemCount)
|
||
|
||
|
||
class ListPayChannelRsp(TafStruct):
|
||
"""shopMiddleUI.listPayChannelV5 响应。"""
|
||
|
||
def __init__(self):
|
||
self.code = 0
|
||
self.message = ""
|
||
self.channels: list[str] = []
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.channels = ins.read_list(0)
|
||
self.code = ins.read_int32(1, default=self.code)
|
||
self.message = ins.read_string(2, default=self.message)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_list(0, self.channels)
|
||
os.write_int32(1, self.code)
|
||
os.write_string(2, self.message)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {"code": self.code, "message": self.message, "channels": self.channels}
|
||
|
||
|
||
class ShopCodeRsp(TafStruct):
|
||
"""checkHyProtocolV5/checkUserBuyAuth 的状态响应。"""
|
||
|
||
def __init__(self):
|
||
self.code = 0
|
||
self.message = ""
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
_skip_to_struct_end(ins)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_int32(0, self.code)
|
||
os.write_string(1, self.message)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {"code": self.code, "message": self.message}
|
||
|
||
|
||
class OrderDetailOrder(TafStruct):
|
||
"""orderDetailV5 中的订单摘要;保留支付轮询所需字段。"""
|
||
|
||
def __init__(self):
|
||
self.orderId: int = 0
|
||
self.orderStatus: int = 0
|
||
self.createTime: int = 0
|
||
self.payTime: int = 0
|
||
self.totalPrice: int = 0
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
while True:
|
||
pos = ins.buf.tell()
|
||
tag, dtype = ins.read_head()
|
||
if dtype == TafType.STRUCT_END:
|
||
ins.buf.seek(pos)
|
||
return
|
||
if tag == 0:
|
||
ins.buf.seek(pos)
|
||
self.orderId = ins.read_int64(tag, default=self.orderId)
|
||
elif tag == 3:
|
||
ins.buf.seek(pos)
|
||
self.orderStatus = ins.read_int32(tag, default=self.orderStatus)
|
||
elif tag == 7:
|
||
ins.buf.seek(pos)
|
||
self.totalPrice = ins.read_int64(tag, default=self.totalPrice)
|
||
elif tag == 13:
|
||
ins.buf.seek(pos)
|
||
self.createTime = ins.read_int64(tag, default=self.createTime)
|
||
elif tag == 14:
|
||
ins.buf.seek(pos)
|
||
self.payTime = ins.read_int64(tag, default=self.payTime)
|
||
else:
|
||
ins.skip_field(dtype)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_int64(0, self.orderId)
|
||
os.write_int32(3, self.orderStatus)
|
||
os.write_int64(7, self.totalPrice)
|
||
os.write_int64(13, self.createTime)
|
||
os.write_int64(14, self.payTime)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"order_id": self.orderId,
|
||
"order_status": self.orderStatus,
|
||
"create_time": self.createTime,
|
||
"pay_time": self.payTime,
|
||
"total_price": self.totalPrice,
|
||
}
|
||
|
||
|
||
class OrderDetailReq(TafStruct):
|
||
"""shopMiddleUI.orderDetailV5 请求。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId()
|
||
self.shopAppInfo = ShopAppInfo()
|
||
self.orderId: int = 0
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.orderId)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.userId = ins.read_struct(0, UserId) or self.userId
|
||
self.shopAppInfo = ins.read_struct(1, ShopAppInfo) or self.shopAppInfo
|
||
self.orderId = ins.read_int64(2, default=self.orderId)
|
||
|
||
|
||
class OrderDetailData(TafStruct):
|
||
def __init__(self):
|
||
self.order: OrderDetailOrder | None = None
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
while True:
|
||
pos = ins.buf.tell()
|
||
tag, dtype = ins.read_head()
|
||
if dtype == TafType.STRUCT_END:
|
||
ins.buf.seek(pos)
|
||
return
|
||
if tag == 1:
|
||
ins.buf.seek(pos)
|
||
self.order = ins.read_struct(1, OrderDetailOrder)
|
||
else:
|
||
ins.skip_field(dtype)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
if self.order:
|
||
os.write_struct(1, self.order)
|
||
|
||
|
||
class OrderDetailRsp(TafStruct):
|
||
"""shopMiddleUI.orderDetailV5 响应。"""
|
||
|
||
def __init__(self):
|
||
self.code = 0
|
||
self.message = ""
|
||
self.data: OrderDetailData | None = None
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
self.data = ins.read_struct(2, OrderDetailData)
|
||
|
||
@property
|
||
def order(self) -> OrderDetailOrder | None:
|
||
return self.data.order if self.data else None
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_int32(0, self.code)
|
||
os.write_string(1, self.message)
|
||
if self.data:
|
||
os.write_struct(2, self.data)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"code": self.code,
|
||
"message": self.message,
|
||
"order": self.order.to_dict() if self.order else None,
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# 下单
|
||
# ============================================================
|
||
|
||
|
||
class CreateOrderExtraParam(TafStruct):
|
||
def __init__(self):
|
||
self.freight: int = 0 # tag 0
|
||
self.channelStockType: str = "" # tag 1
|
||
self.channelStockCode: str = "" # tag 2
|
||
self.relatedBizId: str = "" # tag 3
|
||
self.bizParams: str = "" # tag 4
|
||
self.popupTraceId: str = "" # tag 5
|
||
self.supplierUid: int = 0 # tag 6
|
||
self.categoryId: str = "" # tag 7
|
||
self.ext: str = "" # tag 8
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR 实证:下单 extraParam 会强制写默认 0/空串字段
|
||
os.write_int64(0, self.freight)
|
||
os.write_string(1, self.channelStockType)
|
||
os.write_string(2, self.channelStockCode)
|
||
os.write_string(3, self.relatedBizId)
|
||
os.write_string(4, self.bizParams)
|
||
os.write_string(5, self.popupTraceId)
|
||
os.write_int64(6, self.supplierUid)
|
||
if self.categoryId:
|
||
os.write_string(7, self.categoryId)
|
||
os.write_string(8, self.ext)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class CreateOrderPromotionParam(TafStruct):
|
||
def __init__(self):
|
||
self.yxjDeductPrice: int = 0 # tag 0
|
||
self.userModifyPriceId: int = 0 # tag 1
|
||
self.enablePromotion: int = 1 # tag 2
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR 实证:tag0/tag1 为 0,tag2 为 1
|
||
os.write_int64(0, self.yxjDeductPrice)
|
||
os.write_int64(1, self.userModifyPriceId)
|
||
os.write_int32(2, self.enablePromotion)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class CreateOrderAccountParam(TafStruct):
|
||
def __init__(self):
|
||
self.payoutTypeList: list[int] = [] # tag 0 Vector<INT32>
|
||
self.payoutChargeAmount: int = 0 # tag 1
|
||
self.cancelPayoutTypeList: list[int] = [] # tag 2
|
||
self.recycleSupplierId: int = 0 # tag 3
|
||
self.claimPrice: int = 0 # tag 4
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR 实证:空 list/0 值也会写出
|
||
os.write_list(0, self.payoutTypeList)
|
||
os.write_int64(1, self.payoutChargeAmount)
|
||
os.write_list(2, self.cancelPayoutTypeList)
|
||
os.write_int64(3, self.recycleSupplierId)
|
||
os.write_int64(4, self.claimPrice)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class PromotionItem(TafStruct):
|
||
def __init__(self):
|
||
self.promotionId: int = 0 # tag 0
|
||
self.promotionType: int = 0 # tag 1
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
_opt_int(os, 0, self.promotionId)
|
||
_opt_int(os, 1, self.promotionType)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.promotionId = ins.read_int64(0, default=self.promotionId)
|
||
self.promotionType = ins.read_int32(1, default=self.promotionType)
|
||
|
||
|
||
class CreateOrderReqV5(TafStruct):
|
||
"""下单请求 (shopMiddleUI.createOrderV5)"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.shopAppInfo = ShopAppInfo() # tag 1
|
||
self.receiveId: int = 0 # tag 2
|
||
self.pid: int = 0 # tag 3
|
||
self.skuId: int = 0 # tag 4
|
||
self.itemCount: int = 1 # tag 5
|
||
self.remark: str = "" # tag 6
|
||
self.spuId: str = "" # tag 7
|
||
self.gameId: str = "" # tag 8
|
||
self.orderId: int = 0 # tag 9
|
||
self.src: int = 0 # tag 10
|
||
self.couponUserIds: list[int] = [] # tag 11 Vector<INT64>
|
||
self.orderType: int = 0 # tag 12
|
||
self.extraParam: CreateOrderExtraParam | None = None # tag 13
|
||
self.scene: int = 0 # tag 14
|
||
self.promotionItems: list = [] # tag 15 Vector<PromotionItem>
|
||
self.sourceId: str = "" # tag 16
|
||
self.env: dict[str, str] = {} # tag 17 Map<STRING,STRING>
|
||
self.orderScene: int = 0 # tag 18
|
||
self.watchWord: str = "" # tag 19
|
||
self.marketingChannel: str = "" # tag 20
|
||
self.promotionParam: CreateOrderPromotionParam | None = None # tag 21
|
||
self.externalTraceKey: str = "" # tag 22
|
||
self.kefuUid: int = 0 # tag 23
|
||
self.accountParam: CreateOrderAccountParam | None = None # tag 24
|
||
self.parentOrderId: int = 0 # tag 25
|
||
self.vendorAccountType: str = "" # tag 26
|
||
self.vendorAccountVal: str = "" # tag 27
|
||
self.vendorSubAccountVal: str = "" # tag 28
|
||
self.vendorSubAccountType: str = "" # tag 29
|
||
self.bizType: int = 0 # tag 30
|
||
self.gameCategoryId: int = 0 # tag 31
|
||
self.ext: str = "" # tag 32
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
# HAR 实证:createOrderV5 会写出完整字段,即使值为 0/空串/空 list
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.receiveId)
|
||
os.write_int64(3, self.pid)
|
||
os.write_int64(4, self.skuId)
|
||
os.write_int64(5, self.itemCount)
|
||
os.write_string(6, self.remark)
|
||
os.write_string(7, self.spuId)
|
||
os.write_string(8, self.gameId)
|
||
os.write_int64(9, self.orderId)
|
||
os.write_int32(10, self.src)
|
||
os.write_list(11, self.couponUserIds)
|
||
os.write_int32(12, self.orderType)
|
||
os.write_struct(13, self.extraParam or CreateOrderExtraParam())
|
||
os.write_int32(14, self.scene)
|
||
os.write_list(15, self.promotionItems)
|
||
os.write_string(16, self.sourceId)
|
||
os.write_map(17, self.env)
|
||
os.write_int32(18, self.orderScene)
|
||
os.write_string(19, self.watchWord)
|
||
os.write_string(20, self.marketingChannel)
|
||
os.write_struct(21, self.promotionParam or CreateOrderPromotionParam())
|
||
os.write_string(22, self.externalTraceKey)
|
||
os.write_int64(23, self.kefuUid)
|
||
os.write_struct(24, self.accountParam or CreateOrderAccountParam())
|
||
os.write_int64(25, self.parentOrderId)
|
||
os.write_string(26, self.vendorAccountType)
|
||
os.write_string(27, self.vendorAccountVal)
|
||
os.write_string(28, self.vendorSubAccountVal)
|
||
os.write_string(29, self.vendorSubAccountType)
|
||
os.write_int32(30, self.bizType)
|
||
os.write_int32(31, self.gameCategoryId)
|
||
os.write_string(32, self.ext)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class CreateOrderRsp(TafStruct):
|
||
"""下单响应 (shopMiddleUI.createOrderV5)"""
|
||
|
||
def __init__(self):
|
||
self.code: int = 0 # tag 0
|
||
self.message: str = "" # tag 1
|
||
self.orderId: int = 0 # tag 2 虎牙订单号
|
||
self.subOrderId: int = 0 # tag 4
|
||
self.orderStatus: int = 0 # tag 5
|
||
self.riskUrl: str = "" # tag 8 风控跳转URL(code!=200时可能有)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
self.orderId = ins.read_int64(2, default=self.orderId)
|
||
self.subOrderId = ins.read_int64(4, default=self.subOrderId)
|
||
self.orderStatus = ins.read_int32(5, default=self.orderStatus)
|
||
self.riskUrl = ins.read_string(8, default=self.riskUrl)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
|
||
# ============================================================
|
||
# 支付(payOrderSubmitV5)
|
||
# ============================================================
|
||
|
||
|
||
class PayOrderRes(TafStruct):
|
||
"""
|
||
发起支付响应 (shopMiddleUI.payOrderSubmitV5)
|
||
结构从 state_shop_ts.js 的 payOrderRes 推断,tag 顺序按出现顺序
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.code: int = 0 # tag 0
|
||
self.message: str = "" # tag 1
|
||
self.orderId: int = 0 # tag 2 虎牙订单号
|
||
self.appOrderId: str = "" # tag 3
|
||
self.payOrderId: str = "" # tag 4 支付宝 payOrderId
|
||
self.payUrl: str = "" # tag 5 支付宝下单URL(含sign)
|
||
self.amount: int = 0 # tag 6
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
self.code = ins.read_int32(0, default=self.code)
|
||
self.message = ins.read_string(1, default=self.message)
|
||
self.orderId = ins.read_int64(2, default=self.orderId)
|
||
self.appOrderId = ins.read_string(3, default=self.appOrderId)
|
||
self.payOrderId = ins.read_string(4, default=self.payOrderId)
|
||
self.payUrl = ins.read_string(5, default=self.payUrl)
|
||
self.amount = ins.read_int64(6, default=self.amount)
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
pass
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"code": self.code,
|
||
"message": self.message,
|
||
"order_id": self.orderId,
|
||
"app_order_id": self.appOrderId,
|
||
"pay_order_id": self.payOrderId,
|
||
"pay_url": self.payUrl,
|
||
"amount": self.amount,
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# 积分中心(m-shop.huya.com/points.html)新增 RPC
|
||
#
|
||
# 来源:2026-10-13「开通精英宝典 / 购买幸运币」WSS 抓包逐字段解出。
|
||
# 浏览器实际链路顺序:
|
||
# getGoodsInfoV5 → getSupplierInfoV5 → listPayChannelV5 → checkHyProtocolV5
|
||
# → acceptHyProtocolV5 → calcOrderPromotion → checkUserBuyAuth
|
||
# → createOrderV5 → payOrderSubmitV5 → orderDetailV5(轮询)
|
||
# 这些接口的响应层次较深且随商品变化,统一用 ShopRawRsp 暴露通用解码结果,
|
||
# 不凭空猜测未确认的字段名。
|
||
# ============================================================
|
||
|
||
|
||
class ShopRawRsp:
|
||
"""未逐字段建模的商城响应:暴露 code/message 与通用解码后的原始结构。"""
|
||
|
||
def __init__(self, code: int = 0, message: str = "", payload: dict | None = None):
|
||
self.code = code
|
||
self.message = message
|
||
self.payload = payload or {}
|
||
|
||
def value(self, *path):
|
||
"""按通用解码的 tag 路径取值,例如 value("tag2", "tag0", "tag1")。"""
|
||
node: Any = self.payload
|
||
for key in path:
|
||
if not isinstance(node, dict):
|
||
return None
|
||
node = node.get(key)
|
||
return node
|
||
|
||
@classmethod
|
||
def from_payload(cls, payload: dict | None) -> "ShopRawRsp":
|
||
"""从通用解码结果构造:code/message 支持扁平与嵌套(tag0.tag0/tag0.tag1)两种形态。"""
|
||
data = payload or {}
|
||
code = 0
|
||
message = ""
|
||
head = data.get("tag0")
|
||
if isinstance(head, int):
|
||
code = int(head)
|
||
if isinstance(data.get("tag1"), str):
|
||
message = data["tag1"]
|
||
elif isinstance(head, dict):
|
||
if isinstance(head.get("tag0"), int):
|
||
code = int(head["tag0"])
|
||
if isinstance(head.get("tag1"), str):
|
||
message = head["tag1"]
|
||
return cls(code=code, message=message, payload=data)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {"code": self.code, "message": self.message, "payload": self.payload}
|
||
|
||
|
||
class PlayMallAppInfo(TafStruct):
|
||
"""PlayMallUI 系接口的应用信息。
|
||
|
||
与 ShopAppInfo 字段不同(抓包实证):
|
||
tag0 sAppId, tag1 渠道号(1001), tag2 0, tag3 "", tag5 scene(7),
|
||
tag6 sourceId, tag7 ""
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.sAppId: str = "huya" # tag 0
|
||
self.channelId: int = 1001 # tag 1 抓包固定 1001
|
||
self.flag2: int = 0 # tag 2 抓包固定 0
|
||
self.blank3: str = "" # tag 3 抓包为空串
|
||
self.scene: int = 7 # tag 5 抓包固定 7
|
||
self.sourceId: str = "yellowcarlist" # tag 6
|
||
self.blank7: str = "" # tag 7 抓包为空串
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_string(0, self.sAppId)
|
||
os.write_int32(1, self.channelId)
|
||
os.write_int32(2, self.flag2)
|
||
os.write_string(3, self.blank3)
|
||
os.write_int32(5, self.scene)
|
||
os.write_string(6, self.sourceId)
|
||
os.write_string(7, self.blank7)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class GetSupplierInfoReqV5(ShopAppRequest):
|
||
"""shopMiddleUI.getSupplierInfoV5 请求。"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.supplierId: int = 0 # tag 2
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.supplierId)
|
||
|
||
|
||
class AcceptHyProtocolReq(ShopAppRequest):
|
||
"""shopMiddleUI.acceptHyProtocolV5 请求(同意虎牙商城协议)。"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.protocolVersion: int = 1 # tag 2 抓包固定 1
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int32(2, self.protocolVersion)
|
||
|
||
|
||
class CalcOrderPromotionExtra(TafStruct):
|
||
"""calcOrderPromotion tag12 子结构(抓包全零/空)。"""
|
||
|
||
def __init__(self):
|
||
self.flag0: int = 0 # tag 0
|
||
self.flag1: int = 0 # tag 1
|
||
self.flag2: int = 0 # tag 2
|
||
self.ids: list = [] # tag 3
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_int32(0, self.flag0)
|
||
os.write_int32(1, self.flag1)
|
||
os.write_int32(2, self.flag2)
|
||
os.write_list(3, self.ids)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class CalcOrderPromotionReqV5(ShopAppRequest):
|
||
"""shopMiddleUI.calcOrderPromotion 请求。
|
||
|
||
tag7/tag8/tag9/tag10 在抓包中恒为 1/1/1/0,语义未确认,按抓包原值写出。
|
||
tag2 与 tag14 都是 spuId 去掉 ``hy-`` 前缀后的数字。
|
||
"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.spuNum: int = 0 # tag 2
|
||
self.skuId: int = 0 # tag 3
|
||
self.itemCount: int = 1 # tag 4
|
||
self.pid: int = 0 # tag 5
|
||
self.couponUserIds: list = [] # tag 6
|
||
self.flag7: int = 1 # tag 7 抓包固定 1
|
||
self.flag8: int = 1 # tag 8 抓包固定 1
|
||
self.flag9: int = 1 # tag 9 抓包固定 1
|
||
self.flag10: int = 0 # tag 10 抓包固定 0
|
||
self.extra: CalcOrderPromotionExtra | None = None # tag 12
|
||
self.flag13: int = 0 # tag 13
|
||
self.spuNum2: int = 0 # tag 14 与 tag2 同值
|
||
self.gameId: int = 0 # tag 15 游戏 ID(和平精英 9815)
|
||
self.list16: list = [] # tag 16
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.shopAppInfo)
|
||
os.write_int64(2, self.spuNum)
|
||
os.write_int64(3, self.skuId)
|
||
os.write_int32(4, self.itemCount)
|
||
os.write_int64(5, self.pid)
|
||
os.write_list(6, self.couponUserIds)
|
||
os.write_int32(7, self.flag7)
|
||
os.write_int32(8, self.flag8)
|
||
os.write_int32(9, self.flag9)
|
||
os.write_int32(10, self.flag10)
|
||
os.write_struct(12, self.extra or CalcOrderPromotionExtra())
|
||
os.write_int32(13, self.flag13)
|
||
os.write_int64(14, self.spuNum2)
|
||
os.write_int64(15, self.gameId)
|
||
os.write_list(16, self.list16)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class PlayMallQueryAllocationReq(TafStruct):
|
||
"""PlayMallUI.queryAllocation 请求(积分中心页面配额查询)。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.appInfo = PlayMallAppInfo() # tag 1
|
||
self.pageCode: str = "" # tag 2
|
||
self.blank3: str = "" # tag 3
|
||
self.flag4: int = 0 # tag 4
|
||
self.flag5: int = 1 # tag 5 抓包固定 1
|
||
self.flag6: int = 24 # tag 6 抓包固定 24
|
||
self.blank7: str = "" # tag 7
|
||
self.data8: bytes = b"" # tag 8 空字节数组
|
||
self.flag9: int = 0 # tag 9
|
||
self.flag10: int = 0 # tag 10
|
||
self.flags11: list[int] = [6, 19] # tag 11 抓包固定 [6, 19]
|
||
self.flag12: int = 0 # tag 12
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.appInfo)
|
||
os.write_string(2, self.pageCode)
|
||
os.write_string(3, self.blank3)
|
||
os.write_int32(4, self.flag4)
|
||
os.write_int32(5, self.flag5)
|
||
os.write_int32(6, self.flag6)
|
||
os.write_string(7, self.blank7)
|
||
os.write_bytes(8, self.data8)
|
||
os.write_int32(9, self.flag9)
|
||
os.write_int32(10, self.flag10)
|
||
os.write_list(11, self.flags11)
|
||
os.write_int32(12, self.flag12)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class PromotionGoodsKey(TafStruct):
|
||
"""getMyPromotion tag6 子结构(供应商 + 商品 + 价格)。"""
|
||
|
||
def __init__(self):
|
||
self.supplierId: int = 0 # tag 0
|
||
self.spuNum: int = 0 # tag 1
|
||
self.skuId: int = 0 # tag 2
|
||
self.price: int = 0 # tag 3
|
||
self.flag4: int = 1 # tag 4 抓包固定 1
|
||
self.list5: list = [] # tag 5
|
||
self.gameId: int = 0 # tag 6
|
||
self.spuNum2: int = 0 # tag 7 与 tag1 同值
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_int64(0, self.supplierId)
|
||
os.write_int64(1, self.spuNum)
|
||
os.write_int64(2, self.skuId)
|
||
os.write_int64(3, self.price)
|
||
os.write_int32(4, self.flag4)
|
||
os.write_list(5, self.list5)
|
||
os.write_int64(6, self.gameId)
|
||
os.write_int64(7, self.spuNum2)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class PlayMallGetMyPromotionReq(TafStruct):
|
||
"""PlayMallUI.getMyPromotion 请求(查询用户可用返利/促销)。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.appInfo = PlayMallAppInfo() # tag 1
|
||
self.flag2: int = 1 # tag 2 抓包固定 1
|
||
self.flag3: int = 1 # tag 3 抓包固定 1
|
||
self.flag4: int = 10 # tag 4 抓包固定 10
|
||
self.flag5: int = 0 # tag 5
|
||
self.goods: PromotionGoodsKey | None = None # tag 6
|
||
self.list7: list = [] # tag 7
|
||
self.scene: str = "shop" # tag 8 抓包固定 "shop"
|
||
self.blank9: str = "" # tag 9
|
||
self.list10: list = [] # tag 10
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_struct(1, self.appInfo)
|
||
os.write_int32(2, self.flag2)
|
||
os.write_int32(3, self.flag3)
|
||
os.write_int32(4, self.flag4)
|
||
os.write_int32(5, self.flag5)
|
||
os.write_struct(6, self.goods or PromotionGoodsKey())
|
||
os.write_list(7, self.list7)
|
||
os.write_string(8, self.scene)
|
||
os.write_string(9, self.blank9)
|
||
os.write_list(10, self.list10)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class GetUserProfileReq(TafStruct):
|
||
"""huyauserui.getUserProfile 请求(tag1 为要查询的 uid)。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.targetUid: int = 0 # tag 1
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_int64(1, self.targetUid)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
class FollowWeChatStatusReq(TafStruct):
|
||
"""wupui.getUserFollowWeChatOfficialStatus 请求(tag1 抓包固定 1)。"""
|
||
|
||
def __init__(self):
|
||
self.userId = UserId() # tag 0
|
||
self.flag1: int = 1 # tag 1 抓包固定 1
|
||
|
||
def write_to(self, os: TafOutputStream):
|
||
os.write_struct(0, self.userId)
|
||
os.write_int32(1, self.flag1)
|
||
|
||
def read_from(self, ins: TafInputStream):
|
||
pass
|
||
|
||
|
||
def shop_spu_number(spu_id: str) -> int:
|
||
"""把商品 ID(``hy-8328015``)转成接口需要的数字形式(``8328015``)。"""
|
||
text = str(spu_id or "").strip()
|
||
if "-" in text:
|
||
text = text.rsplit("-", 1)[-1]
|
||
return int(text) if text.isdigit() else 0
|