88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
||
"""按 trade_id 幂等重放风控引擎(B9a 补偿 · trade_gateway engine_error 场景)。
|
||
|
||
背景(PRD FR-1 / 架构 §5.3):网关 INSERT core_trade 成功后 process_trade_event
|
||
异常时交易已成立——响应带 engine_error=true、审计 decision='risk_engine_error',
|
||
预警缺失。用本脚本按 trade_id 补放引擎(也可用于人工补放任何一笔已落库交易)。
|
||
|
||
幂等:trade_id 已存在于任一 risk_alert.payload(含 aml 单与已处置单)即跳过,
|
||
不重复出单、不重复 append 事件。重放以 core_trade.traded_at 为引擎事件时点,
|
||
RISK-004 窗口判定可复现(engine.py 模块注释口径),不受脚本执行时刻影响。
|
||
|
||
用法:
|
||
python scripts/demo/rebuild_alerts.py TRD-20260907-AB12CD34 [更多 trade_id ...]
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from app.repository.core_ro import CoreReadOnlyRepository # noqa: E402
|
||
from app.repository.risk_repository import RiskRepository # noqa: E402
|
||
from app.service.risk.engine import process_trade_event # noqa: E402
|
||
|
||
|
||
def rebuild_trade(
|
||
trade_id: str,
|
||
core: CoreReadOnlyRepository,
|
||
repo: RiskRepository,
|
||
) -> dict[str, Any]:
|
||
"""重放单笔已落库交易(CLI 与单测共用入口)。
|
||
|
||
返回 {"state": "rebuilt"|"skipped"|"missing", "trade_id", "alert_ids", ...}:
|
||
rebuilt 附 triggered_rules/aml_hit;skipped 附已入的预警单 alert_ids;
|
||
missing 表示 core_trade 无此流水(未做任何写操作)。
|
||
"""
|
||
trade = core.get_trade_by_id(trade_id)
|
||
if trade is None:
|
||
return {"state": "missing", "trade_id": trade_id, "alert_ids": []}
|
||
existing = repo.find_alerts_by_trade(trade_id)
|
||
if existing:
|
||
return {
|
||
"state": "skipped",
|
||
"trade_id": trade_id,
|
||
"alert_ids": [row["alert_id"] for row in existing],
|
||
}
|
||
result = process_trade_event(trade, core_ro=core, risk_repo=repo)
|
||
return {
|
||
"state": "rebuilt",
|
||
"trade_id": trade_id,
|
||
"alert_ids": result["alert_ids"],
|
||
"triggered_rules": result["triggered_rules"],
|
||
"aml_hit": result["aml_hit"],
|
||
}
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="按 trade_id 幂等重放风控引擎(引擎异常补偿)")
|
||
parser.add_argument("trade_ids", nargs="+", help="core_trade 中的 trade_id,可一次多笔")
|
||
args = parser.parse_args()
|
||
|
||
core = CoreReadOnlyRepository()
|
||
repo = RiskRepository()
|
||
|
||
missing = 0
|
||
for tid in args.trade_ids:
|
||
out = rebuild_trade(tid, core, repo)
|
||
if out["state"] == "rebuilt":
|
||
rules = ",".join(out["triggered_rules"]) or "-"
|
||
print(f"[rebuilt] {tid}: rules={rules} alerts={out['alert_ids']} aml={out['aml_hit']}")
|
||
elif out["state"] == "skipped":
|
||
print(f"[skipped] {tid}: 已入预警单 {out['alert_ids']}(幂等跳过)")
|
||
else:
|
||
print(f"[missing] {tid}: core_trade 无此流水", file=sys.stderr)
|
||
missing += 1
|
||
|
||
if missing:
|
||
raise SystemExit(f"{missing}/{len(args.trade_ids)} 笔 trade_id 未找到")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|