From 9ac98dad891b22c759e8ac2f56879d7379e08b39 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 4 Oct 2026 21:33:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(automation):=20=E8=A1=A5=E5=85=A8=E4=BD=9C?= =?UTF-8?q?=E4=B8=9A=E6=94=B9=E7=94=A8=E7=95=8C=E9=9D=A2=E4=B8=8A=E7=9A=84?= =?UTF-8?q?=E3=80=8C=E5=90=8C=E4=B8=80=E8=B4=A6=E5=8F=B7=E5=A4=9A=E4=B9=85?= =?UTF-8?q?=E9=87=8D=E6=A3=80=E3=80=8D=E2=80=94=E2=80=94=20=E4=B9=8B?= =?UTF-8?q?=E5=89=8D=E9=82=A3=E4=B8=AA=E5=AD=97=E6=AE=B5=E5=AF=B9=E5=AE=83?= =?UTF-8?q?=E6=97=A0=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户问「同一账号多久重检是啥意思」,顺着查发现**我的作业没在用它**。 ## 「同一账号多久重检」是什么 **每个账号的体检周期**:一个账号这轮被查了,多久之后才重新进入候选。 批量扫描类作业的通用参数,配合另外两个决定"扫不扫得过来": 需要的能力 = 总账号数 ÷ 重检间隔 实际的能力 = 每轮条数 ÷ 运行间隔 **实际 < 需要 → 永远扫不完**(队列越来越长)。 按这个口径核对「CK 自动巡检」的默认值(生产 14,574 个持有 CK 的账号): 需要 14,574 ÷ 6 小时 = 2,429 个/小时 实际 200 条 ÷ 5 分钟 = 2,400 个/小时 **几乎精确匹配** —— 那两个默认值是校准过的,不用动。 ## 但我的作业读的是 config 里的 refresh_days 于是界面上那个「同一账号多久重检」**对本作业是死的**(改了没反应)—— 正是用户上一条批评过的同一类缺陷:「看起来能配、其实配的是不相干的东西」。 我在同一个问题上连着栽了两次(上一次是拿 `interval_seconds` 当调度频率算吞吐)。 改成 `job.interval_seconds`,与其它批量作业**同一套口径**; 默认 7 天(查过的账号 7 天后再查)。config 里的 `refresh_days` 整个去掉。 ## 顺带 - 注册处显式写明两个参数:`interval_seconds=7 天`(重检)+ `run_interval_seconds=5 分钟`(调度) - 用例改名并加注释,钉住"重检间隔必须来自界面那个字段" ## 验证 TZ=Asia/Shanghai **639 passed / 6 skipped**;ruff 全绿;前端 build 通过。 --- tests/test_account_overview_refresh_job.py | 18 +++++--- web/backend/services/automation/__init__.py | 5 ++- .../jobs/account_overview_refresh.py | 43 ++++++++----------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/tests/test_account_overview_refresh_job.py b/tests/test_account_overview_refresh_job.py index 5b13fc2..7b4fdd6 100644 --- a/tests/test_account_overview_refresh_job.py +++ b/tests/test_account_overview_refresh_job.py @@ -24,11 +24,11 @@ class _Base: self.job = AutomationJob( key=job.JOB_KEY, title="账号资料定时补全", - interval_seconds=1800, + # 「同一账号多久重检」= 7 天(就是界面上那个字段) + interval_seconds=7 * 24 * 3600, batch_size=10, concurrency=3, enabled=False, - config={"refresh_days": 7}, ) self.session.add(self.job) self.session.commit() @@ -94,11 +94,19 @@ class TestSelection(_Base): self.account("ovr-stale", queried_days_ago=30) assert "ovr-stale" in [a.username for a in self.selected()] - def test_refresh_days_is_configurable(self): - self.job.config = {"refresh_days": 100} + def test_recheck_interval_comes_from_the_ui_field(self): + """**用界面上那个「同一账号多久重检」** —— 与其它批量作业同一套口径。 + + 之前这里读的是 `job.config["refresh_days"]`,于是界面上那个字段 + 对本作业是**死的**(改了没反应)—— 正是用户批评过的 + 「看起来能配、其实配的是不相干的东西」。 + """ + self.job.interval_seconds = 100 * 24 * 3600 self.session.commit() self.account("ovr-30d", queried_days_ago=30) - assert "ovr-30d" not in [a.username for a in self.selected()] + assert "ovr-30d" not in [a.username for a in self.selected()], ( + "重检间隔 100 天时,30 天前查过的账号不该重新入选" + ) def test_manual_and_unmanaged_accounts_are_excluded(self): """人工状态(停用/归档)与「不参与自动化」的账号不该被自动碰。""" diff --git a/web/backend/services/automation/__init__.py b/web/backend/services/automation/__init__.py index a064ab3..c384411 100644 --- a/web/backend/services/automation/__init__.py +++ b/web/backend/services/automation/__init__.py @@ -118,7 +118,10 @@ def register_default_jobs() -> None: # # 想更快就调大 `batch_size`(账号列表里那套),但**跑得越快越容易被风控**, # 而风控是所有账号一起受影响。 - interval_seconds=10 * 60, + # 「同一账号多久重检」= 查过的账号 7 天后再查(界面上的那个字段)。 + # ⚠️ 它**不参与调度** —— 调度频率由 run_interval_seconds 决定。 + interval_seconds=7 * 24 * 3600, + run_interval_seconds=5 * 60, batch_size=400, concurrency=3, run=run_account_overview_refresh, diff --git a/web/backend/services/automation/jobs/account_overview_refresh.py b/web/backend/services/automation/jobs/account_overview_refresh.py index 53cf7d7..c823c7e 100644 --- a/web/backend/services/automation/jobs/account_overview_refresh.py +++ b/web/backend/services/automation/jobs/account_overview_refresh.py @@ -18,7 +18,7 @@ | 类型 | 条件 | 优先级 | | --- | --- | --- | | **没查过的** | `overview_queried_at IS NULL` | **优先**(补历史欠账)| -| 查过但过期了 | `overview_queried_at < now - refresh_days` | 其次(周期性刷新)| +| 查过但过期了 | `overview_queried_at < now - interval_seconds` | 其次(周期性刷新)| 用户明确要求这两类分开 —— 否则"周期性刷新老账号"会永远挤掉"从没查过的新账号"。 @@ -36,7 +36,6 @@ from __future__ import annotations -import json from datetime import UTC, datetime, timedelta from typing import Any @@ -51,8 +50,8 @@ from ...douyu_service import cookie_operable_filter, create_douyu_planned_tasks JOB_KEY = "account_overview_refresh" -#: 默认多久刷新一次已查过的账号(天) -DEFAULT_REFRESH_DAYS = 7 +#: `interval_seconds` 缺失时的兜底(天)—— 正常都会走界面上那个「同一账号多久重检」 +DEFAULT_RECHECK_SECONDS = 7 * 24 * 3600 #: 触发熔断的最小样本量:样本太少时比例没有意义(1 个失败就是 100%) ERROR_PAUSE_MIN_SCANNED = 10 @@ -69,29 +68,21 @@ def _positive_int(value: Any, default: int) -> int: return parsed if parsed > 0 else default -def _load_config(job: AutomationJob) -> dict[str, Any]: - config = job.config - if isinstance(config, str): - try: - config = json.loads(config) - except ValueError: - config = {} - return dict(config) if isinstance(config, dict) else {} - - def build_select_query(db: Session, job: AutomationJob, now: datetime): """选出本轮要查的账号。 - 只要 **CK 有效**(`ck_check_status == 'valid'`)—— 失效的查了也是白查 - **没查过的优先**:`overview_queried_at IS NULL` 排在最前 (MySQL 没有 `NULLS FIRST`,用 `IS NULL` 的布尔值降序等价实现) - - 查过的只在超过 `refresh_days` 后才重新入选 + - 查过的只在超过 `interval_seconds` 后才重新入选 + (就是界面上那个「同一账号多久重检」—— 与其它作业**同一套口径**) """ - refresh_days = _positive_int( - _load_config(job).get("refresh_days"), DEFAULT_REFRESH_DAYS - ) - cutoff = now - timedelta(days=refresh_days) - batch = _positive_int(job.batch_size, 30) + # ⚠️ 用 `interval_seconds`(界面上那个「同一账号多久重检」), + # 与其它批量作业**同一套口径**。 + # + # 之前这里读的是 `job.config["refresh_days"]` —— 于是界面上那个字段 + # 对本作业是**死的**(改了没反应)。那正是用户刚批评过的 + # 「看起来能配、其实配的是不相干的东西」。 batch = _positive_int(job.batch_size, 30) return ( _base_query(db, job, now) @@ -109,10 +100,14 @@ def _base_query(db: Session, job: AutomationJob, now: datetime): SQLAlchemy 不允许在已 `LIMIT` 的查询上再 `order_by(None)` (抛 "order_by() being called on a Query which already has LIMIT")。 """ - refresh_days = _positive_int( - _load_config(job).get("refresh_days"), DEFAULT_REFRESH_DAYS - ) - cutoff = now - timedelta(days=refresh_days) + # ⚠️ 用 `interval_seconds`(界面上那个「同一账号多久重检」), + # 与其它批量作业**同一套口径**。 + # + # 之前这里读的是 `job.config["refresh_days"]` —— 于是界面上那个字段 + # 对本作业是**死的**(改了没反应)。那正是用户刚批评过的 + # 「看起来能配、其实配的是不相干的东西」。 + recheck_seconds = _positive_int(job.interval_seconds, DEFAULT_RECHECK_SECONDS) + cutoff = now - timedelta(seconds=recheck_seconds) return db.query(Account).filter( cookie_operable_filter(), Account.auto_managed.is_(True),