feat: 登录流程整体重试机制 + 代理池耗尽等待恢复

- DouyuLogin.login() 添加整体重试循环,失败时换新代理从头重跑
  - 新增 max_login_retries(默认3次)和 max_total_time(默认300秒)
  - 重试前重置 session cookies + 换新代理,避免残留状态
  - 成功后归还代理到池;失败时标记代理坏
- 代理池耗尽时不再立即放弃账号,等待最多60秒恢复后再试
- API代理模式改为 DouyuLogin 内部通过 proxy_manager 自治管理
  登录过程中的代理切换(极验、整体重试)不再由服务层预取
- Schema/LoginBatchRequest 添加 max_login_retries 和 max_total_time
- 前端 API 调用改为对象参数形式,透传新参数
This commit is contained in:
yml2213
2026-06-23 22:06:00 +08:00
parent 0a68632c8a
commit cc30697c97
6 changed files with 160 additions and 54 deletions
+2
View File
@@ -56,6 +56,8 @@ async def create_batch(
creator_permissions=get_user_permissions(current),
max_geetest_retries=req.max_geetest_retries,
max_proxy_retries=req.max_proxy_retries,
max_login_retries=req.max_login_retries,
max_total_time=req.max_total_time,
proxy_config=proxy,
log_queue=log_queue,
loop=loop,
+2
View File
@@ -126,6 +126,8 @@ class LoginBatchRequest(BaseModel):
account_ids: list[int]
max_geetest_retries: int = 5
max_proxy_retries: int = 10 # 代理切换次数,0=无限切换直到成功
max_login_retries: int = 3 # 登录整体重试次数(换代理从头重跑)
max_total_time: float = 300 # 单账号登录总时长上限(秒),超时则放弃
concurrency: int = 3 # 并发数,1-10
+65 -18
View File
@@ -2,6 +2,7 @@
import asyncio
import threading
import time
import uuid
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -18,6 +19,9 @@ from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyCon
class LoginBatchRunner:
"""批量登录执行器,在线程中运行,通过 ThreadPoolExecutor 并发登录多个账号。"""
# 代理池耗尽时等待恢复的最大秒数
_PROXY_WAIT_MAX = 60
def __init__(
self,
db: Session,
@@ -26,6 +30,8 @@ class LoginBatchRunner:
creator_permissions: list[str],
max_geetest_retries: int = 5,
max_proxy_retries: int = 10,
max_login_retries: int = 3,
max_total_time: float = 300,
proxy_config: Optional[ProxyConfigModel] = None,
log_queue: Optional[asyncio.Queue] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
@@ -37,6 +43,8 @@ class LoginBatchRunner:
self.creator_permissions = creator_permissions
self.max_geetest_retries = max_geetest_retries
self.max_proxy_retries = max_proxy_retries
self.max_login_retries = max_login_retries
self.max_total_time = max_total_time
self.proxy_config = proxy_config
self.log_queue = log_queue
self.loop = loop
@@ -67,13 +75,13 @@ class LoginBatchRunner:
self.loop,
)
def _resolve_proxy(self) -> tuple[Optional[dict], str]:
def _resolve_static_proxy(self) -> tuple[Optional[dict], str]:
"""
解析代理配置,返回 (proxy_dict, message)
解析静态代理配置(仅处理无代理和静态代理场景)
- 静态代理:直接返回 dict
- API代理:通过共享 ProxyManager(带已验证代理池缓存)获取,自动同步白名单
- 无代理:返回 (None, '')
API代理由 DouyuLogin 通过 proxy_manager 内部管理,
不在此处预先获取——登录过程中的代理切换(极验失败、整体重试)
都在 DouyuLogin 内部自治完成。
"""
if not self.proxy_config or not self.proxy_config.enabled:
return None, ''
@@ -83,13 +91,7 @@ class LoginBatchRunner:
proxy_url = self.proxy_config.http or self.proxy_config.https
return {'http': proxy_url, 'https': proxy_url}, f'使用静态代理: {proxy_url}'
# API代理:通过共享代理管理器获取(优先从已验证代理池复用)
if self._shared_proxy_manager:
proxy_url = self._shared_proxy_manager.get_proxy()
if proxy_url:
return {'http': proxy_url, 'https': proxy_url}, f'使用API代理: {proxy_url}'
return None, '代理不可用'
# API代理:不在此处获取,由 DouyuLogin 通过 proxy_manager 内部管理
return None, ''
def _execute_one(self, task_id: int, acc_info: dict, total: int):
@@ -113,18 +115,61 @@ class LoginBatchRunner:
self._push_log("info", f"[{current}/{total}] 开始登录: {acc_info['username']}")
# 每个账号独立获取代理
proxy_dict, proxy_msg = self._resolve_proxy()
# 解析代理配置
proxy_dict, proxy_msg = self._resolve_static_proxy()
if proxy_msg:
self._push_log("info", f"[{current}] {proxy_msg}")
# 代理预检失败时,该账号标记失败但不终止整个批次
if self.proxy_config and self.proxy_config.enabled and not proxy_dict:
# API代理模式下,验证代理池是否有可用代理;池空时等待恢复(最多 _PROXY_WAIT_MAX 秒)
# 注意:不预取代理传给 DouyuLogin,让 DouyuLogin 通过 proxy_manager 内部自治管理
is_api_proxy = (
self.proxy_config
and self.proxy_config.enabled
and not (self.proxy_config.http or self.proxy_config.https)
and self._shared_proxy_manager
)
if is_api_proxy:
# 先验证代理池是否有可用代理(获取后立即归还,不占用)
test_proxy = self._shared_proxy_manager.get_proxy()
if test_proxy:
self._shared_proxy_manager.release_proxy(test_proxy)
self._push_log("info", f"[{current}] 代理池可用,由 DouyuLogin 内部管理代理获取与切换")
else:
# 代理池暂时耗尽,等待冷却代理恢复或新代理入池
self._push_log("warning", f"[{current}] 代理池暂时耗尽,等待恢复...")
pool_available = False
for wait_sec in range(0, self._PROXY_WAIT_MAX, 10):
if self._stop.is_set():
task.status = "error"
task.message = "任务已停止"
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
return
time.sleep(10)
test_proxy = self._shared_proxy_manager.get_proxy()
if test_proxy:
self._shared_proxy_manager.release_proxy(test_proxy)
pool_available = True
self._push_log("info", f"[{current}] 代理池恢复,由 DouyuLogin 内部管理代理")
break
remaining = self._PROXY_WAIT_MAX - wait_sec - 10
self._push_log("info", f"[{current}] 代理池仍为空,继续等待... (剩余 {remaining}s)")
if not pool_available:
task.status = "error"
task.message = f"代理池耗尽,等待 {self._PROXY_WAIT_MAX}s 后仍无可用代理"
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
self._push_log("error", f"[{current}] {acc_info['username']} 代理池耗尽,放弃")
return
# 静态代理启用但配置为空 → 不可用
if self.proxy_config and self.proxy_config.enabled and not (self.proxy_config.http or self.proxy_config.https) and not self._shared_proxy_manager and not proxy_dict:
task.status = "error"
task.message = f"代理不可用: {proxy_msg}"
task.message = "代理不可用: 未配置代理"
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
self._push_log("error", f"[{current}] {acc_info['username']} 代理不可用: {proxy_msg}")
self._push_log("error", f"[{current}] {acc_info['username']} 代理不可用")
return
try:
@@ -143,6 +188,8 @@ class LoginBatchRunner:
proxy=proxy_dict,
max_geetest_retries=self.max_geetest_retries,
max_proxy_retries=self.max_proxy_retries,
max_login_retries=self.max_login_retries,
max_total_time=self.max_total_time,
proxy_manager=self._shared_proxy_manager,
)
result = loginer.login()