diff --git a/.env.example b/.env.example index be8cba7..0020473 100644 --- a/.env.example +++ b/.env.example @@ -30,3 +30,15 @@ DEEPSEEK_BASE_URL=https://api.deepseek.com # JWT (dev only — production use RS256 + IdP) JWT_DEV_SECRET=change-me-in-dev-only + +# Risk thresholds (defaults = frozen rules, see docs/PRD/附-风控规则表.md) +RISK_ASSESSMENT_VALID_DAYS=365 +RISK_LARGE_AMOUNT=500000 +RISK_DAILY_TOTAL=500000 +RISK_FREQ_COUNT=3 +RISK_PROBE_WINDOW_MINUTES=5 +RISK_PROBE_COUNT=3 +RISK_PROBE_AMOUNT=400000 +RISK_SMALL_AMOUNT=10000 +RISK_SMALL_COUNT=3 +RISK_AML_DEFAULT_THRESHOLD=0.85 diff --git a/docs/PRD/PRD-风控监测Agent.md b/docs/PRD/PRD-风控监测Agent.md new file mode 100644 index 0000000..961b80e --- /dev/null +++ b/docs/PRD/PRD-风控监测Agent.md @@ -0,0 +1,337 @@ +# PRD · 风控监测 Agent 模块 + +> 版本:v1.0(**已冻结** · 2026-09-06 用户确认,风控模块开发唯一依据) +> 日期:2026-09-06 +> 负责人:E(风控/记忆/测试)· 分支 `feature/risk` +> 上游依据:`docs/需求拆解/Agent风险与合规约束汇总.md` §5、`docs/memory/REQUIREMENTS.md` Wave 2、`docs/memory/FRAMEWORK.md`、`docs/项目框架设计/表设计/`(表结构 · Redis key)、规则冻结版 `docs/PRD/附-风控规则表.md` +> 状态流转:草稿 → AI 评审 → 修订 v0.2 → 复审 → 修订 v0.3 → **用户确认冻结 v1.0** +> 修订记录: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 文档联动修正已执行 + +--- + +## 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)` + +--- + +## 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 | + +**通用验收**:所有预警单初始 `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`) | + +--- + +*复审通过并经用户确认后冻结为 v1.0,作为风控模块开发唯一依据。* diff --git a/docs/PRD/附-风控规则表.md b/docs/PRD/附-风控规则表.md new file mode 100644 index 0000000..ca4c84c --- /dev/null +++ b/docs/PRD/附-风控规则表.md @@ -0,0 +1,144 @@ +# 附表 · 风控规则表(冻结版) + +> 版本:v1.0(**已冻结** · 随 PRD v1.0 一并冻结,变更需 PR 审批) +> 定位:团队共识文档。任何 Agent 涉及以下规则时,必须在此表查找,不得自行发明。 +> 编号体系:`SUIT-*` 适当性 / `RISK-*` 事件规则引擎 / `AML-*` 名单 / `AI-*` AI 行为边界 / `TRADE-*` 交易时间 / `DESENS-*` 脱敏 / `MOCK-*` 模拟数据 +> 与 PRD 的关系:本表是 PRD《风控监测Agent》FR-2/FR-3/FR-5/§7 引用的规则唯一权威来源。 +> 变更:冻结后变更需 PR 审批并更新版本号。 + +--- + +## 1. 适当性管理规则(SUIT · 硬编码,不可绕过) + +| 规则编号 | 规则内容 | 代码实现位置 | 违规后果 | +|---------|---------|-------------|---------| +| SUIT-001 | 客户风险等级 `C1` 只能购买 `R1` 产品 | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-002 | 客户风险等级 `C2` 只能购买 `R1/R2` 产品 | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-003 | 客户风险等级 `C3` 只能购买 `R1/R2/R3` 产品 | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-004 | 客户风险等级 `C4` 可购买 `R1-R4` 产品 | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-005 | 客户风险等级 `C5` 可购买 `R1-R5` 产品 | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-006 | 年龄 ≥ 70 岁的客户,适当性判定时**按最高 C3 封顶**(测评得 C4/C5 仍按 C3 校验);`age IS NULL` 跳过本条并在 reasons 标注"年龄缺失,建议人工复核" | `app/service/suitability.py` | 适当性违规 -10 | +| SUIT-007 | Agent 生成**产品推荐**前必须调用 `suitability_check()`;不匹配时**禁止生成推荐语**,必须输出拒绝话术(含原因 + 引导联系持证投顾)。推荐输出须附免责声明并标注"需经持证投顾审核",禁止具体操作指令(见 AI-006) | 客户财富 / 代理人助手 Agent | 适当性违规 -10 | +| SUIT-008 | 客户风险等级有效期为 **365 天**(`.env: RISK_ASSESSMENT_VALID_DAYS` 可配),超期后等级矩阵照常计算,但**阻断新产品购买**并提示重新测评 | `app/service/suitability.py` | 功能缺失 -10 | + +### 适当性匹配矩阵(代码直接用) + +``` + R1 R2 R3 R4 R5 +C1 ✓ ✗ ✗ ✗ ✗ +C2 ✓ ✓ ✗ ✗ ✗ +C3 ✓ ✓ ✓ ✗ ✗ +C4 ✓ ✓ ✓ ✓ ✗ +C5 ✓ ✓ ✓ ✓ ✓ +``` + +> 判定语义(对齐 PRD FR-2):`is_matched` = 纯矩阵结果(含 SUIT-006 封顶后判定);`blocked` = 最终是否阻断(= NOT is_matched 或 SUIT-008 过期)。 + +--- + +## 2. 事件规则引擎(RISK · 风控监测 Agent) + +| 规则编号 | 规则内容 | 触发阈值 | 系统动作 | 代码实现位置 | +|---------|---------|---------|---------|-------------| +| RISK-001 | 单笔大额预警 | 单笔 ≥ 500,000 元 | 预警单(pending_review)+ Redis 推送,**不自动拦截** | `app/service/risk/` | +| RISK-002 | 单日累计大额预警 | 同一客户当日申赎合计 ≥ 500,000 元(口径:`core_trade`) | 同上 | 同上 | +| RISK-003 | 频繁交易预警 | 同一客户同一产品当日申赎合计 ≥ 3 笔 | 同上(当日同客户同类型聚合出单) | 同上 | +| RISK-004 | 接近阈值试探预警 | 5 分钟内 ≥ 3 笔且每笔 ≥ 400,000 元 | 预警单(pattern) | 同上 | +| RISK-005 | 先小后大模式预警 | 当日首次大额前已存在 ≥ 3 笔 ≤ 10,000 元(不要求连续) | 预警单(pattern),文案标注模式 | 同上 | + +### ⚠️ 风控 Agent 行为边界(硬约束) + +- ❌ **禁止自动冻结账户** +- ❌ **禁止自动拦截已发生交易**(唯一例外:SUIT 不匹配时阻断**交易请求**进入流程) +- ❌ **禁止自动调整客户正式风险等级(C1~C5)** +- ❌ **禁止自动上报监管** +- ✅ **只能:监测 → 预警 → 通知 → 记录**(处置由风控专员人工完成) + +--- + +## 3. 反洗钱名单规则(AML) + +| 规则编号 | 规则内容 | 说明 | +|---------|---------|------| +| AML-001 | 名单命中 → 生成 `alert_type='aml'` 最高级预警(risk_score=95)+ 紧急通知(`notify_role=["risk_officer","compliance"]`)+ 标记 L3 `monitor_tier='high'` | **不自动冻结账户**;处置由风控专员/合规官人工完成 | +| AML-002 | 匹配算法一期仅 `display_name` 归一化 + 相似度 ≥ `match_threshold`(默认 0.85);证件/银行卡匹配待 Core 提供证件数据后启用 | 名单镜像表 `risk_aml_list` | + +--- + +## 4. 数据脱敏规则(DESENS) + +| 规则编号 | 字段类型 | 脱敏规则 | 示例 | 代码实现位置 | +|---------|---------|---------|------|-------------| +| DESENS-001 | 身份证号 | 保留前 3 位 + 后 4 位,中间 `****` | `110****1234` | `app/utils/desensitize.py` | +| DESENS-002 | 手机号 | 保留前 3 位 + 后 4 位,中间 `****` | `138****1234` | 同上 | +| DESENS-003 | 姓名 | 保留姓氏,其余 `**` | `张**` | 同上 | +| DESENS-004 | 银行卡号 | 保留后 4 位,其余 `****` | `****1234` | 同上 | +| DESENS-005 | 资产金额 | 前端展示保留整数位,隐藏小数 | `¥1,234,***.**` | 前端组件 | + +### 脱敏原则 + +- **数据库层**:存储原始数据(`customer_id` 为系统内部代理键,**不脱敏**,保证审计索引有效) +- **API 层**:返回脱敏后数据(默认) +- **前端层**:二次确认脱敏(防后端遗漏) +- **LLM 上下文**:传入 LLM 的客户数据必须已脱敏(预警 payload、通知、对话输出) +- **审计 `input_summary`**:脱敏后落库(按上表规则) + +--- + +## 5. AI 行为边界规则(AI) + +| 规则编号 | 规则内容 | 违规后果 | +|---------|---------|---------| +| AI-001 | **AI 不得代替客户执行申购、赎回、转账等交易操作** | 安全事故 -10 | +| AI-002 | 所有投资分析类输出必须附带标准免责声明 | 功能缺失 -5 | +| AI-003 | 产品推荐、投资方案必须标注「需经持证投顾审核」 | 功能缺失 -5 | +| AI-004 | AI 不得生成具体收益承诺(如「保证年化 8%」) | 安全事故 -10 | +| AI-005 | AI 不得询问客户密码、验证码、CVV 等敏感信息 | 安全事故 -10 | +| AI-006 | AI 不得输出具体操作指令("现在买入 X 万元""我帮您下单""帮你调仓"类表述一律拦截);推荐仅限产品与理由 | 安全事故 -10 | + +### 标准免责声明(强制追加) + +``` +本内容仅为投资分析参考,不构成任何直接投资建议,不构成对任何产品的收益承诺, +据此操作风险自负,请谨慎对待。最终投资方案需经持证投资顾问审核确认。 +``` + +### 预警系统级声明(区别于客户免责声明) + +``` +本预警由系统自动生成,最终判定需经风控专员人工审核。 +``` + +--- + +## 6. 交易时间规则(TRADE · 本期风控不涉及,供后续模块沿用) + +| 规则编号 | 规则内容 | 代码实现位置 | +|---------|---------|-------------| +| TRADE-001 | 交易日为周一至周五(不含法定节假日) | `app/utils/trade_calendar.py`(后续模块) | +| TRADE-002 | 交易日 15:00 前提交的申购/赎回,按 T 日净值处理 | 交易模块 | +| TRADE-003 | 交易日 15:00 后提交的申购/赎回,按 T+1 日净值处理 | 交易模块 | +| TRADE-004 | 基金申购确认份额一般为 T+1,查询持仓为 T+2 | 交易模块 | +| TRADE-005 | 非交易日提交的申请,顺延至下一个交易日处理 | 交易模块 | + +> 模拟交易网关一期不校验交易时间窗口(演示可随时发起),真实交易系统接入后由其自行遵守。 + +--- + +## 7. Mock 数据一致性规则(MOCK) + +| 规则编号 | 规则内容 | 校验脚本 | +|---------|---------|---------| +| MOCK-001 | 客户年龄范围:18-80 岁 | `validate_customer_age()` | +| MOCK-002 | 身份证号格式:18 位,校验位正确 | `validate_id_card()` | +| MOCK-003 | 手机号:11 位,以 1 开头 | `validate_phone()` | +| MOCK-004 | 开户时间 < 首次交易时间 | `validate_timeline()` | +| MOCK-005 | 风险测评时间 < 首次购买时间 | `validate_timeline()` | +| MOCK-006 | 单笔交易金额 ≤ 账户总资产 × 120% | `validate_trade_amount()` | +| MOCK-007 | 客户风险等级与年龄匹配(≥70岁最高C3) | `validate_risk_age_match()` | +| MOCK-008 | 交易记录的产品风险等级 ≤ 客户风险等级 | `validate_trade_suitability()` | +| MOCK-009 | 产品收益率范围:货币 1.5%-3%,债券 3%-6%,混合 5%-15%,股票 -20%~30% | `validate_product_return()` | + +--- + +*本表冻结后,任何规则变更需经团队评审并更新版本号。原本地《02-业务规则表》停止维护,以本表为准。* diff --git a/docs/memory/FLOW.md b/docs/memory/FLOW.md index e36e6e1..7326fb0 100644 --- a/docs/memory/FLOW.md +++ b/docs/memory/FLOW.md @@ -20,9 +20,11 @@ ③ Core 模拟库(L0) .\scripts\core\reset.ps1 → 创建 jinrong_core + 28 客户 / 12 产品 / 持仓交易种子 + (风控演示:再执行 scripts/demo/prepare_risk_demo.sql 刷新测评日期 · PRD §10.2) ④ Agent 共用底座(若库未建) mysql -u root -p < docs/项目框架设计/表设计/01-mysql-共用底座.sql + (风控:再建 02-mysql-agent专用.sql 的 risk_aml_list + 灌 scripts/agent/seed-aml-list.sql) ⑤ 同步 python scripts/sync/sync_advisor_rel.py → customer_advisor_rel diff --git a/docs/memory/FRAMEWORK.md b/docs/memory/FRAMEWORK.md index 6a9a79e..ae989e9 100644 --- a/docs/memory/FRAMEWORK.md +++ b/docs/memory/FRAMEWORK.md @@ -49,9 +49,10 @@ ```text api/ → 路由(chat、knowledge、admin);薄,不含业务 【空壳】 -service/ → agent_service、rag_service、memory_service 【空壳】 +service/ → agent_service、rag_service、memory_service、risk/* 【空壳】 tool/ → document_parser、embedding_tool、milvus_tool 【空壳】 repository/ → core_ro(Core 只读);后续 agent 库 Repository 【core_ro 已实现】 +gateway/ → 模拟交易网关(外部 Core 交易系统替身,非 Agent 分层)【PRD v1.0 新增】 model/ → schemas(Pydantic)、entities(ORM) 【占位】 config/ → settings、database 【settings 已实现】 utils/ → response、exceptions、logger 【占位】 @@ -59,12 +60,16 @@ main.py → FastAPI 入口;当前仅 /health 允许:api → service → tool / repository / model / config 禁止:api 直连 Milvus/MySQL 写复杂逻辑;tool 写业务流程;repository 写 Core +例外:app/gateway/ 为模拟外部系统模块(PRD v1.0),仅 gateway_repository + 可 INSERT jinrong_core.core_trade;生产环境由真实交易系统回调替代 ``` **脚本(非 app 包):** ```text scripts/core/ → jinrong_core DDL + 种子 + reset.ps1 +scripts/agent/ → jinrong_agent 种子(risk_aml_list AML 名单等) +scripts/demo/ → 演示数据准备(prepare_risk_demo.sql,reset 后重跑) scripts/sync/ → sync_advisor_rel.py、sync_neo4j.py scripts/dev/ → rbac-seed-reference.md(联调账号) ``` diff --git a/docs/memory/REQUIREMENTS.md b/docs/memory/REQUIREMENTS.md index 953f44b..ae8cbf3 100644 --- a/docs/memory/REQUIREMENTS.md +++ b/docs/memory/REQUIREMENTS.md @@ -35,11 +35,15 @@ ## Wave 2 · 风控 P0 +> 详细需求见 **`docs/PRD/PRD-风控监测Agent.md`(v1.0 已冻结)**;范围含 R-04(P1 顺手做)、R-05 仅预留 L3 接口。 + | ID | 需求 | 验收对照 | 状态 | TODO | | --- | --- | --- | --- | --- | | R-01 | 大额预警 | risk_alert pending_review | 未做 | T-30 | | R-02 | 适当性阻断请求 | 唯一阻断场景 | 未做 | T-31 | | R-03 | AML 命中通知 | 不自动冻户 | 未做 | T-32 | +| R-04 | 频繁/试探模式预警 | 聚合出单 + 去重 | 未做 | T-30 | +| R-05 | L3 画像写入 | 最小写入(最高档合并) | 未做 | T-30 | ## Wave 3 · 客户 P0 diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index 67b997b..a1ba9b2 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -18,6 +18,12 @@ - [ ] T-21 Milvus Lite + kb_product_rules 首批入库 - [ ] 前端 React 多 Agent 入口(HashRouter,`web/` init) +### 风控模块(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 +- [ ] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` + 单测验收 A-8 +- [ ] T-32 预警台账与人工处置 API(`GET /alerts`、`POST /handle`,risk_officer/compliance 权限)+ 对话线(依赖 T-01/T-03/T-07)验收 A-6/A-7 + ## 已完成 - [x] 2026-09-05 init 项目记忆目录 + `app/` 脚手架(api/service/tool/model/config/utils) diff --git a/docs/需求拆解/Agent风险与合规约束汇总.md b/docs/需求拆解/Agent风险与合规约束汇总.md index 040baec..cae0489 100644 --- a/docs/需求拆解/Agent风险与合规约束汇总.md +++ b/docs/需求拆解/Agent风险与合规约束汇总.md @@ -1,6 +1,7 @@ # 四个 Agent 风险与合规约束汇总 > 需求拆解阶段产物 · 2026-09-05 +> **修订 2026-09-06(PRD v1.0 冻结联动)**:产品推荐边界由"全面禁止"修订为"**允许:须过 R-02 适当性校验 + 附免责声明 + 标注'需经持证投顾审核';禁止具体操作指令与收益承诺**"(决策依据 `docs/PRD/PRD-风控监测Agent.md` §2 D2);营销式推荐话术与优劣对比仍禁止(§7 词库不变)。 > 依据:`docs/需求拆解/用户故事`、`业务场景优先级清单.md`、`数据交互矩阵.md` > 用途:研发验收、Prompt/Tool 边界设计、合规评审、测试用例编写 @@ -36,7 +37,7 @@ | 可以做 | 不可以做 | | --- | --- | | 汇总持仓、资金进出、资产分布(纯事实) | 执行转换、下单、自动调仓 | -| 介绍基金/产品官方信息,业绩须溯源 | 对比产品优劣、暗示收益、推荐买卖 | +| 介绍基金/产品官方信息,业绩须溯源 | 对比产品优劣、暗示收益、营销式推荐话术 | | 解读 T+1、涨跌幅等官方交易规则 | 给走势买卖点、替代客户下单 | | 达阈值时陈述跌幅,列出可选动作(加仓/撤出由客户自决) | 说「应该买/卖/加仓/止损」;自动执行 | | 回答现价/净值/涨跌(可用延迟行情) | 承诺实盘时延;做专业毫秒盘口终端 | @@ -55,7 +56,7 @@ | --- | --- | | 产品/规则解读 | 必须溯源官方材料;禁止模型幻觉内容 | | 亏损/波动提醒 | 只陈述事实 + 可选动作清单;禁止指导性买卖语句 | -| 风格匹配 / 产品匹配 | 须联动风控适当性(R-02);只做「是否匹配」说明 | +| 风格匹配 / 产品匹配 | 须联动风控适当性(R-02);推荐产品须附免责声明并标注「需经持证投顾审核」,禁止具体操作指令 | | 投后复盘 / 归因报告 | 必须附免责声明;禁止收益预测 | | 税务提示 | 标明「仅供参考,不构成税务意见」 | | 所有对外可见回复 | 含标准风险提示语(如「投资有风险,决策需自担」类表述) | @@ -171,7 +172,7 @@ A-01 ~ A-09;合规台能力归入本 Agent 配套工作台 | 事项 | 负责方 | 数据分析 Agent | | --- | --- | --- | -| 产品推荐 | 投顾 / 客户 Agent(受限) | **不做** | +| 产品推荐 | 客户/代理人 Agent(须过 R-02 校验 + 免责 + 持证审核标注) | **不做** | | 可疑交易判定与处理 | 风控专员 | **不做** | | 对外产品规则讲解 | 客服 / 代理人 Agent | **不做** | | 事实数据查询与解读 | — | **只做这个** | @@ -267,7 +268,7 @@ R-01 ~ R-05 | --- | --- | --- | --- | --- | | 面向 C 端输出 | **是**(本人) | **否**(严禁直发) | **否**(内部) | **间接**(适当性提示/拒单文案) | | 投资建议 / 收益预测 | **禁止** | **禁止** | **禁止** | **禁止** | -| 产品推荐 | **禁止**(仅匹配说明) | **禁止** | **禁止** | **禁止** | +| 产品推荐 | 允许(须过 R-02 校验 + 免责 + 持证审核标注) | 允许(同左,且不对 C 端直发) | **禁止** | **禁止** | | 修改核心业务数据 | **禁止** | **禁止** | **禁止** | **禁止** | | 阻断交易/请求 | **禁止** | **禁止** | **禁止** | **仅 R-02 可阻断请求** | | 冻结账户 | **禁止** | **禁止** | **禁止** | **禁止**(人工冻户) | diff --git a/docs/项目框架设计/开发计划-风控模块.md b/docs/项目框架设计/开发计划-风控模块.md new file mode 100644 index 0000000..bc3394c --- /dev/null +++ b/docs/项目框架设计/开发计划-风控模块.md @@ -0,0 +1,74 @@ +# 开发计划 · 风控监测 Agent 模块 + +> 版本:v1.1(AI 评审通过:修复 P1×2——假 AuthContext 规范落 deps.py、conftest 测试前置;P2×7 顺手收敛)· 2026-09-06 · 分支 `feature/risk` · 负责人 E +> 依据:PRD v1.0 + 架构设计 v1.1 +> 铁律:**每步完成即测,测试通过才进下一步**;每个任务至少一个 commit,里程碑打 tag;涉及表结构/接口协议的步骤已获 PRD 批准 +> 任务映射:T-31 = 阶段 A(**交付=suitability 服务函数+单测**;check API 随 B6)· T-30 = 阶段 B · T-32 = 阶段 C(**交付=对话线**;台账/处置 API 随 B6)——此边界说明用于对齐 `docs/memory/TODO.md` 文字,M4 时同步修订 TODO + +--- + +## 阶段 A · 基建与公共校验(不依赖 T-01/T-07,可立即开工) + +| # | 任务 | 产出 | 验证方式 | 依赖 | +| --- | --- | --- | --- | --- | +| A1 | `utils/trace.py` + `utils/desensitize.py` + `settings.py` 扩展(risk_* 阈值)+ `.env.example` 同步 | 3 个工具模块 + 配置 | 单测:脱敏 **DESENS-001~004 四规则**(005 资产金额归前端组件,后端不做)、trace set/get | 无 | +| A2 | `core_ro.py` 扩展 `sum_trades_on_date`(其余复用现有方法) | Repository 方法 | 单测/手工 SQL 对照 | **前置:本机已执行 `reset.ps1` 灌库(FLOW §0 ③,一次性本机操作,非等待 T-05 任务)** | +| A3 | `repository/risk_repository.py`(risk_alert / risk_suitability_log / l3 / aml_list 读写) | Repository | 手工 SQL 对照 | agent 库建表(含 risk_aml_list) | +| A4 | `service/suitability.py`:SUIT-001~008 纯函数 + check 服务(落日志)+ **`AuthContext` Pydantic 模型与 `get_auth_context` 工厂签名占位**(实现归 B6) | 公共校验服务 + AuthContext 接口冻结 | **单测:矩阵 25 组合 + 69/70 岁 + NULL 年龄 + 364/365 天(验收 A-8)** | A1、A2、A3 | + +## 阶段 B · 事件驱动线(核心,不依赖 T-07) + +| # | 任务 | 产出 | 验证方式 | 依赖 | +| --- | --- | --- | --- | --- | +| B1 | `service/risk/rules.py`:RISK-001~005 纯函数 | 规则函数 | **单测:各规则命中/不命中 + RISK-004 窗口 + RISK-005 非连续** | A1 | +| B2 | `alert_service.py`:聚合去重(进程内锁 + 锁内 check-insert)+ 审计落库 + `risk:pub:alert` PUBLISH(同步 Redis 单例) | 预警服务 | 单测:聚合合并、risk_score 取 max、去重追加;**并发冒烟:两线程同客户同日首单 → 预警单数=1 且 events[] 含两笔** | A3 | +| B3 | `profile_l3.py`:UPSERT 最高档合并 + tags 追加 + 缓存 DEL | L3 写入 | 单测:normal→high 不降级、AML 后大额不回落 | A3 | +| B4 | `aml_service.py`(归一化+相似度匹配、scan_all)+ `engine.py`(process_trade_event 组装 + 预留客户事件钩子)+ **`scoring.py` 占位签名(FR-7 预留)** | 引擎完整 | 单测:AML 阈值边界;引擎集成冒烟 | B1、B2、B3 | +| B5 | `app/gateway/`(trade_gateway + gateway_repository 仅 INSERT core_trade)+ `api/simulate.py` 薄路由 | 网关 | 集成:convert 400、阻断不落 trade | A4、B4 | +| B6 | **`app/api/deps.py`:`AuthContext`(actor_id/roles/customer_id,字段按 JWT 手册冻结)+ `get_auth_context()` 工厂**——dev 模式从 `X-Debug-Role`/`X-Debug-Actor` 请求头构造、`app_env != development` 启动时检测 debug 头直接拒绝;T-01 就绪后仅替换工厂内部为 JWT 解析,签名不变。另:`api/risk.py` 4 个 API(GET alerts / POST handle / POST suitability/check / POST aml/scan)+ 归属校验(含 compliance 强制 aml 过滤) | 鉴权依赖 + 4 个 API | Swagger 手测 + **权限矩阵(按 debug 头切换角色/身份执行 A-7/A-9 用例)** | A4、B2、**B4**(aml/scan 依赖 scan_all) | +| B7 | `main.py` 集成:路由挂载 + lifespan(双 Engine 单例注入 + Redis 单例 + trace 中间件) | 可运行应用 | `uvicorn` 启动 + `/health` + 全路由可达 | B5、B6 | +| B8 | **`tests/conftest.py`**:a) session fixture 启动校验演示数据就位(CUST-4001 测评 <365 天、risk_aml_list ≥8),缺失则中止并提示先跑 FLOW §0 ③④;b) fixture 幂等代跑 `prepare_risk_demo.sql`;c) teardown 按 `TRD-TEST-` 清 core_trade + 关联 risk_alert/risk_suitability_log/audit_log + 还原 L3 行。集成测试:A-1~A-5、A-7(状态机/compliance 403/GET 强制 aml)、A-9 越权、**trace 一致性断言** | 测试套件 + fixture | `pytest` 全绿 | B7 | +| B9a | 演示/运维脚本开发:`scripts/demo/subscribe_alerts.py`(订阅演示)+ `scripts/demo/rebuild_alerts.py`(按 trade_id 幂等重放补偿) | 2 个脚本 | 手工执行验证 | B2、B4(可与 B5~B8 并行) | +| B9b | 演示链路走查:`reset.ps1` → `prepare_risk_demo.sql` → agent 库建表 → `seed-aml-list.sql` → Swagger 逐条过 **A-1~A-5、A-7~A-9(A-6 归 M3)** | 演示 SOP | 按 PRD §8 验收表逐条打勾 | B8、B9a | + +## 阶段 C · 对话线(依赖 Wave 0 的 T-01 JWT / T-03 输入防护 / T-07 LangGraph) + +| # | 任务 | 产出 | 验证方式 | 依赖 | +| --- | --- | --- | --- | --- | +| C0 | 阶段 C 开工前置:requirements 增补 `pytest-asyncio`(异步 Tool 单测需要) | 依赖落地 | `pytest --version` 正常 | T-07 临近 | +| C1 | `service/risk/chat_tools.py`:alert_query / customer_context / suitability_check / aml_lookup 四个只读 Tool | Tool 集 | 单测:各 Tool 输出结构 + alert_id 溯源 | B6、T-07 | +| C2 | `agent_service.py` 注册 risk 分支 StateGraph(intent → tool → respond)+ DeepSeek + 输出规范(alert_id、仅供参考标注) | 对话闭环 | A-6:"今天有多少待审预警" | C1、T-01、T-03 | +| C3 | A-6 验收 + 边界测试(诱导处置 → 拒绝并引导 API) | 验收记录 | 手测 | C2 | + +--- + +## 里程碑与提交策略 + +| 里程碑 | 内容 | 提交 | +| --- | --- | --- | +| M1 | 阶段 A 完成(suitability 可用) | 逐任务 commit + 打 tag `risk-m1` | +| M2 | 阶段 B 完成(事件线全链路,验收 **A-1~A-5、A-7~A-9**) | 逐任务 commit + 打 tag `risk-m2` | +| M3 | 阶段 C 完成(对话线验收 A-6) | 逐任务 commit + 打 tag `risk-m3` | +| M4 | 收尾:MEMORY/TODO 状态复核与边界文字对齐 | `docs: 风控模块验收记录` | + +**Commit 规范**(自《03-团队分工》内联,该文档当前在仓库外): + +```text +格式: : <描述> +type: feat 新功能 / fix 修复 / docs 文档 / refactor 重构 / test 测试 / chore 辅助 +粒度: 每个任务至少一个 commit(A1、A2…各自独立可回溯);里程碑完成时打 tag +示例: feat: suitability 公共校验服务(SUIT-001~008) +``` + +- **每任务完成 = 代码 + 测试通过 + 验证方式打勾 + 即时更新 `docs/memory/TODO.md` 勾选**,方可进下一任务;M4 仅做最终复核 +- 阶段 B 期间若 T-01/T-02 就绪,B7 的 lifespan 立即接审计中间件(T-02) + +## 已知依赖风险 + +1. **T-05 未执行**:A2/B8 前需本机灌库(`reset.ps1` + 共用底座 SQL,FLOW §0 ③④,一次性本机操作) +2. **T-01 未做 → 假 AuthContext 过渡规范**(B6 落地,零返工): + - `AuthContext` Pydantic 模型(actor_id / roles / customer_id)与 `get_auth_context()` 工厂签名在 A4 冻结 + - dev 模式(`app_env=development`)从 `X-Debug-Role` / `X-Debug-Actor` 请求头构造 AuthContext;**生产环境(`app_env != development`)启动时检测到 debug 依赖注册直接拒绝启动** + - T-01 就绪后仅替换工厂内部为 JWT 解析,接口签名与调用方零改动,无双份维护 + - B8 权限类用例(A-7/A-9)通过切换 debug 头执行,可完整验证权限矩阵 +3. **Ollama 未启动**:不影响风控模块(无 Embedding 依赖) diff --git a/docs/项目框架设计/架构设计-风控模块.md b/docs/项目框架设计/架构设计-风控模块.md new file mode 100644 index 0000000..186278f --- /dev/null +++ b/docs/项目框架设计/架构设计-风控模块.md @@ -0,0 +1,256 @@ +# 架构设计说明书 · 风控监测 Agent 模块 + +> 版本:v1.1(AI 评审通过:修复 P1×5——Redis 同步选型、并发首单锁方案、contextvars 实现约束、测试基建、rebuild 降级为脚本;P2×7 顺手收敛)· 2026-09-06 +> 分支:`feature/risk` · 负责人:E +> 上游:`docs/PRD/PRD-风控监测Agent.md`、`docs/PRD/附-风控规则表.md`、`docs/memory/FRAMEWORK.md` +> 技术选型不变(FastAPI + SQLAlchemy + MySQL 双库 + Redis + LangGraph + DeepSeek),本文只定义**目录划分、模块职责、核心时序、关键技术决策**。 + +--- + +## 1. 设计原则 + +1. **分层遵守 FRAMEWORK §3**:api(薄)→ service(业务)→ repository/tool;**唯一例外** `app/gateway/`(模拟外部交易系统,PRD 授权,仅 `gateway_repository` 可 INSERT `core_trade`) +2. **事件驱动线不用消息队列**:网关落库后进程内同步调规则引擎(阻断演示可靠性优先);Redis Pub/Sub 只做**预警通知广播**,不做事件总线 +3. **规则即纯函数**:RISK-001~005 与 SUIT-001~008 全部实现为无副作用纯函数,输入事实、输出判定,便于单测与阈值调整 +4. **双库无跨库事务**:`core_trade`(core 库)与预警单(agent 库)分开提交,取舍见 §5.3 +5. **审计与 trace_id 用 contextvars 贯通**,业务代码不手工传递 + +--- + +## 2. 目录与文件划分(新增) + +```text +app/ +├── gateway/ # 【PRD 例外层】模拟交易网关 = 外部 Core 交易系统替身 +│ ├── __init__.py +│ ├── trade_gateway.py # 网关编排:生成 trade_id/trace_id → suitability → 落库 → 触发引擎 +│ └── gateway_repository.py # 仅 INSERT jinrong_core.core_trade(不碰其他 core 表) +│ +├── service/ +│ ├── suitability.py # 公共校验:SUIT-001~008 纯函数 + 落 suitability_log +│ └── risk/ +│ ├── __init__.py +│ ├── engine.py # 引擎入口 process_trade_event(trade) → 跑规则 → 聚合出单 +│ │ # → L3 → 推送;预留空钩子 on_customer_created/ +│ │ # on_customer_updated(AML 开户触发,本期 no-op) +│ ├── rules.py # RISK-001~005 纯函数(输入当日流水上下文,输出命中列表) +│ ├── alert_service.py # 预警单聚合/去重/落库/更新 + Pub/Sub 推送 +│ ├── aml_service.py # AML:姓名归一化 + difflib 相似度匹配 + 全量扫描 +│ ├── profile_l3.py # L3 UPSERT(最高档合并 + tags 追加)后 DEL profile:l3:{cid} 缓存 +│ └── scoring.py # R-05 预留:本期静态映射 recompute_customer_score(customer_id) +│ +├── repository/ +│ ├── core_ro.py # 【扩展(最小化)】SUIT 校验复用现有 get_customer_l0 +│ │ # (已含 age+risk_code+evaluated_at)与 get_product; +│ │ # 当日流水明细复用 list_trades(since=当日0点); +│ │ # 仅新增 sum_trades_on_date(当日累计聚合) +│ └── risk_repository.py # agent 库:risk_alert / risk_suitability_log / +│ # customer_profile_l3 / risk_aml_list +│ +├── api/ +│ ├── simulate.py # POST /api/simulate/trade(薄路由 → gateway) +│ └── risk.py # GET /api/risk/alerts、POST /api/risk/alerts/{id}/handle、 +│ # POST /api/risk/suitability/check、POST /api/risk/aml/scan +│ +├── config/ +│ └── settings.py # 【扩展】risk_* 阈值配置(§6) +├── utils/ +│ ├── trace.py # 【新增】contextvars trace_id 生成/读取 +│ └── desensitize.py # 【新增】DESENS-001~005 脱敏纯函数 +└── main.py # 【扩展】挂载 simulate/risk 路由;lifespan:共享 Engine 单例 + # (core+agent 两池,注入各 repository)+ 同步 Redis 单例 + # + trace 中间件注册(T-01 后再加审计中间件) + +chat 对话线(T-32,依赖 T-01/T-03/T-07 就绪后实施): +├── service/agent_service.py # StateGraph(risk) 分支注册(见 §4) +└── service/risk/chat_tools.py # 4 个 Tool:alert_query / customer_context / suitability_check / aml_lookup + +scripts/ +├── agent/seed-aml-list.sql # 已建:AML 名单种子(含演示命中记录) +└── demo/prepare_risk_demo.sql # 已建:演示测评日期刷新(reset 后重跑) + +tests/ +├── test_suitability.py # SUIT 矩阵全组合 + 70 岁/过期/NULL 年龄边界 +├── test_risk_rules.py # RISK-001~005 纯函数用例 +├── test_aml_match.py # 归一化/相似度边界 +└── test_gateway_flow.py # 集成:A-1~A-5 阻断与放行链路 +``` + +**不新增**:tool/(风控不用 Milvus)、model/entities 中无新 ORM(沿用 SQL 文本 + Pydantic schema,与 core_ro 风格一致)。 + +--- + +## 3. 核心时序 + +### 3.1 交易放行链路(正常) + +```text +POST /api/simulate/trade + → api/simulate.py(JWT:risk_demo 或本人)生成 trace_id → utils/trace.set() + → gateway/trade_gateway.py + ① 参数校验(trade_type ∈ {subscribe,redeem},convert→400) + ② suitability.py :: check(customer_id, product_id) + 读 core_ro:customer_risk(C 级+evaluated_at) / customer.age / product.min_risk_code + → SUIT-001~008 纯函数判定 → 落 risk_suitability_log + ③a 不匹配 → alert_service.suitability_alert(去重追加) → 返回 blocked=true+阻断文案 + ③b 匹配 → gateway_repository.insert_trade(core_trade) [core 库提交] + ④ risk/engine.py :: process_trade_event(trade) + rules 跑 RISK-001~005 + aml_service.match_name + → 命中 → alert_service.agg_upsert(聚合/去重)→ profile_l3.upsert + → audit_log INSERT → PUBLISH risk:pub:alert + → 未命中 → audit_log INSERT(decision=pass) + ⑤ 返回 blocked=false + trade_id + 触发规则列表(如有) +``` + +### 3.2 AML 全量扫描 + +```text +POST /api/risk/aml/scan(risk_officer) + → aml_service.scan_all():core_ro 全客户 ↔ risk_aml_list(is_active) + 归一化全等 → 命中;否则 difflib.SequenceMatcher 相似度 ≥ threshold → 命中 + → 每命中客户:独立 aml 预警单(score=95)+ L3 high + audit + PUBLISH(notify_role 含 compliance) +``` + +### 3.3 人工处置 + +```text +POST /api/risk/alerts/{id}/handle(risk_officer) + → 状态机校验 pending_review → 目标态(其余 409)→ UPDATE handler_* 字段 + → audit_log INSERT(agent_type='risk') +``` + +--- + +## 4. 对话线 LangGraph 图(T-32) + +```text +StateGraph: RiskAgentState(messages, intent, tool_results, auth_context) + + entry → intent_node # LLM(DeepSeek) 分类:alert_query / customer_context / + # suitability_check / aml_lookup / chitchat + intent_node →(条件边)→ tool_node[alert_query | customer_context | suitability_check | aml_lookup] + # chitchat 直达 respond + tool_node → respond_node # DeepSeek 汇总;规范:引用必带 alert_id、 + # 评分/分层标注"仅供参考,不自动决策"、不输出处置动作 + respond_node → END + +边界:对话线除 suitability_check(按 FR-2 落日志与预警,属其定义的副作用)外无写 Tool; + 处置引导文案指向 POST /alerts/{id}/handle,图中无其他写 Tool +``` + +Tool 数据源:`risk_repository`(预警台账只读)、`core_ro`(L0)、`customer_profile_l1/l2/l3`(只读);入参均过 AuthContext 归属校验(risk_officer 全量)。 + +--- + +## 5. 关键技术决策 + +### 5.1 trace_id 贯通(contextvars · 含实现约束) + +```python +# utils/trace.py +_trace_id: ContextVar[str] = ContextVar("trace_id", default="") +def new_trace() -> str: tid = f"trc-{uuid4().hex[:16]}"; _trace_id.set(tid); return tid +def current() -> str: return _trace_id.get() +``` + +- api 层中间件:优先透传请求头 `X-Trace-Id`,否则生成 +- suitability_log / risk_alert / audit_log / Pub/Sub payload 全部 `trace.current()`,业务函数不传参 + +**实现约束(防踩坑,编码前必读)**: +1. 中间件必须**在 `call_next` 之前** `set()`——若用 `BaseHTTPMiddleware`,endpoint 内已读不到后续设置;推荐纯 ASGI 中间件或放在鉴权依赖最前 +2. 事件线为同步 def 路由(线程池执行),context 经 `anyio.to_thread` 传播需在 §7 集成测试用断言实测确认 +3. **禁止**在 endpoint/service 内重新 `set()`;后续若引入 `create_task` 后台化必须显式 `contextvars.copy_context()` + +### 5.2 聚合去重实现(PRD FR-4 · 首单并发正确性) + +- **先取锁再 check-insert**(`risk_alert` 无 `(customer_id, date)` 唯一键且冻结不改表,裸 SELECT FOR UPDATE 在并发首单时空结果集锁不住,会产生双单): + - 进程内锁:`threading.Lock` 字典,key=`agg:{customer_id}:{alert_class}:{date}`(alert_class ∈ event/suitability);当前单进程部署即正确,多进程部署时替换为 Redis `SET NX EX` 锁(接口不变) + - **锁内**普通 `SELECT ... WHERE customer_id=? AND status='pending_review' AND alert_type IN (事件类) AND created_at >= 当日 00:00` → 存在则读改写 `payload.events[]` + `triggered_rules` 合并 + `risk_score=max` + `alert_type=最高分规则类型`;不存在 INSERT + - **降级策略**:锁获取超时(>2s)不阻塞交易——放行并照常独立出单(宁多勿漏),记 logger.warning +- suitability 单:同上,键 `customer_id + alert_class='suitability' + payload.product_id` +- Redis `risk:dedup:{cid}:{rule_id}:{date}` 仅作前置短路(规则级防重入),DB 查询为准 +- `date` 口径:服务器本地时区自然日(`datetime.now()`,`traded_at` 同源) + +### 5.3 双库一致性取舍 + +`core_trade` 与预警单分属两库、无跨库事务。顺序固定为 **先 core 后 agent**(交易成立是事实,预警可补偿);引擎异常时 `except` 记 `audit_log(decision='risk_engine_error')` + logger.error。 + +**补偿入口为运维脚本**(不进 HTTP 面,避免动冻结 PRD 的 API 清单):`scripts/demo/rebuild_alerts.py`——按指定日期重放 `core_trade` 给引擎。**重放幂等**:处理每笔前按 `trade_id` 检查该客户当日预警单 `payload.events[]` 是否已含此 `trade_id`,已存在则跳过(防 events/monitor_tags 重复追加)。**不做**分布式事务。 + +### 5.4 金额与时间 + +- 全链路 `Decimal`(SQLAlchemy DECIMAL 原生映射);阈值比较 `Decimal` vs `Decimal(str(env))` +- 时间统一服务器本地时间 `datetime.now()`;`traded_at` 由网关写入;"当日/5 分钟窗口"均基于 `traded_at` + +### 5.5 AML 匹配算法 + +```python +norm = lambda s: re.sub(r"\s+", "", s).lower() # 归一化 +hit = (norm(name) == norm(list_name)) or \ + (SequenceMatcher(None, norm(name), norm(list_name)).ratio() >= threshold) +``` + +- 一期仅 `full_name`(`id_no/bank_card_no` 字段在表中预留,Core 有证件数据后启用精确匹配) +- 阈值默认 0.85,取 `risk_aml_list.match_threshold`(每条名单可独立配置) + +### 5.6 Redis 客户端(同步/异步分线) + +- **事件驱动线:同步 `redis.Redis` 单例**(与同步 Engine、同步调用链一致;lifespan 创建、shutdown 关闭);Pub/Sub PUBLISH 为同步调用 +- **对话线(T-32):LangGraph 异步栈自行引入 `redis.asyncio`**,由 agent_service 管理 +- **两套客户端不共用连接**,各自独立单例;订阅端(演示)见 `scripts/demo/subscribe_alerts.py` + +### 5.7 归属校验落点(全模块统一) + +- 归属校验统一放 **FastAPI 依赖层**(`Depends(get_auth_context)` 内:JWT 解析 + 角色判定 + 归属断言——customer 需 `customer_id == subject`、advisor 走 `core_ro.is_advisor_assigned`(已有方法)、risk_officer 放行、compliance 强制 `alert_type=aml`) +- service 层不重复校验,函数签名接收 `AuthContext`;越权 403 + audit 由依赖层统一产出 + +--- + +## 6. 配置新增(`.env.example` 同步) + +```ini +# ===== Risk(默认值=冻结规则,均可覆盖)===== +RISK_ASSESSMENT_VALID_DAYS=365 +RISK_LARGE_AMOUNT=500000 +RISK_DAILY_TOTAL=500000 +RISK_FREQ_COUNT=3 +RISK_PROBE_WINDOW_MINUTES=5 +RISK_PROBE_COUNT=3 +RISK_PROBE_AMOUNT=400000 +RISK_SMALL_AMOUNT=10000 +RISK_SMALL_COUNT=3 +RISK_AML_DEFAULT_THRESHOLD=0.85 +``` + +--- + +## 7. 测试与验收策略 + +**测试基建**:`requirements.txt` 增补 `pytest`(+`pytest-asyncio` 供 T-32);**不引入第三套测试库**——复用本地 `jinrong_core`/`jinrong_agent` 两库,集成测试前置跑 `reset.ps1` + `prepare_risk_demo.sql`,测试产生的交易统一用 `trade_id` 前缀 `TRD-TEST-`,teardown 按 `trade_id LIKE 'TRD-TEST-%'` + 对应预警/日志清理,不污染演示数据。 + +| 层 | 内容 | 对照 | +| --- | --- | --- | +| 单测 | SUIT 矩阵 25 组合 + SUIT-006(69/70 岁边界、NULL 年龄)+ SUIT-008(364/365 天)| A-8 | +| 单测 | RISK-001~005 纯函数(含 RISK-005 非连续、RISK-004 窗口边界) | 规则表 §2 | +| 单测 | AML 归一化/相似度边界(阈值±) | 规则表 §3 | +| 集成 | 网关链路 A-1~A-5、A-9 越权 | PRD §8 | +| 集成 | 处置状态机:pending→三态、重复处置 409、compliance 调 handle 403 且 GET 仅返 aml 单 | A-7 | +| 集成 | **trace 一致性断言**:响应体 trace_id == suitability_log == risk_alert == audit_log | PRD §7.5 | +| 对话线 | A-6 台账问答(T-32 实施,依赖 T-01/T-03/T-07) | A-6 | +| 演示 | `scripts/demo/prepare_risk_demo.sql` → Swagger 逐条过 A-1~A-9 | PRD §8 | + +--- + +## 8. 与 Wave 0 的集成点(依赖提醒) + +| 集成点 | 说明 | +| --- | --- | +| T-01 JWT | `api/simulate.py`、`api/risk.py` 全部走统一鉴权依赖;`risk_demo` 角色判定在鉴权依赖内 | +| T-02 audit 中间件 | 风控复用统一 audit 写入;网关 `agent_type='platform'`、处置 `'risk'` | +| T-03 输入防护 | 仅对话线(专员输入);事件线无用户输入不受影响 | +| T-04 core_ro | 本模块扩展方法见 §2;扩展后仍是纯 SELECT | +| T-07 LangGraph | 仅 §4 对话线;事件驱动线完全独立可先行 | + +--- + +*开发计划与任务拆分见下一步《开发计划-风控模块》。* diff --git a/docs/项目框架设计/表设计/02-mysql-agent专用.sql b/docs/项目框架设计/表设计/02-mysql-agent专用.sql index 1222c8a..ca3578e 100644 --- a/docs/项目框架设计/表设计/02-mysql-agent专用.sql +++ b/docs/项目框架设计/表设计/02-mysql-agent专用.sql @@ -87,3 +87,22 @@ CREATE TABLE analytics_query_log ( KEY idx_trace (trace_id), KEY idx_sql_hash (sql_hash) ) ENGINE=InnoDB COMMENT='【分析专用】查数 SQL 留痕'; + +-- ⚪ 风控监测 Agent +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 名单本地镜像 · 依据 docs/PRD/PRD-风控监测Agent.md §6.2'; diff --git a/docs/项目框架设计/表设计/02-redis-keys.md b/docs/项目框架设计/表设计/02-redis-keys.md index 862f490..c87046e 100644 --- a/docs/项目框架设计/表设计/02-redis-keys.md +++ b/docs/项目框架设计/表设计/02-redis-keys.md @@ -50,7 +50,7 @@ | Key | 类型 | TTL | 说明 | | --- | --- | --- | --- | -| `risk:pub:alert` | Pub/Sub | — | 新预警广播,payload=`{alert_id, type, customer_id_mask, risk_score}` | +| `risk:pub:alert` | Pub/Sub | — | 新预警广播,payload=`{alert_id, alert_type, customer_id_mask, risk_score, trace_id, notify_role}`(口径以 `docs/PRD/PRD-风控监测Agent.md` FR-4 为准,2026-09-06 修订) | | `risk:dedup:{customer_id}:{rule_id}:{date}` | String | 24h | 同日同规则防重复预警风暴 | ### 2.5 输入防护与限流(F-03) diff --git a/scripts/agent/seed-aml-list.sql b/scripts/agent/seed-aml-list.sql new file mode 100644 index 0000000..15bb0d5 --- /dev/null +++ b/scripts/agent/seed-aml-list.sql @@ -0,0 +1,24 @@ +-- ============================================================================= +-- AML 名单种子(jinrong_agent 库 · 需先执行 docs/项目框架设计/表设计/01-mysql-共用底座.sql +-- 与 02-mysql-agent专用.sql 建 risk_aml_list 表) +-- 依据:docs/PRD/PRD-风控监测Agent.md FR-5 +-- 说明:full_name 与 core_customer.display_name 同为脱敏展示名口径; +-- AML-0001 故意与种子客户 CUST-1002 的展示名「客户·赵**」一致(演示命中,PRD A-5 用例; +-- CUST-1002 测评已被 scripts/demo/prepare_risk_demo.sql 刷新,交易可成功走通 AML 链路)。 +-- ============================================================================= + +USE jinrong_agent; + +INSERT INTO risk_aml_list + (list_id, list_type, full_name, id_no, bank_card_no, match_threshold, source, list_version, effective_date) VALUES +('AML-0001', 'sanction', '客户·赵**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0002', 'terror', '客户·测试命中**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0003', 'sanction', '客户·李**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0004', 'pep', '客户·王**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0005', 'sanction', '张某某', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0006', 'terror', '客户·陈**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0007', 'pep', '客户·刘**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'), +('AML-0008', 'sanction', '客户·周**', NULL, NULL, 0.85, '外部名单镜像(模拟)', 'V2026.09', '2026-01-01'); + +-- 校验 +SELECT list_id, list_type, full_name, is_active FROM risk_aml_list; diff --git a/scripts/core/02-seed-base.sql b/scripts/core/02-seed-base.sql index 7dbbea0..07dd9db 100644 --- a/scripts/core/02-seed-base.sql +++ b/scripts/core/02-seed-base.sql @@ -44,6 +44,10 @@ INSERT INTO core_staff (staff_id, display_name, staff_type, roles) VALUES ('STAFF-30001', '吴风控', 'risk_officer', '["risk_officer"]'), ('STAFF-30002', '郑监测', 'risk_officer', '["risk_officer"]'); +-- 1 风控演示账号(网关演示用,roles 含 risk_demo · 见 docs/PRD/PRD-风控监测Agent.md §10.1) +INSERT INTO core_staff (staff_id, display_name, staff_type, roles) VALUES +('STAFF-90001', '风控演示账号', 'risk_officer', '["risk_officer", "risk_demo"]'); + -- 2 合规专员(兼 advisor 审计台场景用 compliance 单角色) INSERT INTO core_staff (staff_id, display_name, staff_type, roles) VALUES ('STAFF-40001', '孙合规', 'compliance', '["compliance"]'), diff --git a/scripts/demo/prepare_risk_demo.sql b/scripts/demo/prepare_risk_demo.sql new file mode 100644 index 0000000..13e9fb9 --- /dev/null +++ b/scripts/demo/prepare_risk_demo.sql @@ -0,0 +1,20 @@ +-- ============================================================================= +-- 风控演示数据准备(PRD §10.2 · 每次 reset.ps1 重建全库后需重跑) +-- 用途:刷新演示客户的风险测评日期,否则 SUIT-008(测评有效期 365 天) +-- 会阻断所有演示交易;CUST-1004 保留过期,用于演示 SUIT-008 阻断路径。 +-- 依赖:scripts/core/reset.ps1 已执行(jinrong_core 已建库灌种子) +-- 验收对照:docs/PRD/PRD-风控监测Agent.md §8 A-1~A-5 +-- ============================================================================= + +USE jinrong_core; + +-- A-1 CUST-1001(C1) / A-2 CUST-4001(70岁C5) / A-3 CUST-3001(C3) / 其余为通用演示客户 +UPDATE 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'); + +-- 校验(执行后应看到 6 行 evaluated_at 为 90 天前;CUST-1004 保持 2025-01-10 过期) +SELECT customer_id, risk_code, evaluated_at, + DATEDIFF(CURDATE(), evaluated_at) AS days_since_eval +FROM core_customer_risk +WHERE customer_id IN ('CUST-1001','CUST-1002','CUST-1003','CUST-3001','CUST-4001','CUST-9527','CUST-1004') +ORDER BY customer_id;