From eb065a7c7794db5f87e1be118b59926ae8ec7887 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 1 Sep 2026 11:42:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E8=99=8E=E7=89=99=E7=B2=BE?= =?UTF-8?q?=E8=8B=B1=E5=AE=9D=E5=85=B8=E6=9C=80=E6=96=B0=E5=95=86=E5=9F=8E?= =?UTF-8?q?=E5=89=8D=E7=BD=AE=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/huya/http_client.py | 46 ++++++++ core/huya/shop_structs.py | 104 +++++++++++++++++++ core/huya/wss_client.py | 40 +++++++ docs/HUYA_精英宝典-后端与9.1抓包对照.md | 3 +- tests/test_huya_elite_protocol.py | 23 +++- web/backend/services/huya_runner_recharge.py | 29 ++++++ 6 files changed, 243 insertions(+), 2 deletions(-) diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 4168c18..69cce38 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -851,6 +851,52 @@ class HuyaHttpClient: "shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, uid, guid, cookie ) + def list_pay_channels( + self, uid: int, guid: str, cookie: str, spu_id: str, sku_id: int, + supplier_id: int = 87401, timeout: float = 15.0, + ): + from .shop_structs import ListPayChannelReq, ListPayChannelRsp + + req = ListPayChannelReq() + req.userId = self._build_shop_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", 4) + req.supplierId = int(supplier_id or 0) + req.spuId = spu_id + req.skuId = int(sku_id or 0) + return self.call_rpc( + "shopMiddleUI", "listPayChannelV5", req, ListPayChannelRsp, + uid, guid, cookie, timeout=timeout, + ) + + def check_hy_protocol( + self, uid: int, guid: str, cookie: str, timeout: float = 15.0 + ): + from .shop_structs import CheckHyProtocolReq, ShopCodeRsp + + req = CheckHyProtocolReq() + req.userId = self._build_shop_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", 4) + return self.call_rpc( + "shopMiddleUI", "checkHyProtocolV5", req, ShopCodeRsp, + uid, guid, cookie, timeout=timeout, + ) + + def check_user_buy_auth( + self, uid: int, guid: str, cookie: str, spu_id: str, sku_id: int, + timeout: float = 15.0, + ): + from .shop_structs import CheckUserBuyAuthReq, ShopCodeRsp + + req = CheckUserBuyAuthReq() + req.userId = self._build_shop_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", 4) + req.spuId = spu_id + req.skuId = int(sku_id or 0) + return self.call_rpc( + "shopMiddleUI", "checkUserBuyAuth", req, ShopCodeRsp, + uid, guid, cookie, timeout=timeout, + ) + def query_user_order_list( self, uid, diff --git a/core/huya/shop_structs.py b/core/huya/shop_structs.py index ef3d051..4d47c51 100644 --- a/core/huya/shop_structs.py +++ b/core/huya/shop_structs.py @@ -639,6 +639,110 @@ class QueryUserOrderListRsp(TafStruct): } +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 中的订单摘要;保留支付轮询所需字段。""" diff --git a/core/huya/wss_client.py b/core/huya/wss_client.py index ea67522..646974b 100644 --- a/core/huya/wss_client.py +++ b/core/huya/wss_client.py @@ -656,6 +656,46 @@ class HuyaWssClient: "shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, timeout=15.0 ) + async def list_pay_channels( + self, uid: int, guid: str, cookie: str, spu_id: str, sku_id: int = 0, + supplier_id: int = 87401, + ): + from .shop_structs import ListPayChannelReq, ListPayChannelRsp + + req = ListPayChannelReq() + req.userId = self._build_biz_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", WSS_SHOP_SCENE) + req.supplierId = int(supplier_id or 0) + req.spuId = spu_id + req.skuId = int(sku_id or 0) + return await self.call_rpc( + "shopMiddleUI", "listPayChannelV5", req, ListPayChannelRsp, timeout=15.0 + ) + + async def check_hy_protocol(self, uid: int, guid: str, cookie: str): + from .shop_structs import CheckHyProtocolReq, ShopCodeRsp + + req = CheckHyProtocolReq() + req.userId = self._build_biz_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", WSS_SHOP_SCENE) + return await self.call_rpc( + "shopMiddleUI", "checkHyProtocolV5", req, ShopCodeRsp, timeout=15.0 + ) + + async def check_user_buy_auth( + self, uid: int, guid: str, cookie: str, spu_id: str, sku_id: int, + ): + from .shop_structs import CheckUserBuyAuthReq, ShopCodeRsp + + req = CheckUserBuyAuthReq() + req.userId = self._build_biz_user(uid, cookie) + req.shopAppInfo = self._build_shop_app("yellowcarlist", WSS_SHOP_SCENE) + req.spuId = spu_id + req.skuId = int(sku_id or 0) + return await self.call_rpc( + "shopMiddleUI", "checkUserBuyAuth", req, ShopCodeRsp, timeout=15.0 + ) + async def query_user_order_list( self, uid: int, diff --git a/docs/HUYA_精英宝典-后端与9.1抓包对照.md b/docs/HUYA_精英宝典-后端与9.1抓包对照.md index 26fad35..9a30516 100644 --- a/docs/HUYA_精英宝典-后端与9.1抓包对照.md +++ b/docs/HUYA_精英宝典-后端与9.1抓包对照.md @@ -117,7 +117,8 @@ UID、Cookie、guid、baseinfo、WUP requestId、支付宝 payOrderId、ctoken - 兑换提交前读取实时详情和积分,校验可兑换状态、库存、时间窗口和余额;成功后回查积分和兑换记录。 - 增加 `orderDetailV5` 请求/响应结构,支付监听优先按订单号查询,旧订单列表作为 fallback。 - 支付超时改为终态 `timeout`,不再误报成功;支付完成后回查活动积分。 +- 支付前增加 `checkHyProtocolV5`、`listPayChannelV5` 和 `checkUserBuyAuth`,不再直接跳过商城前置校验。 - 商城 HTTP fallback 的 `UserId` 改为 9.1 商城 UA/空 guid 形态;商品详情 `gameId` 改为空字符串,下单仍使用 `gameId="0"`。 - 外部活动默认值由旧值更新为 `17096`,已有旧配置会在读取时迁移。 -仍待下一轮实施:将批处理执行器从 HTTP fallback 迁移到统一活动/商城 WSS session;补齐 `listPayChannelV5`、`checkHyProtocolV5`、`getMyPromotion`、`calcOrderPromotion` 和 `checkUserBuyAuth` 的显式前置调用;按账号隔离商品快照。 +仍待下一轮实施:将批处理执行器从 HTTP fallback 迁移到统一活动/商城 WSS session;补齐 `getMyPromotion`、`calcOrderPromotion` 的显式调用;按账号隔离商品快照。 diff --git a/tests/test_huya_elite_protocol.py b/tests/test_huya_elite_protocol.py index ea1dd93..a36dc8c 100644 --- a/tests/test_huya_elite_protocol.py +++ b/tests/test_huya_elite_protocol.py @@ -1,6 +1,12 @@ from core.huya.activity_structs import ActPrizeDetailItem, GetActPrizeDetailResp from core.huya.http_client import SHOP_BIZ_UA, HuyaHttpClient -from core.huya.shop_structs import OrderDetailData, OrderDetailOrder, OrderDetailRsp +from core.huya.shop_structs import ( + ListPayChannelRsp, + OrderDetailData, + OrderDetailOrder, + OrderDetailRsp, + ShopCodeRsp, +) from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType @@ -66,3 +72,18 @@ def test_order_detail_round_trip_exposes_payment_status(): assert decoded.order.orderId == 10938643 assert decoded.order.orderStatus == 50 assert decoded.order.payTime > 0 + + +def test_shop_preflight_response_contracts(): + channels = ListPayChannelRsp() + channels.channels = ["Zfb", "Weixin"] + channels.code = 200 + decoded_channels = _round_trip(channels) + assert decoded_channels.channels == ["Zfb", "Weixin"] + assert decoded_channels.code == 200 + + status = ShopCodeRsp() + status.code = 200 + status.message = "" + decoded_status = _round_trip(status) + assert decoded_status.code == 200 diff --git a/web/backend/services/huya_runner_recharge.py b/web/backend/services/huya_runner_recharge.py index a55be60..657acaf 100644 --- a/web/backend/services/huya_runner_recharge.py +++ b/web/backend/services/huya_runner_recharge.py @@ -434,6 +434,35 @@ class RechargeMixin: ) return + protocol = client.check_hy_protocol(uid=uid, guid="", cookie=cookie) + if protocol is None or protocol.code != 200: + self._mark_task( + worker_db, task, "failed", + getattr(protocol, "message", "商城协议校验失败") or "商城协议校验失败", + {"goods": detail, "protocol": protocol.to_dict() if protocol else None}, + ) + return + 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: + self._mark_task( + worker_db, task, "failed", "当前支付渠道不可用", + {"goods": detail, "channels": channels.to_dict() if channels else None}, + ) + return + 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: + self._mark_task( + worker_db, task, "failed", + getattr(auth, "message", "购买权限校验失败") or "购买权限校验失败", + {"goods": detail, "auth": auth.to_dict() if auth else None}, + ) + return + order_resp = client.create_order( uid=uid, guid="",