AL-02: risk_suitability_log 重建 21 列对齐 main 基准——底座 SQL 采纳 main 版(product_name/investor_category/match_result 五值/mismatch_type 七值/requires_disclosure/needs_branch_confirm/risk_was_expired/block_response_code/check_source 四值/actor_id/rule_refs JSON + 6 索引);07-说明.md 直接采用 main 版 P0 契约;tests/_ddl.py sqlite 同步(21 列 + core_suitability_rule 新表 + core_customer/core_customer_risk/core_product 扩列,VARCHAR 近似 ENUM 与现有风格一致);修 test_risk_api 位置式 INSERT 补列名。全列 INSERT 验收通过;剩余 15 failed 均为旧契约断言(报告预告作废集,AL-05/06/07 清零)
This commit is contained in:
@@ -180,15 +180,35 @@ CREATE TABLE risk_suitability_log (
|
||||
trace_id VARCHAR(64) NOT NULL,
|
||||
customer_id VARCHAR(64) NOT NULL,
|
||||
product_id VARCHAR(64) NOT NULL,
|
||||
customer_risk_level CHAR(2) NOT NULL,
|
||||
product_risk_level CHAR(2) NOT NULL,
|
||||
product_name VARCHAR(128) NULL COMMENT '判定时产品名快照',
|
||||
customer_risk_level CHAR(2) NOT NULL COMMENT 'L0 C1~C5',
|
||||
product_risk_level CHAR(2) NOT NULL COMMENT '产品最低 R1~R5',
|
||||
investor_category ENUM('ordinary','professional','professional_pending') NOT NULL DEFAULT 'ordinary',
|
||||
match_result ENUM(
|
||||
'allowed','allowed_with_disclosure','forbidden',
|
||||
'risk_expired','professional_exempt'
|
||||
) NOT NULL COMMENT '与 Core check_suitability 输出一致',
|
||||
mismatch_type ENUM(
|
||||
'none','risk_level','risk_expired','age_branch_confirm',
|
||||
'min_subscribe','not_found','professional_exempt'
|
||||
) NOT NULL DEFAULT 'none' COMMENT '阻断/特殊处理原因分类',
|
||||
is_matched TINYINT(1) NOT NULL,
|
||||
is_blocked TINYINT(1) NOT NULL DEFAULT 0,
|
||||
block_reason VARCHAR(512) NULL,
|
||||
request_ref VARCHAR(64) NULL,
|
||||
requires_disclosure TINYINT(1) NOT NULL DEFAULT 0,
|
||||
needs_branch_confirm TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'FM-01 年龄≥70 买 R3+',
|
||||
risk_was_expired TINYINT(1) NOT NULL DEFAULT 0 COMMENT '判定时风评是否已过期',
|
||||
block_reason VARCHAR(512) NULL COMMENT '对人可读原因',
|
||||
block_response_code VARCHAR(32) NULL COMMENT 'API 机器码,如 SUIT_RISK_MISMATCH',
|
||||
check_source ENUM('r02_trade','r02_chat','c11_inquiry','manual') NOT NULL DEFAULT 'r02_trade',
|
||||
actor_id VARCHAR(64) NOT NULL COMMENT '发起者 customer_id / staff_id / svc-trade-suitability',
|
||||
request_ref VARCHAR(64) NULL COMMENT '交易单号 / session_id',
|
||||
profile_l1_version INT UNSIGNED NULL,
|
||||
rule_refs JSON NULL COMMENT '依据规则,如 ["JR-AST-012","FM-03"]',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
KEY idx_trace (trace_id),
|
||||
KEY idx_customer (customer_id, created_at),
|
||||
KEY idx_product (product_id, created_at),
|
||||
KEY idx_blocked (is_blocked, created_at)
|
||||
) ENGINE=InnoDB COMMENT='【交换】适当性记录 · 风控写 / 客户+代理人读';
|
||||
KEY idx_blocked (is_blocked, created_at),
|
||||
KEY idx_match (match_result, created_at),
|
||||
KEY idx_mismatch (mismatch_type, created_at)
|
||||
) ENGINE=InnoDB COMMENT='【交换】适当性记录 · 风控写 / 客户+代理人读 · 字段契约见 07-risk_suitability_log说明.md';
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# risk_suitability_log · P0 字段契约
|
||||
|
||||
> **用途:** 风控 Agent(R-02)写入;客户 / 代理人 Agent **只读**。
|
||||
> **原则:** 一行记录 = 一次 `CoreReadOnlyRepository.check_suitability()` 判定 + 审计上下文。
|
||||
> **SQL:** [01-mysql-共用底座.sql](./01-mysql-共用底座.sql)
|
||||
> **映射代码:** `app/model/suitability.py` → `build_suitability_log_row()`
|
||||
|
||||
---
|
||||
|
||||
## 1. 与 Core 计算器的关系
|
||||
|
||||
```text
|
||||
Core.check_suitability(customer_id, product_id)
|
||||
↓ 返回 dict(match_result / mismatch_type / blocked …)
|
||||
build_suitability_log_row(check, trace_id, actor_id, …)
|
||||
↓
|
||||
INSERT risk_suitability_log ← jinrong_agent 库
|
||||
↓ 只读
|
||||
客户 Agent(本人)· 代理人 Agent(名下客户)
|
||||
```
|
||||
|
||||
**不在此表存:** C×R 规则本身(在 `jinrong_core.core_suitability_rule`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 字段 ↔ check_suitability 映射
|
||||
|
||||
| 表字段 | 来源 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `customer_risk_level` | `customer_risk_code` | L0 正式 C 档 |
|
||||
| `product_risk_level` | `product_risk_code` | 产品最低 R 档 |
|
||||
| `product_name` | `product_name` | 快照,避免日后改名 |
|
||||
| `investor_category` | `investor_category` | 判定时投资者类别 |
|
||||
| `match_result` | `match_result` | 见下表枚举 |
|
||||
| `mismatch_type` | `mismatch_type` | 见下表枚举 |
|
||||
| `is_matched` | `matched` | 是否视为可匹配 |
|
||||
| `is_blocked` | `blocked` | 是否阻断请求 |
|
||||
| `requires_disclosure` | `requires_disclosure` | 是否需风险揭示书 |
|
||||
| `needs_branch_confirm` | `needs_branch_confirm` | FM-01 网点确认 |
|
||||
| `risk_was_expired` | `risk_is_expired` | FM-03 风评过期 |
|
||||
| `block_reason` | `reason` | 对人可读 |
|
||||
| `block_response_code` | `block_response_code` | API 机器码 |
|
||||
|
||||
---
|
||||
|
||||
## 3. match_result 枚举
|
||||
|
||||
| 值 | 含义 | 典型场景 |
|
||||
| --- | --- | --- |
|
||||
| `allowed` | 完全匹配 | C2 买 R2 |
|
||||
| `allowed_with_disclosure` | 可买但需揭示书 | C3 买 R4 |
|
||||
| `forbidden` | 等级不配 | C1 买 R4 |
|
||||
| `risk_expired` | 风评过期 | FM-03 |
|
||||
| `professional_exempt` | 专业投资者豁免 | 已认定 professional |
|
||||
|
||||
---
|
||||
|
||||
## 4. mismatch_type 枚举
|
||||
|
||||
| 值 | 含义 | is_blocked 常见 |
|
||||
| --- | --- | --- |
|
||||
| `none` | 无特殊问题 | 0 |
|
||||
| `risk_level` | C/R 矩阵禁止 | 1 |
|
||||
| `risk_expired` | 风评过期 | 1 |
|
||||
| `age_branch_confirm` | 高龄买 R3+ 需网点 | 1 |
|
||||
| `min_subscribe` | 起购金额不足(P0 预留) | 1 |
|
||||
| `not_found` | 客户或产品不存在 | 1 |
|
||||
| `professional_exempt` | 豁免路径 | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 5. block_response_code(P0)
|
||||
|
||||
| 代码 | 条件 |
|
||||
| --- | --- |
|
||||
| `SUIT_OK` | 匹配且可继续 |
|
||||
| `SUIT_NEED_DISCLOSURE` | 需签署风险揭示书 |
|
||||
| `SUIT_RISK_MISMATCH` | C/R 不配 |
|
||||
| `SUIT_RISK_EXPIRED` | 风评过期 |
|
||||
| `SUIT_AGE_CONFIRM` | 需网点确认(FM-01) |
|
||||
| `SUIT_PROFESSIONAL_EXEMPT` | 专业投资者豁免 |
|
||||
| `SUIT_NOT_FOUND` | 客户或产品不存在 |
|
||||
| `SUIT_MIN_SUBSCRIBE` | 起购不足(预留) |
|
||||
|
||||
---
|
||||
|
||||
## 6. check_source / actor_id
|
||||
|
||||
| check_source | 触发场景 | actor_id 示例 |
|
||||
| --- | --- | --- |
|
||||
| `r02_trade` | 交易前钩子 | `svc-trade-suitability` |
|
||||
| `r02_chat` | 风控 Agent 对话判定 | `STAFF-30001` |
|
||||
| `c11_inquiry` | 客户问「能否买」 | `CUST-DEMO-A` |
|
||||
| `manual` | 人工补录 | 员工 staff_id |
|
||||
|
||||
---
|
||||
|
||||
## 7. 读写权限(不变)
|
||||
|
||||
| Agent | 权限 |
|
||||
| --- | --- |
|
||||
| 风控 | **写** |
|
||||
| 客户 | **读**(仅本人) |
|
||||
| 代理人 | **读**(仅名下客户) |
|
||||
| 数据分析 | 不用 |
|
||||
|
||||
---
|
||||
|
||||
## 8. P0 验收
|
||||
|
||||
- [ ] R-02 写入行与 `check_suitability` 返回值逐项一致
|
||||
- [ ] 阻断时同时写 `audit_log`(同 `trace_id`)
|
||||
- [ ] 客户 Agent 只读本人;代理人只读 `customer_advisor_rel` 名下
|
||||
- [ ] `blocked=true` 仅 R-02 / 交易钩子 API 可对外返回
|
||||
+30
-7
@@ -17,20 +17,35 @@ SQLITE_TABLES: dict[str, str] = {
|
||||
"core_customer": """
|
||||
CREATE TABLE core_customer (
|
||||
customer_id VARCHAR(64) PRIMARY KEY, display_name VARCHAR(128), age INTEGER,
|
||||
occupation VARCHAR(64), open_date DATE, is_active TINYINT DEFAULT 1)
|
||||
occupation VARCHAR(64), open_date DATE, is_active TINYINT DEFAULT 1,
|
||||
is_hnw TINYINT DEFAULT 0, service_tier VARCHAR(16) DEFAULT 'normal')
|
||||
""",
|
||||
"core_customer_risk": """
|
||||
CREATE TABLE core_customer_risk (
|
||||
customer_id VARCHAR(64), risk_code VARCHAR(8), evaluated_at TIMESTAMP)
|
||||
customer_id VARCHAR(64), risk_code VARCHAR(8), evaluated_at TIMESTAMP,
|
||||
expires_at TIMESTAMP, is_authoritative TINYINT DEFAULT 1,
|
||||
source VARCHAR(32) DEFAULT 'risk_questionnaire',
|
||||
investor_category VARCHAR(32) DEFAULT 'ordinary',
|
||||
questionnaire_score INTEGER, max_loss_tolerance_pct REAL,
|
||||
investment_goal VARCHAR(128), investment_horizon VARCHAR(16),
|
||||
professional_approved_at TIMESTAMP)
|
||||
""",
|
||||
"core_customer_advisor": """
|
||||
CREATE TABLE core_customer_advisor (
|
||||
advisor_id VARCHAR(64), customer_id VARCHAR(64), rel_status VARCHAR(16))
|
||||
""",
|
||||
"core_suitability_rule": """
|
||||
CREATE TABLE core_suitability_rule (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_risk_code VARCHAR(8), product_risk_code VARCHAR(8),
|
||||
match_result VARCHAR(32), rule_ref VARCHAR(32) DEFAULT 'JR-AST-012')
|
||||
""",
|
||||
"core_product": """
|
||||
CREATE TABLE core_product (
|
||||
product_id VARCHAR(64) PRIMARY KEY, product_name VARCHAR(128),
|
||||
min_risk_code VARCHAR(8), product_type VARCHAR(32))
|
||||
min_risk_code VARCHAR(8), product_type VARCHAR(32),
|
||||
min_subscribe_amount DECIMAL DEFAULT 1.00, term_days INTEGER,
|
||||
requires_disclosure TINYINT DEFAULT 0)
|
||||
""",
|
||||
"core_holding": """
|
||||
CREATE TABLE core_holding (
|
||||
@@ -92,11 +107,19 @@ SQLITE_TABLES: dict[str, str] = {
|
||||
""",
|
||||
"risk_suitability_log": f"""
|
||||
CREATE TABLE risk_suitability_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id VARCHAR(64), customer_id VARCHAR(64),
|
||||
product_id VARCHAR(64), customer_risk_level VARCHAR(8), product_risk_level VARCHAR(8),
|
||||
is_matched TINYINT, is_blocked TINYINT, block_reason VARCHAR(512),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, trace_id VARCHAR(64) NOT NULL,
|
||||
customer_id VARCHAR(64) NOT NULL, product_id VARCHAR(64) NOT NULL,
|
||||
product_name VARCHAR(128),
|
||||
customer_risk_level VARCHAR(8) NOT NULL, product_risk_level VARCHAR(8) NOT NULL,
|
||||
investor_category VARCHAR(32) DEFAULT 'ordinary',
|
||||
match_result VARCHAR(32) NOT NULL, mismatch_type VARCHAR(32) DEFAULT 'none',
|
||||
is_matched TINYINT, is_blocked TINYINT DEFAULT 0,
|
||||
requires_disclosure TINYINT DEFAULT 0, needs_branch_confirm TINYINT DEFAULT 0,
|
||||
risk_was_expired TINYINT DEFAULT 0,
|
||||
block_reason VARCHAR(512), block_response_code VARCHAR(32),
|
||||
check_source VARCHAR(16) DEFAULT 'r02_trade', actor_id VARCHAR(64),
|
||||
request_ref VARCHAR(64), profile_l1_version VARCHAR(32),
|
||||
created_at {_TS})
|
||||
rule_refs TEXT, created_at {_TS})
|
||||
""",
|
||||
"customer_profile_l3": f"""
|
||||
CREATE TABLE customer_profile_l3 (
|
||||
|
||||
@@ -73,7 +73,7 @@ def env():
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO core_product VALUES"
|
||||
"INSERT INTO core_product (product_id, product_name, min_risk_code, product_type) VALUES"
|
||||
" ('PROD-161725', '科技成长主题', 'R4', 'mixed'),"
|
||||
" ('PROD-510300', '沪深300指数', 'R3', 'index')"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user