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:
@@ -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):
|
||||
"""人工状态(停用/归档)与「不参与自动化」的账号不该被自动碰。"""
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user