2026-09-06 15:09:13 +08:00
|
|
|
|
# PRD · 风控监测 Agent 模块
|
|
|
|
|
|
|
2026-09-07 12:46:59 +08:00
|
|
|
|
> 版本:v1.1(**已冻结部分 v1.0 原文不动** · 2026-09-07 追加 FR-8/FR-9/FR-10 并入,追加内容经独立 AI 评审→修订→复审通过)
|
|
|
|
|
|
> 日期:2026-09-06(v1.0 冻结)· 2026-09-07(v1.1 追加并入)
|
2026-09-07 18:01:16 +08:00
|
|
|
|
> 负责人:E(风控/记忆/测试)· 分支 `risk-control-agent`(旧称 feature/risk 已过时)
|
2026-09-06 15:09:13 +08:00
|
|
|
|
> 上游依据:`docs/需求拆解/Agent风险与合规约束汇总.md` §5、`docs/memory/REQUIREMENTS.md` Wave 2、`docs/memory/FRAMEWORK.md`、`docs/项目框架设计/表设计/`(表结构 · Redis key)、规则冻结版 `docs/PRD/附-风控规则表.md`
|
2026-09-07 12:46:59 +08:00
|
|
|
|
> 状态流转:草稿 → AI 评审 → 修订 v0.2 → 复审 → 修订 v0.3 → **用户确认冻结 v1.0** → 需求追加 v1.1 独立评审不通过(P0×4)→ 修订 v1.2 → 复审通过 → **并入 v1.1**
|
|
|
|
|
|
> 修订记录:v0.2 修复首轮评审 P0×2、P1×9、P2×10;v0.3 修复复审 P1×3 与 P2×3;v1.0 用户确认三个新增项(risk_aml_list 表、STAFF-90001 演示账号、scripts/demo/prepare_risk_demo.sql),§13 文档联动修正已执行;**v1.1 追加 FR-8(RISK-006 集中度)/ FR-9(RISK-007 时效升级)/ FR-10(RISK-008 代理人行为链)与 A-10/A-11/A-12 验收,落库映射按真实 DDL 定案(alert_type 复用 pattern + payload.alert_subtype、status 不扩枚举、payload.actor_id 承载代理人)、新增 risk_manager 上级角色(用户拍板 2026-09-07,roles JSON 承载不改表)**
|
2026-09-06 15:09:13 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 背景与目标
|
|
|
|
|
|
|
|
|
|
|
|
风控监测 Agent 是四 Agent 体系中**唯一可阻断交易请求**的模块(其余一律只监测不拦截)。当前后端仅有脚手架,本 PRD 定义风控模块从零到可演示的完整需求。
|
|
|
|
|
|
|
|
|
|
|
|
**目标(一句话)**:交易事件进来能实时预警,适当性不匹配能在交易前拦住,AML 命中能紧急通知人工,风控专员能对话查台账——全程留痕、全部待人工审核。
|
|
|
|
|
|
|
|
|
|
|
|
**本期不做**:自动冻户、自动上报监管、自动改正式风险等级 C1~C5、限制客户交易次数/金额、R-05 完整评分模型(只预留)、真实支付/TA 清算对接、"仅提示不阻断"的放行路径(R-02 一期所有不匹配一律阻断,放行+提示留二期)、AML 开户/信息变更触发(模拟环境无开户流程,仅预留事件入口定义,见 FR-5)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 已拍板的决策(不重议)
|
|
|
|
|
|
|
|
|
|
|
|
| # | 决策 | 内容 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| D1 | Agent 划分 | 以主分支新四 Agent 为准(客户财富/代理人助手/数据分析/风控监测);本地旧文档中"客服/投顾/业务操作"命名废弃 |
|
|
|
|
|
|
| D2 | 推荐边界(全局修订) | Agent **可以推荐产品并说明理由**,但:①推荐前必须过适当性校验(SUIT-007);②输出附免责声明;③标注"需经持证投顾审核";④**禁止具体操作指令**("现在买入 X 万元""我帮您下单"类表述一律拦截) |
|
|
|
|
|
|
| D3 | 冻结边界 | 任何 Agent 不得自动冻结账户;AML 命中只做"最高级预警 + 多渠道通知 + 标记待复核",处置由**风控专员**人工完成,合规官**知悉**(见 FR-5 通知机制) |
|
|
|
|
|
|
| D4 | 范围 | R-01、R-02、R-03、R-04 全做;R-05 只预留 L3 写入接口,评分模型后置 |
|
|
|
|
|
|
| D5 | 事件来源 | **方案 A:模拟交易网关**(扮演外部 Core 交易系统,独立于 Agent 分层)——交易请求先过 R-02 校验(阻断点),通过后写 `core_trade` 并触发规则引擎 |
|
|
|
|
|
|
| D6 | 模块形态 | 两条线并存:事件驱动线(无对话)+ 对话线(风控专员 chat) |
|
|
|
|
|
|
|
|
|
|
|
|
**D2 的文档联动修正**(PRD 冻结后执行,见 §13)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 系统上下文
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
┌────────────────────────────────────────────┐
|
|
|
|
|
|
│ 客户端 / 前端 │
|
|
|
|
|
|
└──────┬──────────────────────┬──────────────┘
|
|
|
|
|
|
│ 交易请求 │ 风控专员对话
|
|
|
|
|
|
▼ ▼
|
|
|
|
|
|
┌─────────────────────┐ ┌─────────────────────┐
|
|
|
|
|
|
│ 模拟交易网关(外部替身) │ │ api/chat (risk) │
|
|
|
|
|
|
│ app/api/simulate.py │ │ X-Agent-Type=risk │
|
|
|
|
|
|
│ → app/gateway/* │ └──────────┬──────────┘
|
|
|
|
|
|
└──────┬──────────────┘ │ LangGraph
|
|
|
|
|
|
│ ①R-02 适当性校验 ▼
|
|
|
|
|
|
│ ├─不匹配→阻断(不落 trade) service/agent_service
|
|
|
|
|
|
│ └─匹配→②INSERT core_trade │
|
|
|
|
|
|
▼ │
|
|
|
|
|
|
┌─────────────────────┐ │
|
|
|
|
|
|
│ 风控规则引擎 │◄────────────────┘
|
|
|
|
|
|
│ service/risk/* │ R-01/R-03/R-04
|
|
|
|
|
|
└──────┬──────────────┘
|
|
|
|
|
|
│ ③写预警单 risk_alert(单事件聚合出单)
|
|
|
|
|
|
│ ④写 risk_suitability_log
|
|
|
|
|
|
│ ⑤写 L3(取最高档合并)
|
|
|
|
|
|
│ ⑥PUBLISH risk:pub:alert
|
|
|
|
|
|
▼
|
|
|
|
|
|
┌─────────────────────┐ ┌──────────────┐
|
|
|
|
|
|
│ 人工处置 API │ │ Redis 通知 │
|
|
|
|
|
|
│ 风控专员改状态 │ │ risk:pub:alert│
|
|
|
|
|
|
└─────────────────────┘ └──────────────┘
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**角色澄清(红线自证)**:模拟交易网关是**外部 Core 交易系统的替身**(生产环境由真实交易系统回调替代),它写 `core_trade` 是扮演 L0 系统的角色,**不属于 Agent 写 Core**;Agent 侧代码依旧只读 `jinrong_core`。
|
|
|
|
|
|
|
|
|
|
|
|
**分层落地(对照 FRAMEWORK.md)**:网关独立为 `app/gateway/` 包(`trade_gateway.py` 业务 + `gateway_repository.py` **仅 INSERT core_trade**),不在 api/service/tool/repository 四层之内,声明为"模拟外部系统模块";`app/api/simulate.py` 仍为薄路由。§13 联动修正 FRAMEWORK.md 增补该分层例外说明。规则引擎、校验服务、预警处置全部遵守"repository 不写 Core"约束。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 功能需求
|
|
|
|
|
|
|
|
|
|
|
|
### FR-1 模拟交易网关(D5)
|
|
|
|
|
|
|
|
|
|
|
|
- `POST /api/simulate/trade`:入参 `customer_id, product_id, trade_type, amount`;服务端生成 `trade_id` + `trace_id`
|
|
|
|
|
|
- `trade_type` 一期仅接受 `subscribe/redeem`;**`convert` 显式拒绝(400,文案"转换交易暂不支持,请分别发起申购/赎回")**;规则引擎入口对未知 trade_type 抛错兜底(真实交易系统接入时再定义 convert 拆算规则)
|
|
|
|
|
|
- 流程:
|
|
|
|
|
|
1. 调用适当性校验服务(FR-2);**不匹配 → 返回阻断响应(HTTP 200,`blocked=true` + 阻断文案),交易不落 `core_trade`**,但落 `risk_suitability_log`(`is_blocked=1`)+ 生成 suitability 预警单(同客户+产品+日去重,见 FR-4)
|
|
|
|
|
|
2. 匹配 → INSERT `core_trade`(`trade_status='confirmed'`)→ 同步调用规则引擎处理本笔交易(FR-3)→ 返回 `blocked=false` + `trade_id`
|
|
|
|
|
|
- 阻断文案要素(G-08 + 合规 §5.3):**不匹配原因(客户等级 vs 产品等级)+ 引导"请联系持证投资顾问" + "本次请求已记录"声明**
|
|
|
|
|
|
- 鉴权:依赖 T-01 JWT。一期网关接受两类身份:①`roles` 含 `risk_demo` 的演示账号;②客户本人(`customer_id == JWT subject`,客户 JWT 方案由 T-01 提供,未就绪前仅接受 risk_demo)。演示账号方案见 §10
|
|
|
|
|
|
- 阻断/放行的全量输入输出写 `audit_log`(`agent_type='platform'`)
|
|
|
|
|
|
|
|
|
|
|
|
### FR-2 适当性校验服务(R-02 · 全系统唯一阻断点)
|
|
|
|
|
|
|
|
|
|
|
|
- 公共函数 `service/suitability.py :: suitability_check(customer_id, product_id) -> SuitabilityResult`
|
|
|
|
|
|
- 数据源(全只读,经扩展后的 `core_ro`):`core_customer_risk.risk_code`(C1~C5)、`core_customer.age`、`core_product.min_risk_code`(R1~R5)
|
|
|
|
|
|
- 规则(冻结,代码硬编码,详见附表):
|
|
|
|
|
|
- SUIT-001~005:客户 C 级 ≥ 产品 R 级方可购买(C1↔仅R1 … C5↔R1-R5)
|
|
|
|
|
|
- SUIT-006:年龄 ≥70 岁的客户按最高 C3 处理(即使测评得 C4/C5);**`age IS NULL` 时跳过本条,`reasons` 标注"年龄缺失,建议人工复核"**(不单独构成阻断)
|
|
|
|
|
|
- SUIT-008:风险测评有效期默认 **365 天**(`.env: RISK_ASSESSMENT_VALID_DAYS` 可配),`evaluated_at` 超期 → **等级矩阵照常计算(`is_matched` 不受影响),但最终 `blocked=true`**,阻断原因"风险测评已过期,请重新测评"
|
|
|
|
|
|
- 返回结构语义(两字段不冗余):`is_matched` = 纯等级矩阵结果;`blocked` = 最终是否阻断(= `NOT is_matched` **或** 测评过期);`reasons[]` 列明每条规则的判定与封顶说明(如"C4 因年龄≥70 按 C3 处理")
|
|
|
|
|
|
- `risk_suitability_log.customer_risk_level` 记录**原测评等级**(SUIT-006 封顶只进 `reasons`,不篡改落库值)
|
|
|
|
|
|
- 每次校验落 `risk_suitability_log`(含 `trace_id`、`profile_l1_version` 可空)
|
|
|
|
|
|
- **数据归属校验(G-01)**:customer → 仅本人;advisor → 经 `customer_advisor_rel` 归属校验;risk_officer → 全部;越权 403 + audit
|
|
|
|
|
|
- **复用方**:模拟交易网关(FR-1)、客户财富/代理人助手的推荐前校验(D2)、风控对话线(FR-6)
|
|
|
|
|
|
|
|
|
|
|
|
### FR-3 风控规则引擎(R-01 / R-04,事件驱动)
|
|
|
|
|
|
|
|
|
|
|
|
- 入口:网关交易落库后**进程内同步调用**(不用消息队列保证阻断演示可靠性);每个事件处理 <100ms
|
|
|
|
|
|
- 规则清单(阈值支持 `.env` 覆盖,默认值冻结):
|
|
|
|
|
|
|
|
|
|
|
|
| 规则 ID | 名称 | 触发条件 | 优先级 | 出单类型 |
|
|
|
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
|
|
|
| RISK-001 | 单笔大额 | `amount ≥ 500,000` | P0 | large_amount |
|
|
|
|
|
|
| RISK-002 | 单日累计大额 | 同一客户当日 subscribe+redeem 合计 `≥ 500,000`(含本笔) | P0 | large_amount |
|
|
|
|
|
|
| RISK-003 | 频繁交易 | 同一客户同一产品当日申赎合计 `≥ 3 笔` | P1 | freq_trade |
|
|
|
|
|
|
| RISK-004 | 接近阈值试探 | 5 分钟内 `≥3 笔` 且每笔 `≥400,000` | P1 | pattern |
|
|
|
|
|
|
| RISK-005 | 先小后大 | 当日**时间序上首次大额之前**已存在 `≥3 笔 ≤10,000`(不要求连续、中间可穿插其他金额) | P0 | pattern |
|
|
|
|
|
|
|
|
|
|
|
|
- **累计口径统一为 `core_trade` 当日流水**;`core_cash_flow` 不参与触发计算(仅作预警单 payload 上下文展示,避免与 trade 双计)
|
|
|
|
|
|
- "当日"口径:**服务器本地时区自然日(YYYYMMDD)**,以 `traded_at` 为准
|
|
|
|
|
|
- 风险评分一期为**静态映射**:aml=95、suitability=90、pattern=80、large_amount=70、freq_trade=50(R-05 时替换为动态评分)
|
|
|
|
|
|
- **单事件聚合出单**:同一笔交易命中的全部规则合并进**一张**预警单(`triggered_rules` JSON 数组、`risk_score` 取命中规则映射的最高值、`alert_type` 取分值最高规则对应的类型);详见 FR-4 去重与聚合
|
|
|
|
|
|
- 触发数据源:`core_trade` 当日流水 + `core_customer`/`core_customer_risk`(上下文)
|
|
|
|
|
|
|
|
|
|
|
|
### FR-4 预警单生命周期
|
|
|
|
|
|
|
|
|
|
|
|
- 生成:`risk_alert`,`status='pending_review'`,**`triggered_rules` 字段写规则 ID 数组**;`payload` 含脱敏交易明细、客户画像摘要(L0 事实 + 可得的 L1/L2 只读)、近 30 天交易统计、`core_cash_flow` 上下文(如有)
|
|
|
|
|
|
- **聚合与去重**:
|
|
|
|
|
|
- **事件类(large_amount/freq_trade/pattern)视作同一类**:同客户同自然日仅**一张**事件类 pending 单——`alert_type` 随当前命中分值最高的规则**动态更新**,新事件命中的规则一律追加进 `payload.events[]` 与 `triggered_rules`、`risk_score` 取 max(不按类型分单,避免类型漂移产生多单);无 pending 单则新建
|
|
|
|
|
|
- suitability:同客户+产品+自然日仅一张 pending 单(防反复重试刷单),同上追加
|
|
|
|
|
|
- aml:**独立出单不聚合**(最高级,单事件单张)
|
|
|
|
|
|
- Redis 去重键 `risk:dedup:{customer_id}:{rule_id}:{date}` 仅作规则级防重入辅助(24h TTL)
|
|
|
|
|
|
- 通知:`PUBLISH risk:pub:alert`,payload=`{alert_id, alert_type, customer_id_mask, risk_score, trace_id, notify_role}`;以 PRD 版本为准,§13 联动修正 02-redis-keys.md §2.4
|
|
|
|
|
|
- 人工处置(唯一允许改状态的角色 = 风控专员 `risk_officer`,经 JWT):
|
|
|
|
|
|
- `GET /api/risk/alerts`:参数 `status, alert_type, customer_id, start_date, end_date, page, page_size`
|
|
|
|
|
|
- `POST /api/risk/alerts/{alert_id}/handle`:`{handler_result: confirmed_normal|confirmed_suspicious|reported, handler_comment?}`;写 `handler_id/handled_at`,状态机只允许 `pending_review → 其余三种`,禁止跳改已处置单
|
|
|
|
|
|
- 处置动作全量写 `audit_log`(`agent_type='risk'`)
|
|
|
|
|
|
|
|
|
|
|
|
### FR-5 AML 名单监测(R-03)
|
|
|
|
|
|
|
|
|
|
|
|
- 新增表 `risk_aml_list`(agent 库,风控专用,DDL 见 §6.2)
|
|
|
|
|
|
- 种子数据:≥8 条假名单,其中**故意包含 1 条与种子客户 `display_name` 同名的记录**用于演示命中
|
|
|
|
|
|
- 触发时机(P0 做前两个;开户/信息变更触发因模拟环境无开户流程**本期不做**,仅在规则引擎预留 `on_customer_created/on_customer_updated` 事件入口定义):
|
|
|
|
|
|
1. **交易事件触发**:规则引擎处理每笔交易时,比对该客户姓名(同步,<100ms)
|
|
|
|
|
|
2. **手动全量扫描**:`POST /api/risk/aml/scan`(模拟每日批量;P1 起挂定时任务)
|
|
|
|
|
|
- **匹配算法(一期降级)**:仅 `display_name` 归一化(去空格、大小写折叠)+ 相似度 ≥ `match_threshold`(默认 0.85);表结构保留 `id_no`/`bank_card_no` 字段,**证件/银行卡匹配待 Core 提供证件数据后启用**(当前 `core_customer` 无证件字段,不修改 Core 表结构)
|
|
|
|
|
|
- 命中动作:生成 `alert_type='aml'` 独立预警单(risk_score=95,每事件一张)+ Redis 紧急推送(`notify_role=["risk_officer","compliance"]`)+ **标记客户 L3 `monitor_tier='high'` + `monitor_tags` 追加 `"aml_hit_pending_review"`**;**不冻结、不自动上报**
|
|
|
|
|
|
- **合规官知悉路径**:①`risk:pub:alert` payload 带 `notify_role`,风控工作台按角色过滤展示;②compliance 账号(STAFF-40001/40002)登录预警列表可见 aml 类型预警单(只读)
|
|
|
|
|
|
- 命中记录入 `risk_alert.payload`(含名单类型、匹配字段、相似度、名单版本),全量审计
|
|
|
|
|
|
|
|
|
|
|
|
### FR-6 对话线(风控专员 chat)
|
|
|
|
|
|
|
|
|
|
|
|
- 入口:`POST /api/chat`,`X-Agent-Type=risk`,JWT 角色 `risk_officer`;LangGraph StateGraph + DeepSeek(依赖 T-01/T-03/T-07)
|
|
|
|
|
|
- Tool 节点(只读 + 专用查询):
|
|
|
|
|
|
1. 查预警台账(按状态/类型/客户/日期统计,含"今日新增/待审数",复用 FR-4 查询参数)
|
|
|
|
|
|
2. 查客户风险上下文:L0(只读 core_ro)+ L1/L2(只读画像表)+ L3(自有)+ 近 30 天交易摘要
|
|
|
|
|
|
3. 发起适当性校验并解读结果(调 FR-2)
|
|
|
|
|
|
4. AML 名单查询(某客户是否命中过)
|
|
|
|
|
|
- **对话线只读不处置**:改预警状态必须走 FR-4 结构化 API,对话中输出"处置建议"但不执行——防止 LLM 误操作,审计口径清晰
|
|
|
|
|
|
- 输出规范:引用预警单必须带 `alert_id`;两级文案——**面向专员的对话输出不含 G-08 客户免责声明,但评分/分层必须标注"仅供参考,不自动决策"**;系统级预警 `disclaimer`("本预警由系统自动生成,最终判定需经风控专员人工审核")固定出现在预警 API 响应体中,两者是不同文案、各自适用
|
|
|
|
|
|
|
|
|
|
|
|
### FR-7 L3 画像写入(R-05 预留接口)
|
|
|
|
|
|
|
|
|
|
|
|
- 本期仅实现最小写入:预警单生成时 UPSERT `customer_profile_l3`
|
|
|
|
|
|
- **合并规则(防降级)**:`monitor_tier` 取**最高档**(`new_tier = max(existing_tier, mapped_tier)`,normal < watch < high;映射:aml→high、pattern→watch、large_amount→watch、freq_trade→watch、suitability→normal);`monitor_tags` **追加合并**不覆盖;`computed_at` 写当前时间(字段 NOT NULL 必须显式赋值);`last_alert_id` 联动
|
|
|
|
|
|
- `risk_score` 动态评分、定期批量重算、监测报告:**本期不做**,接口签名预留 `service/risk/scoring.py :: recompute_customer_score(customer_id)`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-07 12:46:59 +08:00
|
|
|
|
## 4A. 追加需求(v1.1 并入 · FR-8 / FR-9 / FR-10)
|
|
|
|
|
|
|
|
|
|
|
|
> 2026-09-07 经独立 AI 评审(不通过 P0×4)→ 修订 → 复审通过后并入。冻结部分 FR-1~FR-7 原文不动;本节与冻结条款的关系见 §4A.0。
|
|
|
|
|
|
|
|
|
|
|
|
### 4A.0 与冻结条款的修订关系(不推翻,仅显式扩展)
|
|
|
|
|
|
|
|
|
|
|
|
1. **升级标记的第二写入方**:FR-4 冻结"人工处置是唯一改状态入口"**维持不变**——RISK-007 升级**不修改 `status`**,仅由定时任务写 `payload` 升级标记(`escalation_level`/`escalated_at`),该标记为定时任务独占写入,人工 API 无法触碰
|
|
|
|
|
|
2. **`risk_alert.alert_type` 不扩枚举**:新增两类预警复用冻结枚举值 `pattern`,以 `payload.alert_subtype`(`concentration` / `agent_behavior`)区分子类型(DDL 核实:ENUM 五值不含新类型,不改表)
|
|
|
|
|
|
3. **出单维度扩展(非例外)**:FR-4"同客户同自然日仅一张"继续对**客户维度**成立;RISK-008 是**新增的代理人维度出单线**(去重键 = `payload.actor_id` + 自然日),与客户维度聚合并存、互不影响
|
|
|
|
|
|
4. **`risk_alert.status` ENUM 扩展(`escalated`)列入二期改表挂账**,本期一律不使用该状态值
|
|
|
|
|
|
|
|
|
|
|
|
### 4A.1 新增角色:risk_manager(风控专员上级 · 用户拍板 2026-09-07)
|
|
|
|
|
|
|
|
|
|
|
|
- **角色定位**:`risk_officer`(风控专员)的上级管理岗,升级通知的接收方(系统原先无此角色,通知会推空;用户拍板新增并模拟生成上下级关系)
|
|
|
|
|
|
- **承载方式(不改表)**:`core_staff.staff_type` 为冻结 ENUM(advisor/analyst/risk_officer/compliance/ops)不能加值 → 上级账号 `staff_type='risk_officer'`、`roles=["risk_manager"]`(roles 为 JSON 字段,不撞红线 7);种子新增 2 个模拟账号 `STAFF-31001`/`STAFF-31002`
|
|
|
|
|
|
- **鉴权判定口径**:一律按 JWT `roles` 数组判定(含 `risk_manager` 即可),`staff_type` 不参与鉴权判定;台账查询(GET alerts)新增 risk_manager → 全量只读分支;处置 API(handle)角色白名单维持 roles 含 `risk_officer`,manager 账号不含该角色自然 403,零代码改动
|
|
|
|
|
|
- **权限边界**:预警台账只读全量(同 risk_officer);RISK-008 行为链明细可见(见 FR-10);**无处置权**——FR-4"人工处置唯一角色 = risk_officer"冻结条款不变,经理只升级知悉不代为处置
|
|
|
|
|
|
- **可见渠道**:FR-6 对话线入口维持冻结"仅 risk_officer"不变;`risk_manager` 查看预警台账与行为链明细统一走 HTTP 台账(GET alerts 全量只读分支),不扩展对话线角色
|
|
|
|
|
|
- **联动清单(C5 开工前执行)**:`scripts/core/02-seed-base.sql` 追加账号;JWT 手册 §5.3 角色清单增补;`AGENT_ACCESS_MATRIX` 增补行
|
|
|
|
|
|
- **通知升级链总览**:预警 → `risk_officer`(专员);超时 LEVEL_1 → +`risk_manager`(专员上级);LEVEL_2 → +`compliance`(合规官知悉)
|
|
|
|
|
|
|
|
|
|
|
|
### FR-8 客户资产配置集中度预警(RISK-006)
|
|
|
|
|
|
|
|
|
|
|
|
> 目标:弥补"单笔合规、整体失衡"盲区。现有 SUIT/RISK-001~005 均聚焦单笔交易,不感知客户持仓结构。
|
|
|
|
|
|
|
|
|
|
|
|
**触发时机**:交易网关落库后,规则引擎处理完 RISK-001~005 后**同步追加**本规则;对话线 Tool 实时查询为**只读展示,不触发 RISK-006 出单**。
|
|
|
|
|
|
|
|
|
|
|
|
**规则定义**:
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 定义 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 规则 ID | RISK-006 |
|
|
|
|
|
|
| 名称 | 高风险资产配置集中度预警 |
|
|
|
|
|
|
| 触发条件 | 客户持仓中 R4+R5 产品市值合计 ÷ 总持仓市值 ≥ `RISK_CONCENTRATION_THRESHOLD`(默认 80%,`.env` 可配) |
|
|
|
|
|
|
| 计算口径 | **市值直接取 `core_holding.market_value` 现成列汇总**(与 T-04 `core_ro.list_holdings` 既有返回一致;不按"份额×最新净值"重算,避免 N+1 调 `get_latest_nav` 威胁引擎 <100ms 约束);总持仓 = 全产品 `market_value` 求和;空仓或总持仓为 0 时不触发 |
|
|
|
|
|
|
| 优先级 | P1 |
|
|
|
|
|
|
| 出单类型 | **落库复用 `alert_type='pattern'` + `payload.alert_subtype='concentration'`**(不改表);聚合口径归入事件类,与 large_amount/freq_trade/pattern 同口径 |
|
|
|
|
|
|
| 评分映射 | `risk_score = 60`(静态映射,R-05 接入后替换) |
|
|
|
|
|
|
|
|
|
|
|
|
**系统动作**:
|
|
|
|
|
|
1. 引擎调用 `core_ro.list_holdings(customer_id)` 汇总持仓:按产品风险等级(`core_product.min_risk_code`)分组求和 R4+R5 市值与总市值。**截断防护**:`list_holdings` 默认 limit=500,命中上限即视同达到阈值(保守告警)并在 `input_summary` 标注 `holdings_truncated=true`
|
|
|
|
|
|
2. 命中 → `alert_service.agg_upsert` 并入当日事件类 pending 单:`payload.alert_subtype` 追加 `"concentration"`、`triggered_rules` 追加 `"RISK-006"`、`risk_score` 取 max(60 不覆盖 RISK-001/002 的 70,与 FR-4 聚合语义一致)
|
|
|
|
|
|
3. `profile_l3.upsert`:`monitor_tier = max(existing, watch)`,`monitor_tags` 追加 `"high_risk_concentration"`
|
|
|
|
|
|
4. `audit_log` INSERT(`event_type='risk_concentration'`,`input_summary` 含脱敏后的 R4+R5 占比与总持仓;金额按 DESENS-005 口径截断展示,`customer_id` 为内部键不脱敏)
|
|
|
|
|
|
5. Redis `PUBLISH risk:pub:alert`,`notify_role=["risk_officer"]`
|
|
|
|
|
|
|
|
|
|
|
|
**API 变更**:对话线 Tool `customer_context` 扩展 `concentration_ratio`(R4+R5 占比,脱敏后百分比);无新增 HTTP API。
|
|
|
|
|
|
|
|
|
|
|
|
**验收标准(A-10)**:
|
|
|
|
|
|
- C5 客户持仓 90% 在 R5 产品 → 触发 RISK-006,当日事件类预警单 `triggered_rules` 含 `"RISK-006"` 且 `payload.alert_subtype` 含 `"concentration"`
|
|
|
|
|
|
- C2 客户持仓 100% 在 R1 → 不触发
|
|
|
|
|
|
- 交易落库后 5 秒内完成持仓重算并出单(集成测试以 `TRD-TEST-` 交易断言)
|
|
|
|
|
|
- 对话线查询"哪些客户资产配置过于集中"返回正确列表及占比
|
|
|
|
|
|
- **回归口径**:现有用例全量回归;既有断言按"`triggered_rules` 含新增规则 ID 不判失败"口径预先声明
|
|
|
|
|
|
|
|
|
|
|
|
### FR-9 预警单处置时效监控与自动升级(RISK-007)
|
|
|
|
|
|
|
|
|
|
|
|
> 目标:防止风险事件因人工响应延迟而积压扩大。现有 FR-4 只有生成与处置,无时效监控。
|
|
|
|
|
|
|
|
|
|
|
|
**触发时机**:定时扫描(非交易事件驱动),每 15 分钟执行一次(`RISK_ESCALATION_SCAN_MINUTES` 可配)。
|
|
|
|
|
|
|
|
|
|
|
|
**规则定义**:
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 定义 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 规则 ID | RISK-007 |
|
|
|
|
|
|
| 名称 | 预警单处置时效监控与升级 |
|
|
|
|
|
|
| 触发条件 | `status='pending_review'` 且距 `created_at` 超过配置时效;**每张单独立计时、独立升级**;降噪:同一客户多张单在同一扫描周期命中同级别升级时,合并为一次推送 |
|
|
|
|
|
|
| 升级策略(阈值全部 settings 注入) | LEVEL_1:≥ `RISK_ESCALATION_L1_HOURS`(默认 4h)未处置 → Redis 升级推送,`notify_role` 追加 `risk_manager`(见 4A.1)<br>LEVEL_2:≥ `RISK_ESCALATION_L2_HOURS`(默认 24h)未处置 → `payload.escalation_level=2` + `payload.escalated_at`,`notify_role` 追加 `compliance`,对话线查询置顶<br>AML 特殊通道:`RISK_ESCALATION_AML_L1_HOURS`(默认 1h)/ `RISK_ESCALATION_AML_L2_HOURS`(默认 4h) |
|
|
|
|
|
|
| 优先级 | P0(运营红线) |
|
|
|
|
|
|
| 出单类型 | 不新建预警单、**不改 `status`**(冻结 ENUM 无 `escalated`,升级信息全部由 `payload` 承载) |
|
|
|
|
|
|
| 评分映射 | 不修改 `risk_score` |
|
|
|
|
|
|
|
|
|
|
|
|
**系统动作**:
|
|
|
|
|
|
1. 定时任务 `scripts/cron/escalation_scan.py`(独立脚本优先)查询 `risk_alert`:`status='pending_review'`
|
|
|
|
|
|
2. 按规则计算超时时长,命中升级条件:
|
|
|
|
|
|
- LEVEL_1:先 `UPDATE payload` 持久化 `escalation_level=1` + `escalated_at`(幂等依据,与 LEVEL_2 同机制),再 Redis `PUBLISH`,`notify_role` 追加 `risk_manager`
|
|
|
|
|
|
- LEVEL_2:`UPDATE payload` 写入 `escalation_level=2` + `escalated_at`,`PUBLISH`,`notify_role` 追加 `compliance`
|
|
|
|
|
|
- **幂等约束**:仅当"本次计算级别 > `payload.escalation_level` 已记录级别"才执行升级动作;同级别不重复推送(每级别至多推送一次)
|
|
|
|
|
|
3. 每次升级动作写 `audit_log`(`event_type='alert_escalation'`,`input_summary` 含 `alert_id`、已达级别、升级原因、超时时长);定时任务无 HTTP 上下文,**显式 `new_trace()` 生成 trace_id** 并与预警单 payload、推送消息贯通(§7.5 可还原性)
|
|
|
|
|
|
4. 状态机约束:`status` 全程保持 `pending_review`,人工处置照常(`pending_review → 三态`,冻结不变),处置后单据退出扫描范围;`payload` 升级标记**仅定时任务可写**,人工处置 API 不感知、不覆盖(`handler_result` 枚举外值由 Pydantic 返回 422)
|
|
|
|
|
|
5. 定时任务对升级决策的全量输入输出写 `audit_log`,审计写库失败降级不阻塞但本地日志 error 留底(红线 5 口径)
|
|
|
|
|
|
|
|
|
|
|
|
**API 变更**:新增对话线 Tool `query_overdue_alerts(hours?)`(返回超期预警列表含 `alert_id`、`created_at`、`escalation_level`(读自 payload,缺省 0)、`overdue_hours`);无新增 HTTP API。
|
|
|
|
|
|
|
|
|
|
|
|
**验收标准(A-11)**:
|
|
|
|
|
|
- **免等待测试方案**:升级阈值走 settings 注入(测试用小时以下阈值);fixture 支持直接 INSERT `created_at` 回拨的 pending 单(conftest 扩展)
|
|
|
|
|
|
- pending 单超 L1 阈值 → 收到升级推送,`notify_role` 含 `risk_manager`;**同一单重复扫描不重复推送**(幂等断言)
|
|
|
|
|
|
- AML 预警单超 AML L1 阈值 → 触发 LEVEL_1(与普通单阈值不同,两条断言分开)
|
|
|
|
|
|
- 风控专员在阈值内处置 → 不触发任何升级;处置后超期单不再进入扫描结果
|
|
|
|
|
|
- 状态机:全程 `status='pending_review'`;`handler_result` 枚举外值 → 422;`payload.escalation_level` 与人工处置字段互不覆盖
|
|
|
|
|
|
- 对话线"今天有哪些超期预警?"返回正确列表及超时时长、升级级别
|
|
|
|
|
|
|
|
|
|
|
|
### FR-10 代理人异常行为链识别(RISK-008)
|
|
|
|
|
|
|
|
|
|
|
|
> 目标:单点拦截(如 T-04 的 `AUTH_403_SCOPE`)只能阻断单次请求,无法识别代理人本身的操作风险模式。
|
|
|
|
|
|
|
|
|
|
|
|
**触发时机**:定时扫描(每 30 分钟,`RISK_AGENT_BEHAVIOR_SCAN_MINUTES` 可配),数据源为 T-02 审计中间件留痕的 `audit_log`(**input_guard_log 不作为数据源**:网关路径不写该表且无 code 字段)。
|
|
|
|
|
|
|
|
|
|
|
|
**规则定义**:
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 定义 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 规则 ID | RISK-008 |
|
|
|
|
|
|
| 名称 | 代理人异常行为链识别 |
|
|
|
|
|
|
| 触发条件(阈值/窗口全部 settings 注入可配) | **条件 A(频繁诱导调仓)**:`RISK_AGENT_BEHAVIOR_A_WINDOW_HOURS`(默认 24h)内,同一代理人对同一客户发生 ≥ `RISK_AGENT_BEHAVIOR_A_COUNT`(默认 3)次产品切换操作(赎回某产品后 2 小时内申购另一产品)。**归属口径**:以 `audit_log`(`event_type='trade_request'`,网关审计)中的发起人 actor 归属;**排除发起人 == 客户本人的交易**(防客户正常调仓误报)。系统 `core_trade` 无 actor 字段、无代理人交易路径(FR-1 鉴权仅 risk_demo/客户本人),演示期由 risk_demo 账号扮演代理人发起(写入演示 SOP);真实代理人交易路径接入时以 Core 提供 actor 字段为准(挂账)<br>**条件 B(试探越权)**:`RISK_AGENT_BEHAVIOR_B_WINDOW_HOURS`(默认 72h)内,同一代理人累计 ≥ `RISK_AGENT_BEHAVIOR_B_COUNT`(默认 5)次 `AUTH_403_SCOPE` 越权查询。**唯一数据口径**:`audit_log WHERE event_type='authz' AND input_summary 中 code='AUTH_403_SCOPE'`(拒绝标记为 `decision='forbidden'`);对话线 Tool blocked 事件经 `record_authz_denial` 双写后同口径可查<br>**条件 C(高频查询非名下客户)**:`RISK_AGENT_BEHAVIOR_C_WINDOW_HOURS`(默认 24h)内,同一代理人查询非名下客户信息被拒 ≥ `RISK_AGENT_BEHAVIOR_C_COUNT`(默认 10)次。**数据源**:`event_type='customer_query'` 在系统中无生产方,条件 C 定义为**"非名下客户查询被拒"事件族**:`audit_log WHERE event_type='authz' AND code IN ('AUTH_403_NOT_OWNER','AUTH_403_NOT_ASSIGNED')`(API 线归属校验拒绝)∪ 对话线 `agent_tool_call` blocked 中归属类拒绝码(经 `record_authz_denial` 双写);`customer_query` 事件类型补建列为二期 T-02 侧变更(挂账) |
|
|
|
|
|
|
| 优先级 | P1 |
|
|
|
|
|
|
| 出单类型 | `agent_behavior` 语义预警单:**落库 `alert_type='pattern'` + `payload.alert_subtype='agent_behavior'`**;**独立出单不按客户聚合**(出单维度**扩展**,见 4A.0 第 3 条):去重键 = `payload.actor_id` + 自然日,同代理人同日仅一张 pending 单 |
|
|
|
|
|
|
| 评分映射 | `risk_score = 75`(静态映射,R-05 评分模型接入后替换) |
|
|
|
|
|
|
|
|
|
|
|
|
**系统动作**:
|
|
|
|
|
|
1. 定时任务扫描 `audit_log` 滑动窗口(24h/72h,按上表口径过滤)
|
|
|
|
|
|
2. 命中任一子条件,生成预警单,**落库映射(对照真实 DDL 定案)**:
|
|
|
|
|
|
- `alert_type='pattern'`,`payload.alert_subtype='agent_behavior'`
|
|
|
|
|
|
- `customer_id`(NOT NULL 冻结约束)= 行为链中**主要涉及客户**(按证据条数取众数);payload 显式声明该字段语义为"涉及客户"而非"归属客户"
|
|
|
|
|
|
- 代理人标识:`payload.actor_id` + `payload.actor_type='agent'`(`risk_alert` 无 `actor_id` 列)
|
|
|
|
|
|
- `payload` 含行为链明细:时间线、涉及客户 ID(内部键,不脱敏)、命中子条件(A/B/C)、证据 trace_id 列表
|
|
|
|
|
|
- `risk_score = 75`
|
|
|
|
|
|
3. **代理画像存储**:本期最小实现仅在 `risk_alert.payload` 承载(不新建 `agent_profile_l3` 表、不扩展 `customer_profile_l3` 语义);长期画像追踪 M4 后评估
|
|
|
|
|
|
4. Redis `PUBLISH`,`notify_role=["risk_officer","risk_manager"]`
|
|
|
|
|
|
5. **行为链明细展示范围**:仅 `risk_officer` / `risk_manager` 角色可见(其他角色 fail-closed;对话线入口维持仅 risk_officer,经理走 HTTP 台账);对话输出对客户身份信息二次脱敏
|
|
|
|
|
|
6. 审计:每条命中行为链写 `audit_log`(`event_type='agent_behavior_detected'`),显式 `new_trace()`(同 FR-9 口径)
|
|
|
|
|
|
|
|
|
|
|
|
**API 变更**:对话线 Tool 扩展 `query_agent_behavior(agent_id?)`(检索 `alert_type='pattern' AND payload.alert_subtype='agent_behavior'` 双条件;`agent_id` 匹配 `payload.actor_id`;JSON 检索演示规模可接受,性能挂账);无新增 HTTP API。
|
|
|
|
|
|
|
|
|
|
|
|
**验收标准(A-12)**:
|
|
|
|
|
|
- 代理人在 24h 内对同一客户执行 3 次"赎回→申购" → 触发条件 A,生成代理人维度预警单(`payload.actor_id` 正确、`customer_id` = 涉及客户)
|
|
|
|
|
|
- **客户本人交易不误报**:`actor_id == customer_id` 的正常调仓不计数
|
|
|
|
|
|
- 代理人在 72h 内累计 5 次 `AUTH_403_SCOPE` → 触发条件 B
|
|
|
|
|
|
- 24h 内 10 次 `AUTH_403_NOT_OWNER`/`AUTH_403_NOT_ASSIGNED` 被拒 → 触发条件 C(数据经 `backdated_audit_event` fixture 回拨构造)
|
|
|
|
|
|
- 风控专员对话查询"代理人 STAFF-20001 最近有没有异常行为" → 返回正确预警及证据链
|
|
|
|
|
|
- 同代理人同日多次命中不同子条件 → 只一张 pending 单,`triggered_rules`/`payload` 追加子条件标识
|
|
|
|
|
|
- 预警单 `payload.actor_id` 正确指向代理人而非客户;非授权角色查行为链明细 → fail-closed
|
|
|
|
|
|
- **测试基建**:conftest 扩展 `backdated_audit_event` fixture(显式 `created_at` 回拨写入 `audit_log`,teardown 按 `TRD-TEST-` 标记清理);窗口边界用例(A:2/3;B:4/5;C:9/10);现有用例全量回归
|
|
|
|
|
|
|
|
|
|
|
|
**新增配置项(C4~C6 开工时同步 `.env.example` + `settings.py`)**:
|
|
|
|
|
|
|
|
|
|
|
|
| 配置项 | 默认值 | 用途 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `RISK_CONCENTRATION_THRESHOLD` | 0.80 | RISK-006 集中度阈值 |
|
|
|
|
|
|
| `RISK_ESCALATION_SCAN_MINUTES` | 15 | RISK-007 扫描周期 |
|
|
|
|
|
|
| `RISK_ESCALATION_L1_HOURS` / `L2_HOURS` | 4 / 24 | 普通单升级阈值 |
|
|
|
|
|
|
| `RISK_ESCALATION_AML_L1_HOURS` / `AML_L2_HOURS` | 1 / 4 | AML 单升级阈值 |
|
|
|
|
|
|
| `RISK_AGENT_BEHAVIOR_SCAN_MINUTES` | 30 | RISK-008 扫描周期 |
|
|
|
|
|
|
| `RISK_AGENT_BEHAVIOR_A_WINDOW_HOURS` / `A_COUNT` | 24 / 3 | 条件 A 窗口/次数 |
|
|
|
|
|
|
| `RISK_AGENT_BEHAVIOR_B_WINDOW_HOURS` / `B_COUNT` | 72 / 5 | 条件 B 窗口/次数 |
|
|
|
|
|
|
| `RISK_AGENT_BEHAVIOR_C_WINDOW_HOURS` / `C_COUNT` | 24 / 10 | 条件 C 窗口/次数 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-06 15:09:13 +08:00
|
|
|
|
## 5. 数据设计
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 读写权限总表
|
|
|
|
|
|
|
|
|
|
|
|
| 表 / Key | 操作 | 说明 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `jinrong_core.core_trade` | **INSERT(仅网关 gateway_repository)** | 网关扮演外部交易系统 |
|
|
|
|
|
|
| `jinrong_core.core_*` 其余 | SELECT | 经 `CoreReadOnlyRepository`(需扩展方法),Agent 只读 |
|
|
|
|
|
|
| `jinrong_agent.risk_alert` | INSERT + UPDATE(状态机) | 风控写 |
|
|
|
|
|
|
| `jinrong_agent.risk_suitability_log` | INSERT | 每次校验落一条 |
|
|
|
|
|
|
| `jinrong_agent.customer_profile_l3` | UPSERT(最高档合并) | 风控独写 |
|
|
|
|
|
|
| `jinrong_agent.risk_aml_list` | SELECT + 种子脚本维护 | 风控读,admin 维护 |
|
|
|
|
|
|
| `jinrong_agent.customer_profile_l1/l2` | SELECT | 只读,辅助判断 |
|
|
|
|
|
|
| `jinrong_agent.audit_log` | INSERT | 只增 |
|
|
|
|
|
|
| Redis `risk:pub:alert` | PUBLISH | 预警通知广播(payload 含 `notify_role`) |
|
|
|
|
|
|
| Redis `risk:dedup:{customer_id}:{rule_id}:{date}` | SET EX 24h | 规则级防重复预警 |
|
|
|
|
|
|
| Redis `profile:l3:{customer_id}` | SET EX 5m | L3 热缓存,MySQL 更新时 DEL |
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 新增表 DDL(需用户确认后并入 `02-mysql-agent专用.sql`)
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE risk_aml_list (
|
|
|
|
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
|
|
|
|
list_id VARCHAR(64) NOT NULL,
|
|
|
|
|
|
list_type ENUM('sanction','terror','pep') NOT NULL,
|
|
|
|
|
|
full_name VARCHAR(128) NOT NULL COMMENT '与 core_customer.display_name 同为脱敏展示名口径',
|
|
|
|
|
|
id_no VARCHAR(32) NULL COMMENT '预留:待 Core 提供证件数据后启用匹配',
|
|
|
|
|
|
bank_card_no VARCHAR(32) NULL COMMENT '预留:同上',
|
|
|
|
|
|
match_threshold DECIMAL(3,2) NOT NULL DEFAULT 0.85,
|
|
|
|
|
|
source VARCHAR(64) NOT NULL,
|
|
|
|
|
|
list_version VARCHAR(16) NOT NULL,
|
|
|
|
|
|
effective_date DATE NOT NULL,
|
|
|
|
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
|
|
|
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
|
|
|
|
UNIQUE KEY uk_list_id (list_id),
|
|
|
|
|
|
KEY idx_name (full_name),
|
|
|
|
|
|
KEY idx_active (is_active)
|
|
|
|
|
|
) ENGINE=InnoDB COMMENT='【风控专用】AML 名单本地镜像';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
其余复用底座已有表,**不修改任何现有表结构**(含 Core)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. API 清单汇总
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 路径 | 角色 | 说明 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| POST | `/api/simulate/trade` | risk_demo 演示账号 / customer 本人 | 模拟交易网关(R-02 阻断点) |
|
|
|
|
|
|
| GET | `/api/risk/alerts` | risk_officer 全量;**compliance 只读且服务端强制 `alert_type=aml`** | 预警台账分页查询 |
|
|
|
|
|
|
| POST | `/api/risk/alerts/{id}/handle` | risk_officer | 人工处置(唯一改状态入口) |
|
|
|
|
|
|
| POST | `/api/risk/suitability/check` | customer(本人)/advisor(名下)/risk_officer | 适当性校验(推荐前复用;用 POST 保幂等语义——每次调用落 suitability_log) |
|
|
|
|
|
|
| POST | `/api/risk/aml/scan` | risk_officer | 手动全量 AML 扫描 |
|
|
|
|
|
|
| POST | `/api/chat`(X-Agent-Type=risk) | risk_officer | 对话线 |
|
|
|
|
|
|
|
|
|
|
|
|
统一响应格式复用 `utils/response.py`;预警类 API 响应体含固定 `disclaimer`:"本预警由系统自动生成,最终判定需经风控专员人工审核。"(阻断类 API 响应体含 G-08 客户免责声明。)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 合规与审计(红线)
|
|
|
|
|
|
|
|
|
|
|
|
1. **唯一阻断点**:全系统仅 R-02 交易前校验可阻断**请求**;其余场景只预警不拦截
|
|
|
|
|
|
2. **不自动冻户、不自动上报监管、不自动改 C1~C5**(L3 只是监测标签)
|
|
|
|
|
|
3. **审计字段对齐 `audit_log` 实际表结构**:`trace_id, created_at, agent_type, rule_id, customer_id, event_type, input_summary(脱敏), decision, risk_score, handler_id, handler_result, handler_comment`——任何风控判定(校验/预警/阻断/通知)全落 `audit_log`,只 INSERT;**`customer_id` 为系统内部代理键,不脱敏**(保证按客户检索审计的索引有效性)
|
|
|
|
|
|
4. **脱敏规则**:权威定义见附表 `docs/PRD/附-风控规则表.md` §4——身份证/手机号前 3 后 4、姓名留姓氏、银行卡留后 4;`customer_id` 内部键不脱敏;姓名/证件/手机号在 `input_summary`、预警 payload、通知、对话输出中脱敏;LLM 上下文传入前必须已脱敏
|
|
|
|
|
|
5. **trace_id 贯通**:网关生成 → suitability_log → risk_alert → audit_log → Pub/Sub 消息,同一链路可还原
|
|
|
|
|
|
6. **处置人工化**:预警状态仅 `risk_officer` 经结构化 API 变更;compliance 只读;对话线不处置
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 验收标准(对照 REQUIREMENTS Wave 2 + 评分项)
|
|
|
|
|
|
|
|
|
|
|
|
> 前置:先执行 §10 演示数据准备。
|
|
|
|
|
|
|
|
|
|
|
|
| # | 演示场景 | 预期结果 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| A-1 | C1 客户(测评有效期内,CUST-1001)申购 R4 产品(PROD-161725 科技成长主题) | 响应 `blocked=true`,`block_reason` 命中 SUIT-001(C1 仅可购 R1);`core_trade` 无记录;`risk_suitability_log.is_blocked=1`;suitability 预警单生成;audit 可查 |
|
|
|
|
|
|
| A-2 | CUST-4001(70 岁、测评 C5、已刷新有效)购买 R4 产品(PROD-161725) | `blocked=true`,`reasons[]` 含 SUIT-006 封顶说明("C5 因年龄≥70 按 C3 处理")与 SUIT-003 不匹配,**无 SUIT-008 干扰**;其余同 A-1 |
|
|
|
|
|
|
| A-3 | CUST-3001(C3、已刷新有效)单笔申购 50 万 PROD-510300 沪深300指数(R3,等级匹配) | `core_trade` 落库;事件类预警单 `pending_review`(`triggered_rules` 含 RISK-001+RISK-002,`risk_score=70`);Redis 收到 `risk:pub:alert` 推送 |
|
|
|
|
|
|
| A-4 | 同产品当日第 3 笔申赎 | freq_trade 规则并入当日预警单(不另开新单);当日去重不重复报 |
|
|
|
|
|
|
| A-5 | AML 名单命中客户交易 | aml 独立预警单(score=95)+ L3 `monitor_tier='high'` + 紧急推送(`notify_role` 含 compliance)+ compliance 账号可见该单;**账户未被冻结** |
|
|
|
|
|
|
| A-6 | 风控专员对话 | "今天有多少待审预警" 返回正确统计 + alert_id 可溯源 |
|
|
|
|
|
|
| A-7 | 人工处置 | 状态 `pending_review→confirmed_suspicious`;compliance 调 `/handle` → 403(其调 GET 仅返回 aml 单);audit 留痕 |
|
|
|
|
|
|
| A-8 | `suitability_check` 单元级验收 | 脚本/单测直接调用,断言 C1+R4 → `is_matched=false, blocked=true` 且返回结构完整;"不匹配拒绝生成推荐语"的端到端验收归客户/代理人 Agent 各自 PRD(见 §9 依赖) |
|
|
|
|
|
|
| A-9 | 越权 | customer 查他人 suitability、advisor 查非名下客户 → 403 + audit |
|
2026-09-07 12:46:59 +08:00
|
|
|
|
| A-10 | 集中度预警(FR-8 · v1.1) | C5 客户持仓 90% R5 → 事件类预警单含 RISK-006 + `payload.alert_subtype='concentration'`;C2 全 R1 不触发;落库后 5s 内出单 |
|
|
|
|
|
|
| A-11 | 时效升级(FR-9 · v1.1) | 超阈值 → LEVEL_1 推送含 risk_manager(幂等:重复扫描不重复推);AML 短通道 1h;阈值内处置不升级;全程 status=pending_review;枚举外 handler_result → 422(免等待:阈值 settings 注入 + created_at 回拨 fixture) |
|
|
|
|
|
|
| A-12 | 代理人行为链(FR-10 · v1.1) | 条件 A(3 次赎回→申购,本人交易排除)/ B(5 次 AUTH_403_SCOPE)/ C(10 次归属拒绝)各自触发;代理人维度独立出单(payload.actor_id);同日多子条件只一张单;行为链明细仅 risk_officer/risk_manager 可见 |
|
2026-09-06 15:09:13 +08:00
|
|
|
|
|
|
|
|
|
|
**通用验收**:所有预警单初始 `pending_review`;审计链路任取一条 `trace_id` 可还原全流程;越权 403。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 依赖与前置
|
|
|
|
|
|
|
|
|
|
|
|
| 依赖 | 状态 | 影响 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| T-01 JWT/RBAC 中间件 | 未做 | 所有 /api/risk 接口角色校验 + 客户本人 JWT + 最简签发端点 |
|
|
|
|
|
|
| T-02 audit 中间件 + trace_id | 未做 | §7 审计要求 |
|
|
|
|
|
|
| T-03 输入防护(F-03) | 未做 | 对话线接收专员输入的全员底线 |
|
|
|
|
|
|
| T-04 core_ro 扩展 | **需扩展** | 现仅有部分方法;需补 `get_customer_risk`(含 evaluated_at)、当日交易统计、近 N 天流水等查询(含 SUIT-008 所需 `evaluated_at`) |
|
|
|
|
|
|
| T-05 Core 模拟库灌库 | 脚本已有未执行 | 演示数据 |
|
|
|
|
|
|
| T-07 LangGraph + DeepSeek | 未做 | 仅对话线 FR-6 依赖;事件驱动线不依赖 |
|
|
|
|
|
|
| 共用底座 SQL 灌库 | 待执行 | risk_alert 等表 |
|
|
|
|
|
|
| 客户/代理人 Agent | Wave 1/3 | A-8 端到端部分归各自 PRD;本期只交付 `suitability_check` 公共函数 + 单测 |
|
|
|
|
|
|
|
|
|
|
|
|
**排期策略**:事件驱动线(FR-1~FR-5)不依赖 T-07,可先行;对话线(FR-6)等 Wave 0 底座就绪后接入。具体切任务见后续开发计划文档。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 演示账号与演示数据准备
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 演示账号(需确认后并入 seed)
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 值 | 说明 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 账号 | `STAFF-90001`(display_name=风控演示账号) | `staff_type='risk_officer'`、`roles=["risk_officer","risk_demo"]` |
|
|
|
|
|
|
| 网关鉴权 | roles 含 `risk_demo` 即可发起任意客户的模拟交易 | 演示期口径;生产由真实交易系统回调替代,无此角色 |
|
|
|
|
|
|
| AML 演示名单 | 种子含 1 条与某种子客户 `display_name` 同名的 sanction 记录 | 演示命中 |
|
|
|
|
|
|
|
|
|
|
|
|
### 10.2 测评有效期演示数据(修复 SUIT-008 与种子冲突)
|
|
|
|
|
|
|
|
|
|
|
|
种子库 28 位客户的 `evaluated_at` 均已过期(最晚 2025-09-01),若不处理,**所有交易都会被 SUIT-008 阻断,A-3/A-4/A-5 无法走通**。演示前执行 `scripts/demo/prepare_risk_demo.sql`:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 演示客户测评日期刷新至演示日前 90 天内(仅演示用例涉及的客户)
|
|
|
|
|
|
UPDATE jinrong_core.core_customer_risk SET evaluated_at = CURDATE() - INTERVAL 90 DAY
|
|
|
|
|
|
WHERE customer_id IN ('CUST-1001','CUST-1002','CUST-1003','CUST-3001','CUST-4001','CUST-9527');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 该脚本与 `reset.ps1` 分离(reset 重建全库后需重跑),并在 FLOW.md bootstrap 补一步
|
|
|
|
|
|
- 同时保留 **1 位测评过期客户**(CUST-1004,C4,不刷新)用于演示 SUIT-008 阻断路径:"测评过期 → 等级匹配仍阻断 → 提示重新测评"
|
|
|
|
|
|
- A-1/A-2/A-3 的断言均写明命中的具体规则编号与 `block_reason` 内容,避免规则叠加时断言含糊
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 性能与非功能
|
|
|
|
|
|
|
|
|
|
|
|
- 规则引擎单事件处理 <100ms(含 AML 比对)
|
|
|
|
|
|
- 适当性校验 P95 <300ms(两次索引查询)
|
|
|
|
|
|
- 预警通知从交易落库到 Pub/Sub 推送 <1s(演示用"实时感")
|
|
|
|
|
|
- AML 全量扫描(28 客户 × 8 名单)<3s
|
|
|
|
|
|
- 所有金额用 `Decimal`,禁止 float
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 12. 边界与不做清单(复述强约束)
|
|
|
|
|
|
|
|
|
|
|
|
- 不自动冻结账户、不自动上报监管、不自动调整正式风险等级、不代替客户重测
|
|
|
|
|
|
- 不拦截已发生交易(只拦请求)
|
|
|
|
|
|
- 不限制客户交易次数/金额
|
|
|
|
|
|
- 不生成投资建议/收益承诺
|
|
|
|
|
|
- 不做"仅提示不阻断"的放行路径(R-02 一期全部阻断)
|
|
|
|
|
|
- 不做 AML 开户/信息变更触发(无开户流程,仅预留事件入口)
|
|
|
|
|
|
- 数据分析 Agent 只能统计预警台账,不能处置
|
|
|
|
|
|
- 不做:真实支付对接、TA 清算、毫秒级行情、多租户、convert 交易类型
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 13. 文档联动修正(PRD 冻结后执行)
|
|
|
|
|
|
|
|
|
|
|
|
| 文档 | 修正内容 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `docs/需求拆解/Agent风险与合规约束汇总.md` | "产品推荐:禁止"→"允许:须过 R-02 校验 + 免责声明 + 标注需持证审核;禁止具体操作指令"(§1 G-08 附近及各 Agent 边界表) |
|
|
|
|
|
|
| `docs/PRD/附-风控规则表.md`(已入库) | 后续仅此表维护规则;原本地《02-业务规则表》停止维护(本地工作区文件可在冻结后删除) |
|
|
|
|
|
|
| 本地《03-团队分工》 | Git 分支策略对齐实际仓库(main + feature/*,无 develop) |
|
|
|
|
|
|
| `docs/memory/FRAMEWORK.md` | §3 分层增补例外说明:`app/gateway/` 为模拟外部系统模块,仅 `gateway_repository` 可 INSERT `core_trade`;§3 脚本清单增补 `scripts/demo/`(演示数据准备)与 `scripts/agent/`(agent 库种子:risk_aml_list 名单、演示交易数据) |
|
|
|
|
|
|
| `docs/项目框架设计/表设计/02-redis-keys.md` | §2.4 `risk:pub:alert` payload 口径对齐本 PRD(`{alert_id, alert_type, customer_id_mask, risk_score, trace_id, notify_role}`) |
|
|
|
|
|
|
| `docs/项目框架设计/表设计/02-mysql-agent专用.sql` | 追加 `risk_aml_list`(经用户确认) |
|
|
|
|
|
|
| `scripts/core/02-seed-base.sql` | 追加演示账号 `STAFF-90001`(staff_type='risk_officer',roles=["risk_officer","risk_demo"]) |
|
|
|
|
|
|
| `docs/memory/REQUIREMENTS.md` / `TODO.md` / `FLOW.md` | Wave 2 增加 T-30~T-32 细化任务与 PRD 链接;bootstrap 补演示数据准备步骤(`scripts/demo/prepare_risk_demo.sql`) |
|
2026-09-07 12:46:59 +08:00
|
|
|
|
| (v1.1 追加)`scripts/core/02-seed-base.sql` | 追加 `risk_manager` 上级角色账号 STAFF-31001/31002(staff_type='risk_officer' + roles=["risk_manager"])——C5 开工前 |
|
|
|
|
|
|
| (v1.1 追加)JWT 手册 §5.3 + `AGENT_ACCESS_MATRIX` | 增补 `risk_manager` 角色行(台账只读全量、无处置权、对话线不放行)——C5 开工前 |
|
|
|
|
|
|
| (v1.1 追加)`.env.example` + `settings.py` | §4A FR-8/9/10 配置项清单全量登记(集中度阈值/升级双通道/行为链窗口)——C4~C6 开工时 |
|
|
|
|
|
|
| (v1.1 追加)`02-redis-keys.md` | `risk:pub:alert` payload 增补 `escalation_level`/`alert_subtype` 字段说明 |
|
|
|
|
|
|
| (v1.1 追加)演示 SOP | 补 risk_demo 扮演代理人发起"赎回→申购"演示步骤——C6 开工前 |
|
|
|
|
|
|
| (v1.1 追加)`docs/memory/MEMORY.md` / `TODO.md` / `FRAMEWORK.md` | 登记追加需求 FR-8/9/10 与 C4~C6 任务、挂账 #1~#9 |
|
2026-09-06 15:09:13 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-07 12:46:59 +08:00
|
|
|
|
*复审通过并经用户确认后冻结为 v1.0(FR-1~FR-7,2026-09-06);v1.1 追加 FR-8/FR-9/FR-10(2026-09-07 经独立 AI 评审→修订→复审通过后并入)。本 PRD 为风控模块开发唯一依据。*
|