feat(douyu): support elite handbook lock orders

This commit is contained in:
yml2213
2026-08-28 14:06:11 +08:00
parent 49aae8b74c
commit 9ee4e49a50
8 changed files with 471 additions and 59 deletions
+126 -32
View File
@@ -3,9 +3,7 @@
from __future__ import annotations
import json
import random
import re
import string
import time
from collections.abc import Callable
from typing import Any
@@ -39,7 +37,9 @@ 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"
EXCHANGE_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchange"
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"
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"
@@ -749,45 +749,139 @@ class DouyuActivityClient:
records = (payload.get("data") or {}).get("list") or []
return {"records": records, "raw": payload}
def exchange_goods(self, *, manual_id: str, rid: str, commodity_id: str, ctn: str | None = None) -> dict[str, Any]:
"""兑换活动商品。"""
ctn_value = ctn or cookie_value(self.cookie, "acf_ccn") or self.acf_ccn(refresh_subscribe=False)
@staticmethod
def _multipart_fields(data: dict[str, Any]) -> dict[str, tuple[None, str]]:
"""Build requests multipart fields without uploading files."""
return {key: (None, str(value)) for key, value in data.items()}
def create_exchange_order(
self,
*,
manual_id: str,
rid: str,
commodity_id: str,
num: int = 1,
) -> dict[str, Any]:
"""Lock stock for an activity commodity before spending points."""
if num < 1:
raise DouyuActivityError("锁单数量必须大于 0")
token = self.csrf_token()
randstr = "".join(random.choices(string.ascii_letters + string.digits, k=16))
data = {
"manualID": manual_id,
"rid": rid,
"commodityID": commodity_id,
"ctn": ctn_value,
"csrfToken": token,
"randstr": randstr,
"token[error][code]": "-1",
"token[data]": "",
}
payload = self._request_json(
"post",
self.EXCHANGE_API,
"兑换商品",
data=data,
self.CREATE_EXCHANGE_ORDER_API,
"锁定兑换商品",
files=self._multipart_fields({
"manualID": manual_id,
"rid": rid,
"commodityID": commodity_id,
"num": num,
"csrfToken": token,
}),
headers={
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Referer": f"https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId={rid}",
"sec-ch-ua": f'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
},
)
if payload.get("error") not in (0, "0"):
raise DouyuActivityError(payload.get("msg") or "兑换商品失败")
raise DouyuActivityError(payload.get("msg") or "锁定兑换商品失败")
data = payload.get("data") or {}
order_id = str(data.get("orderId") or "").strip()
if not order_id:
raise DouyuActivityError("锁定兑换商品成功,但响应中没有 orderId")
locked_at = int(time.time())
try:
expire_seconds = int(data.get("expireSeconds") or 0)
except (TypeError, ValueError):
expire_seconds = 0
if expire_seconds > 0:
expires_at = locked_at + expire_seconds
else:
expires_at = None
return {
"order_id": order_id,
"commodity_id": commodity_id,
"commodity_name": data.get("commodityName") or "",
"commodity_image": data.get("commodityImage") or "",
"score": data.get("score"),
"num": data.get("num", num),
"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]:
"""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",
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",
},
)
if payload.get("error") not in (0, "0"):
raise DouyuActivityError(payload.get("msg") or "支付锁单失败")
data = payload.get("data") or {}
return {
"order_id": str(data.get("orderId") or normalized_order_id),
"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"),
"csrf_token": token,
"raw": payload,
}
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",
self.EXCHANGE_ORDER_LIST_API,
"查询锁单",
params={"page": page, "pageSize": page_size, "manualID": manual_id},
headers={
"Origin": "",
"Referer": "https://www.douyu.com/pages/live-peace-handbook/web/lock-order?ditchname=pass0",
},
)
if payload.get("error") not in (0, "0"):
raise DouyuActivityError(payload.get("msg") or "查询锁单失败")
data = payload.get("data") or {}
return {
"orders": data.get("list") or [],
"total": data.get("total") or 0,
"raw": payload,
}
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,
rid=rid,
commodity_id=commodity_id,
num=1,
)
paid = self.pay_exchange_order(manual_id=manual_id, order_id=locked["order_id"])
return {
"commodity_id": commodity_id,
"ctn": ctn_value,
"csrf_token": token,
"randstr": randstr,
"raw": payload,
"order_id": locked["order_id"],
"exchange_id": paid["exchange_id"],
"commodity_image": paid["commodity_image"] or locked["commodity_image"],
"exchange_num": paid["exchange_num"],
"lock_order": locked,
"payment": paid,
}
def gold_account(self) -> dict[str, Any]: