diff --git a/docs/25-风控模块代码评审报告.md b/docs/25-风控模块代码评审报告.md index eca781f..a5258ec 100644 --- a/docs/25-风控模块代码评审报告.md +++ b/docs/25-风控模块代码评审报告.md @@ -175,7 +175,36 @@ Asia/Shanghai 展示。北京 08:00 前生成时,统计窗口是"前一日 08: | 13 | 一条脏数据中断整批 | `risk_scan_service.py:468` `int(level.replace(prefix, ""))` 遇非 C1-C5 抛 `ValueError`,`refresh_alerts` 无逐条隔离,`:411` 整批 rollback | 一条脏数据 ⇒ 整批扫描失败 | | 14 | 年龄口径不一 | `risk_scan_service.py:462` 用 UTC 日期,`risk_judgement_service.py:381` 用服务器 `date.today()` | 生日边界差 1 天,65 岁阈值可能翻面 | | 15 | 幂等仅在应用层 | `risk_scan_service.py:332` `_exists` + JSON 列无唯一索引;`:37` 的 asyncio.Lock 仅进程内 | 多 Web worker 并发存在重复告警窗口 | -| 16 | 状态跳变与静默截断 | `risk_action_service.py:65-66` 允许"待处理+已确认"直接关误报、跳过调查;`:103` `min(20, score_before)` 静默把 >20 压到 20 | 处置流程可被绕过;评分被静默改写 | +| 16 | ~~状态跳变与静默截断~~ **复核后拆成三部分,见下方说明** | `risk_action_service.py:65-66`;`:103` | ~~处置流程可被绕过;评分被静默改写~~ | + +**第 16 条的复核结论(2026-09-11)**:原描述把三件事混成了一条,其中两件不成立、一件是真缺陷。 + +**a. `exclude` 不要求"调查中" —— 这是业务规则,不是技术缺陷。** + +`exclude`(`:63-66`)要求 `_require_acknowledged` + `_require_open`,而 `resolve`(`:90`) +额外要求 `alert.status == "调查中"`。两者不对称是事实,但**误报关闭是否必须先经调查,是业务 +规则**:把明显误报(如系统重复触发)也强制走一遍调查,未必是想要的效果。代码里两道门是有意 +设置的,看不出实现偏差。**需要业务方裁定**,不由技术侧单方面加门禁。 + +**b. `min(20, score_before)` —— 误报。** + +`BEHAVIOR_SCORE_INITIAL = 20` 是**满分**(`:18`),扣分表 `{"低":3, "中":5, "高":20}`(`:19`) +与之自洽(高危扣满、归零)。所以 `min(20, ...)` 是**把越界数据拉回合法上限**,属于数据清洗。 +而且它**不是静默的**:审计里同时记了 `behavior_score_before` / `behavior_score_deduction` / +`behavior_score_after`(`:118-120`)。原描述说"静默改写"不成立。 + +**c. 真缺陷(原报告没写):`behavior_score` 初始值是 0,不是满分 20,导致扣分机制整体失效。** + +实测:`fin_customer_profile.behavior_score` 定义为 `int NOT NULL`(**无默认值**), +现有画像 `customer_id=9001` 的值是 **0**。 + +于是 `:103` 的计算恒为:`min(20, 0) - deduction = -deduction` → `max(0, ...) = 0` —— +**扣分永远扣不动,行为分恒为 0**。而且全仓只有 `risk_action_service.py:109` 一处给 +`behavior_score` 赋值(写入方只有风控结案),说明**初始值来自插入画像时的显式 0**, +没有任何地方把它初始化成 20。 + +影响:行为分是"预警结案 → 客户行为评分下降"这条链路的落点,现在这条链路**产出为零**。 +修复点在**画像创建侧**(把初始值设为满分),不在风控的扣分逻辑里。 ---