diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index f744083..39af10c 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -31,6 +31,8 @@ DOUYU_BIND_ROLE_POLL_SECONDS = 65 DOUYU_BIND_ROLE_POLL_INTERVAL = 5 DOUYU_PAYMENT_POLL_SECONDS = 600 DOUYU_PAYMENT_POLL_INTERVAL = 5 +DOUYU_GIFT_POINTS_REFRESH_TIMES = 3 +DOUYU_GIFT_POINTS_REFRESH_INTERVAL = 2 class DouyuBatchRunner: @@ -476,6 +478,51 @@ class DouyuBatchRunner: result["gold_balance"] = last_gold return False + def _refresh_points_after_elite_gift( + self, + db: Session, + task: DouyuTask, + account: Account, + client: DouyuActivityClient, + cookie: str, + ctn: str | None, + result: dict, + baseline_points: int | None, + gift_count: int, + ) -> dict: + """赠送精英令后短轮询积分;1 个精英令约等于 10 积分。""" + expected_gain = max(0, gift_count) * 10 + target_points = baseline_points + expected_gain if baseline_points is not None else None + result["gift_points_baseline"] = baseline_points + result["gift_points_expected_gain"] = expected_gain + result["gift_points_target"] = target_points + + last_points = None + refresh_result: dict = {} + for index in range(1, DOUYU_GIFT_POINTS_REFRESH_TIMES + 1): + refresh_result = self._refresh_account_points(client, account, cookie, ctn=ctn) + db.commit() + last_points = refresh_result["points"] + result.update(refresh_result) + result["gift_points_refresh_count"] = index + if target_points is None or (last_points is not None and last_points >= target_points): + result["gift_points_confirmed"] = target_points is None or last_points is not None + return refresh_result + if index < DOUYU_GIFT_POINTS_REFRESH_TIMES: + self._update_task_progress( + db, + task, + "running", + f"赠送精英令成功,等待积分同步(当前 {last_points if last_points is not None else '-'},预期 {target_points})", + result, + ) + if self._stop.wait(DOUYU_GIFT_POINTS_REFRESH_INTERVAL): + break + + result["gift_points_confirmed"] = False + result["points"] = last_points + return refresh_result + def _task_payload(self, task: DouyuTask) -> dict: result = task.result if isinstance(task.result, dict) else {} payload = result.get("payload") if isinstance(result.get("payload"), dict) else {} @@ -1088,20 +1135,41 @@ class DouyuBatchRunner: payload = self._task_payload(task) gift_count = int(payload.get("gift_count") or payload.get("count") or 1) client = self._client(cookie) + ctn = None + baseline_points = account.points + try: + ctn = client.acf_ccn(refresh_subscribe=False) + baseline_result = self._refresh_account_points(client, account, cookie, ctn=ctn) + db.commit() + baseline_points = baseline_result["points"] + except Exception as exc: + self._push_log("warning", f"赠送精英令前刷新积分失败: {exc}") result = client.donate_elite_gift( gift_count=gift_count, room_id=str(payload.get("room_id") or config["room_id"]), gift_id=str(payload.get("gift_id") or config["gift_id"]), skin_id=str(payload.get("skin_id") or config["skin_id"]), ) + result["gift_points_baseline"] = baseline_points refresh_errors = [] try: result.update(self._refresh_account_gold_balance(client, account)) except Exception as exc: refresh_errors.append(f"鱼翅余额: {exc}") try: - ctn = client.acf_ccn(refresh_subscribe=False) - result.update(self._refresh_account_points(client, account, cookie, ctn=ctn)) + result.update( + self._refresh_points_after_elite_gift( + db, + task, + account, + client, + cookie, + ctn, + result, + baseline_points, + gift_count, + ) + ) except Exception as exc: refresh_errors.append(f"积分: {exc}") if refresh_errors: @@ -1113,6 +1181,8 @@ class DouyuBatchRunner: message += f",鱼翅余额: {account.gold_balance}" if account.points is not None: message += f",积分: {account.points}" + if result.get("gift_points_target") is not None and not result.get("gift_points_confirmed"): + message += f"(未确认涨到 {result['gift_points_target']})" self._mark_task(db, task, "success", message, result) def _execute_query_points(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index e90c3d2..048c56c 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -40,6 +40,8 @@ const QUICK_ACTIONS = [ const BIND_ACTION_KEYS = ['get_bind_qr', 'confirm_bind', 'query_game_name', 'query_change_bind_time']; const QUERY_ACTION_KEYS = ['query_points', 'query_limited_goods', 'query_gold_balance']; +const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; +const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; const STATUS_COLORS: Record = { planned: 'default', pending: 'default', running: 'processing', @@ -98,6 +100,12 @@ function hasPaymentQrcode(task: DouyuTaskItem | null | undefined): boolean { return Boolean(task && ['create_elite_qr', 'create_gold_qr'].includes(task.task_type) && resultText(task.result, 'pay_url')); } +function paymentArrived(task: DouyuTaskItem | null | undefined): boolean { + if (!task || !['create_elite_qr', 'create_gold_qr'].includes(task.task_type)) return false; + if (task.status !== 'success') return false; + return task.result?.gold_recharged === true || task.result?.elite_opened === true; +} + function bindPhaseText(phase: string, polling: boolean): string { if (phase === 'role_ready') return '已识别角色,待确认'; if (phase === 'change_waiting') return '换绑冷却中'; @@ -136,6 +144,11 @@ function resultNumber(result: Record | null | undefined, key: s return null; } +function savedPositiveInteger(key: string, fallback = 1): number { + const value = Number(localStorage.getItem(key)); + return Number.isInteger(value) && value >= 1 ? value : fallback; +} + export default function DouyuTasksPage() { const { can } = usePermissions(); const [accounts, setAccounts] = useState([]); @@ -148,8 +161,8 @@ export default function DouyuTasksPage() { const [runningBatchId, setRunningBatchId] = useState(''); const [configOpen, setConfigOpen] = useState(false); const [concurrency, setConcurrency] = useState(3); - const [goldAmount, setGoldAmount] = useState(1); - const [giftCount, setGiftCount] = useState(1); + const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY)); + const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY)); const [selectedGoodsId, setSelectedGoodsId] = useState(''); const [accountSearch, setAccountSearch] = useState(''); @@ -242,6 +255,14 @@ export default function DouyuTasksPage() { useEffect(() => { loadData(); }, [loadData]); + useEffect(() => { + localStorage.setItem(DOUYU_GOLD_AMOUNT_STORAGE_KEY, String(goldAmount)); + }, [goldAmount]); + + useEffect(() => { + localStorage.setItem(DOUYU_GIFT_COUNT_STORAGE_KEY, String(giftCount)); + }, [giftCount]); + const hasActiveTasks = useMemo( () => tasks.some((task) => ['pending', 'running', 'planned'].includes(task.status)), [tasks], @@ -279,7 +300,7 @@ export default function DouyuTasksPage() { useEffect(() => { if (!autoOpenPayReady.current || payTask) return; const nextPayTask = tasks - .filter((task) => hasPaymentQrcode(task) && !autoOpenedPayTaskIds.current.has(task.id)) + .filter((task) => hasPaymentQrcode(task) && !paymentArrived(task) && !autoOpenedPayTaskIds.current.has(task.id)) .sort((a, b) => b.id - a.id)[0]; if (nextPayTask) openPayTask(nextPayTask); }, [openPayTask, payTask, tasks]); @@ -294,7 +315,12 @@ export default function DouyuTasksPage() { useEffect(() => { if (!payTask) return; const latest = tasks.find((task) => task.id === payTask.id); - if (latest && latest !== payTask) setPayTask(latest); + if (!latest) return; + if (paymentArrived(latest)) { + setPayTask(null); + return; + } + if (latest !== payTask) setPayTask(latest); }, [payTask, tasks]); const batchBusy = Boolean(runningBatchId || logs.connected || hasActiveTasks); @@ -749,13 +775,14 @@ export default function DouyuTasksPage() { gap: 12, overflow: 'hidden', }}> {/* Left: accounts + tasks */} -
+
已选 {selectedIds.length}/{accounts.length}} - styles={{ body: { flex: 1, overflow: 'auto', padding: 8 } }} + style={{ flex: '1 1 0', minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }} > - + } @@ -773,6 +800,7 @@ export default function DouyuTasksPage() { ({ onContextMenu: (e) => handleRowContextMenu(record, e), style: { cursor: 'context-menu' }, @@ -798,12 +826,14 @@ export default function DouyuTasksPage() { 失败 {tasks.filter((t) => ['failed', 'error'].includes(t.status)).length} } - styles={{ body: { padding: 8 } }} + style={{ flex: '0 0 300px', minHeight: 260, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} + styles={{ body: { flex: 1, minHeight: 0, padding: 8, overflow: 'hidden', display: 'flex', flexDirection: 'column' } }} >