fix(login): 删除任务不再丢 CK —— 只删不持有 CK 的记录【数据安全】
事故:斗鱼账号的 CK **只存在 `login_tasks.cookie`**(`accounts` 表没有 cookie 列),删掉记录即永久丢失登录态。而列表显示的「状态」是最近一次尝试 (`attempt_status`),于是一条**显示「失败」但实际仍持有有效 CK** 的记录 (status=success + cookie + 上次尝试失败)会被「清空失败」当成垃圾删掉。 修复: - 批量删除只删空 cookie 的记录,持有 CK 的一律跳过,并在响应里报告跳过数; - 单条删除遇到持有 CK 的记录直接 409,提示先处理该账号的 CK; - 前端确认弹窗写明「持有 CK 的记录会跳过」。 顺带修统计口径:分页后 success/failed 只统计当前页,却和「共 N 个任务」 并排显示(曾出现「共 8508,成功 48」)。改走 /tasks/summary 的全局统计。 回归测试补 5 条,专门盯「显示失败但持有 CK」这个事故场景。
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
PlayCircleOutlined, StopOutlined, FilterOutlined, ReloadOutlined, DeleteOutlined,
|
||||
SettingOutlined, SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { accountApi, loginApi, type AccountItem, type LoginTaskItem } from '../api/modules';
|
||||
import { accountApi, loginApi, type AccountItem, type LoginTaskItem, type TaskSummary } from '../api/modules';
|
||||
import RealtimeLogPanel from '../components/RealtimeLogPanel';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||
@@ -256,6 +256,25 @@ export default function LoginTasksPage() {
|
||||
void loadTasks();
|
||||
}, [loadTags, loadTasks]);
|
||||
|
||||
// 全局统计:随任务刷新一起更新,保证与列表同频。
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const data = await loginApi.tasksSummary();
|
||||
if (alive) setTaskSummary(data);
|
||||
} catch {
|
||||
// 统计失败不影响列表使用
|
||||
}
|
||||
};
|
||||
void loadSummary();
|
||||
const timer = setInterval(loadSummary, 15000);
|
||||
return () => {
|
||||
alive = false;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [taskTotal]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void loadAccountsPage(1, accountSearch);
|
||||
@@ -396,8 +415,11 @@ export default function LoginTasksPage() {
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
|
||||
const successCount = tasks.filter((t) => t.status === 'success').length;
|
||||
const failedCount = tasks.filter((t) => ['failed', 'error'].includes(t.status)).length;
|
||||
// 全局统计走 /tasks/summary:分页后 tasks 只是当前页,
|
||||
// 用它算「成功/失败」会和「共 N 个任务」对不上(曾显示「共 8508,成功 48」)。
|
||||
const [taskSummary, setTaskSummary] = useState<TaskSummary | null>(null);
|
||||
const successCount = taskSummary?.success ?? 0;
|
||||
const failedCount = taskSummary?.failed ?? 0;
|
||||
const checkedCount = tasks.filter((t) => [
|
||||
'account_cancelled',
|
||||
'password_wrong',
|
||||
@@ -611,7 +633,8 @@ export default function LoginTasksPage() {
|
||||
<span>共 <b>{taskTotal}</b> 个任务</span>
|
||||
<span>成功 <b style={{ color: token.colorSuccess }}>{successCount}</b></span>
|
||||
<span>失败 <b style={{ color: token.colorError }}>{failedCount}</b></span>
|
||||
{checkedCount > 0 && <span>检测 <b>{checkedCount}</b></span>}
|
||||
{taskStatus && <span>(筛选后 {taskTotal} 条)</span>}
|
||||
{checkedCount > 0 && <span>本页检测 <b>{checkedCount}</b></span>}
|
||||
{effectiveBatchId && <span>批次: <b>{effectiveBatchId}</b></span>}
|
||||
<Select
|
||||
size="small"
|
||||
@@ -633,8 +656,8 @@ export default function LoginTasksPage() {
|
||||
<Popconfirm
|
||||
title={
|
||||
selectedAllMatching
|
||||
? `确定删除筛选条件下的全部 ${taskTotal} 个任务?(进行中的会跳过)`
|
||||
: `确定删除选中的 ${selectedRowKeys.length} 个任务?`
|
||||
? `删除筛选条件下的全部 ${taskTotal} 个任务?持有 CK 的记录与进行中的会跳过。`
|
||||
: `确定删除选中的 ${selectedRowKeys.length} 个任务?持有 CK 的记录会被跳过。`
|
||||
}
|
||||
onConfirm={handleDeleteSelected}
|
||||
okText="删除"
|
||||
@@ -654,7 +677,7 @@ export default function LoginTasksPage() {
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<Popconfirm
|
||||
title={`确定清空当前筛选下的失败任务?共 ${taskStatus === 'failed' ? taskTotal : failedCount} 个(进行中的会跳过)`}
|
||||
title="清空失败记录?只删除已不持有 CK 的历史记录,仍持有 CK 的账号登录态不受影响。"
|
||||
onConfirm={handleClearFailed}
|
||||
okText="清空"
|
||||
cancelText="取消"
|
||||
|
||||
Reference in New Issue
Block a user