fix(risk): 扫描健壮性——脏数据不再中断整批,调度器重启后仍会执行
docs/25 P0 的后两项,都属于"风控看起来在工作、其实没在跑"那一类。 **② 一条脏数据中断整批** _level_value 原先直接 int(level.replace(prefix, "")):等级字段只要有一条不是 R1-R5 / C1-C5(例如写了「中风险」),就抛 ValueError 并冒到 scan() 的兜底 → **整批 rollback**,本次扫描前面已经生成的预警全部作废。数据脏属于运维问题,不该升级成 "整个风控停摆"。 改为返回 int | None;调用点跳过该条并记 warning(带上 transaction_id 与两个原始值, 便于运维直接定位)。顺带把 eplace 换成 emoveprefix:原先 "R2R" 会被错当成 2, 现在只去掉开头那一个前缀字符。 **③ 调度器重启后永不执行** last_run_at 只存在内存里,重启后为 None,而 _is_due 此时返回 config.run_immediately(默认 False)⇒ 重启后 _is_due 恒为假,**调度器形同虚设, 而且没有任何告警**。 改为"从未跑过即视为 due":多跑一次的最坏后果是重复扫描,而扫描每条规则都先 _exists 查重、外层还有 MySQL 级锁;反过来"不跑"的后果可能是永远不跑。 config.run_immediately 不再承担"首次是否执行"的语义(它原本想表达"启动后别马上跑", 但那与"永远不跑"在实现上无法区分),字段保留以免破坏既有配置。 新增 tests/unit/service/test_risk_scan_robustness.py(6 条):脏数据返回 None 而不抛异常、 R2R 不被过度剥离、首次必 due(哪怕 run_immediately=False)、以及间隔前后的判定。 ruff / mypy(136 文件) / 622 unit+contract / 29 integration 全绿。
This commit is contained in:
@@ -121,7 +121,17 @@ class RiskScanSchedulerWorker:
|
||||
current: datetime,
|
||||
) -> bool:
|
||||
if self.last_run_at is None:
|
||||
return config.run_immediately
|
||||
# 进程刚起来,不知道自己上次是什么时候跑的 —— `last_run_at` 只存在内存里。
|
||||
# **保守地视为 due**:多跑一次的最坏后果是重复扫描,而扫描本身是幂等的
|
||||
# (每条规则先 `_exists` 查重)并且有 MySQL 级锁;反过来"不跑"的后果可能是
|
||||
# **永远不跑**:原先这里返回 `config.run_immediately`(默认 False),
|
||||
# 重启之后 `_is_due` 恒为假,调度器形同虚设 —— 而且没有任何告警,
|
||||
# 现场只会表现为"风控好像没在扫描"。
|
||||
#
|
||||
# `config.run_immediately` 因此不再承担"首次是否执行"的语义(它原本想表达的
|
||||
# 是"启动后别马上跑",但那与"永远不跑"在实现上无法区分)。字段保留,
|
||||
# 以免破坏既有配置。
|
||||
return True
|
||||
return current - self.last_run_at >= timedelta(minutes=config.interval_minutes)
|
||||
|
||||
async def _execute_with_retry(
|
||||
|
||||
Reference in New Issue
Block a user