fix(risk): 预警行带出 close_reason,日报的"误报原因"不再是空
docs/25 P2。核实确认报告准确,而且这一处缺口造成两个症状:
_alert_row 是**列表 / 详情 / 日报共用**的行构造器
(risk_repository.py:174 / :276 / :321),而它的字段列表里没有 close_reason。于是:
- 日报的"误报原因"分布恒为"未填写"
(risk_daily_report_service.py:139 用 item.get("close_reason") or "未填写");
- :243 的明细里同一字段同样拿不到值。
断的是"关闭误报时写入原因"这条链路的**下半段**:risk_action_service.py:70 把原因赋给
lert.close_reason、也确实存进了库(app/model/fund.py:368 有该字段),只是读取时没带出来。
修法是一行:在 _alert_row 里补上 close_reason。三个调用点同时受益;读取方都是风控侧
接口(需要 risk:alert:read),不涉及客户可见面。
新增 tests/unit/repository/test_risk_alert_row_close_reason.py(2 条):关闭原因出现在行里;
未关闭时为 None 但**键必须在** —— 读取方靠 or "未填写" 兜底,键一旦缺失就永远只能走
兜底分支,那正是修复前的状态。
ruff / mypy(136 文件) / 639 unit+contract / 29 integration 全绿。
This commit is contained in:
@@ -962,6 +962,13 @@ class RiskRepository:
|
||||
"is_escalated": bool(alert.is_escalated),
|
||||
"escalated_at": alert.escalated_at,
|
||||
"evidence_archived": bool(snapshot.get("evidence_archive")),
|
||||
# 关闭误报时写入的原因(risk_action_service.py:70 赋值给 alert.close_reason)。
|
||||
# 原先这一行不在,导致两个症状同一个原因 —— 行里根本没带出来:
|
||||
# · 日报的"误报原因"分布恒为"未填写"
|
||||
# (risk_daily_report_service.py:139 用 `item.get("close_reason") or "未填写"`);
|
||||
# · `:243` 的明细里同一字段同样拿不到值。
|
||||
# 读取方都是风控侧接口(需要 risk:alert:read),带上它不涉及客户可见面。
|
||||
"close_reason": alert.close_reason,
|
||||
"created_at": alert.created_at,
|
||||
"updated_at": alert.updated_at,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""预警行必须带出 `close_reason`,否则日报的"误报原因"永远是空。
|
||||
|
||||
docs/25 P2。`_alert_row` 是**列表 / 详情 / 日报共用**的行构造器
|
||||
(`risk_repository.py:174` / `:276` / `:321`),而它的字段列表里原先没有 `close_reason`。
|
||||
后果是两个症状同一个原因 —— 行里根本没带出来:
|
||||
|
||||
- 日报的"误报原因"分布恒为"未填写"
|
||||
(`risk_daily_report_service.py:139` 用 `item.get("close_reason") or "未填写"`);
|
||||
- `:243` 的明细里同一字段同样为空。
|
||||
|
||||
断掉的是"关闭误报时写入原因"这条链路的下半段:`risk_action_service.py:70` 把原因赋给
|
||||
`alert.close_reason`、也存进了库,只是读取时没带出来。
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from app.repository.risk_repository import RiskRepository
|
||||
|
||||
CLOSED_REASON = "已核验为本人自动定投计划。"
|
||||
|
||||
|
||||
def _alert(**overrides: Any) -> Any:
|
||||
base: dict[str, Any] = {
|
||||
"alert_no": "ALTEST",
|
||||
"customer_id": 9001,
|
||||
"alert_type": "大额频繁交易",
|
||||
"alert_level": "高",
|
||||
"trigger_rule_codes": ["RW-007"],
|
||||
"evidence_summary": "摘要",
|
||||
"evidence_snapshot": {},
|
||||
"priority_score": 90,
|
||||
"event_status": "刚刚发生",
|
||||
"status": "已关闭",
|
||||
"ack_status": "已确认",
|
||||
"ack_at": None,
|
||||
"due_at": None,
|
||||
"is_escalated": 0,
|
||||
"escalated_at": None,
|
||||
"close_reason": CLOSED_REASON,
|
||||
"created_at": None,
|
||||
"updated_at": None,
|
||||
}
|
||||
base.update(overrides)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
def test_alert_row_carries_close_reason() -> None:
|
||||
"""关闭原因必须出现在行里 —— 它是日报"误报原因"的唯一来源。"""
|
||||
row = RiskRepository._alert_row(_alert(), None, None, None, None)
|
||||
|
||||
assert row["close_reason"] == CLOSED_REASON
|
||||
|
||||
|
||||
def test_alert_row_keeps_the_key_even_when_there_is_no_reason() -> None:
|
||||
"""未关闭的预警没有原因,但**键必须在**。
|
||||
|
||||
读取方用 `item.get("close_reason") or "未填写"` 兜底,所以 None 与缺键在展示上等价;
|
||||
可一旦键不存在,读取方就永远只能走到兜底分支 —— 那正是本次修复前的状态。
|
||||
"""
|
||||
row = RiskRepository._alert_row(_alert(close_reason=None), None, None, None, None)
|
||||
|
||||
assert "close_reason" in row
|
||||
assert row["close_reason"] is None
|
||||
Reference in New Issue
Block a user