feat(douyu): 和平小店绑定功能(生成绑定二维码/查询绑定信息)

- activity_client: xpd_bind_qr(roleParam 签发 livelink 绑定链接, 微信扫码进小程序绑定)
  + xpd_bind_info(bindInfo 查询绑定状态/角色/平台), gameRole 非 dict 边界防护
- runner/service: 注册 get_xpd_bind_qr / query_xpd_bind_info 任务, 回写
  xpd_game_name / xpd_bind_status
- router: get_xpd_bind_qr 的 url 加入列表接口保留白名单(二维码展示)
- 前端: 和平小店面板新增两个按钮, 二维码列支持 get_xpd_bind_qr 展示/放大
This commit is contained in:
yml2213
2026-08-06 17:33:11 +08:00
parent fdc74eba8c
commit 5f63d77dc3
5 changed files with 127 additions and 1 deletions
+63
View File
@@ -819,6 +819,8 @@ class DouyuActivityClient:
# ---- 和平小店(腾讯道聚城 H5)----
XPD_EMBED_API = "https://www.douyu.com/japi/carnival/nc/txEmbed/getIframeUrl"
XPD_H5_REFERER = "https://www.douyu.com/topic/h5/hpxd01"
XPD_ROLE_PARAM_API = "https://apiv2.douyucdn.cn/japi/carnivalApi/tencent/roleParam"
XPD_BIND_INFO_API = "https://www.douyu.com/japi/carnivalApi/v2/tencent/bindInfo"
XPD_DAOJU_REFERER = "https://app.daoju.qq.com/"
XPD_GET_ROLE_API = "https://apps.game.qq.com/daoju/igw/live"
# 2026-08 抓包(8.6-和平小店)实证:本期走 xn_live_cjm 变体(旧期为 recommend_live/common)
@@ -1002,6 +1004,67 @@ class DouyuActivityClient:
)
return {"goods": goods, "raw": recommend}
def xpd_bind_qr(self, *, act_alias: str) -> dict[str, Any]:
"""生成和平小店绑定二维码链接(微信扫码进入 livelink 小程序绑定角色)。
返回 data.url(可生成二维码)与 gameRoleBindUrl(直接角色绑定页)。
"""
payload = self._request_json(
"post",
self.XPD_ROLE_PARAM_API,
"生成小店绑定二维码",
params={"client_sys": "android"},
data={
"token": self._xpd_token(),
"uid": cookie_value(self.cookie, "acf_uid") or "",
"client": "mobile",
"actAlias": act_alias,
},
headers={
"User-Device": "MDExZTUxMzEyYmYxMmY5MmQ3YmY1M2M4MDA3MDcxMTF8djcuOS4y",
"aid": "android1",
"User-Agent": "android/7.9.2 (android 9; ; M973Q)",
"Accept-Language": "zh-CN",
"Content-Type": "application/x-www-form-urlencoded",
},
)
if payload.get("error") not in (0, "0", None):
raise DouyuActivityError(payload.get("msg") or "生成小店绑定二维码失败")
data = payload.get("data") or {}
url = str(data.get("url") or "")
if not url:
raise DouyuActivityError(f"生成小店绑定二维码失败: {payload}")
return {
"url": url,
"game_role_bind_url": str(data.get("gameRoleBindUrl") or ""),
"nick_name": str((data.get("queryParams") or {}).get("nickName") or ""),
"raw": payload,
}
def xpd_bind_info(self, *, act_alias: str) -> dict[str, Any]:
"""查询和平小店绑定信息(bindInfo)。"""
payload = self._request_json(
"get",
self.XPD_BIND_INFO_API,
"查询小店绑定信息",
params={"actAlias": act_alias, "token": self._xpd_token()},
)
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 {}
return {
"bind_account": self._xpd_int(data.get("bindAccount")),
"bind_role": self._xpd_int(data.get("bindRole")),
"need_bind_role": self._xpd_int(data.get("needBindRole")),
"nick": str(game_account.get("nick") or ""),
"role_name": str(game_role.get("roleName") or ""),
"area_name": str(game_role.get("areaName") or ""),
"plat_name": str(game_role.get("platName") or ""),
"raw": payload,
}
def xpd_balance(
self,
*,