feat(cookies): CK 有效性检测(双接口) + 结果持久化

- 检测: 鱼丸余额(fishBall) + 用户等级(userLevelDetail)双接口均通过才判有效,
  附带鱼丸数/昵称/等级, 失败时分别说明原因, 8 并发批量检测
- 持久化: login_tasks 新增 ck_check_status/ck_check_result/ck_checked_at,
  刷新翻页不丢失; 列表与详情接口返回检测字段
- 前端: 新增"有效性"列(有效/无效+鱼丸昵称tooltip)与独立"检测时间"列,
  行内/选中批量检测按钮(cookie:view 权限), 加载时初始化历史检测结果
This commit is contained in:
yml2213
2026-08-06 20:58:23 +08:00
parent a515ea6e0a
commit b962f63bd2
6 changed files with 320 additions and 6 deletions
+3 -1
View File
@@ -1,5 +1,5 @@
import api from './client';
import type { BasicSummary, CookieItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
import type { BasicSummary, CookieCheckResult, CookieItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
export const cookieApi = {
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
@@ -8,6 +8,8 @@ export const cookieApi = {
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
exportCsv: (format?: string) => api.get<Blob, Blob>('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
check: (ids: number[]) =>
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
deleteBatch: (ids: number[]) => api.delete<MessageDeletedResponse, MessageDeletedResponse>('/cookies/batch', { params: { task_ids: ids.join(',') } }),
};
+18
View File
@@ -204,6 +204,24 @@ export interface CookieItem {
cookie: string;
cookie_preview: string;
account_password: string;
ck_check_status: string;
ck_check_result: {
fish_ball?: number | null;
nickname?: string | null;
level?: number | null;
message?: string;
} | null;
ck_checked_at: string | null;
}
export interface CookieCheckResult {
id: number;
valid: boolean;
message: string;
fish_ball: number | null;
nickname: string | null;
level: number | null;
checked_at: string;
}
// ==================== Douyu Activity ====================
+107 -4
View File
@@ -1,8 +1,8 @@
import { useEffect, useState, useCallback } from 'react';
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd';
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd';
import { message } from '../utils/antdMessage';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi, type BasicSummary, type CookieItem } from '../api/modules';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined } from '@ant-design/icons';
import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -50,11 +50,43 @@ export default function CookiePage() {
return v ? Number(v) || 20 : 20;
});
const [currentPage, setCurrentPage] = useState(1);
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
const [checkResults, setCheckResults] = useState<Map<number, CookieCheckResult>>(new Map());
const { can } = usePermissions();
const canView = can('cookie:view');
const canExport = can('cookie:export');
const handleCheck = async (ids: number[]) => {
if (ids.length === 0) return;
setCheckingIds((prev) => new Set([...prev, ...ids]));
try {
const res = await cookieApi.check(ids);
const map = new Map(checkResults);
for (const item of res.results) map.set(item.id, item);
setCheckResults(map);
const validCount = res.results.filter((item) => item.valid).length;
message.success(`检测完成: ${validCount}/${res.results.length} 条有效`);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setCheckingIds((prev) => {
const next = new Set(prev);
for (const id of ids) next.delete(id);
return next;
});
}
};
const handleCheckSelected = () => {
const ids = selectedRowKeys.map((k) => Number(k));
if (ids.length === 0) {
message.warning('请先选择要检测的 Cookie');
return;
}
void handleCheck(ids);
};
const loadCookies = useCallback(async () => {
setLoading(true);
try {
@@ -66,6 +98,23 @@ export default function CookiePage() {
});
setCookies(data.items);
setTotal(data.total);
// 合并已持久化的检测结果(刷新后仍保留)
setCheckResults((prev) => {
const next = new Map(prev);
for (const item of data.items) {
if (!item.ck_check_status) continue;
next.set(item.id, {
id: item.id,
valid: item.ck_check_status === 'valid',
message: item.ck_check_result?.message ?? '',
fish_ball: item.ck_check_result?.fish_ball ?? null,
nickname: item.ck_check_result?.nickname ?? null,
level: item.ck_check_result?.level ?? null,
checked_at: item.ck_checked_at ?? '',
});
}
return next;
});
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -191,6 +240,40 @@ export default function CookiePage() {
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{val}</span>;
},
},
{
title: '有效性',
width: 90,
align: 'center' as const,
render: (_: unknown, record: CookieItem) => {
if (checkingIds.has(record.id)) {
return <Tag color="processing" icon={<LoadingOutlined />}>...</Tag>;
}
const result = checkResults.get(record.id);
if (!result) return <Text type="secondary" style={{ fontSize: 12 }}></Text>;
const detail = [
result.fish_ball != null ? `鱼丸: ${result.fish_ball}` : '',
result.nickname ? `昵称: ${result.nickname}${result.level != null ? ` (Lv.${result.level})` : ''}` : '',
result.message && !result.valid ? result.message : '',
].filter(Boolean).join(' | ');
const tag = result.valid
? <Tag color="success"></Tag>
: <Tag color="error"></Tag>;
return detail
? <Tooltip title={detail}>{tag}</Tooltip>
: tag;
},
},
{
title: '检测时间',
width: 170,
align: 'center' as const,
render: (_: unknown, record: CookieItem) => {
const checkedAt = checkResults.get(record.id)?.checked_at || record.ck_checked_at;
return checkedAt
? <Text type="secondary" style={{ fontSize: 12 }}>{formatTime(checkedAt)}</Text>
: <Text type="secondary" style={{ fontSize: 12 }}>-</Text>;
},
},
{
title: '时间',
dataIndex: 'created_at',
@@ -200,7 +283,7 @@ export default function CookiePage() {
},
{
title: '操作',
width: 130,
width: 190,
align: 'center' as const,
fixed: 'right' as const,
render: (_: unknown, record: CookieItem) => (
@@ -212,6 +295,17 @@ export default function CookiePage() {
>
</Button>
{canView && (
<Button
size="small"
icon={checkingIds.has(record.id) ? <LoadingOutlined /> : undefined}
loading={checkingIds.has(record.id)}
disabled={checkingIds.has(record.id)}
onClick={() => handleCheck([record.id])}
>
</Button>
)}
{canExport && (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />} />
@@ -234,6 +328,15 @@ export default function CookiePage() {
>
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
{canView && (
<Button
icon={<LoadingOutlined />}
onClick={handleCheckSelected}
disabled={selectedRowKeys.length === 0}
>
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
)}
{canExport && (
<Popconfirm
title={`确认删除选中的 ${selectedRowKeys.length} 条 Cookie`}