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}
|
||||
|
||||
Reference in New Issue
Block a user