风控 P3 收尾与适当性豁免额度校验 #4

Merged
lzf_0626 merged 34 commits from qyqy_develop_1 into qyqy_develop 2026-09-11 14:38:49 +08:00
2 changed files with 92 additions and 5 deletions
Showing only changes of commit 85a1b33eff - Show all commits
+27 -5
View File
@@ -14,6 +14,12 @@ from typing import Any
from app.core.timeutil import local_date, local_hour
VERDICT_RELEASE = "可考虑放行"
# 定投类工单渠道。扫描侧按"有效定投工单"生成 RW-018 预警时接受这两个值
# (`risk_scan_service.py:294` 的 `work_order.channel in {"定投", "自动定投"}`)。
# 研判侧必须用**同一份口径**:原先详情级只认 `"定投"`,于是"自动定投"的预警
# 会出现"扫描按有效工单生成、详情却说未确认有效定投工单"的自相矛盾。
DIRECT_INVESTMENT_CHANNELS = frozenset({"定投", "自动定投"})
VERDICT_SUSPECTED_FALSE_POSITIVE = "疑似误报"
VERDICT_CONTINUE_REVIEW = "继续复核"
VERDICT_RISK_SUPPORTED = "证据支持风险"
@@ -44,11 +50,27 @@ def assess_alert_detail(detail: dict[str, Any]) -> dict[str, Any]:
def _assess_list_rule(rule: str, item: dict[str, Any]) -> dict[str, Any]:
if rule == "RW-018":
# 原先这里**完全不看 item**,无条件返回"可考虑放行",而且理由文本是硬编码的
# "现有摘要显示交易来自有效定投工单"。于是列表里那些渠道并不匹配、或证据里
# 根本没有工单信息的 RW-018 预警,也被标成"可考虑放行"——等于把待核实的预警
# 在列表层提前放掉了(详情层另有判断,但列表是风控专员最先看到的一屏)。
#
# 现在按 snapshot 里的渠道判断,并与扫描侧(`risk_scan_service.py:294`)
# 共用同一份渠道口径 —— 两处不一致会造成"扫描认为有效、详情认为未确认"的自相矛盾。
snapshot = _mapping(item.get("evidence_snapshot"))
channel = snapshot.get("channel")
if isinstance(channel, str) and channel in DIRECT_INVESTMENT_CHANNELS:
return _assessment(
VERDICT_RELEASE,
"高",
[f"命中低优先级频繁交易初筛,关联工单渠道为 {channel},属有效定投场景。"],
["核验定投工单状态、交易周期和客户授权记录。"],
)
return _assessment(
VERDICT_RELEASE,
"高",
["命中低优先级频繁交易初筛,现有摘要显示交易来自有效定投工单。"],
["核验定投工单状态、交易周期和客户授权记录。"],
VERDICT_SUSPECTED_FALSE_POSITIVE,
"中",
["命中频繁交易初筛,但证据中未确认有效定投工单。"],
["核验定投工单状态与客户授权记录后,由风控专员判断是否放行。"],
)
if rule == "RW-015":
return _assessment(
@@ -261,7 +283,7 @@ def _assess_rw015(detail: dict[str, Any]) -> dict[str, Any]:
def _assess_rw018(detail: dict[str, Any]) -> dict[str, Any]:
work_order = _mapping(detail.get("work_order"))
channel = work_order.get("channel")
if channel == "定投":
if channel in DIRECT_INVESTMENT_CHANNELS:
return _assessment(
VERDICT_RELEASE,
"高",
@@ -0,0 +1,65 @@
"""RW-018 研判的两个口径缺陷(docs/25 P2)。
1. **列表级无条件放行**:`_assess_list_rule` 收到 `item` 却完全不看它,对 RW-018
一律返回"可考虑放行",连理由文本都是硬编码的"现有摘要显示交易来自有效定投工单"。
于是渠道不匹配、或证据里根本没有工单信息的预警,在列表层就被标成可放行 ——
而列表正是风控专员最先看到的一屏。详情层另有判断,但那要等人点进去。
2. **详情级与扫描侧口径不一致**:扫描按 `work_order.channel in {"定投", "自动定投"}`
生成 RW-018 预警(`risk_scan_service.py:294`),详情级却只认 `"定投"`。于是
"自动定投"的预警会出现"扫描认为有效、详情认为未确认"的自相矛盾。
两条现在共用 `DIRECT_INVESTMENT_CHANNELS`,改这份常量即同时影响两侧。
"""
from typing import Any
from app.service.risk_judgement_service import (
DIRECT_INVESTMENT_CHANNELS,
VERDICT_RELEASE,
_assess_list_rule,
_assess_rw018,
)
def _item(**snapshot: Any) -> dict[str, Any]:
return {"alert_no": "ALTEST", "evidence_snapshot": snapshot}
def test_channels_cover_what_the_scanner_accepts() -> None:
"""常量必须覆盖扫描侧接受的全部渠道,否则又会两侧打架。"""
assert DIRECT_INVESTMENT_CHANNELS == {"定投", "自动定投"}
def test_list_level_does_not_release_without_evidence() -> None:
"""证据里没有渠道信息时不能放行 —— 这正是原先无条件放行的地方。"""
result = _assess_list_rule("RW-018", _item())
assert VERDICT_RELEASE not in str(result)
def test_list_level_does_not_release_for_an_unrelated_channel() -> None:
result = _assess_list_rule("RW-018", _item(channel="柜台"))
assert VERDICT_RELEASE not in str(result)
def test_list_level_releases_for_every_direct_investment_channel() -> None:
for channel in sorted(DIRECT_INVESTMENT_CHANNELS):
result = _assess_list_rule("RW-018", _item(channel=channel))
assert VERDICT_RELEASE in str(result), f"{channel} 应当判定为可放行"
def test_detail_level_accepts_the_same_channels_as_the_scanner() -> None:
"""详情级与扫描侧同口径 —— 原先详情只认"定投","自动定投"会被判成未确认。"""
for channel in sorted(DIRECT_INVESTMENT_CHANNELS):
result = _assess_rw018({"work_order": {"channel": channel}})
assert VERDICT_RELEASE in str(result), f"{channel} 在详情级也应判为可放行"
def test_detail_level_rejects_a_channel_the_scanner_would_not_use() -> None:
result = _assess_rw018({"work_order": {"channel": "柜台"}})
assert VERDICT_RELEASE not in str(result)