线上事故:分配给客服的账号(如「远行者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,避免页面永久显示排队中。 新增回归测试覆盖上述状态下的可见性、可执行性与启动清理。
167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
"""回归:只要账号还留着 CK,就不得因为登录批次状态而从工作台消失。
|
||
|
||
线上事故(2026-10-01):账号「远行者103」被分配给客服后,客服与管理员在
|
||
斗鱼工作台都看不到它,只有账号管理里还能搜到。原因是可见性判定写成了
|
||
``LoginTask.status == "success"``,而普通登录批次启动时会把复用的成功记录
|
||
置为 ``pending``,CK 重登失败会置为 ``relogin_failed`` —— 两种状态都保留旧
|
||
Cookie,却让账号从工作台与 CK 列表里凭空消失。
|
||
"""
|
||
|
||
import pytest
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
|
||
from web.backend.database import Base
|
||
from web.backend.models import Account, LoginTask, User
|
||
from web.backend.routers.cookies import list_cookies
|
||
from web.backend.routers.douyu import list_task_account_ids, list_task_accounts
|
||
from web.backend.services.account_service import cookie_account_ids_query
|
||
from web.backend.services.douyu_service import (
|
||
create_douyu_planned_tasks,
|
||
latest_cookie_login_task,
|
||
visible_douyu_task_accounts,
|
||
)
|
||
|
||
|
||
class TestDouyuCookieVisibility:
|
||
def setup_method(self):
|
||
self.engine = create_engine("sqlite://")
|
||
Base.metadata.create_all(self.engine)
|
||
self.session = sessionmaker(bind=self.engine)()
|
||
|
||
self.admin = User(username="admin", password_hash="hash", role="super_admin")
|
||
self.support = User(username="kf_7", password_hash="hash", role="support")
|
||
self.session.add_all([self.admin, self.support])
|
||
self.session.commit()
|
||
|
||
# relogin_failed:重登失败但旧 CK 已按设计保留
|
||
self.relogin_account = self._make_account("远行者103", self.support.id)
|
||
self.session.add(
|
||
LoginTask(
|
||
batch_id="relogin-failed",
|
||
account_id=self.relogin_account.id,
|
||
created_by=self.support.id,
|
||
status="relogin_failed",
|
||
cookie="acf_uid=481802039",
|
||
ck_check_status="valid",
|
||
)
|
||
)
|
||
# pending:普通登录批次排队中,复用了成功记录,旧 CK 仍在
|
||
self.pending_account = self._make_account("排队中账号", self.support.id)
|
||
self.session.add(
|
||
LoginTask(
|
||
batch_id="queued",
|
||
account_id=self.pending_account.id,
|
||
created_by=self.admin.id,
|
||
status="pending",
|
||
cookie="acf_uid=2",
|
||
)
|
||
)
|
||
# 从未登录成功:没有 CK,必须继续隐藏
|
||
self.empty_account = self._make_account("无CK账号", self.support.id)
|
||
self.session.add(
|
||
LoginTask(
|
||
batch_id="failed",
|
||
account_id=self.empty_account.id,
|
||
created_by=self.admin.id,
|
||
status="failed",
|
||
cookie="",
|
||
)
|
||
)
|
||
self.session.commit()
|
||
|
||
def _make_account(self, username: str, assigned_to: int) -> Account:
|
||
account = Account(
|
||
username=username,
|
||
password="password",
|
||
email=f"{username}@example.com",
|
||
email_password="mail-password",
|
||
assigned_to=assigned_to,
|
||
)
|
||
self.session.add(account)
|
||
self.session.flush()
|
||
return account
|
||
|
||
def teardown_method(self):
|
||
self.session.close()
|
||
Base.metadata.drop_all(self.engine)
|
||
self.engine.dispose()
|
||
|
||
def _task_account_names(self, current: User) -> set[str]:
|
||
result = list_task_accounts(
|
||
search="",
|
||
tag="",
|
||
ids="",
|
||
account_names="",
|
||
page=None,
|
||
page_size=50,
|
||
db=self.session,
|
||
current=current,
|
||
)
|
||
return {item.username for item in result}
|
||
|
||
@pytest.mark.parametrize("current_attr", ["admin", "support"])
|
||
def test_workbench_lists_accounts_that_still_hold_cookie(self, current_attr):
|
||
current = getattr(self, current_attr)
|
||
names = self._task_account_names(current)
|
||
assert {"远行者103", "排队中账号"} <= names
|
||
assert "无CK账号" not in names
|
||
|
||
def test_workbench_account_ids_keep_cookie_holders(self):
|
||
result = list_task_account_ids(
|
||
search="",
|
||
tag="",
|
||
account_names="",
|
||
db=self.session,
|
||
current=self.admin,
|
||
)
|
||
assert set(result["account_ids"]) == {
|
||
self.relogin_account.id,
|
||
self.pending_account.id,
|
||
}
|
||
|
||
def test_latest_cookie_login_task_ignores_batch_status(self):
|
||
task = latest_cookie_login_task(self.session, self.relogin_account.id)
|
||
assert task is not None
|
||
assert task.cookie == "acf_uid=481802039"
|
||
|
||
def test_visible_task_accounts_include_retained_cookie(self):
|
||
accounts = visible_douyu_task_accounts(
|
||
self.session,
|
||
[self.relogin_account.id, self.empty_account.id],
|
||
)
|
||
assert [account.id for account in accounts] == [self.relogin_account.id]
|
||
|
||
def test_account_has_cookie_filter_matches_workbench(self):
|
||
ids = {
|
||
account_id for (account_id,) in cookie_account_ids_query(self.session).all()
|
||
}
|
||
assert ids == {self.relogin_account.id, self.pending_account.id}
|
||
|
||
def test_cookie_management_page_lists_retained_cookie(self):
|
||
items = list_cookies(
|
||
search="",
|
||
tag="",
|
||
account_names="",
|
||
page=None,
|
||
page_size=50,
|
||
include_cookie=True,
|
||
db=self.session,
|
||
current=self.admin,
|
||
)
|
||
assert {item["account_username"] for item in items} == {
|
||
"远行者103",
|
||
"排队中账号",
|
||
}
|
||
|
||
def test_retained_cookie_account_can_still_create_task(self):
|
||
batch_id, created = create_douyu_planned_tasks(
|
||
self.session,
|
||
[self.relogin_account.id],
|
||
"create_gold_qr",
|
||
"elite",
|
||
self.admin.id,
|
||
)
|
||
assert batch_id
|
||
assert created == 1
|