type: 收敛测试 schemas 与协议层类型
This commit is contained in:
+193
-73
@@ -1,16 +1,32 @@
|
||||
"""账号管理路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, Account, AuditLog, LoginTask, DouyuTask, DouyuWorkbenchAccount
|
||||
from ..schemas import AccountBulkSelection, AccountBulkTag, AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
|
||||
from ..models import (
|
||||
User,
|
||||
Account,
|
||||
AuditLog,
|
||||
LoginTask,
|
||||
DouyuTask,
|
||||
DouyuWorkbenchAccount,
|
||||
)
|
||||
from ..schemas import (
|
||||
AccountBulkSelection,
|
||||
AccountBulkTag,
|
||||
AccountImport,
|
||||
AccountAssign,
|
||||
AccountTag,
|
||||
AccountOut,
|
||||
BatchAssign,
|
||||
)
|
||||
from ..deps import get_current_user, require_permission
|
||||
from ..permissions import user_has_permission
|
||||
from ..services.account_service import (
|
||||
cookie_account_ids_query, parse_and_build_accounts,
|
||||
cookie_account_ids_query,
|
||||
parse_and_build_accounts,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
|
||||
@@ -46,15 +62,19 @@ def _filter_accounts_query(
|
||||
search_text = (search or "").strip()
|
||||
if search_text:
|
||||
pattern = f"%{search_text}%"
|
||||
query = query.filter(or_(
|
||||
Account.username.ilike(pattern),
|
||||
Account.tag.ilike(pattern),
|
||||
Account.remark.ilike(pattern),
|
||||
))
|
||||
query = query.filter(
|
||||
or_(
|
||||
Account.username.ilike(pattern),
|
||||
Account.tag.ilike(pattern),
|
||||
Account.remark.ilike(pattern),
|
||||
)
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection) -> list[int]:
|
||||
def _selected_account_ids(
|
||||
db: Session, current: User, req: AccountBulkSelection
|
||||
) -> list[int]:
|
||||
"""解析批量操作目标:当前筛选全部或显式选择的 ID。"""
|
||||
if req.all_matching:
|
||||
rows = (
|
||||
@@ -71,7 +91,7 @@ def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
return [account_id for account_id, in rows]
|
||||
return [account_id for (account_id,) in rows]
|
||||
|
||||
seen = set()
|
||||
ids = []
|
||||
@@ -88,7 +108,7 @@ def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection)
|
||||
.order_by(None)
|
||||
.all()
|
||||
)
|
||||
allowed = {account_id for account_id, in rows}
|
||||
allowed = {account_id for (account_id,) in rows}
|
||||
return [account_id for account_id in ids if account_id in allowed]
|
||||
|
||||
|
||||
@@ -131,19 +151,25 @@ def list_accounts(
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
can_include_sensitive = include_sensitive and user_has_permission(current, "account:view_full")
|
||||
can_include_sensitive = include_sensitive and user_has_permission(
|
||||
current, "account:view_full"
|
||||
)
|
||||
options = [joinedload(Account.assigned_user)]
|
||||
if not can_include_sensitive:
|
||||
options.extend([
|
||||
defer(Account.password),
|
||||
defer(Account.email),
|
||||
defer(Account.email_password),
|
||||
])
|
||||
options.extend(
|
||||
[
|
||||
defer(Account.password),
|
||||
defer(Account.email),
|
||||
defer(Account.email_password),
|
||||
]
|
||||
)
|
||||
accounts = query.options(*options).all()
|
||||
result = []
|
||||
for acc in accounts:
|
||||
item = AccountOut(
|
||||
id=acc.id, username=acc.username, remark=acc.remark or "",
|
||||
id=acc.id,
|
||||
username=acc.username,
|
||||
remark=acc.remark or "",
|
||||
tag=acc.tag or "",
|
||||
assigned_to=acc.assigned_to,
|
||||
assigned_username=acc.assigned_user.username if acc.assigned_user else None,
|
||||
@@ -156,7 +182,12 @@ def list_accounts(
|
||||
item.email_password = acc.email_password
|
||||
result.append(item)
|
||||
if page is not None:
|
||||
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
|
||||
return {
|
||||
"items": result,
|
||||
"total": total or 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@@ -209,8 +240,14 @@ def import_accounts(
|
||||
|
||||
if accounts:
|
||||
db.add_all(accounts)
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="account:import", target=f"导入{len(accounts)}个"))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:import",
|
||||
target=f"导入{len(accounts)}个",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
duplicated_note = f",重复跳过 {duplicated} 个" if duplicated else ""
|
||||
@@ -240,17 +277,27 @@ def assign_account(
|
||||
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
||||
|
||||
# 只能分配已成功登录过的账号(有cookie)
|
||||
has_success = db.query(LoginTask).filter(
|
||||
LoginTask.account_id == account_id,
|
||||
LoginTask.status == 'success',
|
||||
LoginTask.cookie != '',
|
||||
).first()
|
||||
has_success = (
|
||||
db.query(LoginTask)
|
||||
.filter(
|
||||
LoginTask.account_id == account_id,
|
||||
LoginTask.status == "success",
|
||||
LoginTask.cookie != "",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not has_success:
|
||||
raise HTTPException(status_code=400, detail="该账号尚未成功登录,无法分配")
|
||||
|
||||
acc.assigned_to = req.assigned_to
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="account:assign", target=acc.username))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:assign",
|
||||
target=acc.username,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": "已分配", "success": True}
|
||||
|
||||
@@ -274,31 +321,52 @@ def batch_assign_accounts(
|
||||
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
||||
|
||||
# 只能分配已成功登录过的账号(有cookie)
|
||||
cookie_ids_query = cookie_account_ids_query(db).subquery()
|
||||
invalid_ids = db.query(Account.id).filter(
|
||||
Account.id.in_(req.account_ids),
|
||||
Account.id.notin_(cookie_ids_query),
|
||||
).all()
|
||||
cookie_ids_query = select(LoginTask.account_id).where(
|
||||
LoginTask.status == "success",
|
||||
LoginTask.cookie != "",
|
||||
LoginTask.cookie.isnot(None),
|
||||
)
|
||||
invalid_ids = (
|
||||
db.query(Account.id)
|
||||
.filter(
|
||||
Account.id.in_(req.account_ids),
|
||||
Account.id.notin_(cookie_ids_query),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if invalid_ids:
|
||||
names = db.query(Account.username).filter(Account.id.in_([i[0] for i in invalid_ids])).all()
|
||||
name_list = ', '.join([n[0] for n in names[:5]])
|
||||
suffix = '...' if len(invalid_ids) > 5 else ''
|
||||
names = (
|
||||
db.query(Account.username)
|
||||
.filter(Account.id.in_([i[0] for i in invalid_ids]))
|
||||
.all()
|
||||
)
|
||||
name_list = ", ".join([n[0] for n in names[:5]])
|
||||
suffix = "..." if len(invalid_ids) > 5 else ""
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"以下账号尚未成功登录,无法分配:{name_list}{suffix}",
|
||||
)
|
||||
|
||||
count = db.query(Account).filter(Account.id.in_(req.account_ids)).update(
|
||||
{Account.assigned_to: req.assigned_to}, synchronize_session=False
|
||||
count = (
|
||||
db.query(Account)
|
||||
.filter(Account.id.in_(req.account_ids))
|
||||
.update({Account.assigned_to: req.assigned_to}, synchronize_session=False)
|
||||
)
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:assign",
|
||||
target=f"批量{'分配' if req.assigned_to else '取消分配'}{count}个账号",
|
||||
)
|
||||
)
|
||||
db.add(AuditLog(
|
||||
user_id=current.id, username=current.username,
|
||||
action="account:assign",
|
||||
target=f"批量{'分配' if req.assigned_to else '取消分配'}{count}个账号"
|
||||
))
|
||||
db.commit()
|
||||
action = "分配" if req.assigned_to else "取消分配"
|
||||
return {"message": f"已批量{action} {count} 个账号", "success": True, "count": count}
|
||||
return {
|
||||
"message": f"已批量{action} {count} 个账号",
|
||||
"success": True,
|
||||
"count": count,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/assignments/summary")
|
||||
@@ -307,11 +375,17 @@ def assignments_summary(
|
||||
current: User = Depends(require_permission("account:assign")),
|
||||
):
|
||||
"""分配概览:每个客服分配了多少账号(仅统计已成功登录的账号)。"""
|
||||
cookie_subq = cookie_account_ids_query(db).subquery()
|
||||
cookie_subq = select(LoginTask.account_id).where(
|
||||
LoginTask.status == "success",
|
||||
LoginTask.cookie != "",
|
||||
LoginTask.cookie.isnot(None),
|
||||
)
|
||||
cookie_accounts = db.query(Account).filter(Account.id.in_(cookie_subq)).subquery()
|
||||
|
||||
results = (
|
||||
db.query(User.id, User.username, func.count(cookie_accounts.c.id).label("count"))
|
||||
db.query(
|
||||
User.id, User.username, func.count(cookie_accounts.c.id).label("count")
|
||||
)
|
||||
.outerjoin(cookie_accounts, cookie_accounts.c.assigned_to == User.id)
|
||||
.filter(User.role == "support")
|
||||
.group_by(User.id, User.username)
|
||||
@@ -345,8 +419,15 @@ def set_account_tag(
|
||||
if not acc:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
acc.tag = (req.tag or "").strip()
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="account:tag", target=acc.username, detail=acc.tag))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:tag",
|
||||
target=acc.username,
|
||||
detail=acc.tag,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": "标签已更新", "success": True}
|
||||
|
||||
@@ -362,8 +443,10 @@ def batch_tag(
|
||||
if not req.account_ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
tag = (req.tag or "").strip()
|
||||
count = _visible_accounts_query(db, current).filter(Account.id.in_(req.account_ids)).update(
|
||||
{Account.tag: tag}, synchronize_session=False
|
||||
count = (
|
||||
_visible_accounts_query(db, current)
|
||||
.filter(Account.id.in_(req.account_ids))
|
||||
.update({Account.tag: tag}, synchronize_session=False)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": f"已为 {count} 个账号设置标签", "success": True}
|
||||
@@ -388,7 +471,11 @@ def batch_tag_selection(
|
||||
)
|
||||
db.commit()
|
||||
scope = "当前筛选下" if req.all_matching else "选中的"
|
||||
return {"message": f"已为{scope} {count} 个账号设置标签", "success": True, "count": count}
|
||||
return {
|
||||
"message": f"已为{scope} {count} 个账号设置标签",
|
||||
"success": True,
|
||||
"count": count,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tags/list")
|
||||
@@ -422,17 +509,29 @@ def batch_delete_accounts(
|
||||
raise HTTPException(status_code=400, detail="无效的账号ID")
|
||||
|
||||
# 先删除关联的登录任务
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuWorkbenchAccount).filter(DouyuWorkbenchAccount.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuWorkbenchAccount).filter(
|
||||
DouyuWorkbenchAccount.account_id.in_(ids)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
# 删除账号
|
||||
deleted = db.query(Account).filter(Account.id.in_(ids)).delete(synchronize_session=False)
|
||||
deleted = (
|
||||
db.query(Account).filter(Account.id.in_(ids)).delete(synchronize_session=False)
|
||||
)
|
||||
|
||||
db.add(AuditLog(
|
||||
user_id=current.id, username=current.username,
|
||||
action="account:delete", target=f"批量删除{deleted}个账号"
|
||||
))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:delete",
|
||||
target=f"批量删除{deleted}个账号",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
|
||||
|
||||
@@ -448,19 +547,28 @@ def batch_delete_accounts_selection(
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(DouyuWorkbenchAccount).filter(DouyuWorkbenchAccount.account_id.in_(ids)).delete(synchronize_session=False)
|
||||
db.query(LoginTask).filter(LoginTask.account_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuWorkbenchAccount).filter(
|
||||
DouyuWorkbenchAccount.account_id.in_(ids)
|
||||
).delete(synchronize_session=False)
|
||||
deleted = (
|
||||
_visible_accounts_query(db, current)
|
||||
.filter(Account.id.in_(ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
db.add(AuditLog(
|
||||
user_id=current.id, username=current.username,
|
||||
action="account:delete",
|
||||
target=f"{'按筛选' if req.all_matching else '批量'}删除{deleted}个账号",
|
||||
))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:delete",
|
||||
target=f"{'按筛选' if req.all_matching else '批量'}删除{deleted}个账号",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
|
||||
|
||||
@@ -476,12 +584,24 @@ def delete_account(
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
# 先删除关联的登录任务,避免外键约束失败
|
||||
db.query(LoginTask).filter(LoginTask.account_id == account_id).delete(synchronize_session=False)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id == account_id).delete(synchronize_session=False)
|
||||
db.query(DouyuWorkbenchAccount).filter(DouyuWorkbenchAccount.account_id == account_id).delete(synchronize_session=False)
|
||||
db.query(LoginTask).filter(LoginTask.account_id == account_id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id == account_id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.query(DouyuWorkbenchAccount).filter(
|
||||
DouyuWorkbenchAccount.account_id == account_id
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
||||
action="account:delete", target=acc.username))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="account:delete",
|
||||
target=acc.username,
|
||||
)
|
||||
)
|
||||
db.delete(acc)
|
||||
db.commit()
|
||||
return {"message": "已删除", "success": True}
|
||||
|
||||
+148
-77
@@ -27,6 +27,7 @@ def _fmt_dt(dt) -> str | None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
||||
cookie_relogin_registry = BatchRegistry()
|
||||
|
||||
@@ -44,7 +45,9 @@ 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"):
|
||||
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)
|
||||
@@ -60,7 +63,9 @@ def _order_cookie_tasks(query, selected_names: list[str]):
|
||||
value=Account.username,
|
||||
else_=len(selected_names),
|
||||
)
|
||||
return query.order_by(input_order, LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||
return query.order_by(
|
||||
input_order, LoginTask.finished_at.desc(), LoginTask.id.desc()
|
||||
)
|
||||
return query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||
|
||||
|
||||
@@ -88,7 +93,9 @@ def _visible_cookie_operation_tasks_query(db: Session, current: User):
|
||||
"""返回可检测/重登的 Cookie 记录,重登中或失败时仍保留在操作列表。"""
|
||||
query = db.query(LoginTask).filter(
|
||||
LoginTask.cookie != "",
|
||||
LoginTask.status.in_(("success", "relogin_pending", "relogin_running", "relogin_failed")),
|
||||
LoginTask.status.in_(
|
||||
("success", "relogin_pending", "relogin_running", "relogin_failed")
|
||||
),
|
||||
)
|
||||
if not user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id).filter(
|
||||
@@ -112,7 +119,9 @@ def list_cookies(
|
||||
if not user_has_permission(current, "cookie:view"):
|
||||
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||
if not include_cookie:
|
||||
query = _visible_cookie_tasks_query(db, current).options(defer(LoginTask.cookie))
|
||||
query = _visible_cookie_tasks_query(db, current).options(
|
||||
defer(LoginTask.cookie)
|
||||
)
|
||||
else:
|
||||
query = _visible_cookie_tasks_query(db, current).options(
|
||||
joinedload(LoginTask.account).joinedload(Account.assigned_user),
|
||||
@@ -137,15 +146,19 @@ def list_cookies(
|
||||
if not account_joined:
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
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),
|
||||
))
|
||||
query = query.outerjoin(User, Account.assigned_to == User.id).filter(
|
||||
or_(
|
||||
Account.username.ilike(pattern),
|
||||
Account.tag.ilike(pattern),
|
||||
User.username.ilike(pattern),
|
||||
)
|
||||
)
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
total = (
|
||||
query.order_by(None).with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
)
|
||||
query = _order_cookie_tasks(query, selected_names)
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
@@ -175,8 +188,10 @@ def list_cookies(
|
||||
"batch_id": t.batch_id,
|
||||
"account_id": t.account_id,
|
||||
"account_username": acc.username if acc else "",
|
||||
"assigned_to": acc.assigned_to,
|
||||
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
|
||||
"assigned_to": acc.assigned_to if acc else None,
|
||||
"assigned_username": acc.assigned_user.username
|
||||
if acc and acc.assigned_user
|
||||
else None,
|
||||
"created_at": _fmt_dt(t.finished_at),
|
||||
"ck_check_status": t.ck_check_status or "",
|
||||
"ck_check_result": t.ck_check_result,
|
||||
@@ -194,7 +209,12 @@ def list_cookies(
|
||||
item["account_password"] = ""
|
||||
result.append(item)
|
||||
if page is not None:
|
||||
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
|
||||
return {
|
||||
"items": result,
|
||||
"total": total or 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@@ -242,10 +262,12 @@ def list_cookie_operations(
|
||||
if tag_value:
|
||||
query = query.filter(Account.tag == tag_value)
|
||||
if search_text:
|
||||
query = query.filter(or_(
|
||||
Account.username.ilike(f"%{search_text}%"),
|
||||
Account.tag.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).with_entities(func.count(LoginTask.id)).scalar() or 0
|
||||
tasks = (
|
||||
@@ -265,7 +287,9 @@ def list_cookie_operations(
|
||||
"created_at": _fmt_dt(task.finished_at),
|
||||
"relogin_status": task.status if task.status != "success" else "",
|
||||
"relogin_message": task.message or "",
|
||||
"relogin_batch_id": task.batch_id if task.status in {"relogin_pending", "relogin_running"} else "",
|
||||
"relogin_batch_id": task.batch_id
|
||||
if task.status in {"relogin_pending", "relogin_running"}
|
||||
else "",
|
||||
}
|
||||
for task in tasks
|
||||
],
|
||||
@@ -292,7 +316,7 @@ def list_cookie_operation_tags(
|
||||
.order_by(Account.tag.asc())
|
||||
.all()
|
||||
)
|
||||
return [tag for tag, in rows if tag]
|
||||
return [tag for (tag,) in rows if tag]
|
||||
|
||||
|
||||
@router.get("/duplicates")
|
||||
@@ -317,7 +341,9 @@ def find_duplicate_cookies(
|
||||
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()
|
||||
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()
|
||||
@@ -337,26 +363,30 @@ def find_duplicate_cookies(
|
||||
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),
|
||||
})
|
||||
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.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 {
|
||||
@@ -432,10 +462,16 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d
|
||||
id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()]
|
||||
if not id_list:
|
||||
raise HTTPException(status_code=400, detail="无效的ID")
|
||||
base_query = _visible_cookie_tasks_query(db, current) if detailed else _visible_cookie_operation_tasks_query(db, current)
|
||||
tasks = base_query.filter(LoginTask.id.in_(id_list)).filter(
|
||||
LoginTask.status.in_(("success", "relogin_failed"))
|
||||
).all()
|
||||
base_query = (
|
||||
_visible_cookie_tasks_query(db, current)
|
||||
if detailed
|
||||
else _visible_cookie_operation_tasks_query(db, current)
|
||||
)
|
||||
tasks = (
|
||||
base_query.filter(LoginTask.id.in_(id_list))
|
||||
.filter(LoginTask.status.in_(("success", "relogin_failed")))
|
||||
.all()
|
||||
)
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
|
||||
@@ -447,15 +483,17 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d
|
||||
results.append(future.result())
|
||||
except Exception as exc:
|
||||
task = futures[future]
|
||||
results.append({
|
||||
"id": task.id,
|
||||
"valid": False,
|
||||
"message": f"检测异常: {exc}",
|
||||
"fish_ball": None,
|
||||
"nickname": None,
|
||||
"level": None,
|
||||
"checked_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"id": task.id,
|
||||
"valid": False,
|
||||
"message": f"检测异常: {exc}",
|
||||
"fish_ball": None,
|
||||
"nickname": None,
|
||||
"level": None,
|
||||
"checked_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
results.sort(key=lambda item: item["id"])
|
||||
|
||||
# 持久化检测结果,刷新/翻页不丢失
|
||||
@@ -472,12 +510,14 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d
|
||||
"message": item.get("message", ""),
|
||||
}
|
||||
task.ck_checked_at = datetime.now(timezone.utc)
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:check",
|
||||
target=f"检测 {len(results)} 条已分配账号 CK",
|
||||
))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:check",
|
||||
target=f"检测 {len(results)} 条已分配账号 CK",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
if not detailed:
|
||||
results = [
|
||||
@@ -510,7 +550,13 @@ def check_cookie_operations(
|
||||
return _check_cookies(ids, db, current, detailed=False)
|
||||
|
||||
|
||||
def _start_relogin_tasks(tasks: list[LoginTask], db: Session, current: User, *, action: str = "cookie:relogin"):
|
||||
def _start_relogin_tasks(
|
||||
tasks: list[LoginTask],
|
||||
db: Session,
|
||||
current: User,
|
||||
*,
|
||||
action: str = "cookie:relogin",
|
||||
):
|
||||
"""启动重登批次:旧 Cookie 保留到新登录成功后才替换。"""
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
@@ -538,7 +584,9 @@ def _start_relogin_tasks(tasks: list[LoginTask], db: Session, current: User, *,
|
||||
|
||||
if not task_ids:
|
||||
db.commit()
|
||||
raise HTTPException(status_code=400, detail="没有可重登的账号,请检查账号凭据或当前重登状态")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="没有可重登的账号,请检查账号凭据或当前重登状态"
|
||||
)
|
||||
|
||||
proxy = db.query(ProxyConfigModel).first()
|
||||
thread_db = SessionLocal()
|
||||
@@ -558,13 +606,15 @@ def _start_relogin_tasks(tasks: list[LoginTask], db: Session, current: User, *,
|
||||
relogin_task_ids=task_ids,
|
||||
)
|
||||
batch_id = runner.batch_id
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action=action,
|
||||
target=f"重登 {len(task_ids)} 条账号 CK",
|
||||
detail="旧 Cookie 将在新登录成功后替换",
|
||||
))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action=action,
|
||||
target=f"重登 {len(task_ids)} 条账号 CK",
|
||||
detail="旧 Cookie 将在新登录成功后替换",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
cookie_relogin_registry.register(batch_id, None, None, runner, owner_id=current.id)
|
||||
|
||||
@@ -599,19 +649,28 @@ def stop_cookie_relogin(
|
||||
_require_cookie_operation_perm(current)
|
||||
batch = cookie_relogin_registry.get(batch_id)
|
||||
if not batch:
|
||||
raise HTTPException(status_code=404, detail="重登批次不存在、已结束或服务已重启")
|
||||
if batch.get("owner_id") != current.id and not user_has_permission(current, "login:view_all"):
|
||||
raise HTTPException(
|
||||
status_code=404, detail="重登批次不存在、已结束或服务已重启"
|
||||
)
|
||||
if batch.get("owner_id") != current.id and not user_has_permission(
|
||||
current, "login:view_all"
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="无权限停止该重登批次")
|
||||
|
||||
batch["runner"].stop()
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:relogin_stop",
|
||||
target=f"停止 CK 重登批次 {batch_id}",
|
||||
))
|
||||
db.add(
|
||||
AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:relogin_stop",
|
||||
target=f"停止 CK 重登批次 {batch_id}",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
return {"message": "已发送停止信号,正在登录的账号会在当前请求结束后停止", "success": True}
|
||||
return {
|
||||
"message": "已发送停止信号,正在登录的账号会在当前请求结束后停止",
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
def _start_relogin(req: CookieReloginRequest, db: Session, current: User):
|
||||
@@ -667,7 +726,9 @@ def relogin_invalid_cookie_operations(
|
||||
query = query.filter(Account.tag == tag.strip())
|
||||
if search.strip():
|
||||
pattern = f"%{search.strip()}%"
|
||||
query = query.filter(or_(Account.username.ilike(pattern), Account.tag.ilike(pattern)))
|
||||
query = query.filter(
|
||||
or_(Account.username.ilike(pattern), Account.tag.ilike(pattern))
|
||||
)
|
||||
tasks = query.order_by(LoginTask.id.asc()).all()
|
||||
return _start_relogin_tasks(tasks, db, current, action="cookie:relogin_invalid")
|
||||
|
||||
@@ -681,7 +742,9 @@ def get_cookie(
|
||||
"""获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。"""
|
||||
if not user_has_permission(current, "cookie:view"):
|
||||
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||
task = (
|
||||
_visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||
)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
acc = db.query(Account).filter(Account.id == task.account_id).first()
|
||||
@@ -691,7 +754,9 @@ def get_cookie(
|
||||
"account_id": task.account_id,
|
||||
"account_username": acc.username if acc else "",
|
||||
"assigned_to": acc.assigned_to if acc else None,
|
||||
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
|
||||
"assigned_username": acc.assigned_user.username
|
||||
if acc and acc.assigned_user
|
||||
else None,
|
||||
"created_at": _fmt_dt(task.finished_at),
|
||||
"ck_check_status": task.ck_check_status or "",
|
||||
"ck_check_result": task.ck_check_result,
|
||||
@@ -726,7 +791,11 @@ def delete_cookies_batch(
|
||||
t.status = "failed"
|
||||
t.message = "Cookie已清除"
|
||||
db.commit()
|
||||
return {"message": f"已删除 {len(tasks)} 条", "deleted": len(tasks), "success": True}
|
||||
return {
|
||||
"message": f"已删除 {len(tasks)} 条",
|
||||
"deleted": len(tasks),
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
@@ -736,7 +805,9 @@ def delete_cookie(
|
||||
current: User = Depends(require_permission("cookie:export")),
|
||||
):
|
||||
"""删除一条 Cookie 记录。"""
|
||||
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||
task = (
|
||||
_visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||
)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
task.cookie = ""
|
||||
|
||||
+494
-238
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user