fix(automation): 补全作业改用界面上的「同一账号多久重检」—— 之前那个字段对它无效

用户问「同一账号多久重检是啥意思」,顺着查发现**我的作业没在用它**。

## 「同一账号多久重检」是什么

**每个账号的体检周期**:一个账号这轮被查了,多久之后才重新进入候选。
批量扫描类作业的通用参数,配合另外两个决定"扫不扫得过来":

    需要的能力 = 总账号数 ÷ 重检间隔
    实际的能力 = 每轮条数 ÷ 运行间隔

**实际 < 需要 → 永远扫不完**(队列越来越长)。

按这个口径核对「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 通过。
This commit is contained in:
yml2213
2026-10-04 21:33:50 +08:00
parent 55f899c615
commit 9ac98dad89
3 changed files with 36 additions and 30 deletions
+13 -5
View File
@@ -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):
"""人工状态(停用/归档)与「不参与自动化」的账号不该被自动碰。"""
+4 -1
View File
@@ -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,
@@ -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),