一键检测重复-仓库管理
This commit is contained in:
@@ -245,6 +245,78 @@ def cookies_summary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/duplicates")
|
||||||
|
def find_duplicate_cookies(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(require_permission("cookie:view")),
|
||||||
|
):
|
||||||
|
"""检测 CK 管理中同一账号出现多次的记录,不返回 Cookie 敏感字段。"""
|
||||||
|
query = (
|
||||||
|
db.query(
|
||||||
|
LoginTask.id.label("cookie_id"),
|
||||||
|
LoginTask.account_id.label("account_id"),
|
||||||
|
LoginTask.batch_id.label("batch_id"),
|
||||||
|
LoginTask.finished_at.label("finished_at"),
|
||||||
|
Account.username.label("username"),
|
||||||
|
)
|
||||||
|
.join(Account, LoginTask.account_id == Account.id)
|
||||||
|
.filter(LoginTask.status == "success")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 与 CK 列表保持相同的数据可见范围,客服只能检测自己被分配的账号。
|
||||||
|
if not user_has_permission(current, "login:view_all"):
|
||||||
|
query = query.filter(Account.assigned_to == current.id)
|
||||||
|
|
||||||
|
rows = query.order_by(Account.username.asc(), LoginTask.finished_at.desc(), LoginTask.id.desc()).all()
|
||||||
|
grouped: dict[str, dict] = {}
|
||||||
|
for row in rows:
|
||||||
|
username = (row.username or "").strip()
|
||||||
|
if not username:
|
||||||
|
continue
|
||||||
|
account_key = username.casefold()
|
||||||
|
group = grouped.setdefault(
|
||||||
|
account_key,
|
||||||
|
{
|
||||||
|
"account_key": account_key,
|
||||||
|
"account_names": set(),
|
||||||
|
"account_ids": set(),
|
||||||
|
"cookie_ids": [],
|
||||||
|
"records": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
group["account_names"].add(username)
|
||||||
|
group["account_ids"].add(row.account_id)
|
||||||
|
group["cookie_ids"].append(row.cookie_id)
|
||||||
|
group["records"].append({
|
||||||
|
"id": row.cookie_id,
|
||||||
|
"account_id": row.account_id,
|
||||||
|
"batch_id": row.batch_id,
|
||||||
|
"finished_at": _fmt_dt(row.finished_at),
|
||||||
|
})
|
||||||
|
|
||||||
|
duplicate_groups = []
|
||||||
|
for group in grouped.values():
|
||||||
|
if len(group["cookie_ids"]) < 2:
|
||||||
|
continue
|
||||||
|
duplicate_groups.append({
|
||||||
|
"account_key": group["account_key"],
|
||||||
|
"account_names": sorted(group["account_names"]),
|
||||||
|
"cookie_count": len(group["cookie_ids"]),
|
||||||
|
"account_count": len(group["account_ids"]),
|
||||||
|
"cookie_ids": group["cookie_ids"],
|
||||||
|
"account_ids": sorted(group["account_ids"]),
|
||||||
|
"records": group["records"],
|
||||||
|
})
|
||||||
|
duplicate_groups.sort(key=lambda item: (-item["cookie_count"], item["account_key"]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"duplicate_groups": len(duplicate_groups),
|
||||||
|
"duplicate_rows": sum(item["cookie_count"] for item in duplicate_groups),
|
||||||
|
"groups": duplicate_groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/export")
|
@router.get("/export")
|
||||||
def export_cookies(
|
def export_cookies(
|
||||||
format: str = "csv",
|
format: str = "csv",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import api from './client';
|
import api from './client';
|
||||||
import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
|
import type { BasicSummary, CookieCheckResult, CookieDuplicateResponse, CookieItem, LoginTaskItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
|
||||||
|
|
||||||
export const cookieApi = {
|
export const cookieApi = {
|
||||||
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
||||||
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
||||||
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
||||||
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
||||||
|
duplicates: () => api.get<CookieDuplicateResponse, CookieDuplicateResponse>('/cookies/duplicates'),
|
||||||
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
||||||
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|||||||
@@ -225,6 +225,30 @@ export interface CookieCheckResult {
|
|||||||
checked_at: string;
|
checked_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CookieDuplicateRecord {
|
||||||
|
id: number;
|
||||||
|
account_id: number;
|
||||||
|
batch_id: string;
|
||||||
|
finished_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookieDuplicateGroup {
|
||||||
|
account_key: string;
|
||||||
|
account_names: string[];
|
||||||
|
cookie_count: number;
|
||||||
|
account_count: number;
|
||||||
|
cookie_ids: number[];
|
||||||
|
account_ids: number[];
|
||||||
|
records: CookieDuplicateRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookieDuplicateResponse {
|
||||||
|
success: boolean;
|
||||||
|
duplicate_groups: number;
|
||||||
|
duplicate_rows: number;
|
||||||
|
groups: CookieDuplicateGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Douyu Activity ====================
|
// ==================== Douyu Activity ====================
|
||||||
|
|
||||||
export interface DouyuTaskAccountItem {
|
export interface DouyuTaskAccountItem {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd';
|
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip, Modal, Alert, Empty } from 'antd';
|
||||||
import { message } from '../utils/antdMessage';
|
import { message } from '../utils/antdMessage';
|
||||||
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined, ScanOutlined } from '@ant-design/icons';
|
||||||
import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieItem } from '../api/modules';
|
import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieDuplicateGroup, type CookieDuplicateResponse, type CookieItem } from '../api/modules';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { formatTime } from '../utils/time';
|
import { formatTime } from '../utils/time';
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
@@ -65,6 +65,9 @@ export default function CookiePage() {
|
|||||||
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
||||||
const [checkResults, setCheckResults] = useState<Map<number, CookieCheckResult>>(new Map());
|
const [checkResults, setCheckResults] = useState<Map<number, CookieCheckResult>>(new Map());
|
||||||
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
||||||
|
const [duplicateOpen, setDuplicateOpen] = useState(false);
|
||||||
|
const [duplicateLoading, setDuplicateLoading] = useState(false);
|
||||||
|
const [duplicateResult, setDuplicateResult] = useState<CookieDuplicateResponse | null>(null);
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
|
|
||||||
const canView = can('cookie:view');
|
const canView = can('cookie:view');
|
||||||
@@ -153,6 +156,24 @@ export default function CookiePage() {
|
|||||||
void handleCheck(ids);
|
void handleCheck(ids);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDuplicateCheck = async () => {
|
||||||
|
setDuplicateOpen(true);
|
||||||
|
setDuplicateLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await cookieApi.duplicates();
|
||||||
|
setDuplicateResult(result);
|
||||||
|
if (result.duplicate_groups > 0) {
|
||||||
|
message.warning(`发现 ${result.duplicate_groups} 个重复账号,共 ${result.duplicate_rows} 条 CK 记录`);
|
||||||
|
} else {
|
||||||
|
message.success('未发现重复账号');
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.error(getErrorMessage(e));
|
||||||
|
} finally {
|
||||||
|
setDuplicateLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadCookies = useCallback(async () => {
|
const loadCookies = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -440,11 +461,63 @@ export default function CookiePage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const duplicateColumns = [
|
||||||
|
{
|
||||||
|
title: '账号',
|
||||||
|
dataIndex: 'account_names',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (names: string[]) => names.join(' / '),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CK 条数',
|
||||||
|
dataIndex: 'cookie_count',
|
||||||
|
width: 90,
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (count: number) => <Tag color="error">{count}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账号记录数',
|
||||||
|
dataIndex: 'account_count',
|
||||||
|
width: 110,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CK 记录 ID',
|
||||||
|
dataIndex: 'cookie_ids',
|
||||||
|
width: 220,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (ids: number[]) => ids.join(', '),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账号 ID',
|
||||||
|
dataIndex: 'account_ids',
|
||||||
|
width: 130,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (ids: number[]) => ids.join(', '),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最近记录',
|
||||||
|
width: 170,
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (_: unknown, group: CookieDuplicateGroup) => formatTime(group.records[0]?.finished_at),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<h2 style={{ margin: 0 }}>Cookie 管理</h2>
|
<h2 style={{ margin: 0 }}>Cookie 管理</h2>
|
||||||
<Space>
|
<Space>
|
||||||
|
{canView && (
|
||||||
|
<Button
|
||||||
|
icon={<ScanOutlined />}
|
||||||
|
loading={duplicateLoading}
|
||||||
|
onClick={handleDuplicateCheck}
|
||||||
|
>
|
||||||
|
检测重复
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={handleCopySelected}
|
onClick={handleCopySelected}
|
||||||
@@ -595,6 +668,43 @@ export default function CookiePage() {
|
|||||||
}}
|
}}
|
||||||
scroll={{ x: 1210 }}
|
scroll={{ x: 1210 }}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
title="数据库重复 CK 检测"
|
||||||
|
open={duplicateOpen}
|
||||||
|
onCancel={() => setDuplicateOpen(false)}
|
||||||
|
footer={null}
|
||||||
|
width={960}
|
||||||
|
>
|
||||||
|
{duplicateLoading ? (
|
||||||
|
<div style={{ padding: '48px 0', textAlign: 'center' }}>
|
||||||
|
<LoadingOutlined spin />
|
||||||
|
<Text style={{ marginLeft: 8 }}>正在检测...</Text>
|
||||||
|
</div>
|
||||||
|
) : duplicateResult ? (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type={duplicateResult.duplicate_groups > 0 ? 'warning' : 'success'}
|
||||||
|
showIcon
|
||||||
|
message={duplicateResult.duplicate_groups > 0
|
||||||
|
? `发现 ${duplicateResult.duplicate_groups} 个重复账号,共 ${duplicateResult.duplicate_rows} 条 CK 记录`
|
||||||
|
: '未发现重复账号'}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
{duplicateResult.groups.length > 0 ? (
|
||||||
|
<Table<CookieDuplicateGroup>
|
||||||
|
rowKey="account_key"
|
||||||
|
size="small"
|
||||||
|
pagination={{ pageSize: 10, showTotal: (count) => `共 ${count} 组` }}
|
||||||
|
columns={duplicateColumns}
|
||||||
|
dataSource={duplicateResult.groups}
|
||||||
|
scroll={{ x: 900 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="未发现重复账号" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user