From 539d4915d731f605a777dd628335ead9598a2348 Mon Sep 17 00:00:00 2001 From: YUAN Date: Mon, 7 Sep 2026 01:59:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20B9b=20=E5=A4=8D=E5=AE=A1=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=E2=80=94=E2=80=94P2-1=20SOP=20=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=87=8D=E7=81=8C=E8=A1=A5=20DROP=20jinrong=5Fagent(reset.ps1?= =?UTF-8?q?=20=E5=8F=AA=E9=87=8D=E5=BB=BA=20core+=E8=A3=B8=20CREATE=20TABL?= =?UTF-8?q?E=20=E4=BC=9A=201050=20=E4=B8=AD=E6=AD=A2);=20P2-2=20=E8=A1=A5?= =?UTF-8?q?=E5=81=9A=20T-31=20=E5=90=8D=E4=B8=8B=20P2-8=20=E6=89=8B?= =?UTF-8?q?=E5=B7=A5=20SQL=20=E5=AF=B9=E7=85=A7=E6=94=B6=E5=8F=A3(4=20?= =?UTF-8?q?=E7=BB=84=E5=90=88=E7=9C=9F=E5=BA=93=20SQL=20=E6=8E=A8=E5=AF=BC?= =?UTF-8?q?=E4=B8=8E=E5=87=BD=E6=95=B0=E5=88=A4=E5=AE=9A=E4=B8=80=E8=87=B4?= =?UTF-8?q?,=20=E7=99=BB=E8=AE=B0=20SOP=20=E6=A0=B8=E6=9F=A5=E5=8D=95?= =?UTF-8?q?=E9=99=84=E8=A1=8C);=20P2-3=20SOP=20=E8=B5=B0=E6=9F=A5=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E4=B8=8E=2011/11=20=E5=8F=A3=E5=BE=84=E7=BB=9F?= =?UTF-8?q?=E4=B8=80(A-6=20=E5=BD=92=20M3/A-8=20=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E4=BA=A4=E4=BB=A3);=20P3:=20A-4/A-9=20COUNT=20=E5=BD=93?= =?UTF-8?q?=E6=97=A5=E7=AA=97+=E5=B7=B2=E5=A4=84=E7=BD=AE=E5=8D=95?= =?UTF-8?q?=E5=B9=82=E7=AD=89=E7=94=A8=E4=BE=8B+suitability/check=20?= =?UTF-8?q?=E9=98=BB=E6=96=AD=E5=93=8D=E5=BA=94=E8=A1=A5=20G-08=20?= =?UTF-8?q?=E4=B8=A4=E8=A6=81=E7=B4=A0(advice/notice)+=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=95=B0=E5=AD=97=E5=90=8C=E6=AD=A5;=20?= =?UTF-8?q?=E7=95=99=E7=97=95=20scan=5Fall=20=E5=B9=82=E7=AD=89=E8=AF=BB?= =?UTF-8?q?=E5=90=8E=E5=86=99=E6=97=A0=E9=94=81=E5=8F=96=E8=88=8D;=20209?= =?UTF-8?q?=20=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/risk.py | 9 ++++++++- docs/memory/FLOW.md | 4 ++-- docs/memory/MEMORY.md | 6 +++--- docs/memory/TODO.md | 4 ++-- docs/项目框架设计/开发计划-风控模块.md | 2 ++ docs/项目框架设计/演示SOP-风控模块.md | 19 +++++++++++++------ tests/test_aml_service.py | 15 +++++++++++++++ tests/test_integration_risk.py | 4 ++-- 8 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/api/risk.py b/app/api/risk.py index 7499a8f..d31e7f5 100644 --- a/app/api/risk.py +++ b/app/api/risk.py @@ -136,7 +136,14 @@ def suitability_check_api(req: SuitabilityCheckRequest, auth: AuthContext = Depe "handler_comment": None, } ) - return asdict(result) + body = asdict(result) + if result.blocked: + # G-08 三要素补齐(PRD §6 阻断类响应;直调阻断面向客户本人,评审 B9b P3-4) + from app.gateway.trade_gateway import ADVICE, RECORDED_NOTICE + + body["advice"] = ADVICE + body["notice"] = RECORDED_NOTICE + return body @router.post("/aml/scan") diff --git a/docs/memory/FLOW.md b/docs/memory/FLOW.md index 6c00cd2..3b48386 100644 --- a/docs/memory/FLOW.md +++ b/docs/memory/FLOW.md @@ -48,9 +48,9 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md Client → Gateway(JWT/RBAC) → api/chat → agent_service(LangGraph) → Tools → 存储 → 响应 + audit_log ``` -当前:**风控事件线已集成 + 测试基建完成**(交易网关 → 规则引擎 → 预警/AML/L3 → 4 API,`main.py` 路由挂载 + trace 中间件 + lifespan 完成,B1~B8;B8 = conftest 演示数据校验 + 集成测试 A-1~A-5/A-7/A-9 + `tests/_ddl.py` DDL 单一事实源;B7/B8 均经独立 AI 评审闭环;B9a = subscribe_alerts/rebuild_alerts 演示与补偿脚本;B9b = 演示走查 + B6/B7/B8 核查单①~⑥收口(disclaimer/aml scan 幂等/集成测试前置断言),208 测试绿,演示 SOP 见 docs/项目框架设计/演示SOP-风控模块.md,M2 tag risk-m2);阶段 B 完结;对话链路待 T-01/T-07。 +当前:**风控事件线已集成 + 测试基建完成**(交易网关 → 规则引擎 → 预警/AML/L3 → 4 API,`main.py` 路由挂载 + trace 中间件 + lifespan 完成,B1~B8;B8 = conftest 演示数据校验 + 集成测试 A-1~A-5/A-7/A-9 + `tests/_ddl.py` DDL 单一事实源;B7/B8 均经独立 AI 评审闭环;B9a = subscribe_alerts/rebuild_alerts 演示与补偿脚本;B9b = 演示走查 + B6/B7/B8 核查单①~⑥收口(disclaimer/aml scan 幂等/集成测试前置断言),209 测试绿,演示 SOP 见 docs/项目框架设计/演示SOP-风控模块.md,M2 tag risk-m2);阶段 B 完结;对话链路待 T-01/T-07。 -**本机已就位状态(2026-09-06 · 已完成上述 ①~⑤,无需重做):** `.env` 已配置(学习项目,`MYSQL_PASSWORD=123456`;NEO4J/DEEPSEEK 留空暂不影响风控);`jinrong_core` + `jinrong_agent` 已灌库(28 客户 / AML 名单 8 条 / 演示测评已刷新 / 归属同步 28 行);`python -m pytest` 208 绿(集成测试真连本机 MySQL;未灌库的机器自动 skip 集成模块,单测不受影响);本机 Redis 服务在跑(`redis://127.0.0.1:6379/0`,B9a 订阅脚本已验证)。 +**本机已就位状态(2026-09-07 更新 · 已完成上述 ①~⑤,无需重做):** `.env` 已配置(学习项目,`MYSQL_PASSWORD=123456`;NEO4J/DEEPSEEK 留空暂不影响风控);`jinrong_core` + `jinrong_agent` 已灌库(28 客户 / AML 名单 8 条 / 演示测评已刷新 / 归属同步 28 行);`python -m pytest` 209 绿(集成测试真连本机 MySQL;未灌库的机器自动 skip 集成模块,单测不受影响);本机 Redis 服务在跑(`redis://127.0.0.1:6379/0`,B9a 订阅脚本已验证)。 **本机已知坑:** `mysql.exe` 不在 PATH(位于 `C:\Program Files\MySQL\MySQL Server 8.0\bin`);`reset.ps1` 的交互式 `-p` 在自动化执行时会卡死——脚本化重灌用 `MYSQL_PWD` 环境变量传密码(交互执行不受影响,不把密码写进仓库脚本);`redis` 包 requirements 有但初始 `pip install -r` 漏装(B9a 于 2026-09-07 补装,重装环境时留意)。 diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 3e6acfd..6db5194 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -9,7 +9,7 @@ **项目是什么:** 金融四 Agent(客户财富 / 代理人 / 数据分析 / 风控)共用数据层与合规底座;**不**互调 LLM,跨 Agent 走 L1/L2/L3 画像与预警表。 -**当前进度:** 需求与表设计已定 · **风控模块 B1~B9b 全部完成(M2 已达成,tag risk-m2)**(规则/预警聚合/L3/AML/引擎/交易网关/鉴权+4 API/适当性校验/main 集成/B8 测试基建/B9a 订阅+重放脚本/B9b 演示走查+核查单①~⑥收口,B7/B8/B9a 均经独立 AI 评审闭环,**208 测试绿**)· 阶段 B 完结剩 M2 文字复核 → **JWT(T-01) / 审计中间件(T-02) / LangGraph 对话线(T-07) 未做**(chat/knowledge/admin 仍空壳)。**开发在分支 `feature/risk`(未合入 main)。** +**当前进度:** 需求与表设计已定 · **风控模块 B1~B9b 全部完成(M2 已达成,tag risk-m2)**(规则/预警聚合/L3/AML/引擎/交易网关/鉴权+4 API/适当性校验/main 集成/B8 测试基建/B9a 订阅+重放脚本/B9b 演示走查+核查单①~⑥收口,B7/B8/B9a 均经独立 AI 评审闭环,**209 测试绿**)· 阶段 B 完结剩 M2 文字复核 → **JWT(T-01) / 审计中间件(T-02) / LangGraph 对话线(T-07) 未做**(chat/knowledge/admin 仍空壳)。**开发在分支 `feature/risk`(未合入 main)。** **仓库地图:** @@ -28,7 +28,7 @@ | `scripts/core/*.sql` + `reset.ps1` | **已实现** | Core 模拟库 DDL + 种子 | | `scripts/agent/` `scripts/demo/` | **已实现** | AML 名单种子 + 风控演示数据 + subscribe_alerts/rebuild_alerts(B9a 订阅与幂等重放补偿) | | `scripts/sync/*.py` | **已实现** | 归属同步 + Neo4j 全图 | -| `tests/` | **已实现** | 16 个测试文件 196 用例(sqlite 隔离;DDL 单一事实源 `_ddl.py`;`test_integration_risk.py` 走真 MySQL + TRD-TEST- 前缀隔离) | +| `tests/` | **已实现** | 17 个测试文件 209 用例(sqlite 隔离;DDL 单一事实源 `_ddl.py`;`test_integration_risk.py` 走真 MySQL + TRD-TEST- 前缀隔离) | | `docs/需求拆解/` | 已定 | 场景 P0、矩阵、合规原文 | | `docs/PRD/PRD-风控监测Agent.md` | **已冻结** | 风控 PRD v1.0 + 规则表附录 | | `docs/项目框架设计/表设计/` | 已定 | Agent 共用 11 表 + agent 专用 SQL | @@ -166,6 +166,6 @@ RBAC 联调账号:scripts/dev/rbac-seed-reference.md 2. 改动属于 api / service / tool / repository 哪一层? 3. 是否需 customer_id 归属与 JWT RBAC? 4. Core 是模拟库只读还是 agent 库读写? -5. 如何验证?(`python -m pytest` 全量(当前 208 绿)· uvicorn 启动 + /health · SQL / sync 脚本 · 对照 REQUIREMENTS 验收列) +5. 如何验证?(`python -m pytest` 全量(当前 209 绿)· uvicorn 启动 + /health · SQL / sync 脚本 · 对照 REQUIREMENTS 验收列) 大任务:FRAMEWORK/FLOW 与实现状态不符时先更新 memory 再编码(用户确认跳过除外)。 diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index c39eb83..d7412ad 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -20,8 +20,8 @@ ### 风控模块(PRD v1.0 已冻结 · `docs/PRD/PRD-风控监测Agent.md`,事件驱动线不依赖 T-07 可先行) -- [ ] T-30 风控事件线:`app/gateway/` 交易网关 + `service/risk/` 规则引擎(RISK-001~005)+ 预警单聚合 + L3 最小写入 + `risk:pub:alert` 推送 + AML(含 `risk_aml_list` 种子)+ 演示数据脚本验收 A-1~A-5/A-9 —— **进度(2026-09-06):B1 规则纯函数 / B2 预警服务 / B3 L3 写入(risk_score 一期不写)/ B4 AML+引擎编排 / B5 交易网关 / B6 鉴权+4 API+处置编排 / B7 main 集成(路由挂载 + trace 中间件 + lifespan 启动期 debug 校验;挂账①锁公共化 ②L3 缓存 DEL ③死代码 ④统一错误体 ⑤启动校验 ⑥引擎工厂 ⑦处置原子事务均落地;⑧ input_guard_log 双写仍随 T-02)均完成并经独立 AI 评审闭环(B7 复审有条件通过→P1-1 dispose 已修闭环;183 测试绿;P2/P3 已登记开发计划 B7/B8/B9b/T-02);**B8 已完成(2026-09-06):conftest(演示数据校验/幂等代跑 prepare_risk_demo/TRD-TEST- teardown + L3 快照还原)+ 集成测试 11 例(A-1~A-5/A-7/A-9 + trace 一致性 + 审计 JSON)+ sqlite DDL 单一事实源 `_ddl.py`(10 文件收敛,localtime 时区收敛 B5 P3-4)+ L3 DEL 行为断言(P2-1)+ locks 文案(P3-2);B8 复审有条件通过→P1-1 conftest skip 路径炸收集已修闭环(故障注入验证),P2-1 演示库耦合前置断言挂 B9b 核查单⑥**;**B9a 已完成(2026-09-07):`scripts/demo/subscribe_alerts.py`(risk:pub:alert 订阅演示:连接自检/--duration/心跳)+ `rebuild_alerts.py`(按 trade_id 幂等重放:`find_alerts_by_trade` 以 payload LIKE 查已入单(含 aml/已处置单)防重复出单与重复 append;engine_error 中断笔 skip 时 warning 提示人工核对;core_ro 新增只读 `get_trade_by_id`)+ tests/test_demo_scripts.py 8 例;真库手工验证通过(rebuilt→skipped→missing exit1;订阅端到端收到假消息+真交易推送且 trace 贯通;中间态警示路径复验),验证现场已清理;独立 AI 评审有条件通过→P2-1 中间态警示/P2-2 非首笔幂等用例已闭环**;**B9b 已完成(2026-09-07):前置代码三件(①预警类 API 固定 disclaimer ②scan_all 幂等防护 `find_recent_aml_alert`+`skipped_existing` ③核查单⑥集成测试前置断言 A-1/A-3/A-4 清洁检查+A-5 按 trade_id 定位),208 绿;演示 SOP `docs/项目框架设计/演示SOP-风控模块.md`;真服务走查(uvicorn+真库)A-1~A-5/A-7/A-9+核查单①②④⑤ 全 PASS(11/11,A-6 归 M3),核查单⑥防护现场复现验证;走查演示数据按 SOP 保留、pytest 集成前需重灌;M2 达成打 tag risk-m2(M1 未打 tag 留痕)——核查单①~⑥ 全部收口,风控阶段 B 完结** -- [ ] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` + 单测验收 A-8 —— **代码已完成(A1~A4 · 77 测试绿;B6 起该 API 带鉴权+直调审计);MySQL 手工 SQL 对照挂账至 B9b 执行(阶段 A 评审 P2-8)** +- [ ] T-30 风控事件线:`app/gateway/` 交易网关 + `service/risk/` 规则引擎(RISK-001~005)+ 预警单聚合 + L3 最小写入 + `risk:pub:alert` 推送 + AML(含 `risk_aml_list` 种子)+ 演示数据脚本验收 A-1~A-5/A-9 —— **进度(2026-09-06):B1 规则纯函数 / B2 预警服务 / B3 L3 写入(risk_score 一期不写)/ B4 AML+引擎编排 / B5 交易网关 / B6 鉴权+4 API+处置编排 / B7 main 集成(路由挂载 + trace 中间件 + lifespan 启动期 debug 校验;挂账①锁公共化 ②L3 缓存 DEL ③死代码 ④统一错误体 ⑤启动校验 ⑥引擎工厂 ⑦处置原子事务均落地;⑧ input_guard_log 双写仍随 T-02)均完成并经独立 AI 评审闭环(B7 复审有条件通过→P1-1 dispose 已修闭环;183 测试绿;P2/P3 已登记开发计划 B7/B8/B9b/T-02);**B8 已完成(2026-09-06):conftest(演示数据校验/幂等代跑 prepare_risk_demo/TRD-TEST- teardown + L3 快照还原)+ 集成测试 11 例(A-1~A-5/A-7/A-9 + trace 一致性 + 审计 JSON)+ sqlite DDL 单一事实源 `_ddl.py`(10 文件收敛,localtime 时区收敛 B5 P3-4)+ L3 DEL 行为断言(P2-1)+ locks 文案(P3-2);B8 复审有条件通过→P1-1 conftest skip 路径炸收集已修闭环(故障注入验证),P2-1 演示库耦合前置断言挂 B9b 核查单⑥**;**B9a 已完成(2026-09-07):`scripts/demo/subscribe_alerts.py`(risk:pub:alert 订阅演示:连接自检/--duration/心跳)+ `rebuild_alerts.py`(按 trade_id 幂等重放:`find_alerts_by_trade` 以 payload LIKE 查已入单(含 aml/已处置单)防重复出单与重复 append;engine_error 中断笔 skip 时 warning 提示人工核对;core_ro 新增只读 `get_trade_by_id`)+ tests/test_demo_scripts.py 8 例;真库手工验证通过(rebuilt→skipped→missing exit1;订阅端到端收到假消息+真交易推送且 trace 贯通;中间态警示路径复验),验证现场已清理;独立 AI 评审有条件通过→P2-1 中间态警示/P2-2 非首笔幂等用例已闭环**;**B9b 已完成(2026-09-07):前置代码三件(①预警类 API 固定 disclaimer ②scan_all 幂等防护 `find_recent_aml_alert`+`skipped_existing` ③核查单⑥集成测试前置断言 A-1/A-3/A-4 清洁检查+A-5 按 trade_id 定位),208 绿;演示 SOP `docs/项目框架设计/演示SOP-风控模块.md`;真服务走查(uvicorn+真库)A-1~A-5/A-7/A-9+核查单①②④⑤ 全 PASS(11/11,A-6 归 M3),核查单⑥防护现场复现验证;走查演示数据按 SOP 保留、pytest 集成前需重灌;M2 达成打 tag risk-m2(M1 未打 tag 留痕)——核查单①~⑥ 全部收口,风控阶段 B 完结**;**B9b 复审有条件通过→闭环(2026-09-07):P2-1 SOP 交互重灌补 DROP agent / P2-2 补做 T-31 名下 P2-8 手工 SQL 对照收口 / P2-3 走查范围与 11/11 口径统一;P3 落地:A-4/A-9 COUNT 当日窗、已处置单幂等用例、suitability/check 阻断响应补 G-08 两要素、记忆文档数字同步;留痕:scan_all 幂等读后写无锁(演示场景可接受,批量时接 run_locked),209 绿** +- [ ] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` + 单测验收 A-8 —— **代码已完成(A1~A4 · 77 测试绿;B6 起该 API 带鉴权+直调审计;B9b 复审补 G-08 两要素 advice/notice);MySQL 手工 SQL 对照(阶段 A 评审 P2-8)已于 2026-09-07 收口:4 组演示组合真库 SQL 推导与函数判定逐项一致(登记演示 SOP 核查单附行)** - [ ] T-32 预警台账与人工处置 API(`GET /alerts`、`POST /handle`,risk_officer/compliance 权限)+ 对话线(依赖 T-01/T-03/T-07)验收 A-6/A-7 —— **API 部分已由 B6 覆盖并复审通过(A-7/A-9 用例绿);对话线仍依赖 T-01/T-03/T-07** ## 已完成 diff --git a/docs/项目框架设计/开发计划-风控模块.md b/docs/项目框架设计/开发计划-风控模块.md index 5a1df69..ba3a5aa 100644 --- a/docs/项目框架设计/开发计划-风控模块.md +++ b/docs/项目框架设计/开发计划-风控模块.md @@ -41,6 +41,8 @@ **B9b 完成(2026-09-07):** 走查前置代码三件:①预警类 API(GET alerts/POST handle/POST aml/scan)响应体加固定 `disclaimer`(核查单①)②`scan_all` 幂等防护——新增 `find_recent_aml_alert`,同客户当日已有 aml 单(含交易触发/已处置)不再出单,summary 增 `skipped_existing`(核查单②)③集成测试前置断言(核查单⑥):A-1/A-3/A-4 清洁检查(交叉时 fail+重灌提示)、A-5 aml 单按本笔 trade_id 定位。208 测试绿(+4:scan 幂等×2/disclaimer/交易触发后 scan skip)。演示 SOP 落地 `docs/项目框架设计/演示SOP-风控模块.md`(含脚本化重灌 MYSQL_PWD 方式)。真服务走查(uvicorn 8123 + 真库):**A-1~A-5/A-7/A-9 + 核查单①②④⑤ 全 PASS(11/11)**,核查单⑥防护价值现场复现(走查残留数据致 A-1 定位失效 → 重灌后过,正是 P2-1 预警的场景)。核查单③=本次真服务手测(区别于 B6 TestClient 等价验证);⑤本机 APP_ENV=development 确认。走查演示数据按 SOP 保留,pytest 集成测试前需重灌。**里程碑 M2 达成(阶段 B 全链路 + A-1~A-5/A-7~A-9 验收),commit 打 tag risk-m2(M1 时点未打 tag,留痕)**。 +**B9b 复审(独立 AI 评审 · 2026-09-07):有条件通过 → 已闭环。** 核查单②幂等口径(不限 status/按日/覆盖交易触发单)、①disclaimer 落点三处与文案逐字、⑥前置断言与聚合锚点口径一致(LIKE 为保守超集 fail-closed)、红线全守、SOP 与 PRD §8 逐项一致,均在审查中证实。闭环三项(全为文档/账务):P2-1 SOP 交互方式重灌补 `DROP DATABASE jinrong_agent`(reset.ps1 只重建 core + 裸 CREATE TABLE 会 1050 中止);P2-2 T-31 名下阶段 A 挂账 P2-8 遗漏——已补做收口(4 组演示组合真库手工 SQL 推导与函数判定逐项一致,登记 SOP 核查单附行);P2-3 SOP 走查范围与 11/11 口径统一(A-6 归 M3、A-8 由单测矩阵 + P2-8 对照覆盖,§4 补映射交代)。P3 落地:A-4/A-9 COUNT 断言加当日窗(跨日残留不再裸失败)、scan→handle→再 scan 幂等用例(防 status 过滤回归)、suitability/check 阻断响应补 G-08 两要素 advice/notice(PRD §6 阻断类口径)、MEMORY tests 行/FLOW 日期戳同步。**留痕(P3-2):scan_all 幂等为读后写(无锁无约束),手动单发演示场景可接受;接定时批量/多操作员时以 run_locked 包住 check+insert**。209 测试绿。 + ## 阶段 C · 对话线(依赖 Wave 0 的 T-01 JWT / T-03 输入防护 / T-07 LangGraph) | # | 任务 | 产出 | 验证方式 | 依赖 | diff --git a/docs/项目框架设计/演示SOP-风控模块.md b/docs/项目框架设计/演示SOP-风控模块.md index 120f1f8..c706aab 100644 --- a/docs/项目框架设计/演示SOP-风控模块.md +++ b/docs/项目框架设计/演示SOP-风控模块.md @@ -1,8 +1,8 @@ # 演示 SOP · 风控监测 Agent(B9b) -> 版本:v1.0 · 2026-09-07 · 配套 PRD v1.0 §8 验收表 + 开发计划 B9b 核查单 +> 版本:v1.1 · 2026-09-07 · 配套 PRD v1.0 §8 验收表 + 开发计划 B9b 核查单 > 适用:演示机/本机(`app_env=development`,debug 鉴权头可用;生产部署禁用,见核查单⑤) -> 首次走查记录:2026-09-07,A-1~A-5/A-7~A-9 + 核查单①~⑥ 全部通过(11/11 PASS) +> 首次走查记录:2026-09-07,A-1~A-5/A-7/A-9(A-6 归 M3 对话线、A-8 由单测矩阵 + P2-8 手工 SQL 对照覆盖)+ 核查单①~⑥ 全部通过(11/11 PASS) --- @@ -33,8 +33,11 @@ done python scripts/sync/sync_advisor_rel.py # 归属同步(28 行) ``` -**交互方式**:`.\scripts\core\reset.ps1`(PowerShell,交互输密码;`-SkipNeo4j` 可跳过 Neo4j 同步), -再手动执行上表后 4 个 SQL(agent 库建表 ×2 / seed-aml-list / prepare_risk_demo)。 +**交互方式**:`.\scripts\core\reset.ps1`(PowerShell,交互输密码;`-SkipNeo4j` 可跳过 Neo4j 同步)。 +注意:reset.ps1 **只重建 jinrong_core**,且 agent 建表 SQL 是裸 `CREATE TABLE`—— +交互方式需先手动 `DROP DATABASE IF EXISTS jinrong_agent;` 再跑后 4 个 SQL,否则 +ERROR 1050(表已存在)中止且残留未清。脚本化方式已含双库 DROP,可照抄。 +(两种方式均不含 Neo4j 同步;风控演示不依赖 Neo4j,需要时手动跑 `scripts/sync/sync_neo4j.py`。) **重灌完成标志**:`seed-aml-list.sql` 输出 8 条名单;`prepare_risk_demo.sql` 输出 7 行测评 (6 行 90 天、CUST-1004 保持过期);`sync_advisor_rel` 输出 upserted 28 rows。 @@ -70,16 +73,20 @@ Swagger 逐条执行;预期响应与库内核验点如下(`trade_id` 以实 python scripts/demo/rebuild_alerts.py # 补偿重放:rebuilt → 再跑 skipped → 不存在 exit 1 ``` +**A-8 映射交代**:`suitability_check` 单元级验收不在 Swagger 走查——由单测矩阵(25 组合 + 边界, +`tests/test_suitability.py`)与真库手工 SQL 对照(P2-8 收口:4 组演示组合 SQL 推导与函数判定一致)覆盖。 + ## 5. 核查单(B6/B7/B8 挂账收口) | # | 挂账来源 | 检查项 | 结论(2026-09-07 走查) | | --- | --- | --- | --- | | ① | B6 复审 P3-7 | 预警类 API(GET alerts / POST handle / POST aml/scan)响应体含固定 `disclaimer`「本预警由系统自动生成,最终判定需经风控专员人工审核。」 | ✅ 已实现并真服务验证 | | ② | B6 评审 P3-6 | aml/scan 幂等:同客户当日已有 aml 单(含交易触发/已处置)不重复出单,响应 `skipped_existing` 指向既有单 | ✅ 已实现并真服务验证 | -| ③ | B6 复审遗漏⑤ | 真服务手测(区别于 TestClient 等价验证):uvicorn 真库走查 A-1~A-9 全量 | ✅ 本 SOP §4 即该手测 | +| ③ | B6 复审遗漏⑤ | 真服务手测(区别于 TestClient 等价验证):uvicorn 真库走查 A-1~A-5/A-7/A-9(A-6 归 M3、A-8 见 §4 映射交代) | ✅ 本 SOP §4 即该手测 | | ④ | B6 复审观察③ | analyst 角色台账 fail-closed:`GET /api/risk/alerts` → 403(risk:alert:read 权限扩展待 Wave 3 分析 Agent 时定矩阵) | ✅ 403 保持 | | ⑤ | B7 复审 P3-3 | 生产/演示机误配检查:本机 `.env` `APP_ENV=development`(演示机允许 debug 头);`app_env != development` 时启动期拒绝 debug 依赖(单测覆盖),部署非 dev 机时必须核对 | ✅ 本机=development 已确认 | -| ⑥ | B8 复审 P2-1 | 集成测试对演示库当日状态的隐式耦合 → 已加前置断言(A-1/A-3/A-4 清洁检查 fail+提示、A-5 按 trade_id 定位),代码随 B9b 落地 | ✅ 已落地;交叉数据在场时 pytest 显式 fail 提示重灌 | +| ⑥ | B8 复审 P2-1 | 集成测试对演示库当日状态的隐式耦合 → 已加前置断言(A-1/A-3/A-4 清洁检查 fail+提示、A-5 按 trade_id 定位、A-4/A-9 COUNT 断言加当日窗),代码随 B9b 落地 | ✅ 已落地;交叉数据在场时 pytest 显式 fail 提示重灌 | +| 附 | 阶段 A 评审 P2-8(T-31 名下) | `suitability_check` 真库手工 SQL 对照:4 组演示组合(SUIT-001 首因 / 70 岁封顶 / 等级匹配 / SUIT-008 过期)SQL 推导与函数判定逐项一致 | ✅ 2026-09-07 收口 | ## 6. 走查后状态与注意事项 diff --git a/tests/test_aml_service.py b/tests/test_aml_service.py index f2a1423..ba4d7e6 100644 --- a/tests/test_aml_service.py +++ b/tests/test_aml_service.py @@ -188,6 +188,21 @@ def test_scan_all_idempotent_same_day(env): assert len(pub.messages) == 2 # 重扫无新推送(出单时的推送各一次) +def test_scan_skips_disposed_alert_same_day(env): + """幂等不限 status(评审 P3-3):scan 出单→人工处置→再 scan 仍 skip, + 防止 SQL 回归成 status='pending_review' 过滤后重扫对已处置客户重复出单。""" + from app.service.risk.alert_service import handle_alert + + core, repo, pub = env + first = scan_all(core_ro=core, risk_repo=repo) + aid = first["alerts"][0] + handle_alert(aid, "confirmed_normal", "STAFF-R1", risk_repo=repo) + second = scan_all(core_ro=core, risk_repo=repo) + assert second["alerts"] == [] + assert sorted(second["skipped_existing"]) == sorted(first["alerts"]) + assert len(pub.messages) == 2 + + def test_scan_skips_customer_already_alerted_by_trade(env): """当日交易触发已出 aml 单的客户,scan 不再重复出单(同日命中留痕一次)。""" from decimal import Decimal diff --git a/tests/test_integration_risk.py b/tests/test_integration_risk.py index bab3e21..c09ab52 100644 --- a/tests/test_integration_risk.py +++ b/tests/test_integration_risk.py @@ -301,7 +301,7 @@ def test_a4_freq_trade_merged_into_same_alert(iclient, risk_demo_env): alerts = _one( env, "SELECT COUNT(*) AS n FROM risk_alert WHERE customer_id = 'CUST-9527'" - " AND alert_type = 'freq_trade'", + " AND alert_type = 'freq_trade' AND created_at >= CURDATE()", ) assert alerts["n"] == 1 row = _one(env, "SELECT payload FROM risk_alert WHERE alert_id = :a", a=alert_id) @@ -414,7 +414,7 @@ def test_a9_cross_customer_and_unassigned_advisor_403(iclient, risk_demo_env): denials = _one( env, "SELECT COUNT(*) AS n FROM audit_log WHERE event_type = 'authz' AND decision = 'forbidden'" - " AND actor_id IN ('CUST-1002', 'STAFF-10087')", + " AND actor_id IN ('CUST-1002', 'STAFF-10087') AND created_at >= CURDATE()", ) assert denials["n"] == 2