From f37fd922a19f7a04b90b61776365aa2f34b75056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Sun, 13 Sep 2026 21:44:08 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(seed):=20=E6=BC=94=E7=A4=BA?= =?UTF-8?q?=E9=A2=84=E8=AD=A6=E7=9A=84=E7=8A=B6=E6=80=81=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E5=AF=B9=E9=BD=90=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=20closed=5Fat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 结论:risk_action_service 没问题,我先前是误判 esolve(完成结案)写全了 status="已结案" + closed_at + handle_result(risk_action_service.py:105-107), exclude 同样(:67-71)。我报的"结案没写 closed_at"看到的那条 ALDEMO0003 其实是**种子预置样本**,不是我以为的自己创建的那条 —— 我的冒烟脚本用 ALDEMO{时分秒} 命名,**撞上了种子的 ALDEMO0001-0003**,"已存在则跳过"把创建挡掉, 随后处置全 409,串起来就像"结案没写时间"。 真正的问题在种子数据,两处: 1) **状态口径与代码不一致**:ALDEMO0003 写成 status="已关闭",而 isk_action_service 里**没有任何动作会产生这个状态**(状态机只有 待处理 / 调查中 / 已排除 / 已结案)。演示时会被当成系统行为,排查时又查不到出处。 按它的 close_reason 语义(低风险 + 已核验为正常交易),它就是**关闭误报** → 改为"已排除"。 2) **终态缺 closed_at**:那条有 close_reason 却没有 closed_at,自相矛盾; 而且 isk_repository 按 closed_at 统计"当日结案",缺了它会被漏掉。 INSERT 补上该列,样本用 closed_at_offset_minutes 给值。 ## 顺带修掉冒烟脚本的撞号 预警编号前缀 ALDEMO → E2E,注释里写明原因 —— 免得下次又操作到种子样本、 还把现象当成新 bug。 ## 验证 - ALDEMO0003 现为 status=已排除 + closed_at 有值 - 库里所有终态预警(已排除/已结案)都带 closed_at;进行中的(调查中)不带 —— 口径一致 - 自己创建的两条走 resolve 后 status=已结案 且 closed_at/handle_result 都有值 - 冒烟重跑:40/40 通过,新造预警编号 E2E134351,处置闭环三段全 PASS 门禁:ruff 通过 / mypy 250 文件 0 错 / 单元+契约 1381 passed 2 skipped。 --- tools/e2e_smoke_test.py | 5 ++++- tools/seed_risk_alert_demo_data.py | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tools/e2e_smoke_test.py b/tools/e2e_smoke_test.py index 9eee92f..475ddf8 100644 --- a/tools/e2e_smoke_test.py +++ b/tools/e2e_smoke_test.py @@ -160,7 +160,10 @@ def create_pending_alert() -> str | None: """造一条「待处理」预警,供处置闭环验证(库里的常常都已被处置过)。""" async def run() -> str: - alert_no = f"ALDEMO{datetime.now(UTC).strftime('%H%M%S')}" + # 前缀**刻意避开 `ALDEMO`**:那是 `seed_risk_alert_demo_data.py` 的演示样本编号 + # (ALDEMO0001-0003)。撞号过一次 —— 本脚本"已存在则跳过"的检查会把创建挡掉, + # 随后处置全 409,看起来像"结案没写 closed_at",其实是操作了别人的样本。 + alert_no = f"E2E{datetime.now(UTC).strftime('%H%M%S')}" now = datetime.now(UTC).replace(tzinfo=None) async with SessionFactory() as session: async with session.begin(): diff --git a/tools/seed_risk_alert_demo_data.py b/tools/seed_risk_alert_demo_data.py index 4d301ba..5418787 100644 --- a/tools/seed_risk_alert_demo_data.py +++ b/tools/seed_risk_alert_demo_data.py @@ -115,11 +115,18 @@ ALERTS: tuple[dict[str, Any], ...] = ( }, "priority_score": 20, "event_status": "盘后预警", - "status": "已关闭", + # ⚠️ 状态必须是**代码能产出**的那几个(待处理 / 调查中 / 已排除 / 已结案)。 + # 这里原写作「已关闭」,而 `risk_action_service` 里没有任何动作会产生它 —— + # 演示时会被当成系统行为、排查时又查不到出处。按 close_reason 的语义 + # (低风险 + 已核验为正常交易),它其实就是「关闭误报」。 + "status": "已排除", "ack_status": "已确认", "due_at_offset_minutes": None, "is_escalated": 0, "close_reason": "已核验为本人自动定投计划,属正常交易。", + # 终态样本必须带关闭时间:只有 close_reason 却没有 closed_at 是自相矛盾的, + # 而且 `risk_repository` 按 closed_at 统计"当日结案",缺了它这条会被漏掉。 + "closed_at_offset_minutes": -120, }, ) @@ -146,13 +153,14 @@ async def main() -> int: print(f"[跳过] {spec['alert_no']} 已存在") continue offset = spec["due_at_offset_minutes"] + closed_offset = spec.get("closed_at_offset_minutes") await cursor.execute( "INSERT INTO fin_risk_alert (" " id, alert_no, customer_id, alert_type, alert_level, trigger_rule_codes," " evidence_summary, evidence_snapshot, priority_score, event_status, status," - " ack_status, handler_id, due_at, is_escalated, close_reason," + " ack_status, handler_id, due_at, closed_at, is_escalated, close_reason," " created_at, updated_at" - ") VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + ") VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( spec["id"], spec["alert_no"], CUSTOMER_ID, spec["alert_type"], spec["alert_level"], json.dumps(spec["trigger_rule_codes"], ensure_ascii=False), @@ -161,6 +169,7 @@ async def main() -> int: spec["priority_score"], spec["event_status"], spec["status"], spec["ack_status"], HANDLER_ID, (now + dt.timedelta(minutes=offset)) if offset else None, + (now + dt.timedelta(minutes=closed_offset)) if closed_offset else None, spec["is_escalated"], spec.get("close_reason"), now, now, ),