fix(ui,test): 说明巡检与重登的联动关系;修掉一条按分钟偶发失败的测试
看图时发现两个问题: 一、页面上那段说明是 P1a 时代的文案,已经过时且误导:「重登仍由人工在 「CK 检测与重登」页面操作」——P1b 上线后自动重登已存在。两个作业其实是联动的 上下游:巡检把账号标成 invalid,「CK 自动重登」的候选条件正是这个字段,所以 巡检一发现失效、重登作业下一周期就会接手。刻意分成两个作业是为了止损粒度 (巡检只读零风险;重登会真实登录,出问题可单独关掉)。据此改写说明文案,并在 两张卡片上标出「下游:CK 自动重登」/「上游:巡检标记为失效且有 CK 的账号」。 二、jobs 接口新增 pending_relogin:当前有多少条正等着自动重登。页面靠它把 上下游关系显性化 —— 没有这个数字,两个开关看起来像互不相干。计数走重登作业 自己的候选查询,保证「页面显示的待办」和「作业实际会捞的」是同一口径。 顺带修掉 tests/test_douyu_email_code_pick.py 里一条既有缺陷:它用 max(0, now.minute - 30) 构造「30 分钟前的邮件」,分钟数小于 30 时会退化成当前 整点(21:00:12 → "今天 21:00"),只比 now 早十几秒,于是断言随「当前几点」偶发 失败 —— 每小时前 30 分钟必红(已确认在我本次所有改动之前的提交上同样失败, 不是本次引入)。改成真实的时间减法,跨天时用「昨天」。 全量 298 通过。提交时另一路 SQLite→MySQL 的测试改造仍在同一工作区进行, 本次只暂存本功能相关文件。
This commit is contained in:
@@ -19,6 +19,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import Account, AutomationJob, LoginTask, User
|
||||
from web.backend.services.automation import register_default_jobs, reset_registry
|
||||
from web.backend.services.automation.jobs import ck_relogin
|
||||
|
||||
|
||||
@@ -27,6 +28,10 @@ class _Base:
|
||||
self.engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session = sessionmaker(bind=self.engine)()
|
||||
# 作业注册表是模块级全局,测试之间会互相污染;重置成生产启动时的状态,
|
||||
# 否则 list_jobs() 里的 ensure_jobs() 建不出作业行。
|
||||
reset_registry()
|
||||
register_default_jobs()
|
||||
self.admin = User(
|
||||
username="admin", password_hash="h", role="super_admin", is_active=True
|
||||
)
|
||||
@@ -375,3 +380,24 @@ class TestManualQueueApi(_Base):
|
||||
def test_empty_queue(self):
|
||||
payload = self._queue()
|
||||
assert payload["total"] == 0 and payload["groups"] == []
|
||||
|
||||
|
||||
class TestLinkageWithHealthJob(_Base):
|
||||
"""两个作业是上下游:jobs 接口要暴露「当前有多少条等着自动重登」。
|
||||
|
||||
页面靠这个数字把巡检 → 重登的关系显性化;没有它,两个开关看起来像互不相干。
|
||||
"""
|
||||
|
||||
def test_pending_relogin_counts_only_actionable_invalid(self):
|
||||
from web.backend.routers import automation as api
|
||||
|
||||
ready = self.make_task("ready")
|
||||
self.make_task("valid", ck_check_status="valid")
|
||||
self.make_task("nocookie", cookie="")
|
||||
self.make_task("optedout", auto_managed=False)
|
||||
|
||||
payload = api.list_jobs(db=self.session, current=self.admin)
|
||||
# 只有 ready 同时满足:invalid + 有 CK + 允许自动管理
|
||||
assert payload["pending_relogin"] == 1
|
||||
assert any(job["key"] == ck_relogin.JOB_KEY for job in payload["jobs"])
|
||||
assert ready.id is not None
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
日期解析按本地时间,仅在拿不到快照时才用时间兜底。
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from core.douyu.email_verifier import EmailVerifier
|
||||
|
||||
@@ -79,10 +79,18 @@ class TestEmailCodePick:
|
||||
assert verifier.read_uids == [4]
|
||||
|
||||
def test_send_time_filter_skips_older_mail(self):
|
||||
"""没有 UID 快照时,比发送时间早的邮件必须被跳过。"""
|
||||
"""没有 UID 快照时,比发送时间早的邮件必须被跳过。
|
||||
|
||||
注意这里必须用**真实的时间减法**构造「30 分钟前」,不能写成
|
||||
``max(0, now.minute - 30)``:分钟数小于 30 时它会退化成当前整点
|
||||
(如 21:00:12 得到 "今天 21:00"),只比 now 早十几秒,断言就随
|
||||
「当前是几点」偶发失败 —— 每小时前 30 分钟必红。跨天时用「昨天」。
|
||||
"""
|
||||
now = datetime.now(tz=UTC).astimezone()
|
||||
earlier = now - timedelta(minutes=30)
|
||||
prefix = "今天" if earlier.date() == now.date() else "昨天"
|
||||
verifier = _FakeVerifier(
|
||||
[_msg(1, f"今天 {now.hour:02d}:{max(0, now.minute - 30):02d}")],
|
||||
[_msg(1, f"{prefix} {earlier.hour:02d}:{earlier.minute:02d}")],
|
||||
{1: "111111"},
|
||||
)
|
||||
|
||||
|
||||
@@ -86,8 +86,21 @@ def list_jobs(
|
||||
db.query(AutomationRun).order_by(AutomationRun.id.desc()).limit(200).all()
|
||||
):
|
||||
latest.setdefault(run.job_key, run)
|
||||
# 巡检与重登是两个作业,但通过数据库联动:巡检把行标成 invalid,重登作业的
|
||||
# 候选条件正是这个字段。这里把「当前有多少条等着重登」一并返回,页面才能把
|
||||
# 这层上下游关系显示出来,而不是让人以为两个开关互不相干。
|
||||
from ..services.automation.jobs.ck_relogin import build_select_query
|
||||
|
||||
pending_relogin = 0
|
||||
relogin_job = next((job for job in jobs if job.key == "ck_relogin"), None)
|
||||
if relogin_job is not None:
|
||||
pending_relogin = build_select_query(
|
||||
db, relogin_job, datetime.now(UTC), max_attempts=10**6
|
||||
).count()
|
||||
|
||||
return {
|
||||
"scheduler": scheduler_status(),
|
||||
"pending_relogin": pending_relogin,
|
||||
"jobs": [
|
||||
{
|
||||
**_job_out(job),
|
||||
|
||||
@@ -883,6 +883,8 @@ export interface AutomationSchedulerStatus {
|
||||
|
||||
export interface AutomationJobsResponse {
|
||||
scheduler: AutomationSchedulerStatus;
|
||||
/** 巡检已标记失效、正等自动重登处理的条数(体现两个作业的上下游关系)。 */
|
||||
pending_relogin: number;
|
||||
jobs: AutomationJobItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function AutomationPage() {
|
||||
const [runs, setRuns] = useState<AutomationRunItem[]>([]);
|
||||
const [manualGroups, setManualGroups] = useState<AutomationManualGroup[]>([]);
|
||||
const [manualTotal, setManualTotal] = useState(0);
|
||||
const [pendingRelogin, setPendingRelogin] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [savingKey, setSavingKey] = useState<string | null>(null);
|
||||
const [runningKey, setRunningKey] = useState<string | null>(null);
|
||||
@@ -57,6 +58,7 @@ export default function AutomationPage() {
|
||||
try {
|
||||
const data = await automationApi.jobs();
|
||||
setJobs(data.jobs);
|
||||
setPendingRelogin(data.pending_relogin ?? 0);
|
||||
setSchedulerEnabled(data.scheduler.enabled);
|
||||
setSchedulerRunning(data.scheduler.running);
|
||||
const history = await automationApi.runs({ limit: 50 });
|
||||
@@ -226,8 +228,8 @@ export default function AutomationPage() {
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="巡检只检测、不改 CK"
|
||||
description="每 5 分钟跑一轮,每轮按「每轮条数」上限挑最久没检测的账号;「单账号重检间隔」指同一个账号最多多久重新检测一次。本作业只标记失效账号,不会替换任何 Cookie、也不会改账号状态,重登仍由人工在「CK 检测与重登」页面操作。"
|
||||
message="巡检只检测、不改 CK;发现失效后由「CK 自动重登」接手"
|
||||
description="两个作业是联动的上下游:巡检把失效账号标记为 invalid,「CK 自动重登」的候选条件正是这个标记,所以巡检一发现失效,重登作业在下一个周期就会把它救回来(需打开重登开关)。刻意分成两个作业是为了止损粒度 —— 巡检只读、零风险;重登会真实登录(消耗邮箱验证码、可能遇到极验),出问题可单独关掉重登而不影响检测。巡检本身不会替换任何 Cookie、也不会改账号状态。"
|
||||
/>
|
||||
|
||||
{jobs.map((job) => (
|
||||
@@ -235,6 +237,18 @@ export default function AutomationPage() {
|
||||
key={job.key}
|
||||
type="inner"
|
||||
title={job.title}
|
||||
extra={
|
||||
job.key === "ck_relogin" ? (
|
||||
<Typography.Text type="secondary">
|
||||
上游:巡检标记为失效且有 CK 的账号,当前待重登{" "}
|
||||
<Typography.Text strong>{pendingRelogin}</Typography.Text> 个
|
||||
</Typography.Text>
|
||||
) : job.key === "ck_health" ? (
|
||||
<Typography.Text type="secondary">
|
||||
下游:CK 自动重登(发现失效后自动接手)
|
||||
</Typography.Text>
|
||||
) : null
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Descriptions size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
|
||||
Reference in New Issue
Block a user