优化列表分页和数据加载
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""账号管理路由"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, Account, AuditLog, LoginTask
|
||||
@@ -16,11 +16,15 @@ from ..services.account_service import (
|
||||
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[AccountOut])
|
||||
@router.get("")
|
||||
def list_accounts(
|
||||
assigned_only: bool = Query(False),
|
||||
tag: str = Query(None),
|
||||
has_cookie: bool = Query(False),
|
||||
search: str = Query(""),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_sensitive: bool = Query(True),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
@@ -44,7 +48,31 @@ def list_accounts(
|
||||
if tag:
|
||||
query = query.filter(Account.tag == tag)
|
||||
|
||||
accounts = query.order_by(Account.id).options(joinedload(Account.assigned_user)).all()
|
||||
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),
|
||||
))
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
query = query.order_by(Account.id)
|
||||
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")
|
||||
options = [joinedload(Account.assigned_user)]
|
||||
if not can_include_sensitive:
|
||||
options.extend([
|
||||
defer(Account.password),
|
||||
defer(Account.email),
|
||||
defer(Account.email_password),
|
||||
])
|
||||
accounts = query.options(*options).all()
|
||||
result = []
|
||||
for acc in accounts:
|
||||
item = AccountOut(
|
||||
@@ -55,14 +83,45 @@ def list_accounts(
|
||||
created_at=acc.created_at,
|
||||
)
|
||||
# 只有管理员可看完整字段(密码、邮箱等)
|
||||
if user_has_permission(current, "account:view_full"):
|
||||
if can_include_sensitive:
|
||||
item.password = acc.password
|
||||
item.email = acc.email
|
||||
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 result
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
def accounts_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""账号管理统计,避免前端为了卡片统计拉全量账号。"""
|
||||
query = db.query(Account)
|
||||
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="无权查看账号")
|
||||
|
||||
total = query.count()
|
||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
||||
tag_count = (
|
||||
query.filter(Account.tag != "", Account.tag.isnot(None))
|
||||
.with_entities(Account.tag)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
return {
|
||||
"total": total,
|
||||
"assigned_count": assigned_count,
|
||||
"unassigned_count": max(0, total - assigned_count),
|
||||
"tag_count": tag_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
def import_accounts(
|
||||
req: AccountImport,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Cookie 管理路由"""
|
||||
|
||||
from datetime import timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
import io
|
||||
import csv
|
||||
|
||||
@@ -38,17 +39,52 @@ def _visible_cookie_tasks_query(db: Session, current: User):
|
||||
|
||||
@router.get("")
|
||||
def list_cookies(
|
||||
search: str = Query(""),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_cookie: bool = Query(True),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""查看登录成功的 Cookie 列表。"""
|
||||
tasks = _visible_cookie_tasks_query(db, current).order_by(LoginTask.finished_at.desc()).all()
|
||||
if not include_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),
|
||||
)
|
||||
search_text = (search or "").strip()
|
||||
if search_text:
|
||||
pattern = f"%{search_text}%"
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
query = query.outerjoin(User, Account.assigned_to == User.id).filter(or_(
|
||||
Account.username.ilike(pattern),
|
||||
User.username.ilike(pattern),
|
||||
))
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
query = query.order_by(LoginTask.finished_at.desc())
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
tasks = query.all()
|
||||
|
||||
# 批量查账号,避免 N+1 查询
|
||||
account_ids = [t.account_id for t in tasks]
|
||||
accounts_map = {}
|
||||
if account_ids:
|
||||
accs = db.query(Account).filter(Account.id.in_(account_ids)).all()
|
||||
account_query = db.query(Account).filter(Account.id.in_(account_ids))
|
||||
if not include_cookie:
|
||||
account_query = account_query.options(
|
||||
joinedload(Account.assigned_user),
|
||||
defer(Account.password),
|
||||
defer(Account.email),
|
||||
defer(Account.email_password),
|
||||
)
|
||||
accs = account_query.all()
|
||||
accounts_map = {a.id: a for a in accs}
|
||||
|
||||
result = []
|
||||
@@ -63,8 +99,8 @@ def list_cookies(
|
||||
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
|
||||
"created_at": _fmt_dt(t.finished_at),
|
||||
}
|
||||
# 只有有 cookie:view 权限才返回 cookie 和密码(用于自定义格式复制)
|
||||
if user_has_permission(current, "cookie:view"):
|
||||
# 分页列表默认只返回预览,复制/导出时再获取完整敏感字段。
|
||||
if include_cookie and user_has_permission(current, "cookie:view"):
|
||||
cookie = t.cookie or ""
|
||||
item["cookie"] = cookie
|
||||
item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie
|
||||
@@ -74,9 +110,60 @@ def list_cookies(
|
||||
item["cookie_preview"] = "***"
|
||||
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 result
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
def cookies_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""Cookie 管理统计,避免前端为了卡片统计拉全量 Cookie。"""
|
||||
query = _visible_cookie_tasks_query(db, current)
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
total = query.count()
|
||||
assigned_count = query.filter(Account.assigned_to.isnot(None)).count()
|
||||
return {
|
||||
"total": total,
|
||||
"assigned_count": assigned_count,
|
||||
"unassigned_count": max(0, total - assigned_count),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{task_id}")
|
||||
def get_cookie(
|
||||
task_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。"""
|
||||
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()
|
||||
item = {
|
||||
"id": task.id,
|
||||
"batch_id": task.batch_id,
|
||||
"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,
|
||||
"created_at": _fmt_dt(task.finished_at),
|
||||
"cookie": "",
|
||||
"cookie_preview": "***",
|
||||
"account_password": "",
|
||||
}
|
||||
if user_has_permission(current, "cookie:view"):
|
||||
cookie = task.cookie or ""
|
||||
item["cookie"] = cookie
|
||||
item["cookie_preview"] = cookie[:50] + "..." if len(cookie) > 50 else cookie
|
||||
item["account_password"] = acc.password if acc else ""
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
def export_cookies(
|
||||
format: str = "csv",
|
||||
|
||||
+188
-9
@@ -8,8 +8,8 @@ from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session, defer, joinedload
|
||||
|
||||
from core.huya import (
|
||||
HuyaCredentialError,
|
||||
@@ -134,6 +134,31 @@ def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccou
|
||||
)
|
||||
|
||||
|
||||
def _account_out_light(account: HuyaAccount, *, has_password: bool = False) -> HuyaAccountOut:
|
||||
"""虎牙账号列表轻量输出,不读取加密字段。"""
|
||||
return HuyaAccountOut(
|
||||
id=account.id,
|
||||
uid=account.uid or "",
|
||||
yyuid=account.yyuid or "",
|
||||
username=account.username or "",
|
||||
has_password=has_password,
|
||||
nickname=account.nickname or "",
|
||||
cookie="",
|
||||
cookie_preview="***",
|
||||
tag=account.tag or "",
|
||||
remark=account.remark or "",
|
||||
status=account.status or "",
|
||||
points=account.points,
|
||||
game_name=account.game_name or "",
|
||||
game_channel=account.game_channel or "",
|
||||
game_phone=account.game_phone or "",
|
||||
assigned_to=account.assigned_to,
|
||||
assigned_username=account.assigned_user.username if account.assigned_user else None,
|
||||
created_at=account.created_at,
|
||||
updated_at=account.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_task_result(result: dict | None, *, include_images: bool = False) -> dict | None:
|
||||
"""列表接口默认剥离 base64 图片,避免轮询每次传 1MB+ 数据。"""
|
||||
if not isinstance(result, dict):
|
||||
@@ -196,17 +221,50 @@ def _cookie_out(account: HuyaAccount, include_cookie: bool) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _cookie_out_light(account: HuyaAccount) -> dict:
|
||||
"""虎牙 Cookie 列表轻量输出,不读取完整加密 Cookie。"""
|
||||
account_name = account.nickname or account.username or account.uid or str(account.id)
|
||||
return {
|
||||
"id": account.id,
|
||||
"account_id": account.id,
|
||||
"account_username": account_name,
|
||||
"uid": account.uid or "",
|
||||
"yyuid": account.yyuid or "",
|
||||
"assigned_to": account.assigned_to,
|
||||
"assigned_username": account.assigned_user.username if account.assigned_user else None,
|
||||
"created_at": account.updated_at.isoformat() if account.updated_at else None,
|
||||
"cookie": "",
|
||||
"cookie_preview": "***",
|
||||
}
|
||||
|
||||
|
||||
def _huya_password_state_map(db: Session, account_ids: list[int]) -> dict[int, bool]:
|
||||
"""不解密密码字段,仅判断密文字段是否非空。"""
|
||||
if not account_ids:
|
||||
return {}
|
||||
rows = (
|
||||
db.query(HuyaAccount.id, (HuyaAccount.account_password != "").label("has_password"))
|
||||
.filter(HuyaAccount.id.in_(account_ids))
|
||||
.all()
|
||||
)
|
||||
return {account_id: bool(has_password) for account_id, has_password in rows}
|
||||
|
||||
|
||||
@router.get("/task-types")
|
||||
def task_types(current: User = Depends(require_permission("huya:task"))):
|
||||
"""返回当前规划的虎牙任务类型。"""
|
||||
return SUPPORTED_TASK_TYPES
|
||||
|
||||
|
||||
@router.get("/accounts", response_model=list[HuyaAccountOut])
|
||||
@router.get("/accounts")
|
||||
def list_accounts(
|
||||
assigned_only: bool = Query(False),
|
||||
tag: str | None = Query(None),
|
||||
has_cookie: bool = Query(False),
|
||||
search: str = Query(""),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_cookie: bool | None = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
@@ -218,9 +276,71 @@ def list_accounts(
|
||||
query = query.filter(HuyaAccount.tag == tag)
|
||||
if has_cookie:
|
||||
query = query.filter(HuyaAccount.cookie != "")
|
||||
accounts = query.order_by(HuyaAccount.id.desc()).all()
|
||||
include_cookie = _can_view_huya_cookie(current)
|
||||
return [_account_out(account, include_cookie=include_cookie) for account in accounts]
|
||||
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),
|
||||
))
|
||||
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
should_include_cookie = _can_view_huya_cookie(current) if include_cookie is None else (include_cookie and _can_view_huya_cookie(current))
|
||||
query = query.order_by(HuyaAccount.id.desc())
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
if not should_include_cookie:
|
||||
query = query.options(defer(HuyaAccount.cookie), defer(HuyaAccount.account_password))
|
||||
accounts = query.all()
|
||||
if should_include_cookie:
|
||||
result = [_account_out(account, include_cookie=True) for account in accounts]
|
||||
else:
|
||||
password_map = _huya_password_state_map(db, [account.id for account in accounts])
|
||||
result = [_account_out_light(account, has_password=password_map.get(account.id, False)) for account in accounts]
|
||||
if page is not None:
|
||||
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/accounts/summary")
|
||||
def accounts_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""虎牙账号统计,避免前端为了卡片统计拉全量账号。"""
|
||||
query = _visible_huya_accounts_query(db, current)
|
||||
total = query.count()
|
||||
assigned_count = query.filter(HuyaAccount.assigned_to.isnot(None)).count()
|
||||
tag_count = (
|
||||
query.filter(HuyaAccount.tag != "", HuyaAccount.tag.isnot(None))
|
||||
.with_entities(HuyaAccount.tag)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
password_ready_count = query.filter(HuyaAccount.account_password != "").count()
|
||||
point_count = query.filter(HuyaAccount.points.isnot(None)).count()
|
||||
bound_count = query.filter(or_(
|
||||
HuyaAccount.game_name != "",
|
||||
HuyaAccount.game_channel != "",
|
||||
HuyaAccount.game_phone != "",
|
||||
)).count()
|
||||
return {
|
||||
"total": total,
|
||||
"assigned_count": assigned_count,
|
||||
"unassigned_count": max(0, total - assigned_count),
|
||||
"tag_count": tag_count,
|
||||
"password_ready_count": password_ready_count,
|
||||
"point_count": point_count,
|
||||
"bound_count": bound_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/accounts/import-cookies")
|
||||
@@ -826,13 +946,59 @@ def list_tags(
|
||||
|
||||
@router.get("/cookies")
|
||||
def list_cookies(
|
||||
search: str = Query(""),
|
||||
page: int | None = Query(None, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=200),
|
||||
include_cookie: bool = Query(True),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""查看当前用户可见的虎牙 Cookie。"""
|
||||
include_cookie = _can_view_huya_cookie(current)
|
||||
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "").order_by(HuyaAccount.updated_at.desc()).all()
|
||||
return [_cookie_out(account, include_cookie=include_cookie) for account in accounts]
|
||||
should_include_cookie = include_cookie and _can_view_huya_cookie(current)
|
||||
query = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "")
|
||||
search_text = (search or "").strip()
|
||||
if search_text:
|
||||
pattern = f"%{search_text}%"
|
||||
query = query.outerjoin(User, HuyaAccount.assigned_to == User.id).filter(or_(
|
||||
HuyaAccount.uid.ilike(pattern),
|
||||
HuyaAccount.yyuid.ilike(pattern),
|
||||
HuyaAccount.username.ilike(pattern),
|
||||
HuyaAccount.nickname.ilike(pattern),
|
||||
User.username.ilike(pattern),
|
||||
))
|
||||
total = None
|
||||
if page is not None:
|
||||
total = query.order_by(None).count()
|
||||
query = query.order_by(HuyaAccount.updated_at.desc())
|
||||
if page is not None:
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
if not should_include_cookie:
|
||||
query = query.options(defer(HuyaAccount.cookie), defer(HuyaAccount.account_password))
|
||||
accounts = query.all()
|
||||
result = (
|
||||
[_cookie_out(account, include_cookie=True) for account in accounts]
|
||||
if should_include_cookie
|
||||
else [_cookie_out_light(account) for account in accounts]
|
||||
)
|
||||
if page is not None:
|
||||
return {"items": result, "total": total or 0, "page": page, "page_size": page_size}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/cookies/summary")
|
||||
def cookies_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""虎牙 Cookie 统计,避免前端为了卡片统计拉全量 Cookie。"""
|
||||
query = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "")
|
||||
total = query.count()
|
||||
assigned_count = query.filter(HuyaAccount.assigned_to.isnot(None)).count()
|
||||
return {
|
||||
"total": total,
|
||||
"assigned_count": assigned_count,
|
||||
"unassigned_count": max(0, total - assigned_count),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/cookies/export")
|
||||
@@ -875,6 +1041,19 @@ def export_cookies(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/cookies/{account_id}")
|
||||
def get_cookie(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取单条虎牙 Cookie 详情,供复制操作按需读取完整敏感字段。"""
|
||||
account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id, HuyaAccount.cookie != "").first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
return _cookie_out(account, include_cookie=_can_view_huya_cookie(current))
|
||||
|
||||
|
||||
@router.delete("/cookies/batch")
|
||||
def delete_cookies_batch(
|
||||
account_ids: str = "",
|
||||
|
||||
Reference in New Issue
Block a user