在既有「管理员分配 → 客服导入工作台 → 手动选号」之外**新增**一条 「客服选皮肤 + 数量 → 系统分配账号 + 按需补积分 → 用完自动收回」的路径。 旧路径只做并集与可选参数扩展,行为不变(有用例守着)。 - 归属与使用权分离:新增 account_leases(生成列 + 唯一索引,并发抢号由数据库裁决), assigned_to 收窄为管理员/运营的维护责任;客服可见范围扩为「名下 ∪ 租用中」 - account_pool 服务:候选筛选、两梯队排序(先花快过期的 / 先选充值少的)、 供给与成本预估、租约领取/释放/回收 - 补给链:查余额 → 充鱼翅(单次 ≤800 元,超限拆单)→ 赠精英令 → 兑换; 运行期两道闸门:_skip_when(余额已够就别再花钱)、_depends_on(前置未成功就跳过) - 转人工:鱼翅支付等 10 分钟未到账转 manual_required,租约先不还; account_lease_reaper 兜 TTL 到期 / 漏释放 / 孤儿批次 - 运营流水:业务事件补 actor_id = task.created_by;账号详情页展示操作人、渠道 与租赁记录 - 新工作台:/douyu/pool + 菜单「账号分配台」(新建,不改造旧工作台) - 修复 D1:官方补录的兑换事件漏写 subject_key(10,612 条主键为空), 会让 365 天同皮肤规则漏判 87% 的历史;补代码 + 回填脚本 - 迁移 20261007_0052;权限 account:pool_use / account:pool_admin 测试:766 passed, 6 skipped(改动前基线 727);ruff 与 tsc -b 干净。 设计稿与实施记录:docs/设计-账号自动分配与回收.md
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
"""自动化调度:作业注册、调度线程与各作业实现。
|
||
|
||
登记作业请放在 ``register_default_jobs()``,由 ``main.py`` 在启动时调用一次,
|
||
避免 import 顺序影响注册结果。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from .jobs.account_lease_reaper import (
|
||
JOB_KEY as ACCOUNT_LEASE_REAPER_KEY,
|
||
)
|
||
from .jobs.account_lease_reaper import (
|
||
run_account_lease_reaper,
|
||
)
|
||
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_LEASE_REAPER_KEY",
|
||
"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_lease_reaper",
|
||
"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="账号资料定时补全",
|
||
# 每 10 分钟一轮、每轮 400 个。
|
||
#
|
||
# 依据是**试水实测**,不是拍脑袋:200 个并发 3 跑了 131 秒
|
||
# = **1.53 个/秒 ≈ 5,500 个/小时**。瓶颈不在查询速度,而在调度频率。
|
||
# 400 个 × 0.65 秒 ≈ **4.4 分钟**跑完,10 分钟一轮
|
||
# = **2,400 个/小时**,7,784 个约 **3.2 小时**补完。
|
||
#
|
||
# ⚠️ 一轮时长必须**明显小于间隔**:调度器是**串行**的
|
||
# (见 scheduler.run_due_jobs 的注释:作业一个一个跑),
|
||
# 我的作业跑着的时候 CK 巡检/重登都得等着。所以宁可轮次多一些,
|
||
# 也不要一轮跑太久 —— 4.4 分钟 / 10 分钟是刻意留出的余量。
|
||
#
|
||
# 想更快就调大 `batch_size`(账号列表里那套),但**跑得越快越容易被风控**,
|
||
# 而风控是所有账号一起受影响。
|
||
# 「同一账号多久重检」= 查过的账号 7 天后再查(界面上的那个字段)。
|
||
# ⚠️ 它**不参与调度** —— 调度频率由 run_interval_seconds 决定。
|
||
interval_seconds=7 * 24 * 3600,
|
||
run_interval_seconds=5 * 60,
|
||
batch_size=400,
|
||
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,
|
||
)
|
||
)
|
||
register_job(
|
||
JobSpec(
|
||
key=ACCOUNT_LEASE_REAPER_KEY,
|
||
title="账号租约回收",
|
||
# 租约的主释放路径是任务终态钩子;本作业只是**保险丝**,
|
||
# 兜住「进程重启 / 释放抛异常 / 任务行被清」三种漏网情况。
|
||
# 跑得勤一点(1 分钟)成本极低:只扫带索引的 status+expires_at,
|
||
# 而且悬空租约会让账号白白锁着 —— 早收早能用。
|
||
# `interval_seconds` 对它是无意义的(本作业自己按三类规则选行),
|
||
# 但 JobSpec 要求必填,给一个与 run_interval 同量级的值。
|
||
interval_seconds=60,
|
||
run_interval_seconds=60,
|
||
batch_size=500,
|
||
concurrency=1,
|
||
run=run_account_lease_reaper,
|
||
)
|
||
)
|