diff --git a/tests/test_cookie_operations.py b/tests/test_cookie_operations.py index 2c166a8..51767c6 100644 --- a/tests/test_cookie_operations.py +++ b/tests/test_cookie_operations.py @@ -14,6 +14,7 @@ from web.backend.models import Account, LoginTask, User from web.backend.routers.cookies import ( check_cookie_operations, get_cookie, + list_cookie_operation_tags, list_cookie_operations, list_cookies, ) @@ -62,6 +63,7 @@ class CookieOperationTests(unittest.TestCase): def test_support_operation_list_is_scoped_and_never_returns_credentials(self): result = list_cookie_operations( search="", + tag="", page=1, page_size=20, db=self.session, @@ -72,6 +74,7 @@ class CookieOperationTests(unittest.TestCase): self.assertEqual(result["items"], [{ "id": self.owned_task.id, "account_username": "owned-account", + "tag": "", "ck_check_status": "", "ck_checked_at": None, "created_at": None, @@ -85,6 +88,45 @@ class CookieOperationTests(unittest.TestCase): with self.assertRaisesRegex(Exception, "cookie:view"): get_cookie(self.owned_task.id, db=self.session, current=self.support) + def test_cookie_list_filters_by_account_tag(self): + admin = User(username="admin", password_hash="hash", role="super_admin") + self.session.add(admin) + self.session.query(Account).filter(Account.id == self.owned_task.account_id).update({"tag": "A组"}) + self.session.query(Account).filter(Account.id == self.other_task.account_id).update({"tag": "B组"}) + self.session.commit() + + result = list_cookies( + search="", + tag="A组", + account_names="", + page=1, + page_size=20, + include_cookie=False, + db=self.session, + current=admin, + ) + + self.assertEqual(result["total"], 1) + self.assertEqual([item["id"] for item in result["items"]], [self.owned_task.id]) + + def test_support_operation_tag_filter_and_list_stay_scoped(self): + self.session.query(Account).filter(Account.id == self.owned_task.account_id).update({"tag": "我的标签"}) + self.session.query(Account).filter(Account.id == self.other_task.account_id).update({"tag": "别人的标签"}) + self.session.commit() + + tags = list_cookie_operation_tags(db=self.session, current=self.support) + result = list_cookie_operations( + search="", + tag="别人的标签", + page=1, + page_size=20, + db=self.session, + current=self.support, + ) + + self.assertEqual(tags, ["我的标签"]) + self.assertEqual(result["total"], 0) + @patch("web.backend.routers.cookies._check_one_cookie") def test_support_can_check_only_assigned_cookie(self, check_one_cookie): check_one_cookie.return_value = { @@ -119,6 +161,7 @@ class CookieOperationTests(unittest.TestCase): with self.assertRaisesRegex(Exception, "cookie:operate"): list_cookie_operations( search="", + tag="", page=1, page_size=20, db=self.session, diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index 505d531..6681b02 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -163,6 +163,7 @@ def _visible_cookie_tasks_query(db: Session, current: User): @router.get("") def list_cookies( search: str = Query(""), + tag: str = Query(""), account_names: str = Query(""), page: int | None = Query(None, ge=1), page_size: int = Query(20, ge=1, le=200), @@ -180,6 +181,7 @@ def list_cookies( joinedload(LoginTask.account).joinedload(Account.assigned_user), ) search_text = (search or "").strip() + tag_value = (tag or "").strip() selected_names = _parse_account_names(account_names) can_view_all = user_has_permission(current, "login:view_all") account_joined = not can_view_all @@ -188,6 +190,11 @@ def list_cookies( query = query.join(Account, LoginTask.account_id == Account.id) account_joined = True query = query.filter(Account.username.in_(selected_names)) + if tag_value: + if not account_joined: + query = query.join(Account, LoginTask.account_id == Account.id) + account_joined = True + query = query.filter(Account.tag == tag_value) if search_text: pattern = f"%{search_text}%" if not account_joined: @@ -195,6 +202,7 @@ def list_cookies( account_joined = True query = query.outerjoin(User, Account.assigned_to == User.id).filter(or_( Account.username.ilike(pattern), + Account.tag.ilike(pattern), User.username.ilike(pattern), )) @@ -276,6 +284,7 @@ def cookies_summary( @router.get("/operations") def list_cookie_operations( search: str = Query(""), + tag: str = Query(""), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=200), db: Session = Depends(get_db), @@ -287,8 +296,14 @@ def list_cookie_operations( if user_has_permission(current, "login:view_all"): query = query.join(Account, LoginTask.account_id == Account.id) search_text = (search or "").strip() + tag_value = (tag or "").strip() + if tag_value: + query = query.filter(Account.tag == tag_value) if search_text: - query = query.filter(Account.username.ilike(f"%{search_text}%")) + query = query.filter(or_( + Account.username.ilike(f"%{search_text}%"), + Account.tag.ilike(f"%{search_text}%"), + )) total = query.order_by(None).count() tasks = ( @@ -302,6 +317,7 @@ def list_cookie_operations( { "id": task.id, "account_username": task.account.username if task.account else "", + "tag": task.account.tag if task.account else "", "ck_check_status": task.ck_check_status or "", "ck_checked_at": _fmt_dt(task.ck_checked_at), "created_at": _fmt_dt(task.finished_at), @@ -314,6 +330,26 @@ def list_cookie_operations( } +@router.get("/operations/tags") +def list_cookie_operation_tags( + db: Session = Depends(get_db), + current: User = Depends(get_current_user), +): + """返回当前用户可操作 CK 记录对应的标签,不跨越账号分配范围。""" + _require_cookie_operation_perm(current) + query = _visible_cookie_tasks_query(db, current) + if user_has_permission(current, "login:view_all"): + query = query.join(Account, LoginTask.account_id == Account.id) + rows = ( + query.filter(Account.tag != "", Account.tag.isnot(None)) + .with_entities(Account.tag) + .distinct() + .order_by(Account.tag.asc()) + .all() + ) + return [tag for tag, in rows if tag] + + @router.get("/duplicates") def find_duplicate_cookies( db: Session = Depends(get_db), diff --git a/web/frontend/src/api/cookies.ts b/web/frontend/src/api/cookies.ts index 11c0901..02f785b 100644 --- a/web/frontend/src/api/cookies.ts +++ b/web/frontend/src/api/cookies.ts @@ -3,11 +3,12 @@ import type { BasicSummary, CookieCheckResult, CookieDuplicateResponse, CookieIt export const cookieApi = { list: () => api.get('/cookies'), - listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) => + listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string; tag?: string }) => api.get, PaginatedResponse>('/cookies', { params }), summary: () => api.get('/cookies/summary'), - listOperations: (params: PageParams) => + listOperations: (params: PageParams & { tag?: string }) => api.get, PaginatedResponse>('/cookies/operations', { params }), + listOperationTags: () => api.get('/cookies/operations/tags'), duplicates: () => api.get('/cookies/duplicates'), get: (id: number) => api.get(`/cookies/${id}`), exportCsv: (format?: string, accountNames?: string) => api.get('/cookies/export', { diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index dc114af..1a7aac3 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -256,6 +256,7 @@ export interface CookieItem { export interface CookieOperationItem { id: number; account_username: string; + tag: string; ck_check_status: string; ck_checked_at: string | null; created_at: string | null; diff --git a/web/frontend/src/pages/CookieOperationsPage.tsx b/web/frontend/src/pages/CookieOperationsPage.tsx index 9089947..2ebd275 100644 --- a/web/frontend/src/pages/CookieOperationsPage.tsx +++ b/web/frontend/src/pages/CookieOperationsPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { Button, Input, Popconfirm, Space, Table, Tag, Typography } from 'antd'; +import { Button, Input, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'; import { message } from '../utils/antdMessage'; import { LoadingOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; import { cookieApi, type CookieOperationCheckResult, type CookieOperationItem } from '../api/modules'; @@ -12,6 +12,8 @@ export default function CookieOperationsPage() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); + const [tagFilter, setTagFilter] = useState(''); + const [tags, setTags] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [checkingIds, setCheckingIds] = useState>(new Set()); const [reloginIds, setReloginIds] = useState>(new Set()); @@ -30,6 +32,7 @@ export default function CookieOperationsPage() { page: currentPage, page_size: pageSize, search: search.trim() || undefined, + tag: tagFilter || undefined, }); setItems(data.items); setTotal(data.total); @@ -50,12 +53,18 @@ export default function CookieOperationsPage() { } finally { setLoading(false); } - }, [currentPage, pageSize, search]); + }, [currentPage, pageSize, search, tagFilter]); useEffect(() => { void loadItems(); }, [loadItems]); + useEffect(() => { + cookieApi.listOperationTags().then(setTags).catch(() => { + // 标签加载失败不影响 CK 检测与重登。 + }); + }, []); + const handleCheck = async (ids: number[]) => { if (ids.length === 0) return; setCheckingIds((previous) => new Set([...previous, ...ids])); @@ -103,6 +112,13 @@ export default function CookieOperationsPage() { const selectedIds = selectedRowKeys.map((key) => Number(key)); const columns = [ { title: '账号', dataIndex: 'account_username', ellipsis: true }, + { + title: '标签', + dataIndex: 'tag', + width: 140, + ellipsis: true, + render: (tag: string) => tag ? {tag} : -, + }, { title: '有效性', width: 110, @@ -168,18 +184,33 @@ export default function CookieOperationsPage() { - } - placeholder="搜索账号" - style={{ width: 280, marginBottom: 12 }} - value={search} - onChange={(event) => { - setSearch(event.target.value); - setCurrentPage(1); - setSelectedRowKeys([]); - }} - /> + + { + setTagFilter(value || ''); + setCurrentPage(1); + setSelectedRowKeys([]); + }} + options={tags.map((tag) => ({ value: tag, label: tag }))} + /> { @@ -639,7 +661,7 @@ export default function CookiePage() { size="small" prefix={} /> - +