Files
live-hub-py/web/backend/services/login_service.py
T
yml2213 e478ac4d09 简化登录流程:砍掉代理池,极验不内部切代理
- 新建 ProxyFetcher 替代 ProxyManager,每次从API取1个代理,无池无锁无冷却
- 极验最多3次尝试,失败直接抛异常回到login整体重试取新代理
- 删掉极验内部的 refresh_proxy/proxy_switches/_soft_fail_streak 逻辑
- login整体重试时取新代理从头走完整流程
- 代理验证超时从(5,8)缩短为(3,5),白名单同步后立即重试不等待
- login_service 删掉60行代理池预检/等待代码
- 修复邮件验证码日期比较:Roundcube分钟精度与秒级时间戳对齐
2026-06-24 15:37:48 +08:00

301 lines
12 KiB
Python

"""登录服务:复用 core/ 核心模块,在线程池中并发执行登录并推送日志。"""
import asyncio
import threading
import time
import uuid
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy.orm import Session
from core.douyu import DouyuLogin
from core.douyu.proxy_fetcher import ProxyFetcher
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
class LoginBatchRunner:
"""批量登录执行器,在线程中运行,通过 ThreadPoolExecutor 并发登录多个账号。"""
def __init__(
self,
db: Session,
account_ids: list[int],
created_by: int,
creator_permissions: list[str],
max_proxy_retries: int = 0,
max_login_retries: int = 0,
max_total_time: float = 0,
proxy_config: Optional[ProxyConfigModel] = None,
log_queue: Optional[asyncio.Queue] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
concurrency: int = 3,
):
self.db = db
self.account_ids = account_ids
self.created_by = created_by
self.creator_permissions = creator_permissions
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
self.batch_id = uuid.uuid4().hex[:12]
self.concurrency = max(1, min(concurrency, 10)) # 限制 1-10
self._stop = threading.Event()
self._counter_lock = threading.Lock()
self._completed = 0
# 共享代理获取器(无池,每次取新代理)
self._shared_proxy_fetcher = None
if proxy_config and proxy_config.enabled and proxy_config.api_url:
wl_platform = "xiequ"
wl_credentials = None
if proxy_config.whitelist_enabled:
wl_platform = getattr(proxy_config, 'whitelist_platform', None) or "xiequ"
wl_credentials = getattr(proxy_config, 'whitelist_credentials', None)
# 向后兼容
if not wl_credentials and proxy_config.whitelist_uid and proxy_config.whitelist_ukey:
wl_platform = "xiequ"
wl_credentials = {"uid": proxy_config.whitelist_uid, "ukey": proxy_config.whitelist_ukey}
self._shared_proxy_fetcher = ProxyFetcher(
api_url=proxy_config.api_url,
whitelist_platform=wl_platform,
whitelist_credentials=wl_credentials,
)
def stop(self):
self._stop.set()
def _sleep_or_stop(self, seconds: float) -> bool:
"""可中断等待;返回 True 表示收到停止信号。"""
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if self._stop.is_set():
return True
time.sleep(min(0.2, deadline - time.monotonic()))
return self._stop.is_set()
def _push_log(self, level: str, message: str):
if self.log_queue and self.loop:
asyncio.run_coroutine_threadsafe(
self.log_queue.put({"level": level, "message": message}),
self.loop,
)
def _resolve_static_proxy(self) -> tuple[Optional[dict], str]:
"""解析静态代理配置。"""
if not self.proxy_config or not self.proxy_config.enabled:
return None, ''
# 静态代理
if self.proxy_config.http or self.proxy_config.https:
proxy_url = self.proxy_config.http or self.proxy_config.https
return {'http': proxy_url, 'https': proxy_url}, f'使用静态代理: {proxy_url}'
# API代理:由 DouyuLogin 通过 proxy_fetcher 内部管理
return None, ''
def _execute_one(self, task_id: int, acc_info: dict, total: int):
"""在独立线程中执行单个账号登录,使用独立的 DB 会话。"""
if self._stop.is_set():
self._push_log("warning", f"任务已停止,跳过: {acc_info['username']}")
return
worker_db = SessionLocal()
try:
task = worker_db.query(LoginTask).filter(LoginTask.id == task_id).first()
if not task:
return
task.status = "running"
worker_db.commit()
with self._counter_lock:
self._completed += 1
current = self._completed
self._push_log("info", f"[{current}/{total}] 开始登录: {acc_info['username']}")
# 解析代理配置
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 (self.proxy_config.http or self.proxy_config.https) and not self._shared_proxy_fetcher and not proxy_dict:
task.status = "error"
task.message = "代理不可用: 未配置代理"
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
self._push_log("error", f"[{current}] {acc_info['username']} 代理不可用")
return
try:
account = SimpleNamespace(
username=acc_info["username"],
password=acc_info["password"],
email=acc_info["email"],
email_password=acc_info["email_password"],
email_imap_server=acc_info["email_imap_server"] or "",
email_imap_port=acc_info["email_imap_port"] or 993,
email_imap_ssl=acc_info["email_imap_ssl"],
)
loginer = DouyuLogin(
account,
proxy=proxy_dict,
max_proxy_retries=self.max_proxy_retries,
max_login_retries=self.max_login_retries,
max_total_time=self.max_total_time,
proxy_fetcher=self._shared_proxy_fetcher,
stop_event=self._stop,
)
result = loginer.login()
if result.success:
task.status = "success"
task.cookie = result.cookie
task.message = result.message or "登录成功"
self._push_log("success", f"[{current}] {acc_info['username']} {task.message}")
else:
task.status = "failed"
task.message = result.message
self._push_log("error", f"[{current}] {acc_info['username']} 登录失败: {result.message}")
except Exception as e:
task.status = "error"
task.message = str(e)
self._push_log("error", f"[{current}] {acc_info['username']} 登录异常: {e}")
task.finished_at = datetime.now(timezone.utc)
worker_db.commit()
finally:
worker_db.close()
def run(self):
"""在线程中执行批量登录。"""
batch_id = self.batch_id
concurrency = self.concurrency
self._push_log("info", f"批量登录任务 {batch_id} 开始,共 {len(self.account_ids)} 个账号,并发数: {concurrency}")
try:
# 创建或复用任务记录(顺序执行,线程安全)
task_infos: list[dict] = [] # {task_id, acc_info}
for aid in self.account_ids:
acc = self.db.query(AccountModel).filter(AccountModel.id == aid).first()
if not acc:
continue
# 权限检查:客服只能跑分配给自己的
if "login:view_all" not in self.creator_permissions:
if acc.assigned_to != self.created_by:
self._push_log("warning", f"跳过无权账号: {acc.username}")
continue
# 复用该账号最近一条失败任务记录,避免重复产生多条
existing_task = (
self.db.query(LoginTask)
.filter(LoginTask.account_id == aid, LoginTask.status.in_(["failed", "error"]))
.order_by(LoginTask.id.desc())
.first()
)
if existing_task:
existing_task.batch_id = batch_id
existing_task.status = "pending"
existing_task.cookie = ""
existing_task.message = ""
existing_task.finished_at = None
task = existing_task
else:
task = LoginTask(
batch_id=batch_id,
account_id=aid,
status="pending",
created_by=self.created_by,
)
self.db.add(task)
self.db.flush() # 获取 task.id
task_infos.append({
"task_id": task.id,
"acc_info": {
"username": acc.username,
"password": acc.password,
"email": acc.email,
"email_password": acc.email_password,
"email_imap_server": acc.email_imap_server or "",
"email_imap_port": acc.email_imap_port or 993,
"email_imap_ssl": acc.email_imap_ssl if acc.email_imap_ssl is not None else True,
},
})
self.db.commit()
total = len(task_infos)
if total == 0:
self._push_log("warning", "没有可执行的账号")
self._push_log("result", "")
return
# 并发执行登录,每个账号独立获取代理
with ThreadPoolExecutor(max_workers=concurrency) as executor:
futures = []
for item in task_infos:
if self._stop.is_set():
self._push_log("warning", "任务已停止,跳过剩余账号")
break
future = executor.submit(
self._execute_one,
item["task_id"],
item["acc_info"],
total,
)
futures.append(future)
# 等待所有任务完成
for future in as_completed(futures):
try:
future.result()
except Exception as e:
self._push_log("error", f"Worker 异常: {e}")
self._push_log("info", f"批量登录任务 {batch_id} 完成")
self._push_log("result", "")
finally:
# 确保 DB Session 被关闭,避免连接泄漏
self.db.close()
class BatchRegistry:
"""管理运行中的登录批次状态。"""
def __init__(self):
self._batches: dict[str, dict] = {}
def register(self, batch_id: str, log_queue: asyncio.Queue,
loop: asyncio.AbstractEventLoop, runner: LoginBatchRunner):
self._batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,
"runner": runner,
}
def get(self, batch_id: str):
return self._batches.get(batch_id)
def pop(self, batch_id: str):
return self._batches.pop(batch_id, None)
# 模块级单例
batch_registry = BatchRegistry()
# 在模块末尾导入 SessionLocal(避免循环导入)
from ..database import SessionLocal