支持账号按筛选全量批量操作
This commit is contained in:
+137
-29
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user