Files
live-hub-py/web/backend/services/automation/__init__.py
T
yml2213 fd578683b6 feat(automation): 新增「账号资料定时补全」作业 —— 分批慢跑,不撞风控
## 为什么不能一次性跑全量

生产约 **8,000 个**「CK 有效但没查过」的账号。一次性发起 ≈ **8 万次斗鱼请求**,
很容易触发风控 —— 而风控一旦触发是**所有账号一起受影响**。试水 200 个时
0.65 秒/账号看着很香(全量 86 分钟),但那正是危险的地方:**跑得越快越容易被封**。

## 新作业 `account_overview_refresh`

照「自动巡检」(`ck_health`)那套做,并复用它的两道保护。

| 项 | 默认 | 说明 |
| --- | --- | --- |
| `interval_seconds` | 30 分钟 | 多久一轮 |
| `batch_size` | 30 | 每轮查多少(约 20 秒跑完)|
| `concurrency` | 3 | 并发 |
| `refresh_days` | 7(config 可调)| 查过的多久后再查 |
| `enabled` | **False** | 默认关,由运维手动开 |

按默认值:8,000 个约 **3 天**补完,全程不打满斗鱼接口。

## 两类账号**分开**(用户明确要求)

- **没查过的**(`overview_queried_at IS NULL`)**优先**
- 查过的只在超过 `refresh_days` 后重新入选

否则"周期性刷新老账号"会永远挤掉"从没查过的新账号"。
MySQL 没有 `NULLS FIRST`,用 `ORDER BY overview_queried_at IS NULL DESC` 等价实现。

## 只查有效 CK

`ck_check_status == 'valid'`。失效的查了也是白查(只会多一条失败记录),
交给「CK 检测与重登」处理。另外沿用 `cookie_operable_filter()`(有 CK 且无在途尝试)、
排除人工状态(停用/归档)与「不参与自动化」的账号。

## 风控熔断(沿用巡检口径)

一轮里失败率 > 50%(且样本 ≥ 10)就**自己把自己关掉**,交给人确认。
消息里写明可能原因,否则运维只看到"失败率高"会一头雾水:

    失败率过高,已自动关闭作业(大面积失败通常是**被风控**或接口抖动,请先确认再重开)

样本 < 10 不熔断 —— 1 个失败就是 100%,比例没意义。

## 查询记录

每轮都建真实的 `douyu_tasks` 行(payload 标 `source=scheduled_refresh`),
「哪个账号、什么时候、查出来什么」都有据可查,也能在任务列表里
区分「定时巡检」与「人工点查询」。账号上的 `overview_queried_at`
则是"下次该不该再查"的依据。

## 验证

- **镜像库实测**:`本轮查 3 个:成功 3 / 失败 0(剩 7784 个待查)`
- 本地 `TZ=Asia/Shanghai` **632 passed / 6 skipped**;ruff 全绿
- 新增 11 条用例:选择口径(只查有效 CK、没查过优先、refresh_days 可调、
  人工状态排除、batch_size 限流)+ 熔断(高失败率关闭、正常不关、小样本不熔断)
  + 一条回归:计数不能走带 LIMIT 的查询(作业第一版就是这么挂的)
2026-10-04 21:14:16 +08:00

128 lines
3.9 KiB
Python

"""自动化调度:作业注册、调度线程与各作业实现。
登记作业请放在 ``register_default_jobs()``,由 ``main.py`` 在启动时调用一次,
避免 import 顺序影响注册结果。
"""
from __future__ import annotations
from .jobs.account_overview_refresh import (
JOB_KEY as ACCOUNT_OVERVIEW_REFRESH_KEY,
)
from .jobs.account_overview_refresh import (
run_account_overview_refresh,
)
from .jobs.ck_health import JOB_KEY as CK_HEALTH_KEY
from .jobs.ck_health import run_ck_health
from .jobs.ck_relogin import JOB_KEY as CK_RELOGIN_KEY
from .jobs.ck_relogin import run_ck_relogin
from .jobs.douyu_tasks_retention import (
JOB_KEY as DOUYU_TASKS_RETENTION_KEY,
)
from .jobs.douyu_tasks_retention import (
run_douyu_tasks_retention,
)
from .jobs.points_expire import JOB_KEY as POINTS_EXPIRE_KEY
from .jobs.points_expire import run_points_expire_check
from .scheduler import (
JobSpec,
automation_enabled,
ensure_jobs,
register_job,
registered_jobs,
reset_registry,
run_due_jobs,
run_job_now,
scheduler_status,
start_scheduler,
stop_scheduler,
)
__all__ = [
"ACCOUNT_OVERVIEW_REFRESH_KEY",
"CK_HEALTH_KEY",
"CK_RELOGIN_KEY",
"DOUYU_TASKS_RETENTION_KEY",
"POINTS_EXPIRE_KEY",
"JobSpec",
"automation_enabled",
"ensure_jobs",
"register_default_jobs",
"register_job",
"registered_jobs",
"reset_registry",
"run_account_overview_refresh",
"run_ck_health",
"run_ck_relogin",
"run_douyu_tasks_retention",
"run_due_jobs",
"run_job_now",
"run_points_expire_check",
"scheduler_status",
"start_scheduler",
"stop_scheduler",
]
def register_default_jobs() -> None:
"""登记内置作业(幂等)。"""
register_job(
JobSpec(
key=POINTS_EXPIRE_KEY,
title="积分到期补记",
# 每天一次足够:到期时刻的精度是「天」级别,晚几小时记也不影响
interval_seconds=24 * 3600,
batch_size=200,
concurrency=1,
run=run_points_expire_check,
)
)
register_job(
JobSpec(
key=DOUYU_TASKS_RETENTION_KEY,
title="任务日志清理",
# 一天一次足够:这是**清理**,不是业务;跑太勤只是白扫索引。
interval_seconds=24 * 3600,
batch_size=2000,
concurrency=1,
run=run_douyu_tasks_retention,
)
)
register_job(
JobSpec(
key=CK_RELOGIN_KEY,
title="CK 自动重登",
# interval_seconds 同样是「同一行多久重试一次」的兜底(真实节奏由
# relogin_next_at 的退避表决定);批次小、跑得慢,单轮 10 条。
interval_seconds=6 * 3600,
batch_size=10,
concurrency=3,
run=run_ck_relogin,
)
)
register_job(
JobSpec(
key=ACCOUNT_OVERVIEW_REFRESH_KEY,
title="账号资料定时补全",
# 默认每 30 分钟一轮、每轮 30 个 —— 约 20 秒跑完,慢而稳。
# 8,000 个待查按这个速度约 3 天补完,全程不打满斗鱼接口。
interval_seconds=30 * 60,
batch_size=30,
concurrency=3,
run=run_account_overview_refresh,
)
)
register_job(
JobSpec(
key=CK_HEALTH_KEY,
title="CK 自动巡检",
# interval_seconds = 单账号多久重新检测一次(不是作业周期;
# 作业每个 tick 都跑)。全库 12,881 条按 5 分钟一轮、单轮 200 条
# 反推,吞吐 40 条/分钟,够 6 小时内覆盖一遍(规划 §7.3)。
interval_seconds=6 * 3600,
batch_size=200,
concurrency=5,
run=run_ck_health,
)
)