Files
group_xinghuo_jinrong/docs/项目框架设计/演示SOP-风控模块.md
T
GaoYiYuan_0626 323d98bdc2 docs: C4~C6 文档落账同步(勾 TODO/MEMORY/交接文档 + 演示 SOP A-12 + 02-redis-keys C6 联动)
- TODO/MEMORY/交接文档:C4/C5/C6 全部标记完成,基线更新为 482 绿(406+30+17+17+12)
- 演示 SOP 补 A-12 代理人行为链走查步骤(debug 头透传 actor + cron 扫描脚本)
- 02-redis-keys.md §2.6 C6 联动清单(代理人维度独立出单线/推送/去重口径)
2026-09-07 19:57:52 +08:00

113 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 演示 SOP · 风控监测 Agent(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(A-6 归 M3 对话线、A-8 由单测矩阵 + P2-8 手工 SQL 对照覆盖)+ 核查单①~⑥ 全部通过(11/11 PASS)
---
## 1. 前置条件
- 本机 bootstrap ①~⑤ 已执行(`.env`、依赖、双库、种子)——见 `docs/memory/FLOW.md` §0;
已就位机器勿重跑,只按本 SOP §2 重灌即可。
- `redis` 包已安装(requirements 内;B9a 曾发现环境漏装,重装环境时留意)。
- 本机 MySQL 可达、Redis 服务在跑(订阅演示需要;不可达时推送降级仅日志,走查其余项不受影响)。
## 2. 演示链路重灌(每次演示/走查前执行)
**目的**:清空上一轮演示/走查残留(trade/预警/审计/L3),使验收断言从干净状态出发。
**脚本化方式(推荐,自动化执行不卡交互)**——Git Bash:
```bash
export MYSQL_PWD=<密码>
MYSQL="C:/Program Files/MySQL/MySQL Server 8.0/bin/mysql.exe" # mysql.exe 不在 PATH(FLOW §0 坑)
"$MYSQL" -h 127.0.0.1 -u root -e "DROP DATABASE IF EXISTS jinrong_core; DROP DATABASE IF EXISTS jinrong_agent;"
for f in scripts/core/00-create-database.sql scripts/core/01-ddl.sql scripts/core/02-seed-base.sql \
scripts/core/03-seed-customers.sql scripts/core/04-seed-holdings.sql scripts/core/05-seed-trades.sql \
scripts/core/06-seed-nav.sql "docs/项目框架设计/表设计/01-mysql-共用底座.sql" \
"docs/项目框架设计/表设计/02-mysql-agent专用.sql" scripts/agent/seed-aml-list.sql \
scripts/demo/prepare_risk_demo.sql; do
"$MYSQL" -h 127.0.0.1 -u root --default-character-set=utf8mb4 < "$f"
done
python scripts/sync/sync_advisor_rel.py # 归属同步(28 行)
```
**交互方式**:`.\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 天前 / expires_at 剩余 275 天,CUST-1004 保持过期演示 FM-03);`sync_advisor_rel` 输出 upserted 33 rows(33 客户口径,AL-01 起)。
## 3. 启动与入口
```bash
uvicorn app.main:app --reload # Swagger: http://127.0.0.1:8000/docs
python scripts/demo/subscribe_alerts.py # 另开终端:订阅预警推送(Ctrl+C 退出)
```
debug 鉴权头(dev 兜底通道,T-01 已接 JWT):`X-Debug-Role` + `X-Debug-Actor`。
走查角色:`risk_officer`(STAFF-90001) / `compliance`(STAFF-40001) / `customer`(CUST-1001/CUST-1002)
/ `advisor`(STAFF-10087,名下无 CUST-3001) / `risk_demo`(STAFF-DEMO,simulate 白名单)。
JWT 通道(T-01,可选演示):`python scripts/dev/issue_dev_token.py --sub STAFF-30001 --roles risk_officer`
签发后请求带 `Authorization: Bearer <token>` + `X-Agent-Type: risk`(JWT 通道强制 X-Agent-Type
交叉校验;debug 头与 JWT 不可同时生效时 JWT 优先)。
## 4. 验收走查(PRD §8 · A-6 归 M3 对话线)
Swagger 逐条执行;预期响应与库内核验点如下(`trade_id` 以实际响应为准)。
| # | 操作(Swagger) | 预期 |
| --- | --- | --- |
| A-1 | `POST /api/simulate/trade`,customer 头 CUST-1001,body:CUST-1001 / PROD-161725 / subscribe / 10000 | `blocked=true`,`block_response_code=SUIT_RISK_MISMATCH`(AL-05 起 main 契约机器码,SUIT-001 退役);库核验:`core_trade` 无该 trade_id;`risk_suitability_log.is_blocked=1`;`risk_alert`(trade_id 定位)suitability/pending;audit `suitability_blocked`(agent_type=platform);订阅端收到推送且 trace 与响应头一致 |
| A-2 | 同上,risk_demo 头,CUST-4001 / PROD-161725 / subscribe / 20000 | `blocked=true`,`block_response_code=SUIT_AGE_CONFIRM`(AL-05 起 FM-01 网点确认替换 SUIT-006 封顶:70 岁买 R4≥R3 一律阻断待网点当面确认),`needs_branch_confirm=true` |
| A-3 | 同上,CUST-3001 / PROD-510300 / subscribe / 500000 | `blocked=false`,`triggered_rules=["RISK-001","RISK-002"]`,预警单 score=70/pending;`core_trade` 落库;audit `trade_accepted`;**订阅端实时打印推送** |
| A-4 | 同上,CUST-9527 / PROD-510300 / subscribe / 1000,连发 4 笔 | 第 3 笔触发 RISK-003;第 4 笔 `alert_ids` 与第 3 笔相同(并入同单),该客户 freq 类单数=1,payload.events 长度=2 |
| A-5 | 同上,CUST-1002 / PROD-005828 / subscribe / 100 | `aml_hit=true`;aml 独立单 score=95(按本笔 trade_id 定位);L3 `monitor_tier=high`;compliance 头 `GET /api/risk/alerts` 仅见 aml 单且含该单;`core_customer.is_active` 仍=1(不冻户) |
| A-7 | `POST /api/risk/alerts/{A-3的alert_id}/handle`,officer 头,`confirmed_suspicious` → 再处置一次 → 换 compliance 头再试 | 200(状态 confirmed_suspicious,handler_id=STAFF-90001)→ 409 STATE_CONFLICT → 403;audit 留痕(alert_handle/alert_handled) |
| A-9 | `POST /api/risk/suitability/check`:customer CUST-1002 查 CUST-3001;advisor STAFF-10087 查 CUST-3001 | 403 `AUTH_403_NOT_OWNER` / `AUTH_403_NOT_ASSIGNED`;audit `event_type=authz, decision=forbidden` 各 1 条 |
| A-12 | 代理人行为链(RISK-008,C6):① `POST /api/simulate/trade` 用代理人 actor(debug 头 `X-Debug-Role=advisor` `X-Debug-Actor=STAFF-10087`)对同一客户发起 3 组「赎回某产品 → 2h 内申购不同产品」(trade_id 经 `input_summary.trade_id` 落 audit_log,`actor_id` 透传发起人);② 另开:advisor 查非本人客户 5 次 → `AUTH_403_SCOPE`(条件 B),或 10 次 → `AUTH_403_NOT_OWNER`(条件 C) | ① 跑 `python scripts/cron/agent_behavior_scan.py` → 出 `agent_behavior` pattern 单(payload.actor_id=STAFF-10087,risk_score=75,triggered_rules=["RISK-008"],同日同代理人一张单);② 对话线 risk_officer 调 `query_agent_behavior` 返回该单(customers 经 mask_name 脱敏);audit `agent_behavior_detected`(仅 INSERT) |
**B9a 脚本演示(运维位)**:
```bash
python scripts/demo/rebuild_alerts.py <trade_id> # 补偿重放:rebuilt → 再跑 skipped → 不存在 exit 1
```
**A-8 映射交代**:`suitability_check` 单元级验收不在 Swagger 走查——由单测矩阵(25 组合 + 边界,
`tests/test_suitability.py`)与真库手工 SQL 对照(P2-8 收口:4 组演示组合 SQL 推导与函数判定一致)覆盖。
**A-12 脚本演示(代理人行为链 · C6)**:
```bash
python scripts/cron/agent_behavior_scan.py # 扫描 audit_log 三条件,命中出 agent_behavior 单并打印 JSON
```
> 前置:A-12 ① 的 3 组赎回+申购须由**同一代理人 actor** 发起(debug 头 `X-Debug-Actor` 透传),否则
> actor==customer / SYSTEM 会被排除(条件 A 不触发);条件 B/C 的越权拒绝须 `decision=forbidden` 且
> `input_summary.code` 命中对应 AUTH_403_* 族。出单后 `risk:pub:alert` 推送 `risk_officer` + `risk_manager`
> (经理仅阅知、无处置权),`query_agent_behavior` 工具按 `agent_id` 过滤、客户脱敏。
## 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-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 定位、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. 走查后状态与注意事项
- 走查产生的交易/预警/审计/L3 **保留在演示库**(演示痕迹即演示资产);再次演示前回到 §2 重灌。
- **重灌后才能跑 pytest 集成测试**:核查单⑥前置断言会对当日演示数据显式 fail(防交叉失真)。
- B9a subscribe 窗口未开时推送不丢失告警——Redis Pub/Sub 无持久化,演示时先开订阅再发交易。
- A-6(对话线验收)归 M3(T-01/T-03/T-07 就绪后补)。