AL-08: 演示库重灌与种子核对对齐 main 基准——prepare_risk_demo.sql 改写 expires_at 口径(演示客户刷新为测评 90 天前/剩余 275 天,CUST-1004 保持过期演示 FM-03,替代 SUIT-008 evaluated_at+365 旧写法);演示 SOP 同步(§2 完成标志 33 客户口径+expires_at 剩余天数,A-1 预期改 SUIT_RISK_MISMATCH,A-2 改 FM-01 SUIT_AGE_CONFIRM 网点确认);网关阻断响应补全 main 契约字段(match_result/mismatch_type/requires_disclosure/needs_branch_confirm/rule_refs)+audit detail 加 block_response_code;集成测试 4 例旧口径断言更新(A-1 trace 一致性断机器码/A-1 audit JSON 断机器码/A-2 重写为 FM-01 语义/settings 退役断言)。SOP §2 脚本化全量重灌双库验证通过(AML 8 条/演示 7 行/sync 33 rows);全量 406 passed 0 failed 0 skipped(集成测试真库走 21 列新表+expires_at 数据恢复执行)

This commit is contained in:
2026-09-07 17:49:39 +08:00
parent ec11ec42c0
commit b5fd52ef02
4 changed files with 37 additions and 19 deletions
+10 -1
View File
@@ -141,13 +141,22 @@ def submit_trade(
trade_id=trade_id,
req=req,
rule_id=result.rule_id,
detail={"block_reason": result.block_reason, "reasons": list(result.reasons)},
detail={
"block_reason": result.block_reason,
"block_response_code": result.block_response_code,
"reasons": list(result.reasons),
},
)
return {
"blocked": True,
"trade_id": trade_id,
"match_result": result.match_result,
"mismatch_type": result.mismatch_type,
"requires_disclosure": result.requires_disclosure,
"needs_branch_confirm": result.needs_branch_confirm,
"block_reason": result.block_reason,
"block_response_code": result.block_response_code,
"rule_refs": result.rule_refs,
"reasons": list(result.reasons),
"advice": ADVICE,
"notice": RECORDED_NOTICE,
@@ -40,7 +40,7 @@ ERROR 1050(表已存在)中止且残留未清。脚本化方式已含双库
(两种方式均不含 Neo4j 同步;风控演示不依赖 Neo4j,需要时手动跑 `scripts/sync/sync_neo4j.py`。)
**重灌完成标志**:`seed-aml-list.sql` 输出 8 条名单;`prepare_risk_demo.sql` 输出 7 行测评
(6 行 90 天、CUST-1004 保持过期);`sync_advisor_rel` 输出 upserted 28 rows。
(6 行测评 90 天前 / expires_at 剩余 275 天,CUST-1004 保持过期演示 FM-03);`sync_advisor_rel` 输出 upserted 33 rows(33 客户口径,AL-01 起)。
## 3. 启动与入口
@@ -63,8 +63,8 @@ Swagger 逐条执行;预期响应与库内核验点如下(`trade_id` 以实
| # | 操作(Swagger) | 预期 |
| --- | --- | --- |
| A-1 | `POST /api/simulate/trade`,customer 头 CUST-1001,body:CUST-1001 / PROD-161725 / subscribe / 10000 | `blocked=true`,`block_reason` 含 SUIT-001;库核验:`core_trade` 无该 trade_id;`risk_suitability_log.is_blocked=1`;`risk_alert`(trade_id 定位)suitability/pending;audit `suitability_blocked`(agent_type=platform);订阅端收到推送且 trace 与响应头一致 |
| A-2 | 同上,risk_demo 头,CUST-4001 / PROD-161725 / subscribe / 20000 | `blocked=true`,reasons 含 SUIT-006(≥70 按 C3)与 SUIT-003,**无 SUIT-008** |
| A-1 | `POST /api/simulate/trade`,customer 头 CUST-1001,body:CUST-1001 / PROD-161725 / subscribe / 10000 | `blocked=true`,`block_response_code=SUIT_RISK_MISMATCH`(AL-05 起 main 契约机器码,SUIT-001 退役);库核验:`core_trade` 无该 trade_id;`risk_suitability_log.is_blocked=1`;`risk_alert`(trade_id 定位)suitability/pending;audit `suitability_blocked`(agent_type=platform);订阅端收到推送且 trace 与响应头一致 |
| A-2 | 同上,risk_demo 头,CUST-4001 / PROD-161725 / subscribe / 20000 | `blocked=true`,`block_response_code=SUIT_AGE_CONFIRM`(AL-05 起 FM-01 网点确认替换 SUIT-006 封顶:70 岁买 R4≥R3 一律阻断待网点当面确认),`needs_branch_confirm=true` |
| A-3 | 同上,CUST-3001 / PROD-510300 / subscribe / 500000 | `blocked=false`,`triggered_rules=["RISK-001","RISK-002"]`,预警单 score=70/pending;`core_trade` 落库;audit `trade_accepted`;**订阅端实时打印推送** |
| A-4 | 同上,CUST-9527 / PROD-510300 / subscribe / 1000,连发 4 笔 | 第 3 笔触发 RISK-003;第 4 笔 `alert_ids` 与第 3 笔相同(并入同单),该客户 freq 类单数=1,payload.events 长度=2 |
| A-5 | 同上,CUST-1002 / PROD-005828 / subscribe / 100 | `aml_hit=true`;aml 独立单 score=95(按本笔 trade_id 定位);L3 `monitor_tier=high`;compliance 头 `GET /api/risk/alerts` 仅见 aml 单且含该单;`core_customer.is_active` 仍=1(不冻户) |
+13 -6
View File
@@ -1,7 +1,9 @@
-- =============================================================================
-- 风控演示数据准备(PRD §10.2 · 每次 reset.ps1 重建全库后需重跑)
-- 用途:刷新演示客户的风险测评日期,否则 SUIT-008(测评有效期 365 天)
-- 会阻断所有演示交易;CUST-1004 保留过期,用于演示 SUIT-008 阻断路径。
-- 用途:刷新演示客户的风险测评有效期,否则 FM-03(风评过期,expires_at 驱动)
-- 会阻断所有演示交易;CUST-1004 保留过期,用于演示 FM-03 过期阻断路径。
-- AL-08 对齐 main 契约:有效期由 core_customer_risk.expires_at 数据驱动
-- (evaluated_at+365 天口径与 settings.risk_assessment_valid_days 已退役)。
-- 依赖:scripts/core/reset.ps1 已执行(jinrong_core 已建库灌种子)
-- 验收对照:docs/PRD/PRD-风控监测Agent.md §8 A-1~A-5
-- =============================================================================
@@ -9,12 +11,17 @@
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
-- 刷新为「测评 90 天前、有效期剩余 275 天」(合计 365 天,保持原语义节奏)
UPDATE core_customer_risk
SET evaluated_at = CURDATE() - INTERVAL 90 DAY,
expires_at = CURDATE() + INTERVAL 275 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
-- 校验(执行后应看到 6 行:测评 90 天前、expires_at 剩余 275 天;
-- CUST-1004 保持种子过期日,演示 FM-03 阻断路径)
SELECT customer_id, risk_code, evaluated_at, expires_at,
DATEDIFF(CURDATE(), evaluated_at) AS days_since_eval,
DATEDIFF(expires_at, CURDATE()) AS days_until_expire
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;
+11 -9
View File
@@ -149,7 +149,7 @@ def _assert_a4_clean(env):
)
# ---------- A-1:C1 申购 R4 → SUIT-001 阻断(含 trace 一致性) ----------
# ---------- A-1:C1 申购 R4 → 矩阵 forbidden 阻断(含 trace 一致性) ----------
def test_a1_suit001_blocked_with_trace_consistency(iclient, risk_demo_env):
@@ -165,7 +165,8 @@ def test_a1_suit001_blocked_with_trace_consistency(iclient, risk_demo_env):
assert r.status_code == 200
body = r.json()
assert body["blocked"] is True
assert "SUIT-001" in body["block_reason"] # C1 仅可购 R1
# AL-05 main 契约:机器码 SUIT_RISK_MISMATCH(SUIT-001 退役)
assert body["block_response_code"] == "SUIT_RISK_MISMATCH"
assert r.headers["X-Trace-Id"] == trace # 中间件透传(PRD §7.5 全链路环①)
trade_id = body["trade_id"]
assert trade_id.startswith("TRD-TEST-")
@@ -204,11 +205,11 @@ def test_a1_audit_input_summary_json(iclient, risk_demo_env):
assert summary["product_id"] == "PROD-161725"
assert summary["trade_type"] == "subscribe"
assert summary["amount"] == "10000"
assert "SUIT-001" in summary["block_reason"]
assert summary["block_response_code"] == "SUIT_RISK_MISMATCH" # AL-05 main 契约
assert isinstance(summary["reasons"], list) and summary["reasons"]
# ---------- A-2:70 岁 C5 → SUIT-006 封顶 + SUIT-003,无 SUIT-008 ----------
# ---------- A-2:70 岁 C5 买 R4 → FM-01 网点确认阻断(替换 SUIT-006 封顶) ----------
def test_a2_age70_cap_suit006_no_suit008(iclient, risk_demo_env):
@@ -222,10 +223,10 @@ def test_a2_age70_cap_suit006_no_suit008(iclient, risk_demo_env):
assert r.status_code == 200
body = r.json()
assert body["blocked"] is True
reasons = body["reasons"]
assert any("SUIT-006" in x for x in reasons), reasons # ≥70 按 C3 封顶
assert any("SUIT-003" in x for x in reasons), reasons # C3 < R4 不匹配
assert not any("SUIT-008" in x for x in reasons), reasons # 测评已刷新,不得干扰
# AL-05 main 契约:≥70 买 R3+ 一律阻断待网点当面确认(FM-01 替换 SUIT-006 封顶)
assert body["block_response_code"] == "SUIT_AGE_CONFIRM"
assert body["needs_branch_confirm"] is True
assert any("网点当面确认" in x for x in body["reasons"]), body["reasons"]
trade_id = body["trade_id"]
assert _core_one(env, "SELECT * FROM core_trade WHERE trade_id = :t", t=trade_id) is None
_state["a2_trade_id"] = trade_id
@@ -446,4 +447,5 @@ def test_missing_trace_header_generates_one(iclient):
def test_settings_thresholds_loaded():
"""冒烟:阈值配置与冻结规则一致(.env 未覆盖时)。"""
assert settings.risk_large_amount == 500000
assert settings.risk_assessment_valid_days == 365
# risk_assessment_valid_days 已随 AL-05 退役(有效期由 core_customer_risk.expires_at 驱动)
assert not hasattr(settings, "risk_assessment_valid_days")