From 8fe9fdfd8bbc4ab65507379813be277ee8574a7d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 30 Aug 2026 11:19:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyu):=20=E6=96=97=E9=B1=BC=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=8E=A5=E5=85=A5=E4=BB=A3=E7=90=86=20=E2=80=94=20?= =?UTF-8?q?=E5=BA=94=E5=AF=B9=E5=85=91=E6=8D=A2=20IP=20=E9=A2=91=E6=8E=A7?= =?UTF-8?q?=20(=E7=B2=BE=E8=8B=B1=E6=89=8B=E5=86=8C=E7=81=AB=E7=88=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配置源 = 代理配置页 (与 CK 检测/虎牙注册同款), 开关在代理配置页: - 静态模式: 手填 http/https → 全任务同一代理 - API 模式: 每任务取新代理 (ProxyFetcher, 白名单同步, 1.5s 间隔限速), 失败降级直连并推送告警 - DouyuActivityClient 支持 proxies (session 级, 覆盖全部请求点; trust_env=False 维持不变, 不受环境变量影响) 按拆分后新结构落在 douyu_runner_core.py; 触及精英手册/和平小店/电竞等 全部斗鱼任务出网。三种模式行为断言 + 94 单测通过 --- core/douyu/activity_client.py | 4 ++ web/backend/services/douyu_runner_core.py | 70 ++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index dfd37b0..a6c243e 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -81,11 +81,15 @@ class DouyuActivityClient: *, logger: Callable[[str], None] | None = None, timeout: tuple[float, float] = (8, 20), + proxies: dict[str, str] | None = None, ): self.cookie = normalize_douyu_cookie(cookie) self.timeout = timeout self.logger = logger or (lambda _msg: None) self.session = requests.Session() + # trust_env=False: 不读环境变量代理, 只用显式传入的 proxies (代理配置页控制) + if proxies: + self.session.proxies.update(proxies) self.session.trust_env = False self.session.headers.update({ "User-Agent": PC_UA, diff --git a/web/backend/services/douyu_runner_core.py b/web/backend/services/douyu_runner_core.py index a14f099..be06d90 100644 --- a/web/backend/services/douyu_runner_core.py +++ b/web/backend/services/douyu_runner_core.py @@ -12,12 +12,24 @@ from loguru import logger from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient -from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, DouyuXpdGoodsSnapshot +from core.douyu.proxy_fetcher import ProxyFetcher +from ..models import ( + Account, + DouyuEsportsGoodsSnapshot, + DouyuGoodsSnapshot, + DouyuTask, + DouyuXpdGoodsSnapshot, + ProxyConfig as ProxyConfigModel, +) from .douyu_service import DOUYU_CONFIG_FIELDS, douyu_config_value, ensure_douyu_config, douyu_task_payload # 支付/到账轮询(手册与充值共用) DOUYU_PAYMENT_POLL_SECONDS = 600 DOUYU_PAYMENT_POLL_INTERVAL = 5 +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .douyu_runner import DouyuBatchRunner class DouyuBatchRunnerCore: """斗鱼任务执行器公共基础:批次状态、日志、任务落库与客户端构造。""" @@ -41,6 +53,55 @@ class DouyuBatchRunnerCore: self._stop = threading.Event() self._counter_lock = threading.Lock() self._started = 0 + # 代理接入 (配置源 = 代理配置页, 与 CK 检测/虎牙注册同款): + # 开启后斗鱼任务(精英手册兑换/和平小店/电竞等)出网统一走代理, 应对 IP 频控 + self._proxy_cfg = db.query(ProxyConfigModel).first() if db else None + self._proxy_fetcher = self._create_proxy_fetcher() + self._static_proxies = self._resolve_static_proxies() + if self._static_proxies: + logger.info(f"[douyu] 任务将走静态代理: {self._static_proxies.get('https', '')}") + elif self._proxy_fetcher: + logger.info("[douyu] 任务将按任务从代理 API 取新代理") + + def _create_proxy_fetcher(self) -> ProxyFetcher | None: + """API 代理模式: 每个任务从代理 API 取 1 个新代理 (与虎牙注册链同款).""" + cfg = self._proxy_cfg + if not cfg or not cfg.enabled or not cfg.api_url: + return None + wl_platform = getattr(cfg, "whitelist_platform", None) or "xiequ" + wl_credentials = getattr(cfg, "whitelist_credentials", None) + if not wl_credentials and getattr(cfg, "whitelist_uid", "") and getattr(cfg, "whitelist_ukey", ""): + wl_credentials = {"uid": cfg.whitelist_uid, "ukey": cfg.whitelist_ukey} + return ProxyFetcher( + api_url=cfg.api_url, + whitelist_platform=wl_platform, + whitelist_credentials=wl_credentials if cfg.whitelist_enabled else None, + stop_event=self._stop, + ) + + def _resolve_static_proxies(self) -> dict[str, str] | None: + """静态代理模式: 代理配置页手填的 http/https 地址.""" + cfg = self._proxy_cfg + if not cfg or not cfg.enabled: + return None + http, https = (cfg.http or "").strip(), (cfg.https or "").strip() + if not http and not https: + return None + return {"http": http or https, "https": https or http} + + def _proxies_for_task(self) -> dict[str, str] | None: + """取本任务出网代理: 静态优先; API 模式每任务取新代理, 失败降级直连并告警.""" + if self._static_proxies: + return self._static_proxies + if self._proxy_fetcher: + try: + proxy_url = self._proxy_fetcher.fetch_new_proxy() + if proxy_url: + return {"http": proxy_url, "https": proxy_url} + self._push_log("warning", "代理 API 未返回可用代理, 本任务降级直连") + except Exception as exc: + self._push_log("warning", f"取代理失败, 本任务降级直连: {exc}") + return None def stop(self): self._stop.set() @@ -226,7 +287,12 @@ class DouyuBatchRunnerCore: return {**payload, **self.payload} def _client(self, cookie: str) -> DouyuActivityClient: - return DouyuActivityClient(cookie, logger=lambda msg: self._push_log("debug", msg)) + """任务 HTTP 客户端: 会话级代理 (覆盖精英手册/和平小店/电竞等全部请求).""" + return DouyuActivityClient( + cookie, + logger=lambda msg: self._push_log("debug", msg), + proxies=self._proxies_for_task(), + ) def _sleep_interruptible(self, seconds: float) -> bool: """分段睡眠,任务停止时提前返回;返回 False 表示已被停止。"""