优化列表分页和数据加载

This commit is contained in:
yml2213
2026-07-24 13:25:27 +08:00
parent 6840728da6
commit f91c05c108
17 changed files with 647 additions and 161 deletions
+64 -5
View File
@@ -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,
+93 -6
View File
@@ -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
View File
@@ -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 = "",