支持账号按筛选全量批量操作

This commit is contained in:
yml2213
2026-07-25 10:50:56 +08:00
parent 09efb23f58
commit 10f298b4e1
8 changed files with 490 additions and 112 deletions
+137 -29
View File
@@ -5,8 +5,8 @@ from sqlalchemy import func, or_
from sqlalchemy.orm import Session, defer, joinedload
from ..database import get_db
from ..models import User, Account, AuditLog, LoginTask
from ..schemas import AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
from ..models import User, Account, AuditLog, LoginTask, DouyuTask
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 (
@@ -16,6 +16,82 @@ from ..services.account_service import (
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
def _visible_accounts_query(db: Session, current: User):
"""返回当前用户可见的斗鱼账号查询。"""
query = db.query(Account)
if user_has_permission(current, "account:view_all"):
return query
if user_has_permission(current, "account:view_assigned"):
return query.filter(Account.assigned_to == current.id)
raise HTTPException(status_code=403, detail="无权查看账号")
def _filter_accounts_query(
db: Session,
query,
current: User,
*,
assigned_only: bool = False,
tag: str | None = None,
has_cookie: bool = False,
search: str = "",
):
"""复用列表筛选条件,供分页列表和全量批量操作保持一致。"""
if has_cookie:
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
if assigned_only and user_has_permission(current, "account:view_all"):
query = query.filter(Account.assigned_to.isnot(None))
if tag:
query = query.filter(Account.tag == tag)
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),
))
return query
def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection) -> list[int]:
"""解析批量操作目标:当前筛选全部或显式选择的 ID。"""
if req.all_matching:
rows = (
_filter_accounts_query(
db,
_visible_accounts_query(db, current),
current,
assigned_only=req.assigned_only,
tag=req.tag,
has_cookie=req.has_cookie,
search=req.search,
)
.with_entities(Account.id)
.order_by(None)
.all()
)
return [account_id for account_id, in rows]
seen = set()
ids = []
for account_id in req.account_ids:
if account_id not in seen:
seen.add(account_id)
ids.append(account_id)
if not ids:
return []
rows = (
_visible_accounts_query(db, current)
.filter(Account.id.in_(ids))
.with_entities(Account.id)
.order_by(None)
.all()
)
allowed = {account_id for account_id, in rows}
return [account_id for account_id in ids if account_id in allowed]
@router.get("")
def list_accounts(
assigned_only: bool = Query(False),
@@ -29,33 +105,15 @@ def list_accounts(
current: User = Depends(get_current_user),
):
"""列表:按角色返回不同字段和范围。"""
query = db.query(Account)
# 只展示已成功登录过的账号
if has_cookie:
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
# 权限控制:客服只能看分配给自己的
if not user_has_permission(current, "account:view_all"):
if user_has_permission(current, "account:view_assigned"):
query = query.filter(Account.assigned_to == current.id)
else:
raise HTTPException(status_code=403, detail="无权查看账号")
if assigned_only and user_has_permission(current, "account:view_all"):
query = query.filter(Account.assigned_to.isnot(None))
if tag:
query = query.filter(Account.tag == tag)
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 = _filter_accounts_query(
db,
_visible_accounts_query(db, current),
current,
assigned_only=assigned_only,
tag=tag,
has_cookie=has_cookie,
search=search,
)
total = None
if page is not None:
@@ -284,6 +342,27 @@ def batch_tag(
return {"message": f"已为 {count} 个账号设置标签", "success": True}
@router.put("/batch-tag-selection")
def batch_tag_selection(
req: AccountBulkTag,
db: Session = Depends(get_db),
current: User = Depends(require_permission("account:import")),
):
"""按显式选择或当前筛选结果批量设置账号标签。"""
ids = _selected_account_ids(db, current, req)
if not ids:
raise HTTPException(status_code=400, detail="请选择账号")
tag = (req.tag_value or "").strip()
count = (
_visible_accounts_query(db, current)
.filter(Account.id.in_(ids))
.update({Account.tag: tag}, synchronize_session=False)
)
db.commit()
scope = "当前筛选下" if req.all_matching else "选中的"
return {"message": f"已为{scope} {count} 个账号设置标签", "success": True, "count": count}
@router.get("/tags/list")
def list_tags(
db: Session = Depends(get_db),
@@ -310,6 +389,7 @@ def batch_delete_accounts(
# 先删除关联的登录任务
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)
# 删除账号
deleted = db.query(Account).filter(Account.id.in_(ids)).delete(synchronize_session=False)
@@ -322,6 +402,33 @@ def batch_delete_accounts(
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
@router.post("/batch-delete")
def batch_delete_accounts_selection(
req: AccountBulkSelection,
db: Session = Depends(get_db),
current: User = Depends(require_permission("account:delete")),
):
"""按显式选择或当前筛选结果批量删除账号及其关联任务。"""
ids = _selected_account_ids(db, current, req)
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)
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.commit()
return {"message": f"已删除 {deleted} 个账号", "deleted": deleted, "success": True}
@router.delete("/{account_id}")
def delete_account(
account_id: int,
@@ -334,6 +441,7 @@ def delete_account(
# 先删除关联的登录任务,避免外键约束失败
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.add(AuditLog(user_id=current.id, username=current.username,
action="account:delete", target=acc.username))
+153 -23
View File
@@ -23,10 +23,22 @@ from core.sms_provider import parse_sms_lines
from ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, get_current_user, require_permission
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, ProxyConfig, User
from ..models import (
HuyaAccount,
HuyaConfig,
HuyaGoodsSnapshot,
HuyaRechargeGoodsSnapshot,
HuyaRegisterItem,
HuyaRegisterSuccessLog,
HuyaTask,
ProxyConfig,
User,
)
from ..permissions import user_has_permission
from ..schemas import (
AccountAssign,
AccountBulkSelection,
AccountBulkTag,
AccountTag,
BatchAssign,
HuyaAccountOut,
@@ -103,6 +115,89 @@ def _visible_huya_accounts_query(db: Session, current: User):
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
def _filter_huya_accounts_query(
query,
current: User,
*,
assigned_only: bool = False,
tag: str | None = None,
has_cookie: bool = False,
search: str = "",
):
"""复用虎牙账号列表筛选条件,供分页和批量操作保持一致。"""
if assigned_only and _can_view_huya_all(current):
query = query.filter(HuyaAccount.assigned_to.isnot(None))
if tag:
query = query.filter(HuyaAccount.tag == tag)
if has_cookie:
query = query.filter(HuyaAccount.cookie != "")
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
query = query.filter(or_(
HuyaAccount.uid.ilike(pattern),
HuyaAccount.yyuid.ilike(pattern),
HuyaAccount.username.ilike(pattern),
HuyaAccount.nickname.ilike(pattern),
HuyaAccount.tag.ilike(pattern),
HuyaAccount.game_name.ilike(pattern),
HuyaAccount.game_phone.ilike(pattern),
))
return query
def _selected_huya_account_ids(db: Session, current: User, req: AccountBulkSelection) -> list[int]:
"""解析虎牙批量操作目标:当前筛选全部或显式选择的 ID。"""
base_query = _visible_huya_accounts_query(db, current)
if req.all_matching:
rows = (
_filter_huya_accounts_query(
base_query,
current,
assigned_only=req.assigned_only,
tag=req.tag,
has_cookie=req.has_cookie,
search=req.search,
)
.with_entities(HuyaAccount.id)
.order_by(None)
.all()
)
return [account_id for account_id, in rows]
seen = set()
requested_ids = []
for account_id in req.account_ids:
if account_id not in seen:
seen.add(account_id)
requested_ids.append(account_id)
if not requested_ids:
return []
rows = (
base_query
.filter(HuyaAccount.id.in_(requested_ids))
.with_entities(HuyaAccount.id)
.order_by(None)
.all()
)
allowed = {account_id for account_id, in rows}
return [account_id for account_id in requested_ids if account_id in allowed]
def _clear_huya_account_references(db: Session, account_ids: list[int]) -> None:
"""删除账号前保留注册历史,把历史流水中的账号引用置空。"""
if not account_ids:
return
db.query(HuyaRegisterItem).filter(HuyaRegisterItem.account_id.in_(account_ids)).update(
{HuyaRegisterItem.account_id: None},
synchronize_session=False,
)
db.query(HuyaRegisterSuccessLog).filter(HuyaRegisterSuccessLog.account_id.in_(account_ids)).update(
{HuyaRegisterSuccessLog.account_id: None},
synchronize_session=False,
)
def _visible_huya_tasks_query(db: Session, current: User):
"""返回当前用户可查看的虎牙任务查询。"""
query = db.query(HuyaTask).options(joinedload(HuyaTask.account))
@@ -305,25 +400,14 @@ def list_accounts(
current: User = Depends(get_current_user),
):
"""查看虎牙 CK 账号。"""
query = _visible_huya_accounts_query(db, current)
if assigned_only and _can_view_huya_all(current):
query = query.filter(HuyaAccount.assigned_to.isnot(None))
if tag:
query = query.filter(HuyaAccount.tag == tag)
if has_cookie:
query = query.filter(HuyaAccount.cookie != "")
search_text = (search or "").strip()
if search_text:
pattern = f"%{search_text}%"
query = query.filter(or_(
HuyaAccount.uid.ilike(pattern),
HuyaAccount.yyuid.ilike(pattern),
HuyaAccount.username.ilike(pattern),
HuyaAccount.nickname.ilike(pattern),
HuyaAccount.tag.ilike(pattern),
HuyaAccount.game_name.ilike(pattern),
HuyaAccount.game_phone.ilike(pattern),
))
query = _filter_huya_accounts_query(
_visible_huya_accounts_query(db, current),
current,
assigned_only=assigned_only,
tag=tag,
has_cookie=has_cookie,
search=search,
)
total = None
if page is not None:
@@ -694,12 +778,13 @@ def password_login_selected_accounts(
):
"""对已导入的虎牙账号执行密码登录。"""
_require_huya_perm(current, "huya:import")
if not req.account_ids:
selected_ids = _selected_huya_account_ids(db, current, req)
if not selected_ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
accounts = (
_visible_huya_accounts_query(db, current)
.filter(HuyaAccount.id.in_(req.account_ids))
.filter(HuyaAccount.id.in_(selected_ids))
.all()
)
account_map = {account.id: account for account in accounts}
@@ -708,7 +793,7 @@ def password_login_selected_accounts(
failed_count = 0
include_cookie = _can_view_huya_cookie(current)
for account_id in req.account_ids:
for account_id in selected_ids:
account = account_map.get(account_id)
if account is None:
failed_count += 1
@@ -827,12 +912,35 @@ def delete_accounts_batch(
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
if not ids:
raise HTTPException(status_code=400, detail="无效的账号ID")
ids = _selected_huya_account_ids(db, current, AccountBulkSelection(account_ids=ids))
if not ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
_clear_huya_account_references(db, ids)
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
db.commit()
return {"message": f"已删除 {deleted} 个虎牙账号", "deleted": deleted, "success": True}
@router.post("/accounts/batch-delete")
def delete_accounts_batch_selection(
req: AccountBulkSelection,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""按显式选择或当前筛选结果批量删除虎牙账号及任务记录。"""
_require_huya_perm(current, "huya:delete")
ids = _selected_huya_account_ids(db, current, req)
if not ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
_clear_huya_account_references(db, ids)
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
db.commit()
scope = "当前筛选下" if req.all_matching else "选中的"
return {"message": f"已删除{scope} {deleted} 个虎牙账号", "deleted": deleted, "success": True}
@router.delete("/accounts/{account_id}")
def delete_account(
account_id: int,
@@ -845,6 +953,7 @@ def delete_account(
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(synchronize_session=False)
_clear_huya_account_references(db, [account_id])
db.delete(account)
db.commit()
return {"message": "已删除", "success": True}
@@ -964,6 +1073,27 @@ def batch_tag(
return {"message": f"已为 {count} 个虎牙账号设置标签", "success": True, "count": count}
@router.put("/accounts/batch-tag-selection")
def batch_tag_selection(
req: AccountBulkTag,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""按显式选择或当前筛选结果批量设置虎牙账号标签。"""
_require_huya_perm(current, "huya:import")
ids = _selected_huya_account_ids(db, current, req)
if not ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
tag = (req.tag_value or "").strip()
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).update(
{HuyaAccount.tag: tag},
synchronize_session=False,
)
db.commit()
scope = "当前筛选下" if req.all_matching else "选中的"
return {"message": f"已为{scope} {count} 个虎牙账号设置标签", "success": True, "count": count}
@router.get("/accounts/tags/list")
def list_tags(
db: Session = Depends(get_db),