type: 收窄豆鱼服务层类型
This commit is contained in:
+275
-103
@@ -11,7 +11,12 @@ from urllib.parse import parse_qs, unquote, urlsplit, urlunsplit
|
||||
|
||||
import requests
|
||||
|
||||
from .cookie_utils import cookie_pairs, cookie_value, normalize_cookie_pairs, normalize_douyu_cookie
|
||||
from .cookie_utils import (
|
||||
cookie_pairs,
|
||||
cookie_value,
|
||||
normalize_cookie_pairs,
|
||||
normalize_douyu_cookie,
|
||||
)
|
||||
|
||||
|
||||
PC_UA = (
|
||||
@@ -37,9 +42,15 @@ class DouyuActivityClient:
|
||||
BIND_INFO_V2_API = "https://www.douyu.com/japi/carnivalApi/v2/tencent/bindInfo"
|
||||
BIND_API = "https://www.douyu.com/japi/carnivalApi/tencent/bind"
|
||||
GOODS_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/storehome"
|
||||
CREATE_EXCHANGE_ORDER_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/createExchangeOrder"
|
||||
PAY_EXCHANGE_ORDER_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/payExchangeOrder"
|
||||
EXCHANGE_ORDER_LIST_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/getExchangeOrderList"
|
||||
CREATE_EXCHANGE_ORDER_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/createExchangeOrder"
|
||||
)
|
||||
PAY_EXCHANGE_ORDER_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/payExchangeOrder"
|
||||
)
|
||||
EXCHANGE_ORDER_LIST_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/getExchangeOrderList"
|
||||
)
|
||||
EXCHANGE_LIST_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchangeList"
|
||||
# 2026-08-30 页面新增链路(对齐浏览器):
|
||||
# 预兑 user/queryExchange -> user/confirmExchange;取消预兑 user/queryCancelExchange -> user/cancelExchange;
|
||||
@@ -47,29 +58,63 @@ class DouyuActivityClient:
|
||||
ELITE_USER_INFO_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/getUserInfo"
|
||||
GOODS_DETAIL_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/storedetail"
|
||||
EXCHANGE_GIFT_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchange"
|
||||
PRE_EXCHANGE_CHECK_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/queryExchange"
|
||||
PRE_EXCHANGE_CONFIRM_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/confirmExchange"
|
||||
PRE_EXCHANGE_CANCEL_CHECK_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/queryCancelExchange"
|
||||
PRE_EXCHANGE_CANCEL_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/cancelExchange"
|
||||
COMMODITY_SUBSCRIBE_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/sub"
|
||||
COMMODITY_UNSUBSCRIBE_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/user/cancelSub"
|
||||
BATCH_EXCHANGE_LIMIT_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/batchExchangeLimit"
|
||||
AUTO_CONVERT_QUERY_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/activity/automaticallyConvert"
|
||||
PRE_EXCHANGE_CHECK_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/queryExchange"
|
||||
)
|
||||
PRE_EXCHANGE_CONFIRM_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/confirmExchange"
|
||||
)
|
||||
PRE_EXCHANGE_CANCEL_CHECK_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/queryCancelExchange"
|
||||
)
|
||||
PRE_EXCHANGE_CANCEL_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/cancelExchange"
|
||||
)
|
||||
COMMODITY_SUBSCRIBE_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/sub"
|
||||
)
|
||||
COMMODITY_UNSUBSCRIBE_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/user/cancelSub"
|
||||
)
|
||||
BATCH_EXCHANGE_LIMIT_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/batchExchangeLimit"
|
||||
)
|
||||
AUTO_CONVERT_QUERY_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/activity/automaticallyConvert"
|
||||
)
|
||||
AUTO_CONVERT_SAVE_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/activity/automaticallyConvertSave"
|
||||
EXCHANGE_HOME_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/activity/exchangeHome"
|
||||
EXCHANGE_HOME_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peace/activity/exchangeHome"
|
||||
)
|
||||
CREDIT_BALANCE_API = "https://www.douyu.com/japi/oms/web/peace/credit/balance"
|
||||
CREDIT_DIFF_API = "https://www.douyu.com/japi/oms/web/peace/credit/diff"
|
||||
PEACE_ITEM_API = "https://www.douyu.com/japi/oms/web/peace/item"
|
||||
PEACE_TIME_API = "https://www.douyu.com/japi/oms/web/peace/time"
|
||||
PAY_QR_API = "https://www.douyu.com/japi/oms/web/pay/getQrCode"
|
||||
ESPORTS_BIND_INFO_API = "https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/bindInfo"
|
||||
ESPORTS_BIND_ROLE_PARAM_API = "https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/bindRoleParam"
|
||||
ESPORTS_CSRF_API = "https://www.douyu.com/wgapi/livenc/liveweb/csrfApi/getCsrfCookie"
|
||||
ESPORTS_AGREEMENT_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/agreement"
|
||||
ESPORTS_ACT_BIND_API = "https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/actBind"
|
||||
ESPORTS_USER_INFO_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/getUserInfo"
|
||||
ESPORTS_GOODS_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/storehome"
|
||||
ESPORTS_EXCHANGE_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/exchange"
|
||||
ESPORTS_BIND_INFO_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/bindInfo"
|
||||
)
|
||||
ESPORTS_BIND_ROLE_PARAM_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/bindRoleParam"
|
||||
)
|
||||
ESPORTS_CSRF_API = (
|
||||
"https://www.douyu.com/wgapi/livenc/liveweb/csrfApi/getCsrfCookie"
|
||||
)
|
||||
ESPORTS_AGREEMENT_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peacematch/agreement"
|
||||
)
|
||||
ESPORTS_ACT_BIND_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/actBind"
|
||||
)
|
||||
ESPORTS_USER_INFO_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peacematch/getUserInfo"
|
||||
)
|
||||
ESPORTS_GOODS_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peacematch/storehome"
|
||||
)
|
||||
ESPORTS_EXCHANGE_API = (
|
||||
"https://www.douyu.com/wgapi/ordnc/activity/peacematch/exchange"
|
||||
)
|
||||
GOLD_QR_API = "https://cz.douyu.com/m/gold/getQrCode"
|
||||
GOLD_ACCOUNT_API = "https://cz.douyu.com/item/gold/account"
|
||||
EXCHANGE_BALANCE_API = "https://www.douyu.com/wjapi/nc/exchange/fim"
|
||||
@@ -91,11 +136,13 @@ class DouyuActivityClient:
|
||||
if proxies:
|
||||
self.session.proxies.update(proxies)
|
||||
self.session.trust_env = False
|
||||
self.session.headers.update({
|
||||
"User-Agent": PC_UA,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
})
|
||||
self.session.headers.update(
|
||||
{
|
||||
"User-Agent": PC_UA,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
}
|
||||
)
|
||||
self._load_cookie(self.cookie)
|
||||
|
||||
@staticmethod
|
||||
@@ -114,18 +161,22 @@ class DouyuActivityClient:
|
||||
def _merge_response_cookie(self, response: requests.Response) -> None:
|
||||
if not response.cookies:
|
||||
return
|
||||
self.cookie = normalize_cookie_pairs([
|
||||
*cookie_pairs(self.cookie),
|
||||
*[(key, value) for key, value in response.cookies.items()],
|
||||
])
|
||||
self.cookie = normalize_cookie_pairs(
|
||||
[
|
||||
*cookie_pairs(self.cookie),
|
||||
*[(key, value) for key, value in response.cookies.items()],
|
||||
]
|
||||
)
|
||||
self._load_cookie(self.cookie)
|
||||
|
||||
def _set_cookie_value(self, key: str, value: str) -> None:
|
||||
"""写入单个 Cookie 值,确保手动 Cookie 请求头与 Session 一致。"""
|
||||
self.cookie = normalize_cookie_pairs([
|
||||
*cookie_pairs(self.cookie),
|
||||
(key, value),
|
||||
])
|
||||
self.cookie = normalize_cookie_pairs(
|
||||
[
|
||||
*cookie_pairs(self.cookie),
|
||||
(key, value),
|
||||
]
|
||||
)
|
||||
self._load_cookie(self.cookie)
|
||||
|
||||
def _headers(self, referer: str = "https://www.douyu.com/") -> dict[str, str]:
|
||||
@@ -158,7 +209,9 @@ class DouyuActivityClient:
|
||||
f"location={safe_location} history={response.status_code}->{safe_location}"
|
||||
)
|
||||
self.logger(f"{source}: 已拦截重定向 | {trace}")
|
||||
return DouyuActivityError(f"{source} 请求被重定向,Cookie 已失效,请重新登录({trace})")
|
||||
return DouyuActivityError(
|
||||
f"{source} 请求被重定向,Cookie 已失效,请重新登录({trace})"
|
||||
)
|
||||
|
||||
def _request(
|
||||
self,
|
||||
@@ -203,7 +256,9 @@ class DouyuActivityClient:
|
||||
except json.JSONDecodeError as exc:
|
||||
raise DouyuActivityError(f"{source} 返回不是 JSON: {text[:200]}") from exc
|
||||
|
||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict[str, Any]:
|
||||
def _request_json(
|
||||
self, method: str, url: str, source: str, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
return self._json(self._request(method, url, source=source, **kwargs), source)
|
||||
|
||||
def csrf_token(self, *, force_refresh: bool = False) -> str:
|
||||
@@ -252,7 +307,9 @@ class DouyuActivityClient:
|
||||
"""精英手册商城页 Referer(对齐浏览器 create/pay/storedetail 等接口)。"""
|
||||
if rid:
|
||||
return f"https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId={rid}"
|
||||
return "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0"
|
||||
return (
|
||||
"https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0"
|
||||
)
|
||||
|
||||
def _post_csrf_form(
|
||||
self,
|
||||
@@ -363,7 +420,9 @@ class DouyuActivityClient:
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询绑定信息失败")
|
||||
parsed = self._parse_bind_info(payload, act_alias=act_alias, api_version="v2" if v2 else "v1")
|
||||
parsed = self._parse_bind_info(
|
||||
payload, act_alias=act_alias, api_version="v2" if v2 else "v1"
|
||||
)
|
||||
self._log_bind_info(parsed)
|
||||
return parsed
|
||||
|
||||
@@ -385,7 +444,9 @@ class DouyuActivityClient:
|
||||
f"can_change_time={parsed.get('can_change_time') if parsed.get('can_change_time') is not None else '-'}"
|
||||
)
|
||||
|
||||
def _parse_bind_info(self, payload: dict[str, Any], *, act_alias: str, api_version: str) -> dict[str, Any]:
|
||||
def _parse_bind_info(
|
||||
self, payload: dict[str, Any], *, act_alias: str, api_version: str
|
||||
) -> dict[str, Any]:
|
||||
"""统一解析不同活动的腾讯游戏绑定信息。"""
|
||||
data = payload.get("data") or {}
|
||||
game_account = data.get("gameAccount") or {}
|
||||
@@ -416,14 +477,18 @@ class DouyuActivityClient:
|
||||
"canChangeBind",
|
||||
)
|
||||
if can_change_role is None and isinstance(tx_act, dict):
|
||||
can_change_role = self._first_present(tx_act, "canChangeRole", "canChangeBind")
|
||||
can_change_role = self._first_present(
|
||||
tx_act, "canChangeRole", "canChangeBind"
|
||||
)
|
||||
can_change_time = self._first_present(
|
||||
data,
|
||||
"canChangeTime",
|
||||
"can_change_time",
|
||||
)
|
||||
if can_change_time is None and isinstance(tx_act, dict):
|
||||
can_change_time = self._first_present(tx_act, "canChangeTime", "can_change_time")
|
||||
can_change_time = self._first_present(
|
||||
tx_act, "canChangeTime", "can_change_time"
|
||||
)
|
||||
return {
|
||||
"act_alias": act_alias,
|
||||
"api_version": api_version,
|
||||
@@ -453,11 +518,16 @@ class DouyuActivityClient:
|
||||
self.ESPORTS_BIND_INFO_API,
|
||||
"查询电竞手册绑定信息",
|
||||
params={"actAlias": act_alias},
|
||||
headers={"Origin": "", "Referer": "https://www.douyu.com/pages/live-peace-pel/web/"},
|
||||
headers={
|
||||
"Origin": "",
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-pel/web/",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询电竞手册绑定信息失败")
|
||||
parsed = self._parse_bind_info(payload, act_alias=act_alias, api_version="esports")
|
||||
parsed = self._parse_bind_info(
|
||||
payload, act_alias=act_alias, api_version="esports"
|
||||
)
|
||||
self._log_bind_info(parsed)
|
||||
return parsed
|
||||
|
||||
@@ -468,7 +538,10 @@ class DouyuActivityClient:
|
||||
self.ESPORTS_BIND_ROLE_PARAM_API,
|
||||
"获取电竞手册绑定二维码",
|
||||
params={"actAlias": act_alias},
|
||||
headers={"Origin": "", "Referer": "https://www.douyu.com/pages/live-peace-pel/web/"},
|
||||
headers={
|
||||
"Origin": "",
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-pel/web/",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "获取电竞手册绑定二维码失败")
|
||||
@@ -503,7 +576,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(f"获取电竞手册 ctn 失败: {response.text[:200]}")
|
||||
return ctn
|
||||
|
||||
def confirm_esports_bind(self, act_alias: str, *, room_id: str = "9263298") -> dict[str, Any]:
|
||||
def confirm_esports_bind(
|
||||
self, act_alias: str, *, room_id: str = "9263298"
|
||||
) -> dict[str, Any]:
|
||||
"""自动完成协议前置请求后,通过 actBind 确认电竞手册绑定。"""
|
||||
referer = (
|
||||
"https://www.douyu.com/pages/live-peace-pel/web"
|
||||
@@ -555,7 +630,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "确认绑定失败")
|
||||
return {"csrf_token": token, "raw": payload}
|
||||
|
||||
def create_elite_qr(self, *, ctn: str, act_alias: str, amount: int, room_id: str) -> dict[str, Any]:
|
||||
def create_elite_qr(
|
||||
self, *, ctn: str, act_alias: str, amount: int, room_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""生成精英宝典支付二维码。"""
|
||||
# E 语言实现会在 peace/item 和 getQrCode 两次请求里显式拼上 acf_ccn=ctn。
|
||||
# 这里也同步 Cookie,避免接口只看 Cookie 时误判未绑定游戏账号。
|
||||
@@ -576,7 +653,11 @@ class DouyuActivityClient:
|
||||
"生成精英宝典支付码",
|
||||
json={
|
||||
"bizType": "peace",
|
||||
"biz": {"actAlias": act_alias, "amount": amount, "roomId": int(room_id or 0)},
|
||||
"biz": {
|
||||
"actAlias": act_alias,
|
||||
"amount": amount,
|
||||
"roomId": int(room_id or 0),
|
||||
},
|
||||
"ctn": ctn,
|
||||
},
|
||||
headers={"Content-Type": "application/json;charset=UTF-8"},
|
||||
@@ -584,9 +665,17 @@ class DouyuActivityClient:
|
||||
qr = (payload.get("data") or {}).get("qr") or ""
|
||||
if not qr:
|
||||
raise DouyuActivityError(payload.get("msg") or "生成精英宝典支付码失败")
|
||||
return {"pay_url": qr, "ctn": ctn, "act_alias": act_alias, "item": item_payload, "raw": payload}
|
||||
return {
|
||||
"pay_url": qr,
|
||||
"ctn": ctn,
|
||||
"act_alias": act_alias,
|
||||
"item": item_payload,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def create_esports_qr(self, *, ctn: str, act_alias: str, amount: int, room_id: str) -> dict[str, Any]:
|
||||
def create_esports_qr(
|
||||
self, *, ctn: str, act_alias: str, amount: int, room_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""生成电竞手册支付二维码。"""
|
||||
self._set_cookie_value("acf_ccn", ctn)
|
||||
time_payload = self._request_json(
|
||||
@@ -604,7 +693,11 @@ class DouyuActivityClient:
|
||||
"生成电竞手册支付码",
|
||||
json={
|
||||
"bizType": "peacePel",
|
||||
"biz": {"actAlias": act_alias, "amount": amount, "roomId": int(room_id or 0)},
|
||||
"biz": {
|
||||
"actAlias": act_alias,
|
||||
"amount": amount,
|
||||
"roomId": int(room_id or 0),
|
||||
},
|
||||
"ctn": ctn,
|
||||
},
|
||||
headers={
|
||||
@@ -633,7 +726,10 @@ class DouyuActivityClient:
|
||||
self.ESPORTS_USER_INFO_API,
|
||||
"查询电竞手册状态",
|
||||
params={"manualID": manual_id},
|
||||
headers={"Origin": "", "Referer": "https://www.douyu.com/pages/live-peace-pel/web/"},
|
||||
headers={
|
||||
"Origin": "",
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-pel/web/",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询电竞手册状态失败")
|
||||
@@ -712,7 +808,9 @@ class DouyuActivityClient:
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def create_gold_qr(self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1") -> dict[str, Any]:
|
||||
def create_gold_qr(
|
||||
self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1"
|
||||
) -> dict[str, Any]:
|
||||
"""生成鱼翅充值支付二维码。"""
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
@@ -846,12 +944,13 @@ class DouyuActivityClient:
|
||||
"""查询当前账号限兑商品。"""
|
||||
result = self.list_goods(manual_id=manual_id, rid=rid)
|
||||
limited = [
|
||||
item for item in result["goods"]
|
||||
if str(item.get("status") or "") == "1"
|
||||
item for item in result["goods"] if str(item.get("status") or "") == "1"
|
||||
]
|
||||
return {"limited_goods": limited, "raw": result["raw"]}
|
||||
|
||||
def exchange_records(self, *, manual_id: str, limit: int = 100, offset: int = 0) -> dict[str, Any]:
|
||||
def exchange_records(
|
||||
self, *, manual_id: str, limit: int = 100, offset: int = 0
|
||||
) -> dict[str, Any]:
|
||||
"""查询兑换记录。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
@@ -918,7 +1017,9 @@ class DouyuActivityClient:
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def pay_exchange_order(self, *, manual_id: str, order_id: str, rid: str = "") -> dict[str, Any]:
|
||||
def pay_exchange_order(
|
||||
self, *, manual_id: str, order_id: str, rid: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""Pay a previously locked exchange order with handbook points."""
|
||||
normalized_order_id = str(order_id).strip()
|
||||
if not normalized_order_id:
|
||||
@@ -946,7 +1047,9 @@ class DouyuActivityClient:
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def exchange_orders(self, *, manual_id: str, page: int = 1, page_size: int = 10) -> dict[str, Any]:
|
||||
def exchange_orders(
|
||||
self, *, manual_id: str, page: int = 1, page_size: int = 10
|
||||
) -> dict[str, Any]:
|
||||
"""Query current locked exchange orders."""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
@@ -967,7 +1070,9 @@ class DouyuActivityClient:
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def exchange_goods(self, *, manual_id: str, rid: str, commodity_id: str) -> dict[str, Any]:
|
||||
def exchange_goods(
|
||||
self, *, manual_id: str, rid: str, commodity_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""Lock an activity commodity and immediately pay the locked order."""
|
||||
locked = self.create_exchange_order(
|
||||
manual_id=manual_id,
|
||||
@@ -1011,7 +1116,9 @@ class DouyuActivityClient:
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def query_goods_detail(self, *, manual_id: str, commodity_id: str, rid: str = "") -> dict[str, Any]:
|
||||
def query_goods_detail(
|
||||
self, *, manual_id: str, commodity_id: str, rid: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""查询商品详情 storedetail(对齐浏览器 getGoodsDetail)。
|
||||
|
||||
详情里带有兑换状态机所需的全部标记:preExchange.isExists / subscribeType /
|
||||
@@ -1021,7 +1128,11 @@ class DouyuActivityClient:
|
||||
"get",
|
||||
self.GOODS_DETAIL_API,
|
||||
"查询商品详情",
|
||||
params={"manualID": manual_id, "commodityID": commodity_id, "rid": rid or "0"},
|
||||
params={
|
||||
"manualID": manual_id,
|
||||
"commodityID": commodity_id,
|
||||
"rid": rid or "0",
|
||||
},
|
||||
headers={"Origin": "", "Referer": self._peace_shop_referer(rid)},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
@@ -1103,7 +1214,9 @@ class DouyuActivityClient:
|
||||
plan.update(action="classic", text="立即兑换")
|
||||
return plan
|
||||
|
||||
def batch_exchange_limit(self, *, manual_id: str, commodity_id: str, rid: str = "") -> dict[str, Any]:
|
||||
def batch_exchange_limit(
|
||||
self, *, manual_id: str, commodity_id: str, rid: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""查询批量兑换上限(对齐页面 batchExchangeLimit,仅 batchExchange=1 商品)。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.BATCH_EXCHANGE_LIMIT_API,
|
||||
@@ -1116,7 +1229,9 @@ class DouyuActivityClient:
|
||||
data = payload.get("data") or {}
|
||||
return {"limit": data.get("limit"), "raw": payload}
|
||||
|
||||
def pre_exchange_check(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
def pre_exchange_check(
|
||||
self, *, manual_id: str, commodity_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""查询预兑状态(对齐页面 user/queryExchange,预兑弹窗前必查)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
@@ -1129,7 +1244,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "查询预兑状态失败")
|
||||
return {"data": payload.get("data") or {}, "raw": payload}
|
||||
|
||||
def pre_exchange_cancel_check(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
def pre_exchange_cancel_check(
|
||||
self, *, manual_id: str, commodity_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""查询取消预兑信息(对齐页面 user/queryCancelExchange)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
@@ -1142,7 +1259,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "查询取消预兑信息失败")
|
||||
return {"data": payload.get("data") or {}, "raw": payload}
|
||||
|
||||
def confirm_pre_exchange(self, *, manual_id: str, commodity_id: str, rid: str = "") -> dict[str, Any]:
|
||||
def confirm_pre_exchange(
|
||||
self, *, manual_id: str, commodity_id: str, rid: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""确认预兑(对齐页面 user/confirmExchange,成功提示"预兑成功")。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.PRE_EXCHANGE_CONFIRM_API,
|
||||
@@ -1154,7 +1273,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "确认预兑失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def cancel_pre_exchange(self, *, manual_id: str, commodity_id: str, rid: str = "") -> dict[str, Any]:
|
||||
def cancel_pre_exchange(
|
||||
self, *, manual_id: str, commodity_id: str, rid: str = ""
|
||||
) -> dict[str, Any]:
|
||||
"""取消预兑(对齐页面 user/cancelExchange)。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.PRE_EXCHANGE_CANCEL_API,
|
||||
@@ -1166,7 +1287,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "取消预兑失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def subscribe_commodity(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
def subscribe_commodity(
|
||||
self, *, manual_id: str, commodity_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""预约到货(对齐页面 user/sub,成功提示"预约成功")。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.COMMODITY_SUBSCRIBE_API,
|
||||
@@ -1178,7 +1301,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "预约到货失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def unsubscribe_commodity(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
def unsubscribe_commodity(
|
||||
self, *, manual_id: str, commodity_id: str
|
||||
) -> dict[str, Any]:
|
||||
"""取消预约到货(对齐页面 user/cancelSub)。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.COMMODITY_UNSUBSCRIBE_API,
|
||||
@@ -1190,7 +1315,9 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError(payload.get("msg") or "取消预约到货失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def exchange_gift(self, *, manual_id: str, commodity_id: str, rid: str = "", num: int = 1) -> dict[str, Any]:
|
||||
def exchange_gift(
|
||||
self, *, manual_id: str, commodity_id: str, rid: str = "", num: int = 1
|
||||
) -> dict[str, Any]:
|
||||
"""单步兑换(对齐页面 postExchangeGift -> /ordnc/activity/peace/exchange)。
|
||||
|
||||
2026-08-30 bundle 中新链路,页面主要用于兑换中心/自动兑换场景;字段按页面
|
||||
@@ -1263,7 +1390,10 @@ class DouyuActivityClient:
|
||||
"get",
|
||||
self.GOLD_ACCOUNT_API,
|
||||
"查询鱼翅余额",
|
||||
headers={"Origin": "https://cz.douyu.com", "Referer": "https://cz.douyu.com/"},
|
||||
headers={
|
||||
"Origin": "https://cz.douyu.com",
|
||||
"Referer": "https://cz.douyu.com/",
|
||||
},
|
||||
)
|
||||
data = payload.get("data") or {}
|
||||
return {"gold": data.get("gold"), "raw": payload}
|
||||
@@ -1315,7 +1445,7 @@ class DouyuActivityClient:
|
||||
match = re.search(rf"var\s+{varname}\s*=", text)
|
||||
if not match:
|
||||
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
|
||||
chunk = text[match.end():].strip()
|
||||
chunk = text[match.end() :].strip()
|
||||
if chunk.endswith(";"):
|
||||
chunk = chunk[:-1]
|
||||
try:
|
||||
@@ -1342,7 +1472,9 @@ class DouyuActivityClient:
|
||||
stk = cookie_value(self.cookie, "acf_stk")
|
||||
ltkid = cookie_value(self.cookie, "acf_ltkid")
|
||||
if not uid or not stk or not ltkid:
|
||||
raise DouyuActivityError("Cookie 缺少 acf_uid/acf_stk/acf_ltkid,无法获取小店参数")
|
||||
raise DouyuActivityError(
|
||||
"Cookie 缺少 acf_uid/acf_stk/acf_ltkid,无法获取小店参数"
|
||||
)
|
||||
return f"{uid}_1_{stk}_0_{ltkid}"
|
||||
|
||||
def xpd_embed_query(self, *, act_alias: str, rid: str) -> dict[str, Any]:
|
||||
@@ -1473,9 +1605,15 @@ class DouyuActivityClient:
|
||||
)
|
||||
recommend = self._xpd_parse_var(response.text, "recommend")
|
||||
if str(recommend.get("errcode") or "0") != "0":
|
||||
raise DouyuActivityError(str(recommend.get("errdesc") or "小店商品查询失败"))
|
||||
raise DouyuActivityError(
|
||||
str(recommend.get("errdesc") or "小店商品查询失败")
|
||||
)
|
||||
pages.append(recommend)
|
||||
items = (((recommend.get("data") or {}).get("client_data") or {}).get("itemsdetail")) or []
|
||||
items = (
|
||||
((recommend.get("data") or {}).get("client_data") or {}).get(
|
||||
"itemsdetail"
|
||||
)
|
||||
) or []
|
||||
if not isinstance(items, list):
|
||||
raise DouyuActivityError("小店商品响应格式异常")
|
||||
|
||||
@@ -1554,8 +1692,12 @@ class DouyuActivityClient:
|
||||
info = self._xpd_parse_var(response.text, "info")
|
||||
if str(info.get("ret") or "0") != "0":
|
||||
raise DouyuActivityError(str(info.get("msg") or "小店购买记录查询失败"))
|
||||
data = info.get("data") if isinstance(info.get("data"), dict) else {}
|
||||
page_records = data.get("list") if isinstance(data.get("list"), list) else []
|
||||
data: dict[str, Any] = (
|
||||
info.get("data") if isinstance(info.get("data"), dict) else {}
|
||||
)
|
||||
page_records: list[Any] = (
|
||||
data.get("list") if isinstance(data.get("list"), list) else []
|
||||
)
|
||||
total = self._xpd_int(data.get("cnt")) or total
|
||||
pages.append(info)
|
||||
|
||||
@@ -1568,10 +1710,14 @@ class DouyuActivityClient:
|
||||
goods_info = json.loads(goods_info)
|
||||
except json.JSONDecodeError:
|
||||
goods_info = {}
|
||||
goods_list = goods_info.get("list") if isinstance(goods_info, dict) else []
|
||||
goods_list = (
|
||||
goods_info.get("list") if isinstance(goods_info, dict) else []
|
||||
)
|
||||
goods = (
|
||||
goods_list[0]
|
||||
if isinstance(goods_list, list) and goods_list and isinstance(goods_list[0], dict)
|
||||
if isinstance(goods_list, list)
|
||||
and goods_list
|
||||
and isinstance(goods_list[0], dict)
|
||||
else {}
|
||||
)
|
||||
app_ext = record.get("sAppExt")
|
||||
@@ -1580,31 +1726,41 @@ class DouyuActivityClient:
|
||||
app_ext = json.loads(app_ext)
|
||||
except json.JSONDecodeError:
|
||||
app_ext = {}
|
||||
records.append({
|
||||
"serial": str(record.get("sSerialNum") or ""),
|
||||
"goods_name": str(
|
||||
goods.get("sGoodsName")
|
||||
or goods.get("sPacketName")
|
||||
or record.get("sActionDesc")
|
||||
or ""
|
||||
),
|
||||
"goods_image": str(goods.get("sGoodsPic") or (app_ext or {}).get("sGoodsPic") or ""),
|
||||
"buy_time": str(record.get("dtBuyTime") or ""),
|
||||
"pay_time": str(record.get("dtPayTime") or ""),
|
||||
"status": str(record.get("iStatus") or ""),
|
||||
"price": self._xpd_int(record.get("iPrice")),
|
||||
"pay_amount": self._xpd_int(record.get("iPayAmount")),
|
||||
"pay_type": str(record.get("sPayType") or ""),
|
||||
"role_name": str(record.get("sRoleName") or ""),
|
||||
"send_status": str(goods.get("iSendStatus") or ""),
|
||||
"delivery_message": str(goods.get("deliveryMsg") or ""),
|
||||
"raw": record,
|
||||
})
|
||||
records.append(
|
||||
{
|
||||
"serial": str(record.get("sSerialNum") or ""),
|
||||
"goods_name": str(
|
||||
goods.get("sGoodsName")
|
||||
or goods.get("sPacketName")
|
||||
or record.get("sActionDesc")
|
||||
or ""
|
||||
),
|
||||
"goods_image": str(
|
||||
goods.get("sGoodsPic")
|
||||
or (app_ext or {}).get("sGoodsPic")
|
||||
or ""
|
||||
),
|
||||
"buy_time": str(record.get("dtBuyTime") or ""),
|
||||
"pay_time": str(record.get("dtPayTime") or ""),
|
||||
"status": str(record.get("iStatus") or ""),
|
||||
"price": self._xpd_int(record.get("iPrice")),
|
||||
"pay_amount": self._xpd_int(record.get("iPayAmount")),
|
||||
"pay_type": str(record.get("sPayType") or ""),
|
||||
"role_name": str(record.get("sRoleName") or ""),
|
||||
"send_status": str(goods.get("iSendStatus") or ""),
|
||||
"delivery_message": str(goods.get("deliveryMsg") or ""),
|
||||
"raw": record,
|
||||
}
|
||||
)
|
||||
|
||||
if len(page_records) < page_size or len(records) >= total:
|
||||
break
|
||||
|
||||
return {"records": records, "total": total or len(records), "raw": {"pages": pages}}
|
||||
return {
|
||||
"records": records,
|
||||
"total": total or len(records),
|
||||
"raw": {"pages": pages},
|
||||
}
|
||||
|
||||
def xpd_bind_qr(self, *, act_alias: str) -> dict[str, Any]:
|
||||
"""生成和平小店绑定二维码链接(微信扫码进入 livelink 小程序绑定角色)。
|
||||
@@ -1653,9 +1809,15 @@ class DouyuActivityClient:
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询小店绑定信息失败")
|
||||
data = payload.get("data") or {}
|
||||
game_role = data.get("gameRole") if isinstance(data.get("gameRole"), dict) else {}
|
||||
game_account = data.get("gameAccount") if isinstance(data.get("gameAccount"), dict) else {}
|
||||
data: dict[str, Any] = (
|
||||
payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
)
|
||||
game_role: dict[str, Any] = (
|
||||
data.get("gameRole") if isinstance(data.get("gameRole"), dict) else {}
|
||||
)
|
||||
game_account: dict[str, Any] = (
|
||||
data.get("gameAccount") if isinstance(data.get("gameAccount"), dict) else {}
|
||||
)
|
||||
return {
|
||||
"bind_account": self._xpd_int(data.get("bindAccount")),
|
||||
"bind_role": self._xpd_int(data.get("bindRole")),
|
||||
@@ -1790,7 +1952,8 @@ class DouyuActivityClient:
|
||||
"_biz_code": "cjm",
|
||||
"_act_id": act_id,
|
||||
"sLiveUserInfo": json.dumps(
|
||||
{"sAnchorId": rid, "sVideoId": 0}, separators=(",", ":"),
|
||||
{"sAnchorId": rid, "sVideoId": 0},
|
||||
separators=(",", ":"),
|
||||
),
|
||||
"appext": json.dumps(
|
||||
{"user_price": price, "sGoodsPic": picture, "paytype": pay_type},
|
||||
@@ -1801,7 +1964,16 @@ class DouyuActivityClient:
|
||||
"_sid": "8",
|
||||
"set": "8",
|
||||
}
|
||||
for key in ("gameId", "actId", "appId", "livePlatId", "code", "timestamp", "v", "sig"):
|
||||
for key in (
|
||||
"gameId",
|
||||
"actId",
|
||||
"appId",
|
||||
"livePlatId",
|
||||
"code",
|
||||
"timestamp",
|
||||
"v",
|
||||
"sig",
|
||||
):
|
||||
value = str(embed_query.get(key) or "")
|
||||
if value:
|
||||
params[key] = value
|
||||
|
||||
Reference in New Issue
Block a user