优化客服CK重登流程
This commit is contained in:
+132
-116
@@ -3,7 +3,6 @@
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import case, or_
|
||||
@@ -16,7 +15,8 @@ from ..models import User, LoginTask, Account, AuditLog, ProxyConfig as ProxyCon
|
||||
from ..deps import get_current_user, require_permission
|
||||
from ..permissions import user_has_permission, get_user_permissions
|
||||
from ..schemas import CookieReloginRequest
|
||||
from ..services.login_service import LoginBatchRunner
|
||||
from ..services.login_service import BatchRegistry, LoginBatchRunner, get_relogin_limits
|
||||
from ..services.cookie_check_service import check_douyu_cookie
|
||||
|
||||
|
||||
def _fmt_dt(dt) -> str | None:
|
||||
@@ -28,6 +28,7 @@ def _fmt_dt(dt) -> str | None:
|
||||
return dt.isoformat()
|
||||
|
||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
||||
cookie_relogin_registry = BatchRegistry()
|
||||
|
||||
|
||||
def _require_cookie_operation_perm(current: User) -> None:
|
||||
@@ -63,89 +64,12 @@ def _order_cookie_tasks(query, selected_names: list[str]):
|
||||
return query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||
|
||||
|
||||
# 斗鱼 Cookie 有效性检测接口(抓包参考:钱包中心鱼丸余额 + 用户等级详情)
|
||||
CHECK_UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
|
||||
)
|
||||
FISH_BALL_API = "https://www.douyu.com/wjapi/nc/exchange/fishBall"
|
||||
USER_LEVEL_API = "https://www.douyu.com/japi/interactnc/web/userLevel/userLevelDetail"
|
||||
|
||||
|
||||
def _check_one_cookie(task: LoginTask) -> dict:
|
||||
"""检测单条斗鱼 Cookie 有效性:鱼丸 + 用户等级双接口均通过才算有效。
|
||||
|
||||
附带鱼丸数、昵称/等级与检测时间;任一接口失败会说明原因。
|
||||
"""
|
||||
cookie = task.cookie or ""
|
||||
checked_at = datetime.now(timezone.utc).isoformat()
|
||||
base = {"id": task.id, "checked_at": checked_at, "fish_ball": None, "nickname": None, "level": None}
|
||||
if not cookie:
|
||||
return {**base, "valid": False, "message": "Cookie 为空"}
|
||||
|
||||
fish_ok = False
|
||||
fish_msg = ""
|
||||
fish_ball = None
|
||||
try:
|
||||
fish_data = requests.get(
|
||||
FISH_BALL_API,
|
||||
params={"appCode": "YJTX"},
|
||||
headers={
|
||||
"User-Agent": CHECK_UA,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.douyu.com/member/walletcenter",
|
||||
"Cookie": cookie,
|
||||
},
|
||||
timeout=(5, 10),
|
||||
).json()
|
||||
if isinstance(fish_data, dict) and fish_data.get("error") in (0, "0"):
|
||||
fish_ok = True
|
||||
fish_ball = (fish_data.get("data") or {}).get("count") if isinstance(fish_data.get("data"), dict) else None
|
||||
else:
|
||||
fish_msg = str(fish_data.get("msg") or fish_data.get("error") or "响应异常") if isinstance(fish_data, dict) else "响应异常"
|
||||
except Exception as exc:
|
||||
fish_msg = f"请求失败: {exc}"
|
||||
|
||||
level_ok = False
|
||||
level_msg = ""
|
||||
nickname = None
|
||||
level = None
|
||||
try:
|
||||
level_data = requests.get(
|
||||
USER_LEVEL_API,
|
||||
params={"rid": "0"},
|
||||
headers={
|
||||
"User-Agent": CHECK_UA,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": "https://www.douyu.com/pages/ord-user-level?clientType=web",
|
||||
"Cookie": cookie,
|
||||
},
|
||||
timeout=(5, 10),
|
||||
).json()
|
||||
if isinstance(level_data, dict) and level_data.get("error") in (0, "0"):
|
||||
level_ok = True
|
||||
info = level_data.get("data") if isinstance(level_data.get("data"), dict) else {}
|
||||
nickname = str(info.get("nn") or "") or None
|
||||
level = info.get("lv")
|
||||
else:
|
||||
level_msg = str(level_data.get("msg") or level_data.get("error") or "响应异常") if isinstance(level_data, dict) else "响应异常"
|
||||
except Exception as exc:
|
||||
level_msg = f"请求失败: {exc}"
|
||||
|
||||
valid = fish_ok and level_ok
|
||||
if valid:
|
||||
message = "有效"
|
||||
else:
|
||||
parts = [f"鱼丸接口: {'ok' if fish_ok else (fish_msg or '失败')}", f"等级接口: {'ok' if level_ok else (level_msg or '失败')}"]
|
||||
message = ";".join(parts)
|
||||
return {
|
||||
**base,
|
||||
"valid": valid,
|
||||
"message": message[:200],
|
||||
"fish_ball": fish_ball,
|
||||
"nickname": nickname,
|
||||
"level": level,
|
||||
}
|
||||
"""将共享检测结果补充为 Cookie 记录的接口返回格式。"""
|
||||
result = check_douyu_cookie(task.cookie or "")
|
||||
result["id"] = task.id
|
||||
result["checked_at"] = result["checked_at"].isoformat()
|
||||
return result
|
||||
|
||||
|
||||
def _visible_cookie_tasks_query(db: Session, current: User):
|
||||
@@ -160,6 +84,19 @@ def _visible_cookie_tasks_query(db: Session, current: User):
|
||||
return query
|
||||
|
||||
|
||||
def _visible_cookie_operation_tasks_query(db: Session, current: User):
|
||||
"""返回可检测/重登的 Cookie 记录,重登中或失败时仍保留在操作列表。"""
|
||||
query = db.query(LoginTask).filter(
|
||||
LoginTask.cookie != "",
|
||||
LoginTask.status.in_(("success", "relogin_pending", "relogin_running", "relogin_failed")),
|
||||
)
|
||||
if not user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id).filter(
|
||||
Account.assigned_to == current.id
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_cookies(
|
||||
search: str = Query(""),
|
||||
@@ -292,7 +229,7 @@ def list_cookie_operations(
|
||||
):
|
||||
"""脱敏的 CK 操作列表,只用于检测与重登。"""
|
||||
_require_cookie_operation_perm(current)
|
||||
query = _visible_cookie_tasks_query(db, current)
|
||||
query = _visible_cookie_operation_tasks_query(db, current)
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
search_text = (search or "").strip()
|
||||
@@ -321,6 +258,9 @@ def list_cookie_operations(
|
||||
"ck_check_status": task.ck_check_status or "",
|
||||
"ck_checked_at": _fmt_dt(task.ck_checked_at),
|
||||
"created_at": _fmt_dt(task.finished_at),
|
||||
"relogin_status": task.status if task.status != "success" else "",
|
||||
"relogin_message": task.message or "",
|
||||
"relogin_batch_id": task.batch_id if task.status in {"relogin_pending", "relogin_running"} else "",
|
||||
}
|
||||
for task in tasks
|
||||
],
|
||||
@@ -337,7 +277,7 @@ def list_cookie_operation_tags(
|
||||
):
|
||||
"""返回当前用户可操作 CK 记录对应的标签,不跨越账号分配范围。"""
|
||||
_require_cookie_operation_perm(current)
|
||||
query = _visible_cookie_tasks_query(db, current)
|
||||
query = _visible_cookie_operation_tasks_query(db, current)
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
rows = (
|
||||
@@ -487,7 +427,10 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d
|
||||
id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()]
|
||||
if not id_list:
|
||||
raise HTTPException(status_code=400, detail="无效的ID")
|
||||
tasks = _visible_cookie_tasks_query(db, current).filter(LoginTask.id.in_(id_list)).all()
|
||||
base_query = _visible_cookie_tasks_query(db, current) if detailed else _visible_cookie_operation_tasks_query(db, current)
|
||||
tasks = base_query.filter(LoginTask.id.in_(id_list)).filter(
|
||||
LoginTask.status.in_(("success", "relogin_failed"))
|
||||
).all()
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
|
||||
@@ -562,48 +505,46 @@ def check_cookie_operations(
|
||||
return _check_cookies(ids, db, current, detailed=False)
|
||||
|
||||
|
||||
def _start_relogin(
|
||||
req: CookieReloginRequest,
|
||||
db: Session,
|
||||
current: User,
|
||||
):
|
||||
"""重新登录失效的 Cookie:用账号信息重新登录,成功后替换旧 Cookie。
|
||||
|
||||
复用原 LoginTask 记录,行不消失;重新登录失败保留旧 Cookie 并记录原因。
|
||||
"""
|
||||
if not req.ids:
|
||||
raise HTTPException(status_code=400, detail="请选择要重新登录的 Cookie")
|
||||
|
||||
tasks = _visible_cookie_tasks_query(db, current).filter(LoginTask.id.in_(req.ids)).all()
|
||||
def _start_relogin_tasks(tasks: list[LoginTask], db: Session, current: User, *, action: str = "cookie:relogin"):
|
||||
"""启动重登批次:旧 Cookie 保留到新登录成功后才替换。"""
|
||||
if not tasks:
|
||||
raise HTTPException(status_code=404, detail="记录不存在")
|
||||
|
||||
# 过滤缺少登录凭据的账号(密码/邮箱),避免启动后全部失败
|
||||
task_ids: list[int] = []
|
||||
skipped: list[str] = []
|
||||
accounts_map = {t.account_id: t for t in tasks}
|
||||
for acc_id, task in accounts_map.items():
|
||||
acc = db.query(Account).filter(Account.id == acc_id).first()
|
||||
skipped: list[int] = []
|
||||
for task in tasks:
|
||||
acc = db.query(Account).filter(Account.id == task.account_id).first()
|
||||
if not acc:
|
||||
continue
|
||||
if not acc.password or not acc.email:
|
||||
skipped.append(task.id)
|
||||
continue
|
||||
if not acc.password or not acc.email:
|
||||
task.status = "relogin_failed"
|
||||
task.message = "重新登录失败: 账号缺少密码或邮箱,旧 Cookie 已保留"
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
skipped.append(task.id)
|
||||
continue
|
||||
if task.status in {"relogin_pending", "relogin_running"}:
|
||||
skipped.append(task.id)
|
||||
continue
|
||||
task.status = "relogin_pending"
|
||||
task.message = "等待重新登录,旧 Cookie 已保留"
|
||||
task.finished_at = None
|
||||
task_ids.append(task.id)
|
||||
|
||||
if not task_ids:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="所选 Cookie 对应账号缺少密码或邮箱信息,无法重新登录,请先在账号管理中补充",
|
||||
)
|
||||
db.commit()
|
||||
raise HTTPException(status_code=400, detail="没有可重登的账号,请检查账号凭据或当前重登状态")
|
||||
|
||||
proxy = db.query(ProxyConfigModel).first()
|
||||
thread_db = SessionLocal()
|
||||
max_login_retries, max_total_time = get_relogin_limits()
|
||||
runner = LoginBatchRunner(
|
||||
db=thread_db,
|
||||
account_ids=[],
|
||||
created_by=current.id,
|
||||
creator_permissions=get_user_permissions(current),
|
||||
max_login_retries=max_login_retries,
|
||||
max_total_time=max_total_time,
|
||||
proxy_config=proxy,
|
||||
log_queue=None,
|
||||
loop=None,
|
||||
@@ -612,16 +553,29 @@ def _start_relogin(
|
||||
relogin_task_ids=task_ids,
|
||||
)
|
||||
batch_id = runner.batch_id
|
||||
thread = threading.Thread(target=runner.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:relogin",
|
||||
target=f"重登 {len(task_ids)} 条已分配账号 CK",
|
||||
action=action,
|
||||
target=f"重登 {len(task_ids)} 条账号 CK",
|
||||
detail="旧 Cookie 将在新登录成功后替换",
|
||||
))
|
||||
db.commit()
|
||||
cookie_relogin_registry.register(batch_id, None, None, runner, owner_id=current.id)
|
||||
|
||||
def run_relogin_batch():
|
||||
try:
|
||||
runner.run()
|
||||
except Exception as exc:
|
||||
runner._push_log("error", f"批量重新登录异常: {exc}")
|
||||
message = f"重新登录异常: {exc}(旧 Cookie 已保留)"
|
||||
for task_id in task_ids:
|
||||
runner._mark_relogin_stopped(task_id, message)
|
||||
runner.db.close()
|
||||
finally:
|
||||
cookie_relogin_registry.pop(batch_id)
|
||||
|
||||
threading.Thread(target=run_relogin_batch, daemon=True).start()
|
||||
return {
|
||||
"batch_id": batch_id,
|
||||
"count": len(task_ids),
|
||||
@@ -630,6 +584,44 @@ def _start_relogin(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/operations/relogin/{batch_id}/stop")
|
||||
def stop_cookie_relogin(
|
||||
batch_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""停止当前用户发起的 CK 重登批次。"""
|
||||
_require_cookie_operation_perm(current)
|
||||
batch = cookie_relogin_registry.get(batch_id)
|
||||
if not batch:
|
||||
raise HTTPException(status_code=404, detail="重登批次不存在、已结束或服务已重启")
|
||||
if batch.get("owner_id") != current.id and not user_has_permission(current, "login:view_all"):
|
||||
raise HTTPException(status_code=403, detail="无权限停止该重登批次")
|
||||
|
||||
batch["runner"].stop()
|
||||
db.add(AuditLog(
|
||||
user_id=current.id,
|
||||
username=current.username,
|
||||
action="cookie:relogin_stop",
|
||||
target=f"停止 CK 重登批次 {batch_id}",
|
||||
))
|
||||
db.commit()
|
||||
return {"message": "已发送停止信号,正在登录的账号会在当前请求结束后停止", "success": True}
|
||||
|
||||
|
||||
def _start_relogin(req: CookieReloginRequest, db: Session, current: User):
|
||||
"""按选中的 Cookie 记录启动重新登录。"""
|
||||
if not req.ids:
|
||||
raise HTTPException(status_code=400, detail="请选择要重新登录的 Cookie")
|
||||
tasks = (
|
||||
_visible_cookie_operation_tasks_query(db, current)
|
||||
.filter(LoginTask.id.in_(req.ids))
|
||||
.filter(LoginTask.status.in_(("success", "relogin_failed")))
|
||||
.all()
|
||||
)
|
||||
return _start_relogin_tasks(tasks, db, current)
|
||||
|
||||
|
||||
@router.post("/relogin")
|
||||
def relogin_cookies(
|
||||
req: CookieReloginRequest,
|
||||
@@ -651,6 +643,30 @@ def relogin_cookie_operations(
|
||||
return _start_relogin(req, db, current)
|
||||
|
||||
|
||||
@router.post("/operations/relogin-invalid")
|
||||
def relogin_invalid_cookie_operations(
|
||||
search: str = Query(""),
|
||||
tag: str = Query(""),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""批量重新登录当前用户可见且最近检测为无效的账号。"""
|
||||
_require_cookie_operation_perm(current)
|
||||
query = _visible_cookie_operation_tasks_query(db, current).filter(
|
||||
LoginTask.ck_check_status == "invalid",
|
||||
LoginTask.status.in_(("success", "relogin_failed")),
|
||||
)
|
||||
if user_has_permission(current, "login:view_all"):
|
||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||
if tag.strip():
|
||||
query = query.filter(Account.tag == tag.strip())
|
||||
if search.strip():
|
||||
pattern = f"%{search.strip()}%"
|
||||
query = query.filter(or_(Account.username.ilike(pattern), Account.tag.ilike(pattern)))
|
||||
tasks = query.order_by(LoginTask.id.asc()).all()
|
||||
return _start_relogin_tasks(tasks, db, current, action="cookie:relogin_invalid")
|
||||
|
||||
|
||||
@router.get("/{task_id}")
|
||||
def get_cookie(
|
||||
task_id: int,
|
||||
|
||||
Reference in New Issue
Block a user