diff --git a/sql/risk_rule_seed.sql b/sql/risk_rule_seed.sql new file mode 100644 index 0000000..cc58fe2 --- /dev/null +++ b/sql/risk_rule_seed.sql @@ -0,0 +1,33 @@ +-- ===================================================================== +-- 风控规则初始化(20 条,见《风控Agent需求文档》§7) +-- 执行:mysql --default-character-set=utf8mb4 -u -p < sql/risk_rule_seed.sql +-- 说明:INSERT IGNORE 依赖 uk_rule_id 唯一键,重复执行不报错、不覆盖已有运营调整。 +-- ===================================================================== +SET NAMES utf8mb4; + +INSERT IGNORE INTO risk_rule + (rule_id, rule_name, trigger_condition, threshold, risk_level, weight, status) +VALUES + -- A 类 · 单笔规则 + ('R001', '大额申购-红', '单笔申购金额 ≥ 100万', '{"amount":1000000}', '高', 0.90, '启用'), + ('R002', '大额赎回-红', '单笔赎回金额 ≥ 100万', '{"amount":1000000}', '高', 0.90, '启用'), + ('R003', '大额申购-黄', '单笔申购金额 ≥ 50万', '{"amount":500000}', '中', 0.70, '启用'), + ('R004', '大额赎回-黄', '单笔赎回金额 ≥ 50万', '{"amount":500000}', '中', 0.70, '启用'), + ('R014', '适当性异常', '客户风险等级 < 产品风险等级', '{}', '高', 0.95, '启用'), + ('R020', '夜间交易', '下单时间 00:00–06:00', '{"start":"00:00","end":"06:00"}', '低', 0.35, '启用'), + -- B 类 · 聚合规则 + ('R005', '高频交易', '近 7 天申赎 ≥ 10 笔', '{"days":7,"count":10}', '中', 0.65, '启用'), + ('R006', '当日密集交易', '近 1 天申赎 ≥ 5 笔', '{"days":1,"count":5}', '中', 0.60, '启用'), + ('R007', '密集小额申购', '近 3 天 ≥ 5 笔、每笔 < 1万', '{"days":3,"count":5,"max_amount":10000}', '低', 0.50, '启用'), + ('R008', '短炒赎回', '同产品申购后 7 天内赎回', '{"days":7}', '中', 0.65, '启用'), + ('R009', '快进快出', '同产品申购后 3 天内全额赎回', '{"days":3}', '高', 0.85, '启用'), + ('R010', '资金快进快出', '入账后 24 小时内赎回', '{"hours":24}', '高', 0.90, '启用'), + ('R011', '短期累计大额', '近 7 天累计申赎 ≥ 200万', '{"days":7,"amount":2000000}', '高', 0.85, '启用'), + ('R012', '中期累计大额', '近 30 天累计申赎 ≥ 500万', '{"days":30,"amount":5000000}', '中', 0.65, '启用'), + ('R013', '拆分规避大额', '同 1 天多笔合计 ≥ 100万、单笔 < 50万', '{"days":1,"total":1000000,"each_lt":500000}', '中', 0.70, '启用'), + ('R018', '全额清仓赎回', '赎回后该产品持仓归 0 且金额 ≥ 10万', '{"amount":100000}', '低', 0.45, '启用'), + ('R019', '对倒交易', '近 7 天多产品互买互卖', '{"days":7}', '中', 0.70, '停用'), + -- C 类 · 客户维度规则 + ('R015', '新客户大额', '注册 < 30 天且单笔 ≥ 50万', '{"days":30,"amount":500000}', '中', 0.65, '启用'), + ('R016', '休眠账户激活', '90 天无交易后首笔 ≥ 10万', '{"days":90,"amount":100000}', '低', 0.55, '启用'), + ('R017', '整数金额', '金额为整万且 ≥ 10万', '{"amount":100000}', '低', 0.40, '启用'); diff --git a/风控Agent需求文档.md b/风控Agent需求文档.md new file mode 100644 index 0000000..b9bdef0 --- /dev/null +++ b/风控Agent需求文档.md @@ -0,0 +1,341 @@ +# 风控 Agent 需求文档 + +> 版本:v1.0 | 日期:2026-09-12 | 状态:待评审 +> 依据:《开发计划.md》§2.5 风控控制台、§5 风控Agent、Phase 4 +> 关联:《风控Agent设计方案.md》 + +--- + +## 1. 背景与目标 + +为系统接入**风控 Agent**:每一笔申购/赎回在下单落账前,先经过风控规则引擎检测,命中规则的订单被「挂起」等待人工处置,未命中的正常成交。 + +**核心目标**:形成「检测 → 阻断 → 人工处置 → 回写」的合规闭环。 + +**合规铁律**:机器只负责告警 + 阻断;放行/拦截/冻结的处置权永远在人工。 + +--- + +## 2. 术语与核心概念 + +| 术语 | 表 | 定位 | +|---|---|---| +| 交易申请单 | `trade_order` | 风控**拦截对象**,状态机主表(事前) | +| 成交流水 | `fin_transaction` | 账务凭证,只含真正成交的单(事后) | +| 风控预警 | `fin_risk_alert` | 命中记录 + 人工处置留痕 | +| 风控规则 | `risk_rule` | 规则引擎的表驱动配置(参数) | +| 业务工单 | `biz_work_order` | 拦截/冻结后开启的独立审批链 | + +**两张链的关系**:预警链(`fin_risk_alert`)在人工处置后结束;工单链(`biz_work_order`)只在「拦截/冻结」时**额外开启**,是处置的下游产物。 + +--- + +## 3. 现状与差距 + +- 当前 `service/purchase.py` / `service/redeem.py` 为**即时成交**(直接扣款+加仓 / 减仓+入账),不产生任何交易留痕。 +- `trade_order` / `fin_transaction` / `fin_risk_alert` / `risk_rule` / `biz_work_order` 五张表**只有 DDL,无 model/repo/schema 实现**。 +- 结论:风控 Agent 当前无数据可检测,需先补齐交易留痕。 + +--- + +## 4. 核心需求 + +### FR1 · 前置拦截(交易留痕) + +申购/赎回由「即时成交」改为「先建单 → 检测 → 通过才落账」。 + +``` +请求(申购/赎回) + ↓ +FastAPI 鉴权(get_current_user)→ 获取当前用户 + ↓ +适当性校验(硬拦截,申购专用): + 客户风险等级 ≥ 产品风险等级,否则 1005 直接拒绝,不生成订单 + ↓ +创建 trade_order,status = '待确认' + - 生成唯一 order_no(前缀+日期+序列,如 PO20260912xxxx) + - 填入 order_type / amount / shares / nav(申请口径) + - ★ 不碰账户余额、不碰持仓 + ↓ +同步进入风控规则引擎检测(FR2) + ↓ +├─ 未命中 ──► status='已确认' + 落账(扣款+加仓 / 减仓+入账) +│ + 写 fin_transaction + confirm_time=now +│ +└─ 命中 ────► 生成 fin_risk_alert(未处理) + + trade_order.status='风控挂起' + risk_alert_id 关联 + 账户、持仓保持不动,等待人工处置(FR4) +``` + +**关键约束**: +- 建单 → 检测 → 落账/挂起 **在单个事务内原子完成**(无外部调用,全为 DB + 纯计算)。 +- 落账逻辑**复用**现有 `deduct_balance`/`holdings.upsert`、`holdings.redeem`/`credit_balance`,不重写。 + +### FR2 · 规则引擎检测(纯代码,不用大模型) + +- **同步执行**:在下单请求内当场完成,结果驱动状态流转。 +- **纯规则引擎**:阈值比较 + 历史流水统计,全程 `if/else + SQL`,**不调用 LLM**。 +- **表驱动**:`risk_rule` 表存参数(阈值/级别/权重/启停),代码存判断逻辑。 + +检测输入:当前 `trade_order` + 该客户历史流水 + 客户画像/账户/注册时间。 +检测输出:命中规则列表(规则 ID + 规则名 + 命中详情)。 + +### FR3 · 预警生成 + +- **聚合粒度**:一笔订单命中多条规则,只生成**一条**预警;`alert_type` 取主规则,`trigger_detail` 记录全部命中规则,`alert_level` 取最高档。 +- **分级**:级别取命中规则中 `risk_level` 最高档(低→蓝 / 中→黄 / 高→红)。 +- **置信度**:`confidence = 1 − Π(1 − weight_i)`(命中规则越多、权重越高,置信度越高)。 + +### FR4 · 人工处置(挂起自动,处置人工) + +三个处置动作均为**风控专员手动**,系统绝不自动放行/自动作废: + +| 动作 | 订单 | 预警 | 客户 | 工单 | +|---|---|---|---|---| +| 放行 | `已确认` + 落账 | `已排除` | 正常成交 | 无 | +| 拦截 | `失败`(作废) | `已确认` | 站内信通知 | 生成「可疑交易上报」 | +| 冻结 | `失败`(作废) | `已确认` | `sys_user.status=冻结` + 站内信通知 | 生成「冻结复核」 | + +**约束**: +- 处置接口的 `handler_id` **由后端从当前登录用户获取**,不信任前端传入。 +- 处置接口需校验角色:风控专员(`user_type=EMPLOYEE` 且 `employee_role=风控专员`,或 `ADMIN`)。 +- 处置结果回写订单与预警表(`handler_id`/`handle_result`/`handle_time`),全程留痕可追责。 +- **放行时重新校验余额**:挂起期间余额可能变化,申购放行若余额不足则放行失败、订单转「失败」。 + +### FR5 · 工单链 + +- **生成时机**:仅在「拦截」或「冻结」处置时自动创建,系统自动挂起阶段不产生工单。 +- **创建方式**:系统自动 INSERT,预填 `order_type`(可疑交易上报/冻结复核)、`customer_id`、`submitter_id`(当前专员)、`priority`(按预警级别)、`biz_content`(打包命中规则+订单号+预警 id);`status` 默认「待处理」、`current_node` 默认「初审」。 +- **流转(后端接口驱动)**: + +``` + 认领 提交审核 复核通过 + 待处理 ──────────► 处理中 ──────────► 待审核 ──────────► 已完成 + (handler_id=自己) │ + └──────► 已驳回(复核驳回,终态) +``` + +- **职责分离**:复核人 ≠ 处理人,先做**软校验**(仅提示,不强制阻断)。 +- **终点**:工单「已完成」即项目内终点,**不上报人行**(监管报送为系统外动作,不在本项目范围)。 +- **每次流转写 `audit_log`**(谁、何时、从什么态改到什么态)。 + +--- + +## 5. 状态机 + +### 5.1 `trade_order` 订单状态机 + +``` +待确认 ──(未命中)──────► 已确认 ──► 落账 + 写 fin_transaction + │ + └──(命中)──────────► 风控挂起 + │ + ├──(放行)──► 已确认 ──► 落账 + 写 fin_transaction + ├──(拦截)──► 失败(作废)+ 通知客户 + 生成工单 + └──(冻结)──► 失败(作废)+ 客户冻结 + 通知客户 + 生成工单 +``` + +### 5.2 `fin_risk_alert` 预警状态机(无「处理中」中间态) + +``` +未处理 ──(放行)────► 已排除 + │ + ├──(拦截)──────► 已确认 + └──(冻结)──────► 已确认 +``` + +**防并发**:处置接口用条件更新 `WHERE id=? AND status='未处理'`,更新不到(rowcount=0)即视为已被他人处置,返回提示,不重复落账/工单/通知。 + +### 5.3 `biz_work_order` 工单状态机 + +``` +待处理 ──认领──► 处理中 ──提交审核──► 待审核 ──复核通过──► 已完成 + └──复核驳回──► 已驳回(终态) +``` + +--- + +## 6. 数据表设计 + +> 五张表 DDL 已在 `sql/schema.sql`,本次需补 ORM model + repository + schema。 + +### 6.1 `trade_order` 字段映射 + +| 字段 | 来源 | +|---|---| +| `order_no` | 生成唯一单号 `PO+日期+序列` | +| `customer_id` | 当前用户 id | +| `product_id` | 入参 product_id | +| `advisor_id` | 客户自购为 null | +| `order_type` | 申购 / 赎回 | +| `amount` / `shares` | 申购金额+折算份额;赎回份额+折算金额 | +| `nav` | 产品当前净值 | +| `fee` | 0(暂无手续费) | +| `status` | 待确认 / 风控挂起 / 已确认 / 失败 | +| `risk_alert_id` | 挂起时关联预警 id | +| `confirm_time` | 确认时间 | + +### 6.2 `fin_transaction` 成交流水 + +仅在**风控通过、落账成交**时写一笔:`transaction_no`、`order_id`(关联申请单)、`customer_id`、`product_id`、`transaction_type`(申购/赎回)、`amount`、`shares`、`nav`、`fee`、`status='已确认'`。 + +### 6.3 `fin_risk_alert` 预警 + +| 字段 | 填什么 | +|---|---| +| `customer_id` | 交易客户 | +| `order_id` | 触发预警的申请单 | +| `alert_type` | 主命中规则名 | +| `alert_level` | 低(蓝)/中(黄)/高(红),取命中规则最高档 | +| `trigger_detail` | 全部命中规则 + 触发详情 | +| `transaction_ids` | 关联历史交易 ID 列表(聚合规则用) | +| `confidence` | 预警置信度 | +| `status` | 未处理 → 已确认 / 已排除 | +| `handler_id` / `handle_result` / `handle_time` | 处置留痕 | + +### 6.4 `risk_rule` 规则(表驱动) + +20 条规则配置见 §7,字段:`rule_id` / `rule_name` / `trigger_condition` / `threshold`(JSON) / `risk_level` / `weight` / `status`。 + +### 6.5 `biz_work_order` 工单 + +见 §4 FR5,`work_order_no` 生成 `WO+日期+序列`。 + +--- + +## 7. 规则清单(20 条) + +### A 类 · 单笔规则(只看当前订单) + +| 编号 | 规则名 | 触发条件 | 阈值 JSON | 级别 | 权重 | +|---|---|---|---|---|---| +| R001 | 大额申购-红 | 单笔申购金额 ≥ 100万 | `{"amount":1000000}` | 高 | 0.90 | +| R002 | 大额赎回-红 | 单笔赎回金额 ≥ 100万 | `{"amount":1000000}` | 高 | 0.90 | +| R003 | 大额申购-黄 | 单笔申购金额 ≥ 50万 | `{"amount":500000}` | 中 | 0.70 | +| R004 | 大额赎回-黄 | 单笔赎回金额 ≥ 50万 | `{"amount":500000}` | 中 | 0.70 | +| R014 | 适当性异常 | 客户风险等级 < 产品风险等级 | `{}` | 高 | 0.95 | +| R020 | 夜间交易 | 下单时间 00:00–06:00 | `{"start":"00:00","end":"06:00"}` | 低 | 0.35 | + +### B 类 · 聚合规则(查客户历史流水) + +| 编号 | 规则名 | 触发条件 | 阈值 JSON | 级别 | 权重 | +|---|---|---|---|---|---| +| R005 | 高频交易 | 近 7 天申赎 ≥ 10 笔 | `{"days":7,"count":10}` | 中 | 0.65 | +| R006 | 当日密集交易 | 近 1 天申赎 ≥ 5 笔 | `{"days":1,"count":5}` | 中 | 0.60 | +| R007 | 密集小额申购 | 近 3 天 ≥ 5 笔、每笔 < 1万 | `{"days":3,"count":5,"max_amount":10000}` | 低 | 0.50 | +| R008 | 短炒赎回 | 同产品申购后 7 天内赎回 | `{"days":7}` | 中 | 0.65 | +| R009 | 快进快出 | 同产品申购后 3 天内全额赎回 | `{"days":3}` | 高 | 0.85 | +| R010 | 资金快进快出 | 入账后 24 小时内赎回 | `{"hours":24}` | 高 | 0.90 | +| R011 | 短期累计大额 | 近 7 天累计申赎 ≥ 200万 | `{"days":7,"amount":2000000}` | 高 | 0.85 | +| R012 | 中期累计大额 | 近 30 天累计申赎 ≥ 500万 | `{"days":30,"amount":5000000}` | 中 | 0.65 | +| R013 | 拆分规避大额 | 同 1 天多笔合计 ≥ 100万、单笔 < 50万 | `{"days":1,"total":1000000,"each_lt":500000}` | 中 | 0.70 | +| R018 | 全额清仓赎回 | 赎回后该产品持仓归 0 且金额 ≥ 10万 | `{"amount":100000}` | 低 | 0.45 | +| R019 | 对倒交易 | 近 7 天多产品互买互卖 | `{"days":7}` | 中 | 0.70 | + +### C 类 · 客户维度规则(看画像/账户/注册时间) + +| 编号 | 规则名 | 触发条件 | 阈值 JSON | 级别 | 权重 | +|---|---|---|---|---|---| +| R015 | 新客户大额 | 注册 < 30 天且单笔 ≥ 50万 | `{"days":30,"amount":500000}` | 中 | 0.65 | +| R016 | 休眠账户激活 | 90 天无交易后首笔 ≥ 10万 | `{"days":90,"amount":100000}` | 低 | 0.55 | +| R017 | 整数金额 | 金额为整万且 ≥ 10万 | `{"amount":100000}` | 低 | 0.40 | + +**统计**:高 6 条、中 8 条、低 6 条。 + +**规则启用状态**:R019 对倒交易初始 `status=停用`(实现最复杂、Mock 下难触发),其余 19 条默认启用。 + +--- + +## 8. 接口清单 + +### 8.1 交易接口(改造) + +| 接口 | 说明 | +|---|---| +| `POST /api/purchase` | 改造为前置拦截:适当性校验 → 建单 → 检测 → 落账/挂起 | +| `POST /api/redeem` | 同上(赎回无适当性校验) | + +### 8.2 处置接口(新增,风控专员) + +| 接口 | 动作 | 副作用 | +|---|---|---| +| `POST /api/risk/alert/{id}/release` | 放行 | 订单已确认 + 落账;预警已排除 | +| `POST /api/risk/alert/{id}/block` | 拦截 | 订单失败 + 通知;预警已确认 + 生成工单 | +| `POST /api/risk/alert/{id}/freeze` | 冻结 | 订单失败 + 客户冻结 + 通知;预警已确认 + 生成工单 | + +### 8.3 工单接口(新增) + +| 接口 | 动作 | 状态变化 | +|---|---|---| +| `GET /api/work-order/list` | 列表 | — | +| `GET /api/work-order/{id}` | 详情 | — | +| `POST /api/work-order/claim` | 认领 | 待处理 → 处理中 | +| `POST /api/work-order/submit-review` | 提交审核 | 处理中 → 待审核 | +| `POST /api/work-order/review` | 复核 | 待审核 → 已完成 / 已驳回 | + +### 8.4 规则引擎(内部服务,非 HTTP 接口) + +- `detect(order, customer) -> 命中规则列表`:加载启用规则 → 按 `rule_id` 路由到判断函数 → 传入交易数据 + `threshold` 执行 → 汇总命中。 + +--- + +## 9. 关键设计约束(汇总) + +1. **单事务原子**:建单 → 检测 → 落账/挂起在单个事务内完成。 +2. **落账复用**:复用现有扣款/加仓、减仓/入账逻辑,不重写。 +3. **纯代码检测**:规则匹配用 `if/else + SQL`,不用 LLM。 +4. **人工处置**:挂起自动,放行/拦截/冻结人工,系统不自动处置。 +5. **防并发**:处置接口条件更新 `status='未处理'`,防重复处置。 +6. **handler_id 后端取**:不信任前端传入;处置/工单接口均校验角色权限。 +7. **放行重新校验余额**:防挂起期间余额变化导致落账失败。 +8. **预警聚合**:一笔订单一条预警;级别取最高;置信度按权重合成。 +9. **工单后置**:仅拦截/冻结时生成,为处置的下游产物。 +10. **审计留痕**:处置与工单流转均写 `audit_log`。 + +--- + +## 10. 实施分期 + +``` +Step 1 建 5 张表的 model + repo + schema +Step 2 改 purchase/redeem:适当性校验 → 建单(待确认) → 调风控引擎 +Step 3 写规则引擎(读 risk_rule + 查历史流水,输出命中列表) +Step 4 命中分支:挂起 + 生成预警;未命中:落账 + 写流水 +Step 5 处置接口(放行/拦截/冻结)+ 权限 + 防并发 + 审计 +Step 6 工单链(自动建单 + claim/review 接口) ← 可后置 +``` + +--- + +## 11. 验收标准 + +1. **正常申购/赎回**:未命中规则 → 订单「已确认」、账户/持仓变动、`fin_transaction` 生成一条成交流水。 +2. **命中挂起**:大额申购(如 ≥100万)→ 订单「风控挂起」、生成一条 `fin_risk_alert`(未处理)、账户与持仓不变。 +3. **放行**:风控专员放行 → 订单「已确认」+ 落账、预警「已排除」。 +4. **拦截**:风控专员拦截 → 订单「失败」、预警「已确认」、生成「可疑交易上报」工单、客户收到站内信。 +5. **冻结**:风控专员冻结 → 订单「失败」、预警「已确认」、客户 `sys_user.status=冻结`、生成「冻结复核」工单。 +6. **防并发**:同一预警被两次处置,第二次返回"已被处理",不重复落账/工单/通知。 +7. **权限**:非风控专员调用处置接口被拒绝。 +8. **工单链**:拦截/冻结生成的工单能走完「待处理 → 处理中 → 待审核 → 已完成/已驳回」。 +9. **留痕**:处置与工单流转均有 `audit_log` 记录(谁、何时、从什么态到什么态)。 + +--- + +## 12. 已确认决策清单 + +| # | 决策点 | 结论 | +|---|---|---| +| 1 | 风控时机 | 前置拦截(先建单 → 检测 → 通过才落账) | +| 2 | 检测方式 | 同步 + 纯规则引擎,不用 LLM | +| 3 | 落表 | `trade_order` 为风控主表,`fin_transaction` 为成交凭证 | +| 4 | 预警粒度 | 一笔订单聚合一条预警 | +| 5 | 分级置信度 | 级别取最高;`confidence = 1 − Π(1 − weight_i)` | +| 6 | 处置方式 | 挂起自动,放行/拦截/冻结人工 | +| 7 | 预警中间态 | 不加「处理中」,用条件更新防并发 | +| 8 | 工单时机 | 仅拦截/冻结时生成,人工处置之后 | +| 9 | 工单复核分离 | 软校验(仅提示),不强制 | +| 10 | 工单驳回 | 已驳回为终态,不回流 | +| 11 | 工单终点 | 「已完成」即终点,不上报人行 | +| 12 | R019 对倒交易 | 初始停用 | +| 13 | R014 适当性异常 | 保留作兜底 | +| 14 | 单号生成 | 前缀+日期+序列(`PO...` / `WO...`) |