增加重试

This commit is contained in:
yml2213
2026-06-22 15:45:48 +08:00
parent 2443d23316
commit 9fba12aad0
8 changed files with 199 additions and 60 deletions
Binary file not shown.
+1
View File
@@ -61,6 +61,7 @@ async def create_batch(
proxy_config=proxy, proxy_config=proxy,
log_queue=log_queue, log_queue=log_queue,
loop=loop, loop=loop,
concurrency=req.concurrency,
) )
batch_id = runner.batch_id batch_id = runner.batch_id
+1
View File
@@ -83,6 +83,7 @@ class AccountOut(BaseModel):
class LoginBatchRequest(BaseModel): class LoginBatchRequest(BaseModel):
account_ids: list[int] account_ids: list[int]
max_geetest_retries: int = 5 max_geetest_retries: int = 5
concurrency: int = 3 # 并发数,1-10
class LoginTaskOut(BaseModel): class LoginTaskOut(BaseModel):
+118 -47
View File
@@ -1,8 +1,9 @@
"""登录服务:复用 core/ 核心模块,在线程池中执行登录并推送日志。""" """登录服务:复用 core/ 核心模块,在线程池中并发执行登录并推送日志。"""
import asyncio import asyncio
import threading import threading
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -16,7 +17,7 @@ from ..permissions import has_permission
class LoginBatchRunner: class LoginBatchRunner:
"""批量登录执行器,在线程中运行,通过 asyncio.Queue 推送日志""" """批量登录执行器,在线程中运行,通过 ThreadPoolExecutor 并发登录多个账号"""
def __init__( def __init__(
self, self,
@@ -28,6 +29,7 @@ class LoginBatchRunner:
proxy_config: Optional[ProxyConfigModel] = None, proxy_config: Optional[ProxyConfigModel] = None,
log_queue: Optional[asyncio.Queue] = None, log_queue: Optional[asyncio.Queue] = None,
loop: Optional[asyncio.AbstractEventLoop] = None, loop: Optional[asyncio.AbstractEventLoop] = None,
concurrency: int = 3,
): ):
self.db = db self.db = db
self.account_ids = account_ids self.account_ids = account_ids
@@ -38,7 +40,10 @@ class LoginBatchRunner:
self.log_queue = log_queue self.log_queue = log_queue
self.loop = loop self.loop = loop
self.batch_id = uuid.uuid4().hex[:12] self.batch_id = uuid.uuid4().hex[:12]
self.concurrency = max(1, min(concurrency, 10)) # 限制 1-10
self._stop = threading.Event() self._stop = threading.Event()
self._counter_lock = threading.Lock()
self._completed = 0
def stop(self): def stop(self):
self._stop.set() self._stop.set()
@@ -86,13 +91,73 @@ class LoginBatchRunner:
return None, '' return None, ''
def _execute_one(self, task_id: int, acc_info: dict, proxy_dict: Optional[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']}")
try:
account = Account(
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,
)
loginer = DouyuLogin(
account,
proxy=proxy_dict,
max_geetest_retries=self.max_geetest_retries,
)
result = loginer.login()
if result.success:
task.status = "success"
task.cookie = result.cookie
task.message = "登录成功"
self._push_log("success", f"[{current}] {acc_info['username']} 登录成功")
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.utcnow()
worker_db.commit()
finally:
worker_db.close()
def run(self): def run(self):
"""在线程中执行批量登录。""" """在线程中执行批量登录。"""
batch_id = self.batch_id batch_id = self.batch_id
self._push_log("info", f"批量登录任务 {batch_id} 开始,共 {len(self.account_ids)} 个账号") concurrency = self.concurrency
self._push_log("info", f"批量登录任务 {batch_id} 开始,共 {len(self.account_ids)} 个账号,并发数: {concurrency}")
# 创建任务记录 # 创建任务记录(顺序执行,线程安全)
tasks = [] task_infos: list[dict] = [] # {task_id, acc_info}
for aid in self.account_ids: for aid in self.account_ids:
acc = self.db.query(AccountModel).filter(AccountModel.id == aid).first() acc = self.db.query(AccountModel).filter(AccountModel.id == aid).first()
if not acc: if not acc:
@@ -110,9 +175,26 @@ class LoginBatchRunner:
created_by=self.created_by, created_by=self.created_by,
) )
self.db.add(task) self.db.add(task)
tasks.append((task, acc)) 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,
},
})
self.db.commit() self.db.commit()
total = len(task_infos)
if total == 0:
self._push_log("warning", "没有可执行的账号")
self._push_log("result", "")
return
# 代理预检 # 代理预检
proxy_dict, proxy_msg = self._resolve_proxy() proxy_dict, proxy_msg = self._resolve_proxy()
@@ -122,57 +204,46 @@ class LoginBatchRunner:
# 如果启用了代理但预检失败,终止任务 # 如果启用了代理但预检失败,终止任务
if self.proxy_config and self.proxy_config.enabled and not proxy_dict: if self.proxy_config and self.proxy_config.enabled and not proxy_dict:
self._push_log("error", f"代理不可用,任务终止: {proxy_msg}") self._push_log("error", f"代理不可用,任务终止: {proxy_msg}")
for task, _ in tasks: for item in task_infos:
worker_db = SessionLocal()
try:
task = worker_db.query(LoginTask).filter(LoginTask.id == item["task_id"]).first()
if task:
task.status = "error" task.status = "error"
task.message = f"代理不可用: {proxy_msg}" task.message = f"代理不可用: {proxy_msg}"
task.finished_at = datetime.utcnow() task.finished_at = datetime.utcnow()
self.db.commit() worker_db.commit()
finally:
worker_db.close()
self._push_log("result", "")
return return
for i, (task, acc) in enumerate(tasks): # 并发执行登录
with ThreadPoolExecutor(max_workers=concurrency) as executor:
futures = []
for item in task_infos:
if self._stop.is_set(): if self._stop.is_set():
self._push_log("warning", "任务已停止") self._push_log("warning", "任务已停止,跳过剩余账号")
break break
future = executor.submit(
self._execute_one,
item["task_id"],
item["acc_info"],
proxy_dict,
total,
)
futures.append(future)
task.status = "running" # 等待所有任务完成
self.db.commit() for future in as_completed(futures):
self._push_log("info", f"[{i+1}/{len(tasks)}] 开始登录: {acc.username}")
try: try:
account = Account( future.result()
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,
)
loginer = DouyuLogin(
account,
proxy=proxy_dict,
max_geetest_retries=self.max_geetest_retries,
)
result = loginer.login()
if result.success:
task.status = "success"
task.cookie = result.cookie
task.message = "登录成功"
self._push_log("success", f"[{i+1}] {acc.username} 登录成功")
else:
task.status = "failed"
task.message = result.message
self._push_log("error", f"[{i+1}] {acc.username} 登录失败: {result.message}")
except Exception as e: except Exception as e:
task.status = "error" self._push_log("error", f"Worker 异常: {e}")
task.message = str(e)
self._push_log("error", f"[{i+1}] {acc.username} 登录异常: {e}")
task.finished_at = datetime.utcnow()
self.db.commit()
self._push_log("info", f"批量登录任务 {batch_id} 完成") self._push_log("info", f"批量登录任务 {batch_id} 完成")
self._push_log("result", "") self._push_log("result", "")
# 在模块末尾导入 SessionLocal(避免循环导入)
from ..database import SessionLocal
+2 -2
View File
@@ -50,8 +50,8 @@ export const accountApi = {
}; };
export const loginApi = { export const loginApi = {
createBatch: (account_ids: number[], max_geetest_retries?: number) => createBatch: (account_ids: number[], max_geetest_retries?: number, concurrency?: number) =>
api.post<any, any>('/login/batch', { account_ids, max_geetest_retries }), api.post<any, any>('/login/batch', { account_ids, max_geetest_retries, concurrency }),
listTasks: (batch_id?: string) => listTasks: (batch_id?: string) =>
api.get<any, any[]>('/login/tasks', { params: batch_id ? { batch_id } : {} }), api.get<any, any[]>('/login/tasks', { params: batch_id ? { batch_id } : {} }),
stop: (batch_id: string) => api.post<any, any>(`/login/stop/${batch_id}`), stop: (batch_id: string) => api.post<any, any>(`/login/stop/${batch_id}`),
+71 -5
View File
@@ -1,8 +1,8 @@
import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import { import {
Table, Button, Select, message, Tag, Space, Card, Row, Col, Statistic, Spin, Table, Button, Select, message, Tag, Space, Card, Row, Col, Statistic, Spin, InputNumber, Tooltip,
} from 'antd'; } from 'antd';
import { PlayCircleOutlined, StopOutlined, FilterOutlined } from '@ant-design/icons'; import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined } from '@ant-design/icons';
import { accountApi, loginApi } from '../api/modules'; import { accountApi, loginApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth'; import { getUser, hasPerm } from '../store/auth';
@@ -31,6 +31,7 @@ export default function LoginTasksPage() {
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]); const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
const [wsConnected, setWsConnected] = useState(false); const [wsConnected, setWsConnected] = useState(false);
const [selectedTags, setSelectedTags] = useState<string[]>([]); const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [concurrency, setConcurrency] = useState(3);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const user = getUser(); const user = getUser();
@@ -111,15 +112,16 @@ export default function LoginTasksPage() {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [batchId]); }, [batchId]);
const handleBatchLogin = async () => { // 共享的批量登录启动逻辑
if (selectedIds.length === 0) { const startBatch = async (accountIds: number[]) => {
if (accountIds.length === 0) {
message.warning('请选择账号'); message.warning('请选择账号');
return; return;
} }
setLoading(true); setLoading(true);
setLogs([]); setLogs([]);
try { try {
const result = await loginApi.createBatch(selectedIds); const result = await loginApi.createBatch(accountIds, 5, concurrency);
setBatchId(result.batch_id); setBatchId(result.batch_id);
message.success(`已创建登录任务,共 ${result.count} 个账号`); message.success(`已创建登录任务,共 ${result.count} 个账号`);
@@ -148,6 +150,27 @@ export default function LoginTasksPage() {
} }
}; };
const handleBatchLogin = () => startBatch(selectedIds);
// 重试当前批次所有失败的任务
const handleRetryFailed = () => {
const failedIds = tasks
.filter((t) => ['failed', 'error'].includes(t.status))
.map((t) => t.account_id);
if (failedIds.length === 0) {
message.info('没有失败的任务');
return;
}
startBatch(failedIds);
};
// 重试单个失败任务
const handleRetryOne = (taskId: number) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
startBatch([task.account_id]);
};
const handleStop = async () => { const handleStop = async () => {
if (batchId) { if (batchId) {
try { try {
@@ -172,6 +195,25 @@ export default function LoginTasksPage() {
}, },
{ title: '消息', dataIndex: 'message', ellipsis: true }, { title: '消息', dataIndex: 'message', ellipsis: true },
{ title: '时间', dataIndex: 'created_at', width: 180 }, { title: '时间', dataIndex: 'created_at', width: 180 },
{
title: '操作',
width: 80,
render: (_: any, record: any) => {
if (['failed', 'error'].includes(record.status) && !wsConnected) {
return (
<Button
type="link"
size="small"
icon={<ReloadOutlined />}
onClick={() => handleRetryOne(record.id)}
>
</Button>
);
}
return null;
},
},
]; ];
return ( return (
@@ -252,6 +294,17 @@ export default function LoginTasksPage() {
</> </>
)} )}
/> />
<Tooltip title="同时登录的账号数,1为顺序执行">
<ThunderboltOutlined style={{ color: '#888' }} />
</Tooltip>
<InputNumber
min={1}
max={10}
value={concurrency}
onChange={(v) => setConcurrency(v || 1)}
style={{ width: 60 }}
size="small"
/>
<Button <Button
type="primary" type="primary"
icon={<PlayCircleOutlined />} icon={<PlayCircleOutlined />}
@@ -291,6 +344,19 @@ export default function LoginTasksPage() {
<Card <Card
title="任务列表" title="任务列表"
size="small" size="small"
extra={
!wsConnected && failedCount > 0 && (
<Button
type="primary"
size="small"
icon={<ReloadOutlined />}
onClick={handleRetryFailed}
loading={loading}
>
({failedCount})
</Button>
)
}
style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }} style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
bodyStyle={{ flex: 1, overflow: 'auto', padding: 0 }} bodyStyle={{ flex: 1, overflow: 'auto', padding: 0 }}
> >