yml2213
|
55f899c615
|
feat(automation): 拆开「多久跑一次」与「同一账号多久重检」—— 调度频率终于可配
用户反馈:「单账号重检间隔(小时) 很别扭,直接设置多久跑一次、一次跑多少个更符合直觉」。
**对,而且比"别扭"更严重:调度频率以前根本改不了。**
## 问题:两个概念挤在一个字段里
`_run_one` 里原本写着:
job.next_run_at = finished + timedelta(seconds=JOB_INTERVAL_SECONDS) # 硬编码 300 秒
也就是不管界面上填什么,下一轮永远是「跑完 + 5 分钟」。
而界面那个「单账号重检间隔(小时)」对应的是 `interval_seconds` ——
它只被作业**自己的选行逻辑**使用("这行多久该重检"),**完全不参与调度**。
于是出现最坏的情况:**看起来能配调度,其实配的是一个不相干的参数**。
上一轮我还拿它算过吞吐("10 分钟一轮"),算错了 —— 已在调度器注释里纠正。
## 拆法
| 概念 | 字段 | 界面 |
| --- | --- | --- |
| **多久跑一次** | `run_interval_seconds`(迁移 0051,默认 300)| 「多久跑一次(分钟)」|
| 同一账号多久重检 | `interval_seconds`(不变)| 「同一账号多久重检(小时)」|
- `next_run_at = finished + run_interval_seconds`(有下限 30 秒,避免忙轮询)
- 接口:`GET` 返回新字段、`PATCH` 可改
- 前端:两个输入框分开,并把原来那段绕口的 tooltip
("它不决定调度频率 —— 调度周期见页面顶部说明")改掉 ——
那句话本身就是这个设计缺陷的自白
## 默认 300 秒
与改动前**实际行为一致** —— 不会因为这次迁移让已有作业突然变快或变慢。
想调快就自己填(比如「账号资料定时补全」填 5 分钟 + 每轮 400 条 ≈ 3 小时补完)。
## ⚠️ 顺带说明一个你截图里可能看到的现象
截图里「账号资料定时补全」显示的是 **0.5 小时 / 30 条** —— 那是我**最初的默认值**。
因为 `ensure_jobs` **只创建不更新**,我后面改成 10 分钟/400 **不会作用到已存在的行**。
这是刻意的(否则会覆盖运维的手工调整),但意味着**默认值改了要手动在界面上改**,
或者删掉那行让 `ensure_jobs` 重建。现在有了「多久跑一次」这个框,直接填就行。
## 测试(+3)
- 下一轮按**运行间隔**排,不是按「同一行重检间隔」(两个值刻意设成不同来钉住)
- 不填时默认 300 秒(保持旧行为)
- 过小的间隔被夹到下限
## 验证
TZ=Asia/Shanghai **639 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 21:30:13 +08:00 |
|
yml2213
|
aea8e4f73d
|
perf(automation): 调度器改为**作业之间并发**,慢作业不再挡住快作业
用户要求做这条(原本串行)。串行的代价现在很明显:新加的「账号资料定时补全」
一轮要 4 分钟以上,而源码注释里写的是「目前只有 CK 巡检,单轮约 20~40 秒」——
那 4 分钟里 CK 巡检/重登只能干等。
## 两处保护(缺一不可)
**① 在途守卫**(并发化之后最容易出的事故)
`next_run_at` 是作业**跑完之后**才更新的,所以一个跑得比 tick 还久的作业,
下一个 tick 看它仍然是「到期」状态 —— 没有守卫就会被**再派发一次**,
等于把速率翻倍(对斗鱼接口就是"并发放开",正是风控最敏感的地方)。
所以用 `_IN_FLIGHT` 记录在跑的作业 key,且**成功/失败都必须清标记**
(否则该作业以后再也不跑了)。两条都有用例钉住。
**② 独立 session**
SQLAlchemy 的 Session **不是线程安全的**。作业现在真的并行了,
所以每个作业在自己的 `SessionLocal()` 里跑,按 key 重新取行
(调度线程里那行可能已经过期)。共用 session 会互相踩事务。
## tick 不再被拖住
调度循环改用 `run_due_jobs(db, wait=False)`:**派发完就返回**,
tick 保持 5 分钟一轮,不再等作业跑完。
`wait=True`(默认)保留原语义:顺序执行、用调用方的 session 并返回 outcomes ——
测试与手动触发走这条,所以既有的 33 条调度器用例**一条没改**。
## ⚠️ 顺带纠正我自己上一轮的一个错误说法
我上一条说「interval_seconds=10 分钟」会生效 —— **不对**。`_run_one` 里写着:
下一轮固定在一个 tick 之后;interval_seconds 是「同一行多久重检」,
由作业自己在选行时用,**不参与调度**。
也就是下一轮永远是「跑完 + 5 分钟」。所以「账号资料定时补全」的真实节奏是
`4.4 分钟跑 + 5 分钟等 ≈ 9.4 分钟一轮` ≈ 2,553 个/小时 ≈ **3.05 小时**,
和之前估的 3.2 小时吻合。
## 测试(+4)
- `wait=False` 立刻返回、作业在后台真的开始了
- **在途作业不会被重复派发**(速率翻倍那个事故的守卫)
- 失败后仍会清在途标记、作业能再跑(别被永久卡死)
- `_release` 即使对不存在的 key 也不炸
## 验证
TZ=Asia/Shanghai **636 passed / 6 skipped**;ruff 全绿。
|
2026-10-04 21:24:35 +08:00 |
|
yml2213
|
00125fbfc4
|
refactor(account): S2 读写一次切换 —— CK/健康/重登彻底搬到账号,登录记录退化为历史
S1 加好了列,本提交把**读与写一起切**过去。这是设计里「同一份数据的读与写必须
在同一个提交里切」的要求:先切读或先切写都会出现「写账号、读记录」的中间态,
跑起来不报错,只是默默用错数据。
## 核心改动
**判定「谁有 CK」不再借道登录记录**
- `cookie_holding_filter()` / `cookie_operable_filter()` / `cookie_holding_account_ids_query()`
/ `visible_douyu_task_accounts()` 全部改为作用于 `Account`;
- `cookie_operable_filter()` 的「在途排除」仍查 `login_tasks`(那是**某次尝试**的属性),
用 `NOT EXISTS` 子查询 + 迁移 0042 新增的 `(account_id, attempt_status)` 索引;
- 删除死代码 `latest_cookie_login_task()`;`latest_cookie()` 改为直接读账号。
**CK 写入**
- `_write_login_success()` 写 `Account.cookie / cookie_updated_at / cookie_source`;
- `_mark_attempt_failed()` 改看**账号**有没有 CK(决定是否沿用旧的失败语义);
- 检测模式的 `task.cookie = ""` **不再清空账号的 CK**(见下「行为变更」)。
**自动化作业按账号调度**
- `ck_health`:`_select_tasks` → `_select_accounts`,`db.query(Account)`;新增
`count_remaining()`(刻意不带 LIMIT,否则积压数恒等于批次上限);
- `ck_relogin`:同样按账号选行;结算改为「账号 + 本轮尝试记录」,
退避进度写账号;`settle_fresh_results(batch_id)` 按 `attempt_batch_id` 配对,
`run_ck_relogin` 把 **account_ids** 交给 runner;
- 顺带让**没有任何登录历史的账号**也能被巡检/重登(以前它们永远进不了队列)。
**登录批次**
- `LoginBatchRunner` 去掉 `relogin_task_ids`,重登与登录统一走 `account_ids`;
- 批次行复用逻辑重写:只保证「同一账号不并发两行」,不再关心「哪一行持有 CK」。
失败行复用同时看 `attempt_status`(账号有 CK 时 `_mark_attempt_failed` 刻意不动
`status`,只看 status 会永远命不中 → 每次失败刷一行)。
**删除护栏语义变更(用户可感知)**
- 去掉「持有 CK 的记录不可删」——CK 在账号上,删历史不再影响登录态,历史终于可清理;
- **保留**「在途尝试不可删」(批次线程正拿着那条记录)。
## 行为变更(需评审)
1. **账号检测不再清空 CK**。旧代码在 check 模式写 `task.cookie = ""`,而那时记录的
cookie 就是账号唯一凭证 —— 一次「批量检测」会把账号登录态删掉。解耦后
「检测账号凭据」与「CK 是否有效」本就是两件事(后者归 `ck_health` 判),
继续清空等于凭空制造不可逆的数据丢失(重登当前成功率极低)。
2. **Cookie 页的记录单位从「登录记录」变成「账号」**,所有接口的 id 入参/出参
变成 `account_id`;`DELETE /cookies/{id}` 清的是该账号的 CK,**必写审计**。
3. 重复检测从「同一账号多行持 CK」改为「**同一个 CK 出现在多个账号上**」——
后者才是真风险(两个账号共用一个登录态)。用 sha256 指纹分组:
CK 是 AES-GCM + 随机 nonce 加密,**同一明文每次密文都不同**,比密文永远比不出重复。
## 修掉一个真实并发缺陷
`_write_login_success` 原先被传入 `self.db`(runner 主会话),而它是在**工作线程**里
用 `worker_db` 执行的。`emit()` 会往 `account_events` 插一行,其 `account_id` 外键对
`accounts` 那一行加**共享锁**;这把锁加在另一个会话上,紧接着本会话的
`UPDATE accounts SET cookie=...` 就被自己人挡住 —— 实测 **50 秒后
`Lock wait timeout exceeded`,登录明明成功却写不回 CK**(改造前 UPDATE 打在
`login_tasks` 上所以没撞上)。改为传 `worker_db`:事件本就该和状态变更同事务提交,
顺带修掉跨线程共用一个 Session 的隐患。
## 验证
- 全量 pytest **368 passed / 6 skipped**;迁移链 3 passed;
- 前端 `tsc -b && vite build` 通过;
- `ruff check` / `ruff format --check` 全绿(顺带修掉 main 上遗留的 F821);
- 新增/改写的关键回归:**删掉全部登录历史后账号的 CK 与健康状态仍在**、
**无登录历史的账号也能进巡检队列**、结算不碰 CK、删除护栏改为在途语义。
仍未做(设计里排期的后续阶段):`account_lifecycle` 状态收口(S3)、事件发射点补齐(S4)、
业务事实直发事件(S5)、账号页重构(S6)。注意 `status` 目前只有模型默认值与迁移回填,
所以 `disabled/archived` 还没有任何写入方——S3 落地时要把自动化选行的状态过滤一起补上。
|
2026-10-04 14:10:13 +08:00 |
|
yml2213
|
d6c6113112
|
fix(automation): 被重启打断的轮次记为 interrupted,不再混进 failed
部署重建容器会打断正在跑的作业,那轮永远写不回结果,由启动时的
cleanup_orphan_runs 收尾。之前收尾成 failed,于是页面上一片红 —— 但这并不是
作业本身的失败:账号侧的 CK 一个没丢(旧 Cookie 已保留),被中断的账号也会按
退避自动重试。混在 failed 里会掩盖真正的失败(如 SQL 语法错误)。
改为独立状态 interrupted,前端用中性灰显示「被中断」,与红色的「失败」区分开。
|
2026-10-02 22:08:18 +08:00 |
|
yml2213
|
df62898d22
|
fix(automation): 调度心跳与作业周期解耦,修掉系统性跳轮
线上现象:同一页面上「CK 自动巡检」的最近运行是 21:45:34,而「下次运行」显示
21:50:34 且已经过去了 —— 巡检明明只要 19 秒,却总是等在那里。
根因是把心跳周期直接当成了作业周期:TICK_SECONDS=300 既是「循环多久醒一次」,
又被写进 next_run_at = 作业结束 + 300s。两者同频会系统性跳轮:
心跳 t=0 ck_health 19 秒跑完 → next = 5:19
心跳 t=5:00 差 19 秒没到点 → 整轮空转,什么都不做
心跳 t=10:00 才真正跑到
于是作业实际是每 10 分钟一轮,而不是设计的 5 分钟;只要作业耗时不是刚好 0,
就会稳定地"差一点点"被跳过。两个作业共享同一个心跳时,还会互相把对方的到期
时刻推后。
改动:
- 拆成两个常量:TICK_SECONDS=30(心跳,只做「有没有到点的作业」检查,几乎不
耗资源)、JOB_INTERVAL_SECONDS=300(作业周期,跑完一轮后隔多久再跑)。
- next_run_at 改用 JOB_INTERVAL_SECONDS。
- /api/automation/jobs 的 scheduler 同时暴露两个值;启动日志一并打印。
- 页面:「单账号重检间隔」加 tooltip 说明它不决定调度频率(它只控制同一行多久
重检/重试),避免和调度周期混淆。
回归测试(3 条):心跳必须比作业周期密至少 4 倍;周期未到时连跳多次心跳都不跑;
把 next_run_at 拨到过去后下一次心跳必须跑到。
全量 304 通过。
|
2026-10-02 21:57:41 +08:00 |
|
yml2213
|
34d667cba0
|
fix(automation): 启动时收尾卡在 running 的运行记录
作业是在调度线程里同步跑的:进程一重启(部署、崩溃、手工重启),那一轮就再没
机会写回结果,automation_runs 会永远停在 running,页面上一直显示「执行中」。
登录任务早有孤儿清理,运行记录原先漏了。
线上就是这么暴露的:部署时重建容器打断了正在跑的 ck_relogin 第 14 轮,重启清理
把 20 个在途账号标成了「重新登录中断(旧 Cookie 已保留)」(这部分是对的),
但 run 14 本身永远卡在 running,detail 为空。
新增 cleanup_orphan_runs(),在启动清理链里调用:把所有 running 的运行记录收尾为
failed + finished_at + 「服务重启中断」。如实记为 failed 而不是 success —— 那轮
确实没跑完;被中断的账号由各自的孤儿清理负责标记并按退避重试。
新增回归测试:只有 running 的被收尾,已完成的不受影响。
(另一件事一并说明:上一次部署我报告「零中断」是验证过早 —— 检查时部署的 up -d
还没执行到。实际情况是 mysql 未被重建(达到了本次改动目的),但应用容器因镜像
变化被正常重建,因此打断了那轮重登。)
|
2026-10-02 21:35:42 +08:00 |
|
yml2213
|
c3fd299f3a
|
test(db): 测试改用真实 MySQL,方言差异不再漏到生产
生产是 MySQL 8、测试是 SQLite,两者的方言差异在本地完全测不出来。今天为此
付出了两次代价:
1. `ORDER BY ck_checked_at ASC NULLS FIRST` 在 SQLite 上完全正常,本地 258 条
全绿,一部署到生产第一次巡检就 1064 语法错误(MySQL 不支持 NULLS FIRST);
2. P1b 自动重登的结算复用调度器事务读取,在 SQLite 下看不出问题,但在 MySQL 的
REPEATABLE READ 下读到的是批次开始前的快照 —— 10 条实际成功的重登全部被记成
失败。SQLite 的读一致性与 MySQL 不同,这类问题跑一万遍也复现不出来。
改动:
- 新增 tests/support/db.py:测试专用引擎/会话工具,连接串从 .env 读取,
默认指向独立的 douyu_login_test 库;
- 新增 tests/conftest.py:会话级建好表结构,每个用例前后清空所有表做隔离;
- 根 conftest.py 接管连接串设置:它在 pytest 收集阶段最先执行,在这里把
DATABASE_URL 指向测试库,应用模块首次 import 拿到就是 MySQL,不依赖任何
import 顺序(原先这里是 sqlite:// 兜底,改造后会变成「先建 SQLite engine
再打补丁换成 MySQL」,顺序一变就静默跑错库);
- 21 个测试文件的 setup 样板统一改用共享测试引擎;
- 两个迁移测试改为在临时库 douyu_login_mig_<随机> 上跑完整 alembic upgrade head,
跑完 drop —— 迁移脚本本身也终于在 MySQL 上被验证;
- dev.sh 与 README 的过时说明一并更新(原先写着「不需要 MySQL / .env 配置」;
「测试各自建 SQLite 内存库」)。
安全与易用:
- 测试库名与开发库同名时**直接拒绝运行**(clear_tables 会 DELETE 所有表,
一旦 TEST_MYSQL_DATABASE 被指到 douyu_login 就是数据灾难);
- 连不上 MySQL 时给出可操作提示(先 ./dev.sh 起库 / 用 TEST_MYSQL_DATABASE 覆盖),
而不是抛一段 SQLAlchemy 原始堆栈。
验收:
- 全量 301 条通过,连跑两次结果一致(隔离有效),耗时约 12.5 秒(原 SQLite 约 3.6 秒);
- 单文件独立运行通过(不依赖全量执行顺序);
- `grep -rn sqlite tests/ conftest.py` 已无实际用法;
- **方言确实生效**:临时把一条查询改回 `NULLS FIRST`,13 条测试立刻变红(含运行期
失败),还原即恢复;这条由 test_generated_sql_is_mysql_compatible 长期守着。
代价(已在 README 与规划附录 C.2 记明):跑测试前必须先有 MySQL,pre-push 钩子
同理;CI 需要 service container。
|
2026-10-02 21:22:08 +08:00 |
|
yml2213
|
8fc8e623b9
|
feat(automation): CK 自动重登(P1b)+ 失败分型与待人工队列
P1a 的巡检只产出「谁失效了」的清单,不兑现修复:1,356 个失效账号仍要人工逐个
选集→点重登→过滑块。线上实测 20 个失效账号重登 20/20 成功(2 分 13 秒、零极验),
证实失效是会话级过期而非凭证问题,可救量约 1,350。
后端:
- 迁移 20261002_0038:login_tasks 加 relogin_failure_kind(枚举),供作业决定
「继续退避重试」还是「转人工」;不用 attempt_message 做字符串匹配(文案一改
就失效,页面也要按类型筛选);
- jobs/ck_relogin.py:
- 候选 = 巡检判定 invalid + 允许自动管理 + 无在途尝试 + 退避到期 + 未超每日上限
+ **未转人工**(极验/凭证问题不再自动重登);
- 复用 LoginBatchRunner(mode="relogin"),成功才替换 CK(沿用既有安全语义)。
自动化身份取最早的超管做 created_by,权限给 login:view_all 以免受归属限制;
总时长上限压到 240 秒,避免长时间占住调度线程;
- settle_results() 独立成纯函数便于单测:成功清空退避状态;失败按分型分流 ——
captcha/credential 转人工(不排期)、email_quota 退避到次日零点+随机抖动、
email_code/interrupted/network 走 1h→3h→12h→24h 阶梯;次数用尽也停下等人工;
- 未知文案默认按可重试处理(有每日上限兜底),别把可能只是抖动的账号推给人工;
- 注册第二个作业「CK 自动重登」(batch 10 / 并发 3,**默认关闭**);
- 新增 GET /api/automation/manual-queue:按原因分组列出待人工处理的账号。可重试
类型但已无下次排期的(次数用尽)单独成组 —— 否则会混进「验证码失败」里,客服
看不出这个已经不会再自动试了。
前端:
- /automation 页新增「待人工处理」卡片,按分型分组展示(极验要过滑块、凭证要核对、
次数用尽要人工确认),各带账号与最近失败原因。
测试:新增 26 条(分型顺序陷阱「上限」优先于「验证码」、退避阶梯、配额退避次日、
候选筛选、MySQL 方言编译、结算、待人工队列分组)。
提交前全量 297 通过、ruff 全绿、前端 tsc 通过。
注意:提交时另一路「测试改用 MySQL」的改造正在同一工作区进行,本次刻意只暂存
本功能相关文件,未触碰 tests/conftest.py、tests/support/ 及对方已转换的测试文件。
|
2026-10-02 20:51:19 +08:00 |
|
yml2213
|
f8c5e896d4
|
fix(automation): 巡检区分「查不到」与「失效」,并在大面积失败时自动暂停
P1a 原本把「检测没通过」一律写成 invalid。但 check_douyu_cookie 的失败分两类:
斗鱼明确回「未登录/登陆异常」(CK 真失效),以及请求超时/响应异常(我们查不到)。
一视同仁的后果是:斗鱼接口抖动或被风控时,每 5 分钟 200 个、一小时就能把两千多个
好账号标成失效,客服会误判成「CK 大面积挂掉」。规划里的失败率熔断只给了 P1b,
P1a 漏了。
改动:
- 新增 classify_result():valid / invalid(命中斗鱼鉴权类说法)/ error(其余);
- error 时**不覆盖该行原来的 ck_check_status**,只更新 ck_check_result(带
verdict 字段)与 ck_checked_at,并计入本轮 skipped;
- 一轮 error 占比 > 30% 且条数 >= 20 时自动把作业 enabled 置 false,message 与
detail.paused 写明原因,避免继续污染;
- 真实死号占比高**不触发**暂停 —— 首轮 100% invalid 是预期情况(选行「从没检测过
的优先」,积压的是两个月前批量导入、attempt_status 为空、从未登录的陈旧账号,
生产实测 200 条错误信息一致为斗鱼侧「用户未登录」)。
新增 6 条测试:分类正确性、error 保留原结论、异常不再标失效、大面积失败自动暂停、
真实死号不暂停。全量 271 条通过。
|
2026-10-02 20:35:30 +08:00 |
|
yml2213
|
3d5d0f551f
|
fix(automation): 去掉 MySQL 不认的 NULLS FIRST,排序改用可移植写法
生产 MySQL 上手动跑第一轮就报 1064:
You have an error in your SQL syntax ... near 'NULLS FIRST, login_tasks.id ASC'
NULLS FIRST 是 PostgreSQL/SQLite 语法,MySQL 不支持。本地测试全绿是因为跑在
SQLite 上(它认识这个语法),属于方言差异漏网 —— 生产是 MySQL、测试是 SQLite,
行为测试覆盖不到这类问题。
改成 `ck_checked_at IS NULL DESC, ck_checked_at ASC, id ASC`:三个方言下语义
一致(没检测过的优先)且都是合法 SQL。顺带把选行查询抽成 build_select_query,
便于按方言编译做回归。
新增回归测试:用 MySQL 方言 compile() 选行 SQL,断言不含 NULLS FIRST/LAST,
且「没检测过优先」用可移植写法表达。
规划附录 C.2 记下这条:写 SQL 前先确认方言;P0 连的是 order_site 的 PostgreSQL,
项目里已经出现 MySQL / SQLite / PG 三套方言。
|
2026-10-02 20:14:49 +08:00 |
|
yml2213
|
7aaaeebe2b
|
fix(automation): 修正调度口径——作业每 5 分钟跑一轮,interval_seconds 只是单行重检间隔
P1a 把两个「间隔」混成了一个:next_run_at 用的是 job.interval_seconds,于是
作业 6 小时才跑一轮、每轮 200 条 = 800 条/天,全库 12,881 条要 16 天才能过
一遍 —— 等于没巡检,与规划里「账号 6 小时内被查一次」的目标差了两个数量级。
按规划原意修正:
- TICK_SECONDS 60 → 300:作业每个 tick(5 分钟)跑一轮;
- next_run_at 改为 finished + TICK_SECONDS,不再用 interval_seconds;
- interval_seconds 只作为「同一行多久重新检测」的选行 cutoff(选行逻辑本来就
是这么用的,无需改);
- 容量口径随之更新:5 分钟一轮 × 200 条 = 40 条/分钟 ≥ 需要的 36 条/分钟,
冷启动约 5.4 小时覆盖全库,稳态每轮一百多条;
- 前端标签由「巡检周期(小时)」改为「单账号重检间隔(小时)」,说明文案写清
「每 5 分钟跑一轮」,避免运维误以为是作业周期。
同步更新规划文档 §7.2/§7.3(含两个间隔不能混的说明)与附录 C 的三段上线
步骤(立即跑一次 → 每轮 50 观察 → 每轮 200 启用)。
|
2026-10-02 19:54:31 +08:00 |
|
yml2213
|
f745a61df1
|
feat(automation): CK 自动巡检(P1a),只检测不改 CK
后台原本靠人工逐批点「CK 检测」发现失效账号,1.2 万个账号根本盯不过来。
P1a 把「哪些账号失效了」自动化:自动挑久未检测的账号、调现成检测接口、
把结果写回 ck_check_*。**本作业不替换任何 CK**,重登仍由人工点,符合规划里
「先做只读/只提示,确认无误再做自动动作」的渐进上线原则。
实现:
- 迁移 20261002_0037:automation_jobs / automation_runs 两张表,
login_tasks 加 relogin_attempts / relogin_next_at / relogin_last_error
(为 P1b 备好),accounts 加 auto_managed(特殊账号不参与自动化);
- services/automation/scheduler.py:进程内调度线程,60s tick,状态全部落库
(重启不丢调度进度、不重复跑轮);AUTOMATION_ENABLED 默认关闭,作业开关
也默认关闭;ensure_jobs 只补缺失行、不覆盖运维改过的配置;
作业异常只记 failed,不拖垮调度线程;单容器 --workers 1,无需分布式锁;
- services/automation/jobs/ck_health.py:选行口径与人工检测一致
(cookie_holding_filter:持有 CK 且无在途尝试),叠加久未检测、
auto_managed、batch_size,按 ck_checked_at 升序(最久没查的优先);
只写 ck_check_status / ck_check_result / ck_checked_at;
- routers/automation.py:GET /jobs、PATCH /jobs/{key}、POST /jobs/{key}/run、
GET /runs;新增权限点 automation:view(运营可看)与 automation:manage
(仅超管),改配置与手动触发都写审计;
- 顺带补 POST /api/login/batch 的审计(此前批次是谁发起、多少账号无据可查);
- 前端 /automation 页:作业卡片(开关/周期/单轮条数/并发)、运行历史、
「立即跑一次」;总开关未打开、调度线程未运行时给明确提示。
验证:
- 22 条新测试覆盖调度器行为(只跑到点的、异常不抛出、下轮时间、配置不被重置)、
选行条件(在途尝试/auto_managed/间隔/batch_size)、接口与权限,其中一条
专门断言巡检后 cookie/status/message/attempt_status 一字未动;
- 全量 257 条通过;前端 tsc 与构建通过;
- 端到端用 3 个真实账号跑真实斗鱼接口:巡检结果与直接调 check_douyu_cookie
逐条一致(3/3 valid),CK 与尝试状态全程未变。
顺带修掉一处会炸生产的坑:DB 时间列读出来是 naive UTC,直接与
datetime.now(UTC) 比较会 TypeError,现统一走 _as_utc() 补时区。
|
2026-10-02 19:47:41 +08:00 |
|