线上事故:分配给客服的账号(如「远行者103」)在斗鱼工作台与 CK 列表里 查不到,只有账号管理里还能搜到。原因是可见性判定写成了 LoginTask.status == "success",而按设计保留旧 CK 的三种状态都不再是 success: - 普通登录批次启动时会把复用的成功记录置为 pending/running(旧 CK 保留), 批次被停止或失败后一直停在 pending,账号永久消失; - CK 重登失败置为 relogin_failed(旧 CK 保留),账号从工作台消失; - 普通登录失败置为 failed(复用记录的旧 CK 未清空),同样消失。 线上现状:12545 个账号持有 CK,但只有 11703 个能出现在工作台,842 个 (644 重登失败 + 176 排队中 + …)对客服和管理员都不可见。 改动: - douyu_service 新增 cookie_holding_filter()/latest_cookie_login_task(), 以「记录里还留着 Cookie」作为唯一判据,并抽出 LOGIN_IN_FLIGHT_STATUSES 与 cookie_operable_filter() 供检测/重登入口复用; - 斗鱼工作台账号列表、任务账号校验、账号管理 has_cookie 筛选、分配校验与 概览、CK 管理/操作列表、首页 CK 统计统一改用该判据; - 工作台取 CK 从 latest_success_login_task 换成 latest_cookie_login_task, 保证重登失败/排队中的账号仍能被任务正常执行; - 新增 cleanup_orphan_login_tasks():进程重启后把遗留的 pending/running 收敛为 failed 并保留旧 CK,避免页面永久显示排队中。 新增回归测试覆盖上述状态下的可见性、可执行性与启动清理。
149 lines
4.8 KiB
Python
149 lines
4.8 KiB
Python
"""首页聚合统计路由。"""
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..database import get_db
|
|
from ..deps import get_current_user
|
|
from ..models import (
|
|
Account,
|
|
HuyaAccount,
|
|
HuyaGoodsSnapshot,
|
|
HuyaRechargeGoodsSnapshot,
|
|
HuyaTask,
|
|
LoginTask,
|
|
User,
|
|
)
|
|
from ..permissions import user_has_permission
|
|
from ..services.douyu_service import cookie_holding_filter
|
|
|
|
router = APIRouter(prefix="/api/dashboard", tags=["首页概览"])
|
|
|
|
|
|
def _task_summary(query, task_model) -> dict:
|
|
"""按状态汇总任务,避免首页读取任务列表。"""
|
|
rows = (
|
|
query.order_by(None)
|
|
.with_entities(task_model.status, func.count(task_model.id))
|
|
.group_by(task_model.status)
|
|
.all()
|
|
)
|
|
status_counts = {status or "": count for status, count in rows}
|
|
return {
|
|
"total": sum(status_counts.values()),
|
|
"success": status_counts.get("success", 0),
|
|
"failed": sum(status_counts.get(status, 0) for status in ("failed", "error")),
|
|
"status_counts": status_counts,
|
|
}
|
|
|
|
|
|
def _empty_task_summary() -> dict:
|
|
return {"total": 0, "success": 0, "failed": 0, "status_counts": {}}
|
|
|
|
|
|
def _can_view_huya_all(user: User) -> bool:
|
|
"""兼容旧 huya:account 全量权限。"""
|
|
return user_has_permission(user, "huya:view_all") or user_has_permission(
|
|
user, "huya:account"
|
|
)
|
|
|
|
|
|
@router.get("/summary")
|
|
def dashboard_summary(
|
|
db: Session = Depends(get_db),
|
|
current: User = Depends(get_current_user),
|
|
):
|
|
"""返回首页所需的全部轻量统计。"""
|
|
douyu_accounts = 0
|
|
if user_has_permission(current, "account:view_all"):
|
|
douyu_accounts = db.query(func.count(Account.id)).scalar() or 0
|
|
elif user_has_permission(current, "account:view_assigned"):
|
|
douyu_accounts = (
|
|
db.query(func.count(Account.id))
|
|
.filter(Account.assigned_to == current.id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
login_tasks = _empty_task_summary()
|
|
if any(
|
|
user_has_permission(current, permission)
|
|
for permission in (
|
|
"login:batch",
|
|
"login:view_all",
|
|
"login:view_assigned",
|
|
)
|
|
):
|
|
login_query = db.query(LoginTask).join(
|
|
Account, LoginTask.account_id == Account.id
|
|
)
|
|
if not user_has_permission(current, "login:view_all"):
|
|
login_query = login_query.filter(Account.assigned_to == current.id)
|
|
login_tasks = _task_summary(login_query, LoginTask)
|
|
|
|
cookies = 0
|
|
if user_has_permission(current, "cookie:view"):
|
|
cookie_query = (
|
|
db.query(LoginTask)
|
|
.join(Account, LoginTask.account_id == Account.id)
|
|
.filter(cookie_holding_filter())
|
|
)
|
|
if not user_has_permission(current, "login:view_all"):
|
|
cookie_query = cookie_query.filter(Account.assigned_to == current.id)
|
|
cookies = cookie_query.with_entities(func.count(LoginTask.id)).scalar() or 0
|
|
|
|
huya_accounts = 0
|
|
can_view_huya_accounts = any(
|
|
user_has_permission(current, permission)
|
|
for permission in (
|
|
"huya:account",
|
|
"huya:view_all",
|
|
"huya:view_assigned",
|
|
)
|
|
)
|
|
if can_view_huya_accounts:
|
|
huya_account_query = db.query(HuyaAccount)
|
|
if not _can_view_huya_all(current):
|
|
huya_account_query = huya_account_query.filter(
|
|
HuyaAccount.assigned_to == current.id
|
|
)
|
|
huya_accounts = (
|
|
huya_account_query.with_entities(func.count(HuyaAccount.id)).scalar() or 0
|
|
)
|
|
|
|
huya_tasks = _empty_task_summary()
|
|
huya_goods = 0
|
|
huya_recharge_goods = 0
|
|
if user_has_permission(current, "huya:task"):
|
|
huya_task_query = db.query(HuyaTask).join(
|
|
HuyaAccount, HuyaTask.account_id == HuyaAccount.id
|
|
)
|
|
if not _can_view_huya_all(current):
|
|
huya_task_query = huya_task_query.filter(
|
|
HuyaAccount.assigned_to == current.id
|
|
)
|
|
huya_tasks = _task_summary(huya_task_query, HuyaTask)
|
|
huya_goods = db.query(func.count(HuyaGoodsSnapshot.id)).scalar() or 0
|
|
huya_recharge_goods = (
|
|
db.query(func.count(HuyaRechargeGoodsSnapshot.id)).scalar() or 0
|
|
)
|
|
|
|
return {
|
|
"douyu": {
|
|
"accounts": douyu_accounts,
|
|
"tasks": login_tasks["total"],
|
|
"success": login_tasks["success"],
|
|
"failed": login_tasks["failed"],
|
|
"cookies": cookies,
|
|
},
|
|
"huya": {
|
|
"accounts": huya_accounts,
|
|
"tasks": huya_tasks["total"],
|
|
"success": huya_tasks["success"],
|
|
"failed": huya_tasks["failed"],
|
|
"goods": huya_goods,
|
|
"rechargeGoods": huya_recharge_goods,
|
|
},
|
|
}
|