增加重试
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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}`),
|
||||||
|
|||||||
@@ -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 }}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user