Files
yml2213 18bc7be4b1 feat(pool): 账号自动分配与回收(租约 + 补给链 + 独立工作台)
在既有「管理员分配 → 客服导入工作台 → 手动选号」之外**新增**一条
「客服选皮肤 + 数量 → 系统分配账号 + 按需补积分 → 用完自动收回」的路径。
旧路径只做并集与可选参数扩展,行为不变(有用例守着)。

- 归属与使用权分离:新增 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
2026-10-07 12:29:08 +08:00

168 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""自动化调度:作业注册、调度线程与各作业实现。
登记作业请放在 ``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,
)
)