type: 收窄豆鱼服务层类型

This commit is contained in:
yml2213
2026-08-30 19:48:41 +08:00
parent 17577ff734
commit 6a16d561d2
4 changed files with 284 additions and 114 deletions
+275 -103
View File
@@ -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
+4 -4
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping
import requests
from requests.cookies import RequestsCookieJar
def cookie_pairs(cookie: str) -> list[tuple[str, str]]:
@@ -21,7 +21,7 @@ def cookie_pairs(cookie: str) -> list[tuple[str, str]]:
return pairs
def normalize_cookie_pairs(pairs: Iterable[tuple[str, str]]) -> str:
def normalize_cookie_pairs(pairs: Iterable[tuple[str, str | None]]) -> str:
"""按 Cookie key 去重,保留最后一次出现的值。"""
ordered_keys: list[str] = []
values: dict[str, str] = {}
@@ -36,7 +36,7 @@ def normalize_cookie_pairs(pairs: Iterable[tuple[str, str]]) -> str:
def normalize_douyu_cookie(
cookie: str | Mapping[str, str] | requests.cookies.RequestsCookieJar | None,
cookie: str | Mapping[str, str] | RequestsCookieJar | None,
) -> str:
"""把 Cookie 字符串、dict 或 CookieJar 转成去重后的浏览器 Cookie 字符串。"""
if not cookie:
@@ -47,7 +47,7 @@ def normalize_douyu_cookie(
def cookie_value(
cookie: str | Mapping[str, str] | requests.cookies.RequestsCookieJar | None,
cookie: str | Mapping[str, str] | RequestsCookieJar | None,
key: str,
) -> str:
"""从 Cookie 中读取 key;有重复时以最后一次出现为准。"""
+4 -6
View File
@@ -10,8 +10,6 @@
import threading
import time
from typing import Optional
from loguru import logger
from .proxy_resolver import ProxyResolver
@@ -28,8 +26,8 @@ class ProxyFetcher:
self,
api_url: str,
whitelist_platform: str = "xiequ",
whitelist_credentials: dict = None,
stop_event: Optional[threading.Event] = None,
whitelist_credentials: dict | None = None,
stop_event: threading.Event | None = None,
):
self.api_url = api_url
self.stop_event = stop_event
@@ -66,7 +64,7 @@ class ProxyFetcher:
return True, f"出口 IP {local_ip} 已同步: {msg}"
return False, f"出口 IP {local_ip} 同步失败: {msg}"
def fetch_new_proxy(self, max_attempts: int = 3) -> Optional[str]:
def fetch_new_proxy(self, max_attempts: int = 3) -> str | None:
"""从代理 API 获取 1 个可用代理,失败返回 None。
不主动检测/同步本地出口 IP;如果代理 API 返回"白名单错误"
@@ -115,7 +113,7 @@ class ProxyFetcher:
def get_proxy_fetcher(
api_url: str,
whitelist_platform: str = "xiequ",
whitelist_credentials: dict = None,
whitelist_credentials: dict | None = None,
) -> ProxyFetcher:
"""获取代理获取器实例。"""
return ProxyFetcher(
+1 -1
View File
@@ -165,7 +165,7 @@ class ProxyResolver:
def resolve_working_proxy(
api_url: str,
whitelist_platform: str = "xiequ",
whitelist_credentials: dict = None,
whitelist_credentials: dict | None = None,
max_attempts: int = 4,
log_func: Optional[LogFunc] = None,
) -> tuple[Optional[str], str]: