一键检测重复-仓库管理

This commit is contained in:
yml2213
2026-08-08 20:51:50 +08:00
parent 65271124c0
commit b39780f26d
4 changed files with 211 additions and 4 deletions
+72
View File
@@ -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",
+2 -1
View File
@@ -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',
+24
View File
@@ -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 {
+113 -3
View File
@@ -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>
); );
} }