refactor(douyu): runner 按功能域拆分 Mixin + 精英手册兑换对齐真机流程 (HAR 审核通过)
拆分: douyu_runner.py (-3186行) → core/bind/manual/gold/donate/goods/xpd 七个 Mixin + 入口聚合类, douyu_batch_registry 供 routers 重导出 精英手册兑换对齐 HAR 抓包 (activity_client +379 行): - csrf 复用 Cookie 值不再每次 generateCsrf (HAR 实测全程零 csrf 请求), 仅服务端报 csrf 错误时 force_refresh 重试一次 - pay Referer 补 roomId 对齐浏览器 - 新增手册链路接口: elite_user_info/storedetail/pre_exchange check+confirm/ subscribe/batch_limit/赠品兑换/自动转换 - resolve_exchange_plan: 浏览器兑换按钮状态机路由 (normal/subscribe/ pre_exchange/wait/blocked) - 兑换执行: 人类节奏抖动 → 锁单(火爆重试1次) → 支付分级退避 (频控 30/60/90s 4次上限, 普错 2/5/10/20s 5次上限, 300s 锁单期 15s 余量, 可中断睡眠) 测试: 新增 17 项 (csrf 复用语义/状态机路由/Referer), 全量 94 通过; 未夹带代理改动
This commit is contained in:
+358
-21
@@ -41,6 +41,22 @@ class DouyuActivityClient:
|
||||
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;
|
||||
# 预约到货 user/sub / user/cancelSub;批量兑换 batchExchangeLimit;单步兑换 exchange;自动兑换开关。
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
@@ -186,8 +202,18 @@ class DouyuActivityClient:
|
||||
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) -> str:
|
||||
"""获取 cvl_csrf_token。"""
|
||||
def csrf_token(self, *, force_refresh: bool = False) -> str:
|
||||
"""获取 cvl_csrf_token;优先复用 Cookie 中已有值,与浏览器行为一致。
|
||||
|
||||
2026-08-30 抓包实证:页面只读 Cookie `cvl_csrf_token` 的值作为 csrfToken
|
||||
复用(同一 token 跨多次接口、跨十几秒复用),Cookie 里没有/失效时才调用
|
||||
generateCsrf,生成的 token 写回 Cookie 继续复用。
|
||||
force_refresh=True 用于服务端提示 token 失效后的强制刷新。
|
||||
"""
|
||||
if not force_refresh:
|
||||
token = cookie_value(self.cookie, "cvl_csrf_token")
|
||||
if token:
|
||||
return token
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
self.CSRF_API,
|
||||
@@ -205,6 +231,58 @@ class DouyuActivityClient:
|
||||
raise DouyuActivityError("响应 Cookie 中没有 cvl_csrf_token")
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def _is_csrf_error(payload: dict[str, Any]) -> bool:
|
||||
"""判断响应是否为 csrfToken 校验失败(此时应刷新并重试一次)。"""
|
||||
return "csrf" in str(payload.get("msg") or "").lower()
|
||||
|
||||
@staticmethod
|
||||
def _peace_page_referer(rid: str = "") -> str:
|
||||
"""精英手册首页 Referer(对齐浏览器 getUserInfo 等接口)。"""
|
||||
if rid:
|
||||
return f"https://www.douyu.com/pages/live-peace-handbook/web?ditchname=pass0&roomId={rid}"
|
||||
return "https://www.douyu.com/pages/live-peace-handbook/web?ditchname=pass0"
|
||||
|
||||
@staticmethod
|
||||
def _peace_shop_referer(rid: str = "") -> str:
|
||||
"""精英手册商城页 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"
|
||||
|
||||
def _post_csrf_form(
|
||||
self,
|
||||
url: str,
|
||||
source: str,
|
||||
fields: dict[str, Any],
|
||||
referer: str,
|
||||
) -> dict[str, Any]:
|
||||
"""提交带 csrfToken 的 multipart 表单(对齐浏览器请求层)。
|
||||
|
||||
浏览器 csrf 配置 {tvk: cvl_csrf_token, tn: csrfToken} 会把 Cookie 中的
|
||||
token 值拼进表单;这里同样复用 Cookie 值,仅在服务端提示 token 失效时
|
||||
刷新一次后重试。
|
||||
"""
|
||||
token = self.csrf_token()
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
url,
|
||||
source,
|
||||
files=self._multipart_fields({**fields, "csrfToken": token}),
|
||||
headers={"Referer": referer},
|
||||
)
|
||||
if payload.get("error") not in (0, "0") and self._is_csrf_error(payload):
|
||||
self.logger(f"{source}: csrfToken 校验失败,刷新后重试一次")
|
||||
token = self.csrf_token(force_refresh=True)
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
url,
|
||||
source,
|
||||
files=self._multipart_fields({**fields, "csrfToken": token}),
|
||||
headers={"Referer": referer},
|
||||
)
|
||||
return payload
|
||||
|
||||
def acf_ccn(self, *, refresh_subscribe: bool = True) -> str:
|
||||
"""获取 acf_ccn;必要时通过订阅接口刷新一次。"""
|
||||
response = self._request(
|
||||
@@ -797,21 +875,16 @@ class DouyuActivityClient:
|
||||
"""Lock stock for an activity commodity before spending points."""
|
||||
if num < 1:
|
||||
raise DouyuActivityError("锁单数量必须大于 0")
|
||||
token = self.csrf_token()
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
payload = self._post_csrf_form(
|
||||
self.CREATE_EXCHANGE_ORDER_API,
|
||||
"锁定兑换商品",
|
||||
files=self._multipart_fields({
|
||||
{
|
||||
"manualID": manual_id,
|
||||
"rid": rid,
|
||||
"commodityID": commodity_id,
|
||||
"num": num,
|
||||
"csrfToken": token,
|
||||
}),
|
||||
headers={
|
||||
"Referer": f"https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId={rid}",
|
||||
},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "锁定兑换商品失败")
|
||||
@@ -838,28 +911,22 @@ class DouyuActivityClient:
|
||||
"expire_seconds": data.get("expireSeconds"),
|
||||
"locked_at": locked_at,
|
||||
"expires_at": expires_at,
|
||||
"csrf_token": token,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def pay_exchange_order(self, *, manual_id: str, order_id: 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:
|
||||
raise DouyuActivityError("锁单订单号不能为空")
|
||||
token = self.csrf_token()
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
payload = self._post_csrf_form(
|
||||
self.PAY_EXCHANGE_ORDER_API,
|
||||
"支付锁单",
|
||||
files=self._multipart_fields({
|
||||
{
|
||||
"orderID": normalized_order_id,
|
||||
"manualID": manual_id,
|
||||
"csrfToken": token,
|
||||
}),
|
||||
headers={
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0",
|
||||
},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "支付锁单失败")
|
||||
@@ -872,7 +939,6 @@ class DouyuActivityClient:
|
||||
"exchange_num": data.get("exchangeNum"),
|
||||
"s_type": data.get("sType"),
|
||||
"g_type": data.get("gType"),
|
||||
"csrf_token": token,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
@@ -916,6 +982,277 @@ class DouyuActivityClient:
|
||||
"payment": paid,
|
||||
}
|
||||
|
||||
def elite_user_info(self, *, manual_id: str, rid: str = "") -> dict[str, Any]:
|
||||
"""查询精英手册状态(对齐页面 getUserInfo,含 manualType/manualScore)。
|
||||
|
||||
浏览器每次进入手册页都会先请求该接口,用于判断商品是否"精英专享"、
|
||||
是否已达兑换上限等状态;兑换前先查一次与页面行为一致。
|
||||
"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.ELITE_USER_INFO_API,
|
||||
"查询手册状态",
|
||||
params={"manualID": manual_id},
|
||||
headers={"Origin": "", "Referer": self._peace_page_referer(rid)},
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询手册状态失败")
|
||||
user_info = (payload.get("data") or {}).get("userInfo") or {}
|
||||
return {
|
||||
"manual_id": manual_id,
|
||||
"manual_type": user_info.get("manualType"),
|
||||
"manual_score": user_info.get("manualScore"),
|
||||
"expire_time": user_info.get("expireTime"),
|
||||
"user_info": user_info,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def query_goods_detail(self, *, manual_id: str, commodity_id: str, rid: str = "") -> dict[str, Any]:
|
||||
"""查询商品详情 storedetail(对齐浏览器 getGoodsDetail)。
|
||||
|
||||
详情里带有兑换状态机所需的全部标记:preExchange.isExists / subscribeType /
|
||||
exchangeInfo.userStatus / batchExchange / roomLimited / storeStatus 等。
|
||||
"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.GOODS_DETAIL_API,
|
||||
"查询商品详情",
|
||||
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"):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询商品详情失败")
|
||||
return {"detail": payload.get("data") or {}, "raw": payload}
|
||||
|
||||
@staticmethod
|
||||
def resolve_exchange_plan(
|
||||
detail: dict[str, Any],
|
||||
*,
|
||||
manual_type: Any = None,
|
||||
batch_num_limit: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""对齐浏览器兑换按钮状态机($e / Ve / Ge)给出自动化兑换计划。
|
||||
|
||||
浏览器判定顺序(2026-08-30 bundle index.4872272b.js / index.0539fbf8.js):
|
||||
存在预扣订单 -> 已达兑换上限 -> 无库存 -> 限定房间 -> 活动未开启(预约/预兑) ->
|
||||
精英专享未开通 -> 已预兑等待开放 -> 立即兑换(经典锁单/支付)。
|
||||
action:
|
||||
classic 经典链路 createExchangeOrder -> payExchangeOrder(批量商品 num 受上限约束)
|
||||
pre_exchange 预兑商品(subscribeType=2 且未开启):user/queryExchange -> user/confirmExchange
|
||||
subscribe 预约到货商品(subscribeType=1 且未开启):user/sub
|
||||
wait 已预兑(userStatus=1)但预扣订单尚未生成,等待开放后重试
|
||||
blocked 明确不可兑换,text 为原因(文案对齐浏览器/服务端)
|
||||
"""
|
||||
detail = detail or {}
|
||||
pre = detail.get("preExchange") or {}
|
||||
pre_is_exists = int(pre.get("isExists") or 0)
|
||||
exchange_info = detail.get("exchangeInfo") or {}
|
||||
user_status = int(exchange_info.get("userStatus") or 0)
|
||||
subscribe_type = int(detail.get("subscribeType") or 0)
|
||||
status = int(detail.get("status") or 0)
|
||||
store_status = int(detail.get("storeStatus") or 0)
|
||||
open_status = int(detail.get("openStatus") or 0)
|
||||
unlimited = (
|
||||
int(detail.get("unlimitedMode") or 0) == 1
|
||||
and int(detail.get("unlimitedStatus") or 0) == 1
|
||||
)
|
||||
room_limited = int(detail.get("roomLimited") or 0) == 1
|
||||
is_limit_room = int(detail.get("isLimitRoom") or 0) == 1
|
||||
elite_limited = int(detail.get("eliteLimited") or 0) == 1
|
||||
batch_exchange = int(detail.get("batchExchange") or 0) > 0
|
||||
max_num = 1
|
||||
if batch_exchange and batch_num_limit is not None:
|
||||
try:
|
||||
max_num = max(1, int(batch_num_limit or 1))
|
||||
except (TypeError, ValueError):
|
||||
max_num = 1
|
||||
|
||||
plan: dict[str, Any] = {
|
||||
"action": "classic",
|
||||
"text": "",
|
||||
"max_num": max_num,
|
||||
"batch_exchange": batch_exchange,
|
||||
"pre_exchange": bool(pre_is_exists),
|
||||
"subscribe_type": subscribe_type,
|
||||
"user_status": user_status,
|
||||
}
|
||||
if pre_is_exists:
|
||||
plan.update(action="classic", text="存在预扣订单,继续兑换")
|
||||
elif status == 1 or (subscribe_type == 2 and user_status == 2):
|
||||
plan.update(action="blocked", text="已达兑换上限")
|
||||
elif store_status == 0 and not unlimited:
|
||||
plan.update(action="blocked", text="无库存")
|
||||
elif room_limited and not is_limit_room:
|
||||
plan.update(action="blocked", text="限定房间兑换,请前往指定直播间")
|
||||
elif open_status == 0:
|
||||
if subscribe_type == 1:
|
||||
plan.update(action="subscribe", text="活动未开启,预约到货")
|
||||
elif subscribe_type == 2:
|
||||
plan.update(action="pre_exchange", text="活动未开启,预兑")
|
||||
else:
|
||||
plan.update(action="blocked", text="活动未开启")
|
||||
elif elite_limited and manual_type is not None and int(manual_type or 0) != 1:
|
||||
plan.update(action="blocked", text="需要开通精英手册,才可以兑换本道具")
|
||||
elif subscribe_type == 2 and user_status == 1:
|
||||
plan.update(action="wait", text="已预兑,等待开放后继续兑换")
|
||||
else:
|
||||
plan.update(action="classic", text="立即兑换")
|
||||
return plan
|
||||
|
||||
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,
|
||||
"查询批量兑换上限",
|
||||
{"manualID": manual_id, "commodityID": commodity_id, "rid": rid or "0"},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询批量兑换上限失败")
|
||||
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]:
|
||||
"""查询预兑状态(对齐页面 user/queryExchange,预兑弹窗前必查)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.PRE_EXCHANGE_CHECK_API,
|
||||
"查询预兑状态",
|
||||
params={"manualID": manual_id, "commodityID": commodity_id},
|
||||
headers={"Origin": "", "Referer": self._peace_shop_referer()},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
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]:
|
||||
"""查询取消预兑信息(对齐页面 user/queryCancelExchange)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.PRE_EXCHANGE_CANCEL_CHECK_API,
|
||||
"查询取消预兑信息",
|
||||
params={"manualID": manual_id, "commodityID": commodity_id},
|
||||
headers={"Origin": "", "Referer": self._peace_shop_referer()},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
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]:
|
||||
"""确认预兑(对齐页面 user/confirmExchange,成功提示"预兑成功")。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.PRE_EXCHANGE_CONFIRM_API,
|
||||
"确认预兑",
|
||||
{"manualID": manual_id, "commodityID": commodity_id, "rid": rid or "0"},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
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]:
|
||||
"""取消预兑(对齐页面 user/cancelExchange)。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.PRE_EXCHANGE_CANCEL_API,
|
||||
"取消预兑",
|
||||
{"manualID": manual_id, "commodityID": commodity_id, "rid": rid or "0"},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "取消预兑失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def subscribe_commodity(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
"""预约到货(对齐页面 user/sub,成功提示"预约成功")。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.COMMODITY_SUBSCRIBE_API,
|
||||
"预约到货",
|
||||
{"manualID": manual_id, "commodityID": commodity_id},
|
||||
referer=self._peace_shop_referer(),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "预约到货失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def unsubscribe_commodity(self, *, manual_id: str, commodity_id: str) -> dict[str, Any]:
|
||||
"""取消预约到货(对齐页面 user/cancelSub)。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.COMMODITY_UNSUBSCRIBE_API,
|
||||
"取消预约到货",
|
||||
{"manualID": manual_id, "commodityID": commodity_id},
|
||||
referer=self._peace_shop_referer(),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
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]:
|
||||
"""单步兑换(对齐页面 postExchangeGift -> /ordnc/activity/peace/exchange)。
|
||||
|
||||
2026-08-30 bundle 中新链路,页面主要用于兑换中心/自动兑换场景;字段按页面
|
||||
调用({manualID, commodityID, num, rid} + csrfToken)对齐。
|
||||
"""
|
||||
payload = self._post_csrf_form(
|
||||
self.EXCHANGE_GIFT_API,
|
||||
"单步兑换",
|
||||
{
|
||||
"manualID": manual_id,
|
||||
"commodityID": commodity_id,
|
||||
"num": max(1, int(num)),
|
||||
"rid": rid or "0",
|
||||
},
|
||||
referer=self._peace_shop_referer(rid),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "兑换失败")
|
||||
data = payload.get("data") or {}
|
||||
return {
|
||||
"exchange_id": str(data.get("exchangeId") or ""),
|
||||
"commodity_type": data.get("commodityType"),
|
||||
"commodity_image": data.get("commodityImage") or "",
|
||||
"exchange_num": data.get("exchangeNum"),
|
||||
"s_type": data.get("sType"),
|
||||
"g_type": data.get("gType"),
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def auto_convert_switch(self, *, manual_id: str) -> dict[str, Any]:
|
||||
"""查询自动兑换开关(对齐页面 activity/automaticallyConvert)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.AUTO_CONVERT_QUERY_API,
|
||||
"查询自动兑换开关",
|
||||
params={"manualID": manual_id},
|
||||
headers={"Origin": "", "Referer": self._peace_shop_referer()},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询自动兑换开关失败")
|
||||
return {"data": payload.get("data") or {}, "raw": payload}
|
||||
|
||||
def set_auto_convert(self, *, manual_id: str, switch: Any) -> dict[str, Any]:
|
||||
"""设置自动兑换开关(对齐页面 automaticallyConvertSave {manualID, switch})。"""
|
||||
payload = self._post_csrf_form(
|
||||
self.AUTO_CONVERT_SAVE_API,
|
||||
"设置自动兑换开关",
|
||||
{"manualID": manual_id, "switch": switch},
|
||||
referer=self._peace_shop_referer(),
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "设置自动兑换开关失败")
|
||||
return {"raw": payload}
|
||||
|
||||
def exchange_home_info(self) -> dict[str, Any]:
|
||||
"""查询兑换中心信息(对齐页面 activity/exchangeHome)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.EXCHANGE_HOME_API,
|
||||
"查询兑换中心",
|
||||
headers={"Origin": "", "Referer": self._peace_shop_referer()},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询兑换中心失败")
|
||||
return {"data": payload.get("data") or {}, "raw": payload}
|
||||
|
||||
def gold_account(self) -> dict[str, Any]:
|
||||
"""查询 cz 侧鱼翅余额。"""
|
||||
payload = self._request_json(
|
||||
|
||||
Reference in New Issue
Block a user