diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py
index a5e914f..92afbdf 100644
--- a/web/backend/services/douyu_runner.py
+++ b/web/backend/services/douyu_runner.py
@@ -29,6 +29,8 @@ from .douyu_service import (
DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX"
DOUYU_BIND_ROLE_POLL_SECONDS = 65
DOUYU_BIND_ROLE_POLL_INTERVAL = 5
+DOUYU_XPD_BIND_POLL_SECONDS = 300
+DOUYU_XPD_BIND_POLL_INTERVAL = 5
DOUYU_PAYMENT_POLL_SECONDS = 600
DOUYU_PAYMENT_POLL_INTERVAL = 5
DOUYU_GIFT_POINTS_REFRESH_TIMES = 3
@@ -1084,7 +1086,7 @@ class DouyuBatchRunner:
cookie: str,
config: dict,
):
- """生成和平小店绑定二维码(微信扫码在小程序内绑定角色)。"""
+ """生成和平小店绑定二维码并轮询等待微信扫码绑定完成。"""
client = self._client(cookie)
act_alias = str(config.get("xpd_act_alias") or "").strip()
if not act_alias:
@@ -1094,7 +1096,84 @@ class DouyuBatchRunner:
account.xpd_bind_status = "xpd_bind_qr_ready"
account.updated_at = datetime.now(timezone.utc)
db.commit()
- self._mark_task(db, task, "success", "小店绑定二维码已生成", result)
+ result["bind_polling"] = True
+ self._update_task_progress(
+ db,
+ task,
+ "running",
+ "二维码已生成,请微信扫码在小程序中绑定角色",
+ result,
+ )
+ bound = self._wait_xpd_bind(db, task, account, client, act_alias, result)
+ if self._stop.is_set():
+ result["bind_polling"] = False
+ self._mark_task(db, task, "stopped", "任务已停止", result)
+ return
+ if bound:
+ account.xpd_bind_status = "xpd_bound"
+ account.updated_at = datetime.now(timezone.utc)
+ db.commit()
+ role_text = str(result.get("role_name") or account.xpd_game_name or "-")
+ self._mark_task(db, task, "success", f"小店绑定成功: {role_text}", result)
+ return
+ result["bind_polling"] = False
+ self._mark_task(
+ db,
+ task,
+ "failed",
+ "未检测到小店绑定(二维码仍有效,可再次生成后扫码)",
+ result,
+ )
+
+ def _wait_xpd_bind(
+ self,
+ db: Session,
+ task: DouyuTask,
+ account: Account,
+ client: DouyuActivityClient,
+ act_alias: str,
+ result: dict,
+ ) -> bool:
+ """轮询 bindInfo,检测到绑定角色即回写账号并返回 True。"""
+ deadline = time.monotonic() + DOUYU_XPD_BIND_POLL_SECONDS
+ poll_count = 0
+ while not self._stop.is_set() and time.monotonic() <= deadline:
+ try:
+ info = client.xpd_bind_info(act_alias=act_alias)
+ poll_count += 1
+ result["bind_poll_count"] = poll_count
+ result["bind_polling"] = True
+ role_name = str(info.get("role_name") or "")
+ if info.get("bind_role") and role_name:
+ result.update({key: value for key, value in info.items() if key != "raw"})
+ result["bind_polling"] = False
+ result["xpd_bound"] = True
+ account.xpd_game_name = role_name
+ account.updated_at = datetime.now(timezone.utc)
+ db.commit()
+ return True
+ self._update_task_progress(
+ db,
+ task,
+ "running",
+ f"等待扫码绑定(第 {poll_count} 次)",
+ result,
+ )
+ except Exception as exc:
+ poll_count += 1
+ result["bind_poll_count"] = poll_count
+ result["bind_poll_error"] = str(exc)
+ self._update_task_progress(
+ db,
+ task,
+ "running",
+ f"等待扫码绑定: {exc}",
+ result,
+ )
+ if self._stop.wait(DOUYU_XPD_BIND_POLL_INTERVAL):
+ break
+ result["bind_polling"] = False
+ return False
def _execute_query_xpd_bind_info(
self,
diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx
index 873290f..165be49 100644
--- a/web/frontend/src/pages/DouyuTasksPage.tsx
+++ b/web/frontend/src/pages/DouyuTasksPage.tsx
@@ -176,7 +176,8 @@ function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean {
}
function hasBindQrcode(task: DouyuTaskItem | null | undefined): boolean {
- if (!task || task.task_type !== 'get_bind_qr') return false;
+ if (!task) return false;
+ if (!['get_bind_qr', 'get_xpd_bind_qr'].includes(task.task_type)) return false;
// 有二维码,或已有待确认角色(无需再扫码)都应可展示绑定面板
return Boolean(resultText(task.result, 'url'))
|| bindReadyForConfirm(task)
@@ -1053,10 +1054,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
)
),
);
- const qrBindReady = bindReadyForConfirm(qrTask) || qrQueryPending;
+ const isXpdBindTask = qrTask?.task_type === 'get_xpd_bind_qr';
+ const qrXpdBound = Boolean(isXpdBindTask && resultFlag(qrResult, 'xpd_bound'));
+ const qrBindReady = bindReadyForConfirm(qrTask) || qrQueryPending || qrXpdBound;
const qrRoleSourceResult = qrQueryPending
? qrQueryResult
- : (bindReadyForConfirm(qrTask) ? qrResult : null);
+ : (bindReadyForConfirm(qrTask) || qrXpdBound ? qrResult : null);
// 待确认角色优先用 pending_* 字段(已绑定角色之外的扫码新角色)
const qrRoleName = resultText(qrRoleSourceResult, 'pending_role_name') || resultText(qrRoleSourceResult, 'role_name');
const qrAreaName = resultText(qrRoleSourceResult, 'pending_area_name') || resultText(qrRoleSourceResult, 'area_name');
@@ -1090,12 +1093,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
: '';
const qrBindSummary = resultText(qrResult, 'bind_summary') || resultText(qrQueryResult, 'bind_summary');
const qrStatusText = qrBindReady
- ? '已识别角色,待确认'
- : qrQueryRunning
- ? '查询角色中'
- : qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.status === 'success' && qrQueryIsBoundAct
- ? '查询结果:仍是当前绑定角色'
- : bindPhaseText(qrBindPhase, qrAutoPolling);
+ ? (isXpdBindTask ? '绑定成功' : '已识别角色,待确认')
+ : (isXpdBindTask && qrTask?.status === 'failed')
+ ? '未检测到绑定'
+ : (isXpdBindTask && qrTask?.status === 'running')
+ ? '等待扫码绑定'
+ : qrQueryRunning
+ ? '查询角色中'
+ : qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.status === 'success' && qrQueryIsBoundAct
+ ? '查询结果:仍是当前绑定角色'
+ : bindPhaseText(qrBindPhase, qrAutoPolling);
const qrAccountName = qrTask
? (qrTask.account_nickname || qrTask.account_username || qrTask.account_uid || `#${qrTask.account_id}`)
: '';
@@ -2112,24 +2119,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
最近查询: {qrQueryTask.message}
) : null}
-
- }
- onClick={queryQrRole}
- loading={qrQueryRunning}
- disabled={qrAutoPolling}
- >
- {qrAutoPolling ? '自动识别中' : '查询角色'}
-
- }
- onClick={confirmQrBind}
- disabled={!qrBindReady}
- >
- 确认绑定
-
-
+ {!isXpdBindTask && (
+
+ }
+ onClick={queryQrRole}
+ loading={qrQueryRunning}
+ disabled={qrAutoPolling}
+ >
+ {qrAutoPolling ? '自动识别中' : '查询角色'}
+
+ }
+ onClick={confirmQrBind}
+ disabled={!qrBindReady}
+ >
+ 确认绑定
+
+
+ )}
)}