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:
yml2213
2026-10-03 10:50:23 +08:00
parent 1722b0a81b
commit 8310a9115e
4 changed files with 608 additions and 19 deletions
+30 -7
View File
@@ -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="取消"