增加斗鱼自定义CK筛选功能

This commit is contained in:
yml2213
2026-08-08 19:03:23 +08:00
parent 39ea0d2184
commit 65271124c0
4 changed files with 121 additions and 7 deletions
+30 -1
View File
@@ -29,6 +29,18 @@ def _fmt_dt(dt) -> str | None:
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
def _parse_account_names(raw_names: str) -> list[str]:
"""解析前端粘贴的 Excel 账号名,每行一个并去重。"""
names = []
seen = set()
for value in (raw_names or "").replace("\r\n", "\n").replace("\r", "\n").split("\n"):
name = value.strip()
if name and name not in seen:
names.append(name)
seen.add(name)
return names
# 斗鱼 Cookie 有效性检测接口(抓包参考:钱包中心鱼丸余额 + 用户等级详情)
CHECK_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
@@ -129,6 +141,7 @@ def _visible_cookie_tasks_query(db: Session, current: User):
@router.get("")
def list_cookies(
search: str = Query(""),
account_names: str = Query(""),
page: int | None = Query(None, ge=1),
page_size: int = Query(20, ge=1, le=200),
include_cookie: bool = Query(True),
@@ -143,6 +156,13 @@ def list_cookies(
joinedload(LoginTask.account).joinedload(Account.assigned_user),
)
search_text = (search or "").strip()
selected_names = _parse_account_names(account_names)
if selected_names:
query = query.filter(
LoginTask.account_id.in_(
db.query(Account.id).filter(Account.username.in_(selected_names))
)
)
if search_text:
pattern = f"%{search_text}%"
if user_has_permission(current, "login:view_all"):
@@ -228,6 +248,7 @@ def cookies_summary(
@router.get("/export")
def export_cookies(
format: str = "csv",
account_names: str = Query(""),
db: Session = Depends(get_db),
current: User = Depends(require_permission("cookie:export")),
):
@@ -235,7 +256,15 @@ def export_cookies(
csv: 账号, Cookie, 时间
custom: 账号----密码----ck
"""
tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all()
query = _visible_cookie_tasks_query(db, current)
selected_names = _parse_account_names(account_names)
if selected_names:
query = query.filter(
LoginTask.account_id.in_(
db.query(Account.id).filter(Account.username.in_(selected_names))
)
)
tasks = query.order_by(LoginTask.finished_at.desc()).all()
# 批量查账号,避免 N+1
account_ids = [t.account_id for t in tasks]
+8 -2
View File
@@ -3,11 +3,17 @@ import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PagePa
export const cookieApi = {
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
listPaged: (params: PageParams & { include_cookie?: boolean }) =>
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
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 } : {} }),
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
responseType: 'blob',
params: {
...(format ? { format } : {}),
...(accountNames ? { account_names: accountNames } : {}),
},
}),
check: (ids: number[]) =>
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
relogin: (ids: number[]) =>
+1
View File
@@ -21,6 +21,7 @@ export interface PageParams {
page: number;
page_size: number;
search?: string;
account_names?: string;
}
export interface AccountBulkSelection {
+82 -4
View File
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback } from 'react';
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, LoadingOutlined, ReloadOutlined } from '@ant-design/icons';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined, FilterOutlined } 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';
@@ -37,12 +37,24 @@ function copyToClipboard(text: string): Promise<void> {
});
}
function parseAccountNames(text: string): string[] {
const seen = new Set<string>();
return text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n')
.map((name) => name.trim())
.filter((name) => name.length > 0 && !seen.has(name) && seen.add(name));
}
export default function CookiePage() {
const { token } = theme.useToken();
const [cookies, setCookies] = useState<CookieItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [customNamesText, setCustomNamesText] = useState('');
const [customAccountNames, setCustomAccountNames] = useState<string[]>([]);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
@@ -148,6 +160,7 @@ export default function CookiePage() {
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
account_names: customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined,
include_cookie: false,
});
setCookies(data.items);
@@ -174,7 +187,7 @@ export default function CookiePage() {
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
}, [currentPage, pageSize, searchText, customAccountNames]);
const loadSummary = useCallback(async () => {
try {
@@ -195,19 +208,60 @@ export default function CookiePage() {
const handleExport = async (format: string = 'csv') => {
try {
const blob = await cookieApi.exportCsv(format);
const blob = await cookieApi.exportCsv(
format,
customAccountNames.length > 0 ? customAccountNames.join('\n') : undefined,
);
const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]));
const a = document.createElement('a');
a.href = url;
a.download = format === 'custom' ? 'cookies_custom.txt' : 'cookies.csv';
a.click();
URL.revokeObjectURL(url);
message.success('已导出');
message.success(customAccountNames.length > 0 ? '已导出匹配的 Cookie' : '已导出');
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const handleApplyCustomFilter = () => {
const names = parseAccountNames(customNamesText);
if (names.length === 0) {
message.warning('请先粘贴斗鱼账户名,每行一个');
return;
}
setCustomAccountNames(names);
setSelectedRowKeys([]);
setCurrentPage(1);
message.success(`已载入 ${names.length} 个账户名,正在筛选 Cookie`);
};
const handleClearCustomFilter = () => {
setCustomNamesText('');
setCustomAccountNames([]);
setSelectedRowKeys([]);
setCurrentPage(1);
};
const handleCopyCustomCookies = async () => {
if (customAccountNames.length === 0) {
message.warning('请先筛选自定义账户名');
return;
}
try {
const blob = await cookieApi.exportCsv('custom', customAccountNames.join('\n'));
const text = blob instanceof Blob ? await blob.text() : String(blob);
if (!text.trim()) {
message.warning('没有找到对应的 Cookie');
return;
}
await copyToClipboard(text);
message.success(`已复制 ${total} 条匹配的自定义 CK`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleCopyCookie = async (record: CookieItem) => {
try {
const detail = await cookieApi.get(record.id);
@@ -475,6 +529,30 @@ export default function CookiePage() {
</Card>
</Col>
</Row>
<Card size="small" title="获取自定义 CK" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }} size={8}>
<Input.TextArea
value={customNamesText}
onChange={(e) => setCustomNamesText(e.target.value)}
placeholder="从 Excel 复制斗鱼账户名后粘贴到这里,每行一个"
autoSize={{ minRows: 3, maxRows: 7 }}
/>
<Space wrap>
<Button type="primary" icon={<FilterOutlined />} onClick={handleApplyCustomFilter}>
CK
</Button>
<Button icon={<CopyOutlined />} onClick={handleCopyCustomCookies} disabled={customAccountNames.length === 0}>
CK
</Button>
<Button onClick={handleClearCustomFilter} disabled={!customNamesText && customAccountNames.length === 0}>
</Button>
{customAccountNames.length > 0 && (
<Text type="secondary"> {customAccountNames.length} {total} </Text>
)}
</Space>
</Space>
</Card>
<div style={{ marginBottom: 12 }}>
<Input.Search
placeholder="搜索账号或分配客服"