完善虎牙账号管理并规范Cookie
This commit is contained in:
+298
-12
@@ -1,18 +1,27 @@
|
||||
"""虎牙基础管理路由"""
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import io
|
||||
import threading
|
||||
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 core.huya import HuyaCredentialError, HuyaLoginError, login_huya_password
|
||||
from core.huya.cookie_utils import normalize_huya_cookie
|
||||
|
||||
from ..database import SessionLocal, get_db
|
||||
from ..deps import authenticate_websocket, require_permission
|
||||
from ..deps import authenticate_websocket, get_current_user, require_permission
|
||||
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
AccountAssign,
|
||||
AccountTag,
|
||||
BatchAssign,
|
||||
HuyaAccountOut,
|
||||
HuyaConfigOut,
|
||||
HuyaConfigUpdate,
|
||||
@@ -39,22 +48,54 @@ from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
|
||||
router = APIRouter(prefix="/api/huya", tags=["虎牙"])
|
||||
|
||||
|
||||
def _has_huya_perm(user: User, permission: str) -> bool:
|
||||
"""虎牙新权限兼容旧的 huya:account 大权限。"""
|
||||
return user_has_permission(user, permission) or user_has_permission(user, "huya:account")
|
||||
|
||||
|
||||
def _require_huya_perm(user: User, permission: str) -> None:
|
||||
if not _has_huya_perm(user, permission):
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
|
||||
|
||||
def _can_view_huya_all(user: User) -> bool:
|
||||
return _has_huya_perm(user, "huya:view_all")
|
||||
|
||||
|
||||
def _can_view_huya_assigned(user: User) -> bool:
|
||||
return _has_huya_perm(user, "huya:view_assigned")
|
||||
|
||||
|
||||
def _can_view_huya_cookie(user: User) -> bool:
|
||||
return _has_huya_perm(user, "huya:cookie:view") or _has_huya_perm(user, "huya:cookie:export")
|
||||
|
||||
|
||||
def _visible_huya_accounts_query(db: Session, current: User):
|
||||
"""返回当前用户可见的虎牙账号查询。"""
|
||||
query = db.query(HuyaAccount).options(joinedload(HuyaAccount.assigned_user))
|
||||
if _can_view_huya_all(current):
|
||||
return query
|
||||
if _can_view_huya_assigned(current):
|
||||
return query.filter(HuyaAccount.assigned_to == current.id)
|
||||
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
|
||||
|
||||
|
||||
def _fmt_cookie_preview(cookie: str) -> str:
|
||||
if not cookie:
|
||||
return ""
|
||||
return cookie[:50] + "..." if len(cookie) > 50 else cookie
|
||||
|
||||
|
||||
def _account_out(account: HuyaAccount) -> HuyaAccountOut:
|
||||
cookie = account.cookie or ""
|
||||
def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccountOut:
|
||||
cookie = normalize_huya_cookie(account.cookie or "")
|
||||
return HuyaAccountOut(
|
||||
id=account.id,
|
||||
uid=account.uid or "",
|
||||
yyuid=account.yyuid or "",
|
||||
username=account.username or "",
|
||||
nickname=account.nickname or "",
|
||||
cookie=cookie,
|
||||
cookie_preview=_fmt_cookie_preview(cookie),
|
||||
cookie=cookie if include_cookie else "",
|
||||
cookie_preview=_fmt_cookie_preview(cookie) if include_cookie else "***",
|
||||
tag=account.tag or "",
|
||||
remark=account.remark or "",
|
||||
status=account.status or "",
|
||||
@@ -98,6 +139,23 @@ def _config_out(config: HuyaConfig) -> HuyaConfigOut:
|
||||
)
|
||||
|
||||
|
||||
def _cookie_out(account: HuyaAccount, include_cookie: bool) -> dict:
|
||||
cookie = normalize_huya_cookie(account.cookie or "")
|
||||
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 if include_cookie else "",
|
||||
"cookie_preview": _fmt_cookie_preview(cookie) if include_cookie else "***",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/task-types")
|
||||
def task_types(current: User = Depends(require_permission("huya:task"))):
|
||||
"""返回当前规划的虎牙任务类型。"""
|
||||
@@ -106,25 +164,33 @@ def task_types(current: User = Depends(require_permission("huya:task"))):
|
||||
|
||||
@router.get("/accounts", response_model=list[HuyaAccountOut])
|
||||
def list_accounts(
|
||||
assigned_only: bool = Query(False),
|
||||
tag: str | None = Query(None),
|
||||
has_cookie: bool = Query(False),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:account")),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""查看虎牙 CK 账号。"""
|
||||
query = db.query(HuyaAccount).options(joinedload(HuyaAccount.assigned_user))
|
||||
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 != "")
|
||||
accounts = query.order_by(HuyaAccount.id.desc()).all()
|
||||
return [_account_out(account) for account in accounts]
|
||||
include_cookie = _can_view_huya_cookie(current)
|
||||
return [_account_out(account, include_cookie=include_cookie) for account in accounts]
|
||||
|
||||
|
||||
@router.post("/accounts/import-cookies")
|
||||
def import_cookies(
|
||||
req: HuyaCookieImport,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:account")),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""粘贴并导入虎牙 Cookie。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
count, skipped = import_huya_cookies(db, req.text, req.tag)
|
||||
return {
|
||||
"message": f"导入/更新 {count} 条,跳过 {skipped} 条",
|
||||
@@ -138,9 +204,10 @@ def import_cookies(
|
||||
def password_login_account(
|
||||
req: HuyaPasswordLoginRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:account")),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""使用账号密码登录虎牙,成功后保存 Cookie。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
try:
|
||||
result = login_huya_password(
|
||||
username=req.username.strip(),
|
||||
@@ -173,9 +240,10 @@ def password_login_account(
|
||||
def delete_accounts_batch(
|
||||
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:account")),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量删除虎牙 CK 账号及任务记录。"""
|
||||
_require_huya_perm(current, "huya:delete")
|
||||
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="无效的账号ID")
|
||||
@@ -189,9 +257,10 @@ def delete_accounts_batch(
|
||||
def delete_account(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:account")),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除单个虎牙 CK 账号。"""
|
||||
_require_huya_perm(current, "huya:delete")
|
||||
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
@@ -201,6 +270,223 @@ def delete_account(
|
||||
return {"message": "已删除", "success": True}
|
||||
|
||||
|
||||
@router.put("/accounts/{account_id}/assign")
|
||||
def assign_account(
|
||||
account_id: int,
|
||||
req: AccountAssign,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""分配单个虎牙账号给客服。"""
|
||||
_require_huya_perm(current, "huya:assign")
|
||||
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
if req.assigned_to:
|
||||
target = db.query(User).filter(User.id == req.assigned_to).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="目标用户不存在")
|
||||
if target.role != "support":
|
||||
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
||||
account.assigned_to = req.assigned_to
|
||||
db.commit()
|
||||
return {"message": "已分配", "success": True}
|
||||
|
||||
|
||||
@router.post("/accounts/batch-assign")
|
||||
def batch_assign_accounts(
|
||||
req: BatchAssign,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量分配/取消分配虎牙账号给客服。"""
|
||||
_require_huya_perm(current, "huya:assign")
|
||||
if not req.account_ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
if req.assigned_to is not None:
|
||||
target = db.query(User).filter(User.id == req.assigned_to).first()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="目标用户不存在")
|
||||
if target.role != "support":
|
||||
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
||||
|
||||
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update(
|
||||
{HuyaAccount.assigned_to: req.assigned_to},
|
||||
synchronize_session=False,
|
||||
)
|
||||
db.commit()
|
||||
action = "分配" if req.assigned_to else "取消分配"
|
||||
return {"message": f"已批量{action} {count} 个虎牙账号", "success": True, "count": count}
|
||||
|
||||
|
||||
@router.get("/accounts/assignments/summary")
|
||||
def assignments_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""虎牙分配概览:每个客服分配了多少虎牙账号。"""
|
||||
_require_huya_perm(current, "huya:assign")
|
||||
results = (
|
||||
db.query(User.id, User.username, func.count(HuyaAccount.id).label("count"))
|
||||
.outerjoin(HuyaAccount, HuyaAccount.assigned_to == User.id)
|
||||
.filter(User.role == "support")
|
||||
.group_by(User.id, User.username)
|
||||
.order_by(func.count(HuyaAccount.id).desc())
|
||||
.all()
|
||||
)
|
||||
total_unassigned = (
|
||||
db.query(func.count(HuyaAccount.id))
|
||||
.filter(HuyaAccount.assigned_to.is_(None))
|
||||
.scalar()
|
||||
) or 0
|
||||
return {
|
||||
"support_users": [
|
||||
{"id": uid, "username": uname, "assigned_count": cnt}
|
||||
for uid, uname, cnt in results
|
||||
],
|
||||
"unassigned_count": total_unassigned,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/accounts/{account_id}/tag")
|
||||
def set_account_tag(
|
||||
account_id: int,
|
||||
req: AccountTag,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""设置单个虎牙账号标签。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
account.tag = (req.tag or "").strip()
|
||||
db.commit()
|
||||
return {"message": "标签已更新", "success": True}
|
||||
|
||||
|
||||
@router.put("/accounts/batch-tag")
|
||||
def batch_tag(
|
||||
req: AccountTag,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量设置虎牙账号标签。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
if not req.account_ids:
|
||||
raise HTTPException(status_code=400, detail="请选择账号")
|
||||
tag = (req.tag or "").strip()
|
||||
count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update(
|
||||
{HuyaAccount.tag: tag},
|
||||
synchronize_session=False,
|
||||
)
|
||||
db.commit()
|
||||
return {"message": f"已为 {count} 个虎牙账号设置标签", "success": True, "count": count}
|
||||
|
||||
|
||||
@router.get("/accounts/tags/list")
|
||||
def list_tags(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取虎牙账号标签列表。"""
|
||||
tag_query = db.query(HuyaAccount.tag)
|
||||
if not _can_view_huya_all(current):
|
||||
if _can_view_huya_assigned(current):
|
||||
tag_query = tag_query.filter(HuyaAccount.assigned_to == current.id)
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="无权查看虎牙账号")
|
||||
tags = tag_query.filter(HuyaAccount.tag != "", HuyaAccount.tag.isnot(None)).distinct().all()
|
||||
return [item[0] for item in tags if item[0]]
|
||||
|
||||
|
||||
@router.get("/cookies")
|
||||
def list_cookies(
|
||||
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]
|
||||
|
||||
|
||||
@router.get("/cookies/export")
|
||||
def export_cookies(
|
||||
format: str = "csv",
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""导出虎牙 Cookie,支持 csv 和 custom 格式。"""
|
||||
_require_huya_perm(current, "huya:cookie:export")
|
||||
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.cookie != "").order_by(HuyaAccount.updated_at.desc()).all()
|
||||
if format == "custom":
|
||||
lines = []
|
||||
for account in accounts:
|
||||
username = account.username or account.nickname or account.uid or ""
|
||||
lines.append(f"{username}----{normalize_huya_cookie(account.cookie or '')}")
|
||||
content = "\r\n".join(lines)
|
||||
filename = "huya_cookies_custom.txt"
|
||||
media = "text/plain"
|
||||
else:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["账号", "UID", "YYUID", "Cookie", "时间"])
|
||||
for account in accounts:
|
||||
username = account.nickname or account.username or account.uid or ""
|
||||
writer.writerow([
|
||||
username,
|
||||
account.uid or "",
|
||||
account.yyuid or "",
|
||||
normalize_huya_cookie(account.cookie or ""),
|
||||
account.updated_at.isoformat() if account.updated_at else "",
|
||||
])
|
||||
content = output.getvalue()
|
||||
filename = "huya_cookies.csv"
|
||||
media = "text/csv"
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type=media,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/cookies/batch")
|
||||
def delete_cookies_batch(
|
||||
account_ids: str = "",
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量清除虎牙 Cookie,保留账号记录。"""
|
||||
_require_huya_perm(current, "huya:cookie:export")
|
||||
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="无效的账号ID")
|
||||
accounts = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(ids)).all()
|
||||
for account in accounts:
|
||||
account.cookie = ""
|
||||
account.status = "invalid"
|
||||
db.commit()
|
||||
return {"message": f"已清除 {len(accounts)} 条虎牙 Cookie", "deleted": len(accounts), "success": True}
|
||||
|
||||
|
||||
@router.delete("/cookies/{account_id}")
|
||||
def delete_cookie(
|
||||
account_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""清除单个虎牙 Cookie,保留账号记录。"""
|
||||
_require_huya_perm(current, "huya:cookie:export")
|
||||
account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
account.cookie = ""
|
||||
account.status = "invalid"
|
||||
db.commit()
|
||||
return {"message": "已清除", "success": True}
|
||||
|
||||
|
||||
@router.get("/config", response_model=HuyaConfigOut)
|
||||
def get_config(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
Reference in New Issue
Block a user