From 5f63d77dc3b9a09a3d0f98e9595cb90bbfab7dc0 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 6 Aug 2026 17:33:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyu):=20=E5=92=8C=E5=B9=B3=E5=B0=8F?= =?UTF-8?q?=E5=BA=97=E7=BB=91=E5=AE=9A=E5=8A=9F=E8=83=BD(=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=BB=91=E5=AE=9A=E4=BA=8C=E7=BB=B4=E7=A0=81/?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E7=BB=91=E5=AE=9A=E4=BF=A1=E6=81=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 展示/放大 --- core/douyu/activity_client.py | 63 +++++++++++++++++++++++ web/backend/routers/douyu.py | 2 +- web/backend/services/douyu_runner.py | 53 +++++++++++++++++++ web/backend/services/douyu_service.py | 2 + web/frontend/src/pages/DouyuTasksPage.tsx | 8 +++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index d1643b2..84c5845 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -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, *, diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index 44e0a95..9bae603 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -190,7 +190,7 @@ def _sanitize_task_result(result: dict | None, task_type: str, *, include_detail if "limited_goods" in data: data["limited_goods"] = _slim_limited_goods(data.get("limited_goods")) - if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr"}: + if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr", "get_xpd_bind_qr"}: data.pop("url", None) if task_type not in {"create_elite_qr", "create_esports_qr", "create_gold_qr"}: data.pop("pay_url", None) diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index b3a58fb..a5e914f 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -1076,6 +1076,57 @@ class DouyuBatchRunner: {"role": role, "area_id": area_id}, ) + def _execute_get_xpd_bind_qr( + self, + db: Session, + task: DouyuTask, + account: Account, + cookie: str, + config: dict, + ): + """生成和平小店绑定二维码(微信扫码在小程序内绑定角色)。""" + client = self._client(cookie) + act_alias = str(config.get("xpd_act_alias") or "").strip() + if not act_alias: + self._mark_task(db, task, "failed", "请先配置小店活动代号 actAlias") + return + result = client.xpd_bind_qr(act_alias=act_alias) + account.xpd_bind_status = "xpd_bind_qr_ready" + account.updated_at = datetime.now(timezone.utc) + db.commit() + self._mark_task(db, task, "success", "小店绑定二维码已生成", result) + + def _execute_query_xpd_bind_info( + self, + db: Session, + task: DouyuTask, + account: Account, + cookie: str, + config: dict, + ): + """查询和平小店绑定信息(bindInfo)。""" + client = self._client(cookie) + act_alias = str(config.get("xpd_act_alias") or "").strip() + if not act_alias: + self._mark_task(db, task, "failed", "请先配置小店活动代号 actAlias") + return + result = client.xpd_bind_info(act_alias=act_alias) + role_name = str(result.get("role_name") or "") + if role_name: + account.xpd_game_name = role_name + account.xpd_bind_status = "xpd_bound" if result.get("bind_role") else "xpd_not_bound" + account.updated_at = datetime.now(timezone.utc) + db.commit() + status = "已绑定" if result.get("bind_role") else "未绑定" + text = str(result.get("role_name") or result.get("nick") or "-") + self._mark_task( + db, + task, + "success", + f"小店绑定: {status} ({text})", + result, + ) + def _execute_refresh_xpd_goods( self, db: Session, @@ -2443,6 +2494,8 @@ class DouyuBatchRunner: "query_gold_balance": self._execute_query_gold_balance, "query_exchange_records": self._execute_query_exchange_records, "prefetch_csrf_token": self._execute_prefetch_csrf_token, + "get_xpd_bind_qr": self._execute_get_xpd_bind_qr, + "query_xpd_bind_info": self._execute_query_xpd_bind_info, "query_xpd_role": self._execute_query_xpd_role, "refresh_xpd_goods": self._execute_refresh_xpd_goods, "query_xpd_balance": self._execute_query_xpd_balance, diff --git a/web/backend/services/douyu_service.py b/web/backend/services/douyu_service.py index 9b4212d..3e3dd93 100644 --- a/web/backend/services/douyu_service.py +++ b/web/backend/services/douyu_service.py @@ -38,6 +38,8 @@ SUPPORTED_DOUYU_TASK_TYPES = { "refresh_goods": "刷新商品列表", "query_exchange_records": "一键查询兑换记录", "prefetch_csrf_token": "一键获取兑换 CSRF Token", + "get_xpd_bind_qr": "生成小店绑定二维码", + "query_xpd_bind_info": "查询小店绑定信息", "query_xpd_role": "查询小店绑定角色", "refresh_xpd_goods": "刷新小店商品列表", "query_xpd_balance": "查询小店点券余额", diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index 978c0c0..873290f 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -84,6 +84,8 @@ const ESPORTS_QUICK_ACTIONS = [ ]; const PEACE_QUICK_ACTIONS = [ + { key: 'get_xpd_bind_qr', icon: }, + { key: 'query_xpd_bind_info', icon: }, { key: 'query_xpd_role', icon: }, { key: 'refresh_xpd_goods', icon: }, { key: 'query_xpd_balance', icon: }, @@ -122,6 +124,8 @@ const ESPORTS_TASK_TYPES = new Set([ 'donate_esports_firework_gift', ]); const PEACE_TASK_TYPES = new Set([ + 'get_xpd_bind_qr', + 'query_xpd_bind_info', 'query_xpd_role', 'refresh_xpd_goods', 'query_xpd_balance', @@ -149,6 +153,7 @@ function resultFlag(result: Record | null | undefined, key: str function taskQrUrl(task: DouyuTaskItem | null | undefined): string { if (!task) return ''; if (task.task_type === 'get_bind_qr') return resultText(task.result, 'url'); + if (task.task_type === 'get_xpd_bind_qr') return resultText(task.result, 'url'); if (['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)) { return resultText(task.result, 'pay_url'); } @@ -1323,6 +1328,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) if (!task) return; if (hasBindQrcode(task)) openQrTask(task); else if (hasPaymentQrcode(task)) openPayTask(task); + else if (task.task_type === 'get_xpd_bind_qr') openQrTask(task); else openEsportsBindTask(task); }; return ( @@ -1442,6 +1448,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) 小店查询 + {renderActionButton('get_xpd_bind_qr', 'primary')} + {renderActionButton('query_xpd_bind_info', 'primary')} {renderActionButton('query_xpd_role', 'primary')} {renderActionButton('query_xpd_balance', 'primary')} {renderActionButton('refresh_xpd_goods')}