新增电竞手册独立绑定流程

This commit is contained in:
yml2213
2026-07-28 17:12:08 +08:00
parent 63962811f8
commit d7a8666fe4
13 changed files with 3725 additions and 145 deletions
+163 -1
View File
@@ -42,7 +42,14 @@ class DouyuActivityClient:
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"
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"
@@ -238,6 +245,10 @@ class DouyuActivityClient:
)
if payload.get("error") not in (0, "0", None):
raise DouyuActivityError(payload.get("msg") or "查询绑定信息失败")
return self._parse_bind_info(payload, act_alias=act_alias, api_version="v2" if v2 else "v1")
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 {}
game_role = data.get("gameRole") or {}
@@ -270,7 +281,7 @@ class DouyuActivityClient:
can_change_role = self._first_present(tx_act, "canChangeRole", "canChangeBind")
return {
"act_alias": act_alias,
"api_version": "v2" if v2 else "v1",
"api_version": api_version,
"bind_account": bind_account,
"bind_role": bind_role,
"bind_act": bind_act,
@@ -289,6 +300,96 @@ class DouyuActivityClient:
"raw": payload,
}
def esports_bind_info(self, act_alias: str) -> dict[str, Any]:
"""电竞手册唯一的角色状态查询,返回角色、绑定状态和换绑冷却。"""
payload = self._request_json(
"get",
self.ESPORTS_BIND_INFO_API,
"查询电竞手册绑定信息",
params={"actAlias": act_alias},
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 "查询电竞手册绑定信息失败")
return self._parse_bind_info(payload, act_alias=act_alias, api_version="esports")
def get_esports_bind_qr(self, act_alias: str) -> dict[str, Any]:
"""生成电竞手册的腾讯角色绑定二维码链接。"""
payload = self._request_json(
"get",
self.ESPORTS_BIND_ROLE_PARAM_API,
"获取电竞手册绑定二维码",
params={"actAlias": act_alias},
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 "获取电竞手册绑定二维码失败")
data = payload.get("data") or {}
# 电竞手册网页实际打开 data.urlgameRoleBindUrl 是兼容旧入口。
bind_url = data.get("url") or data.get("gameRoleBindUrl") or ""
if not bind_url:
raise DouyuActivityError(payload.get("msg") or "电竞手册绑定二维码链接为空")
return {
"url": bind_url,
"bind_url": bind_url,
"role_param_url": data.get("url") or "",
"game_role_bind_url": data.get("gameRoleBindUrl") or "",
"act_alias": act_alias,
"raw": payload,
}
def esports_ctn(self, *, referer: str) -> str:
"""获取电竞手册页面当前 ctn,仅在 Cookie 缺失时走页面 CSRF 接口。"""
ctn = cookie_value(self.cookie, "acf_ccn")
if ctn:
return ctn
response = self._request(
"get",
self.ESPORTS_CSRF_API,
source="获取电竞手册 ctn",
headers={"Origin": "", "Referer": referer},
)
ctn = cookie_value(self.cookie, "acf_ccn")
if not ctn:
raise DouyuActivityError(f"获取电竞手册 ctn 失败: {response.text[:200]}")
return ctn
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"
f"?roomId={room_id or '9263298'}&isAnchorSide=0"
)
# 页面成功流程固定为 agreement -> actBind;协议不作为独立业务状态暴露。
# 两次 POST 都使用当前 Cookie 中的 acf_ccn,不额外主动刷新 CSRF。
agreement_ctn = self.esports_ctn(referer=referer)
agreement = self._request_json(
"post",
self.ESPORTS_AGREEMENT_API,
"同意电竞手册协议",
files={"ctn": (None, agreement_ctn)},
headers={"Referer": referer},
)
if agreement.get("error") not in (0, "0", None):
raise DouyuActivityError(agreement.get("msg") or "同意电竞手册协议失败")
bind_ctn = self.esports_ctn(referer=referer)
bind = self._request_json(
"post",
self.ESPORTS_ACT_BIND_API,
"完成电竞手册绑定",
files={
"actAlias": (None, act_alias),
"ctn": (None, bind_ctn),
},
headers={"Referer": referer},
)
if bind.get("error") not in (0, "0", None):
raise DouyuActivityError(bind.get("msg") or "完成电竞手册绑定失败")
return {"agreement": agreement, "act_bind": bind}
def confirm_bind(self, act_alias: str) -> dict[str, Any]:
"""确认绑定当前扫码选择的游戏角色。"""
token = self.csrf_token()
@@ -337,6 +438,67 @@ class DouyuActivityClient:
raise DouyuActivityError(payload.get("msg") or "生成精英宝典支付码失败")
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]:
"""生成电竞手册支付二维码。"""
self._set_cookie_value("acf_ccn", ctn)
time_payload = self._request_json(
"get",
self.PEACE_TIME_API,
"校验电竞手册开通时间",
headers={"Referer": "https://www.douyu.com/pages/live-peace-pel/web/"},
)
if time_payload.get("error") not in (0, "0"):
raise DouyuActivityError(time_payload.get("msg") or "电竞手册当前不可开通")
payload = self._request_json(
"post",
self.PAY_QR_API,
"生成电竞手册支付码",
json={
"bizType": "peacePel",
"biz": {"actAlias": act_alias, "amount": amount, "roomId": int(room_id or 0)},
"ctn": ctn,
},
headers={
"Content-Type": "application/json;charset=UTF-8",
"Referer": "https://www.douyu.com/pages/live-peace-pel/web/",
},
)
data = payload.get("data") or {}
qr = data.get("qr") or ""
if not qr:
raise DouyuActivityError(payload.get("msg") or "生成电竞手册支付码失败")
return {
"pay_url": qr,
"pay_token": data.get("token") or "",
"pay_ttl": data.get("ttl"),
"ctn": ctn,
"act_alias": act_alias,
"time": time_payload,
"raw": payload,
}
def esports_user_info(self, *, manual_id: str) -> dict[str, Any]:
"""查询电竞手册的开通状态和积分。"""
payload = self._request_json(
"get",
self.ESPORTS_USER_INFO_API,
"查询电竞手册状态",
params={"manualID": manual_id},
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 "查询电竞手册状态失败")
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 create_gold_qr(self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1") -> dict[str, Any]:
"""生成鱼翅充值支付二维码。"""
payload = self._request_json(