diff --git a/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go b/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go index 31af9d4..5b70b91 100644 --- a/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go +++ b/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go @@ -9,10 +9,44 @@ import ( const raScoreMax = 1000 const ( - raWeightVerify = 0.10 - raWeightJudicialBase = 0.50 - raWeightCreditBase = 0.40 - raHighRiskThreshold = 400 + raWeightVerify = 0.10 + raWeightJudicialBase = 0.50 + raWeightCreditBase = 0.40 + raHighRiskThreshold = 400 + raForcedFScoreCap = 500 // 双高风险强制 F 档时,ra_score 上限(保留加权分,不置 0) + raForcedFTotalHitThreshold = 5 // 司法 + 借贷命中总条数须超过此值才触发强制 F + raFraudPointsPerHit = 23 // 欺诈每命中一项扣分(非整齐倍数) + + // 司法涉诉扣分系数(件数/条数 × 系数,带非整齐上限) + raJudicialLawsuitTotalPerCase = 47 + raJudicialLawsuitTotalCap = 387 + raJudicialLawsuitWeiPerCase = 38 + raJudicialLawsuitWeiCap = 293 + raJudicialLawsuitBeigaoPerCase = 33 + raJudicialLawsuitBeigaoCap = 247 + raJudicialBreachPerCase = 143 + raJudicialBreachCap = 437 + raJudicialConsumptionPerCase = 187 + raJudicialConsumptionCap = 413 + + // 借贷维度扣分 + raCreditOverduePoints = 287 + raCreditSleepPoints = 143 + raCreditPerformancePoints = 97 + raCreditProbeHitPoints = 103 + raCreditIntentReject = 387 + raCreditIntentReview = 237 + raCreditIntentWeightMult = 4 // Rule_final_weight × 4 + raCreditIntentWeightCap = 243 + + // 身份核验扣分 + raVerifyMismatchPoints = 387 + raVerifyPresenceDesc = 73 + raVerifyPresenceStatus = 47 + + xypModelScoreMin = 350 + xypModelScoreRiskThreshold = 650 // [350,950] 区间中位,低于此值视为高风险 + xypModelScoreRiskCapPerField = 97 // 单项上限,三项合计最高 291 ) // buildDWBG9FB3RA 构建顶层 RA 总体安全评估(千分制,分值越高越安全) @@ -23,27 +57,18 @@ const ( // - ra_judicial_score → calcRAJudicialScore // - ra_verify_score → calcRAVerifyScore // - ra_score → 本方法,身份 10% 固定 + 司法/借贷动态加权 -// - ra_level → raLevelFromScore(ra_score),司法+借贷双高时强制 F +// - ra_level → raLevelFromScore(ra_score),司法+借贷双高时强制 F 档(分数仍加权,上限 500) func buildDWBG9FB3RA(data map[string]interface{}) map[string]interface{} { fraudScore := calcRAFraudScore(data) creditScore := calcRACreditScore(data) judicialScore := calcRAJudicialScore(data) verifyScore := calcRAVerifyScore(data) - total := 0 - level := "F" - if isRAForcedFGrade(judicialScore, creditScore) { - total = 0 + total := calcRAWightedScore(verifyScore, judicialScore, creditScore) + level := raLevelFromScore(total) + if isRAForcedFGrade(data, judicialScore, creditScore) { level = "F" - } else { - wJudicial, wCredit := calcRADynamicWeights(judicialScore, creditScore) - total = int(math.Round( - float64(verifyScore)*raWeightVerify + - float64(judicialScore)*wJudicial + - float64(creditScore)*wCredit, - )) - total = clampRAInt(total, 0, raScoreMax) - level = raLevelFromScore(total) + total = clampRAInt(total, 0, raForcedFScoreCap) } return map[string]interface{}{ @@ -56,6 +81,17 @@ func buildDWBG9FB3RA(data map[string]interface{}) map[string]interface{} { } } +// calcRAWightedScore 三维度动态加权汇总(身份 10% 固定 + 司法/借贷动态 90%) +func calcRAWightedScore(verifyScore, judicialScore, creditScore int) int { + wJudicial, wCredit := calcRADynamicWeights(judicialScore, creditScore) + total := int(math.Round( + float64(verifyScore)*raWeightVerify + + float64(judicialScore)*wJudicial + + float64(creditScore)*wCredit, + )) + return clampRAInt(total, 0, raScoreMax) +} + // calcRADynamicWeights 按司法/借贷风险场景分配剩余 90% 权重(身份固定 10%) func calcRADynamicWeights(judicialScore, creditScore int) (wJudicial, wCredit float64) { hasJudicialRisk := judicialScore < raScoreMax @@ -73,25 +109,99 @@ func calcRADynamicWeights(judicialScore, creditScore int) (wJudicial, wCredit fl } } -// isRAForcedFGrade 司法 + 借贷双重高风险时强制 F 级 -func isRAForcedFGrade(judicialScore, creditScore int) bool { - return judicialScore <= raHighRiskThreshold && creditScore <= raHighRiskThreshold +// isRAForcedFGrade 司法 + 借贷双重高风险且命中总条数 > 5 时强制 F 档 +func isRAForcedFGrade(data map[string]interface{}, judicialScore, creditScore int) bool { + if judicialScore > raHighRiskThreshold || creditScore > raHighRiskThreshold { + return false + } + totalHits := calcRAJudicialHitCount(data) + calcRACreditHitCount(data) + return totalHits > raForcedFTotalHitThreshold } -// raLevelFromScore 由 ra_score 映射等级(千分制,越高越安全) -// A: 800-1000 B: 600-799 C: 400-599 D: 200-399 E: 0-199 +// calcRAJudicialHitCount 统计司法维度命中条数(涉诉件数 + 失信条数 + 限高条数) +func calcRAJudicialHitCount(data map[string]interface{}) int { + judicial := raAsMap(data["judicial"]) + if judicial == nil { + return 0 + } + judicialData := raAsMap(judicial["judicial_data"]) + if judicialData == nil { + return 0 + } + + hits := 0 + if lawsuitStat := raAsMap(judicialData["lawsuitStat"]); lawsuitStat != nil { + for _, section := range lawsuitStat { + sectionMap := raAsMap(section) + if sectionMap == nil { + continue + } + count := raAsMap(sectionMap["count"]) + if count == nil { + continue + } + hits += raAsInt(count["count_total"]) + } + } + hits += len(raAsSlice(judicialData["breachCaseList"])) + hits += len(raAsSlice(judicialData["consumptionRestrictionList"])) + return hits +} + +// calcRACreditHitCount 统计借贷维度命中条数(探针 / 意向 / 借选模型分各算 1 条) +func calcRACreditHitCount(data map[string]interface{}) int { + hits := 0 + + if probe := raAsMap(data["probe"]); probe != nil { + if raAsString(probe["currently_overdue"]) == "1" { + hits++ + } + if raAsString(probe["acc_sleep"]) == "1" { + hits++ + } + if raAsString(probe["currently_performance"]) == "0" { + hits++ + } + if raAsString(probe["result_code"]) == "1" { + hits++ + } + } + + if intent := raAsMap(data["intent"]); intent != nil { + switch raAsString(intent["Rule_final_decision"]) { + case "Reject", "Review": + hits++ + } + } + + if rating := raAsMap(data["rating"]); rating != nil { + for _, key := range []string{"xyp_model_score_high", "xyp_model_score_mid", "xyp_model_score_low"} { + score := raXypModelScore(rating[key]) + if score >= xypModelScoreMin && score < xypModelScoreRiskThreshold { + hits++ + } + } + } + + return hits +} + +// raLevelFromScore 由 ra_score 映射等级(千分制,越高越安全,每档 100 分) +// A: 900-1000 B: 800-899 C: 700-799 D: 600-699 E: 500-599 F: 0-499 func raLevelFromScore(score int) string { switch { - case score >= 800: + case score >= 900: return "A" - case score >= 600: + case score >= 800: return "B" - case score >= 400: + case score >= 700: return "C" - case score >= 200: + case score >= 600: return "D" - default: + case score >= 500: return "E" + default: + return "F" } } @@ -107,20 +217,19 @@ func calcRAFraudScore(data map[string]interface{}) int { } // calcRAFraudRiskPoints 统计欺诈维度风险扣分(仅内部使用,分值越高代表越不安全) +// 每命中一项扣 raFraudPointsPerHit(23)分,多项独立累加 func calcRAFraudRiskPoints(data map[string]interface{}) int { - risk := 0 + hits := 0 // 来源子字段 behavior(JRZQV0MD 行为黑名单) if behavior := raAsMap(data["behavior"]); behavior != nil { if result := raAsMap(behavior["result"]); result != nil { - // behavior.result.black_list = "1" → 命中行为黑名单,扣 500 if raAsString(result["black_list"]) == "1" { - risk += 500 + hits++ } - // behavior.result.black_tag04~12 任意为 "1" → 每个扣 80 for k, v := range result { if strings.HasPrefix(k, "black_tag") && raAsString(v) == "1" { - risk += 80 + hits++ } } } @@ -129,30 +238,28 @@ func calcRAFraudRiskPoints(data map[string]interface{}) int { // 来源子字段 complaint(JRZQVT43 投诉风险筛查) if complaint := raAsMap(data["complaint"]); complaint != nil { if result := raAsMap(complaint["result"]); result != nil { - // complaint.result.score × 10,上限扣 300 - risk += clampRAInt(raAsInt(result["score"])*10, 0, 300) + if raAsInt(result["score"]) > 0 { + hits++ + } } } // 来源子字段 fraud(JRZQV3HM 债务欺诈黑名单) if fraud := raAsMap(data["fraud"]); fraud != nil { - // fraud.hit = 1 → 命中欺诈黑名单,扣 400 if raAsInt(fraud["hit"]) == 1 || raAsString(fraud["hit"]) == "1" { - risk += 400 + hits++ } } // 来源子字段 special(JRZQV7MD 特殊名单) if special := raAsMap(data["special"]); special != nil && len(special) > 0 { switch raAsString(special["Rule_final_decision"]) { - case "Reject": - risk += 350 // 特殊名单建议拒绝 - case "Review": - risk += 200 // 特殊名单建议复议 + case "Reject", "Review": + hits++ } } - return clampRAInt(risk, 0, raScoreMax) + return clampRAInt(hits*raFraudPointsPerHit, 0, raScoreMax) } // calcRACreditScore 借贷/逾期维度安全分(来源:calcRACreditRiskPoints) @@ -168,16 +275,16 @@ func calcRACreditRiskPoints(data map[string]interface{}) int { // 来源子字段 probe(JRZQ4B6C 探针C) if probe := raAsMap(data["probe"]); probe != nil { if raAsString(probe["currently_overdue"]) == "1" { - risk += 300 // 当前逾期 + risk += raCreditOverduePoints } if raAsString(probe["acc_sleep"]) == "1" { - risk += 150 // 睡眠账户 + risk += raCreditSleepPoints } if raAsString(probe["currently_performance"]) == "0" { - risk += 100 // 当前未履约 + risk += raCreditPerformancePoints } if raAsString(probe["result_code"]) == "1" { - risk += 100 // 探针命中风险 + risk += raCreditProbeHitPoints } } @@ -185,28 +292,47 @@ func calcRACreditRiskPoints(data map[string]interface{}) int { if intent := raAsMap(data["intent"]); intent != nil { switch raAsString(intent["Rule_final_decision"]) { case "Reject": - risk += 400 + risk += raCreditIntentReject case "Review": - risk += 250 + risk += raCreditIntentReview } - // intent.Rule_final_weight × 5,上限扣 250 weight := raAsInt(intent["Rule_final_weight"]) if weight > 0 { - risk += clampRAInt(weight*5, 0, 250) + risk += clampRAInt(weight*raCreditIntentWeightMult, 0, raCreditIntentWeightCap) } } - // 来源子字段 rating(JRZQ5E9F 借选指数) + // 来源子字段 rating(JRZQ5E9F / loanRiskTagV21 借选指数) if rating := raAsMap(data["rating"]); rating != nil { - // rating.score 越低风险越高:扣 (500 - score),上限 300 - if ratingScore := raAsInt(rating["score"]); ratingScore > 0 && ratingScore < 500 { - risk += clampRAInt(500-ratingScore, 0, 300) - } + risk += calcRARatingXypModelRiskPoints(rating) } return clampRAInt(risk, 0, raScoreMax) } +// calcRARatingXypModelRiskPoints 根据借选指数三个模型分统计风险扣分 +// xyp_model_score_* 范围 [350,950],越大逾期率越低;-1 为未命中,不参与计分 +func calcRARatingXypModelRiskPoints(rating map[string]interface{}) int { + risk := 0 + for _, key := range []string{"xyp_model_score_high", "xyp_model_score_mid", "xyp_model_score_low"} { + score := raXypModelScore(rating[key]) + if score < xypModelScoreMin || score >= xypModelScoreRiskThreshold { + continue + } + risk += clampRAInt(xypModelScoreRiskThreshold-score, 0, xypModelScoreRiskCapPerField) + } + return risk +} + +// raXypModelScore 解析借选指数模型分,未命中(-1 / 空)返回 -1 +func raXypModelScore(v interface{}) int { + s := raAsString(v) + if s == "" || s == "-1" { + return -1 + } + return raAsInt(v) +} + // calcRAJudicialScore 司法涉诉维度安全分(来源:calcRAJudicialRiskPoints) // 统计方式:满分 1000,根据 judicial.judicial_data 涉诉统计扣分后取补集 func calcRAJudicialScore(data map[string]interface{}) int { @@ -238,14 +364,14 @@ func calcRAJudicialRiskPoints(data map[string]interface{}) int { if count == nil { continue } - risk += clampRAInt(raAsInt(count["count_total"])*80, 0, 400) // 涉诉总件数 - risk += clampRAInt(raAsInt(count["count_wei_total"])*60, 0, 300) // 未结案数 - risk += clampRAInt(raAsInt(count["count_beigao"])*50, 0, 250) // 被告件数 + risk += clampRAInt(raAsInt(count["count_total"])*raJudicialLawsuitTotalPerCase, 0, raJudicialLawsuitTotalCap) + risk += clampRAInt(raAsInt(count["count_wei_total"])*raJudicialLawsuitWeiPerCase, 0, raJudicialLawsuitWeiCap) + risk += clampRAInt(raAsInt(count["count_beigao"])*raJudicialLawsuitBeigaoPerCase, 0, raJudicialLawsuitBeigaoCap) } } - risk += clampRAInt(len(raAsSlice(judicialData["breachCaseList"]))*150, 0, 450) // 失信案件条数 - risk += clampRAInt(len(raAsSlice(judicialData["consumptionRestrictionList"]))*200, 0, 400) // 限高条数 + risk += clampRAInt(len(raAsSlice(judicialData["breachCaseList"]))*raJudicialBreachPerCase, 0, raJudicialBreachCap) + risk += clampRAInt(len(raAsSlice(judicialData["consumptionRestrictionList"]))*raJudicialConsumptionPerCase, 0, raJudicialConsumptionCap) return clampRAInt(risk, 0, raScoreMax) } @@ -263,14 +389,14 @@ func calcRAVerifyRiskPoints(data map[string]interface{}) int { // 来源子字段 triple(YYSYK9R4 三要素验证) if triple := raAsMap(data["triple"]); triple != nil { if state := raAsString(triple["state"]); state != "" && state != "1" { - risk += 400 // 三要素不一致 + risk += raVerifyMismatchPoints } } // 来源子字段 identity(IVYZN2P8 二要素认证) if identity := raAsMap(data["identity"]); identity != nil { if result := raAsInt(identity["result"]); result != 0 { - risk += 400 // 二要素不一致或无记录 + risk += raVerifyMismatchPoints } } @@ -278,10 +404,10 @@ func calcRAVerifyRiskPoints(data map[string]interface{}) int { if presence := raAsMap(data["presence"]); presence != nil { desc := raAsString(presence["desc"]) if strings.Contains(desc, "停机") || strings.Contains(desc, "销号") || strings.Contains(desc, "不在网") { - risk += 80 // 在网状态异常 + risk += raVerifyPresenceDesc } if status := raAsInt(presence["status"]); status > 1 { - risk += 50 // 在网状态码异常 + risk += raVerifyPresenceStatus } } diff --git a/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra_test.go b/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra_test.go index af2399f..be0e152 100644 --- a/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra_test.go +++ b/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra_test.go @@ -58,15 +58,17 @@ func TestBuildDWBG9FB3RAFromSample(t *testing.T) { func TestRALevelFromScore(t *testing.T) { cases := map[int]string{ - 0: "E", - 199: "E", - 200: "D", - 399: "D", - 400: "C", - 599: "C", - 600: "B", - 799: "B", - 800: "A", + 0: "F", + 499: "F", + 500: "E", + 599: "E", + 600: "D", + 699: "D", + 700: "C", + 799: "C", + 800: "B", + 899: "B", + 900: "A", 1000: "A", } for score, want := range cases { @@ -132,6 +134,7 @@ func TestRAForcedFGrade(t *testing.T) { }, "intent": map[string]interface{}{ "Rule_final_decision": "Reject", + "Rule_final_weight": "40", }, "rating": nil, "triple": map[string]interface{}{"state": "1"}, @@ -154,14 +157,162 @@ func TestRAForcedFGrade(t *testing.T) { } ra := buildDWBG9FB3RA(data) - if raAsInt(ra["ra_score"]) != 0 { - t.Fatalf("forced F should have ra_score=0, got %d", raAsInt(ra["ra_score"])) + // 司法 197 + 借贷 0,双风险权重 70%/20%,身份 1000 → 加权分 238,强制 F 档保留分数 + if raAsInt(ra["ra_score"]) != 238 { + t.Fatalf("forced F should keep weighted ra_score=238, got %d", raAsInt(ra["ra_score"])) } if raAsString(ra["ra_level"]) != "F" { t.Fatalf("forced F should have ra_level=F, got %s", raAsString(ra["ra_level"])) } } +func TestIsRAForcedFGradeRequiresMoreThan5Hits(t *testing.T) { + fiveHits := map[string]interface{}{ + "judicial": map[string]interface{}{ + "judicial_data": map[string]interface{}{ + "breachCaseList": []interface{}{ + map[string]interface{}{"caseNo": "1"}, + map[string]interface{}{"caseNo": "2"}, + map[string]interface{}{"caseNo": "3"}, + }, + "consumptionRestrictionList": []interface{}{ + map[string]interface{}{"caseNo": "4"}, + map[string]interface{}{"caseNo": "5"}, + }, + }, + }, + } + if calcRAJudicialHitCount(fiveHits)+calcRACreditHitCount(fiveHits) != 5 { + t.Fatal("test setup want 5 total hits") + } + if isRAForcedFGrade(fiveHits, 150, 100) { + t.Fatal("total hits = 5 should not trigger forced F") + } + + sixHits := map[string]interface{}{ + "probe": map[string]interface{}{"currently_overdue": "1"}, + "judicial": map[string]interface{}{ + "judicial_data": map[string]interface{}{ + "breachCaseList": []interface{}{ + map[string]interface{}{"caseNo": "1"}, + map[string]interface{}{"caseNo": "2"}, + map[string]interface{}{"caseNo": "3"}, + }, + "consumptionRestrictionList": []interface{}{ + map[string]interface{}{"caseNo": "4"}, + map[string]interface{}{"caseNo": "5"}, + }, + }, + }, + } + if calcRAJudicialHitCount(sixHits)+calcRACreditHitCount(sixHits) != 6 { + t.Fatal("test setup want 6 total hits") + } + if !isRAForcedFGrade(sixHits, 150, 100) { + t.Fatal("total hits = 6 with dual high should trigger forced F") + } +} + +func TestRAForcedFGradeScoreCap(t *testing.T) { + // 加权分高于 500 时,强制 F 档封顶 500 + verify, judicial, credit := 1000, 1000, 1000 + raw := calcRAWightedScore(verify, judicial, credit) + if raw != 1000 { + t.Fatalf("baseline weighted score want 1000, got %d", raw) + } + + // 模拟双高:司法/借贷维度分均 ≤ 400,但加权后可能 > 500(此处用边界值构造) + verify, judicial, credit = 1000, 400, 400 + raw = calcRAWightedScore(verify, judicial, credit) + // 1000×0.10 + 400×0.70 + 400×0.20 = 100+280+80 = 460 + if raw != 460 { + t.Fatalf("dual-risk weighted score want 460, got %d", raw) + } + + data := map[string]interface{}{ + "behavior": nil, "complaint": nil, "fraud": nil, "special": nil, + "probe": map[string]interface{}{"currently_overdue": "1"}, + "intent": map[string]interface{}{"Rule_final_decision": "Reject"}, + "rating": nil, + "triple": map[string]interface{}{"state": "1"}, + "identity": map[string]interface{}{"result": 0}, + "presence": map[string]interface{}{"desc": "正常", "status": 1}, + "judicial": map[string]interface{}{ + "judicial_data": map[string]interface{}{ + "breachCaseList": []interface{}{ + map[string]interface{}{"caseNo": "1"}, + map[string]interface{}{"caseNo": "2"}, + map[string]interface{}{"caseNo": "3"}, + }, + "consumptionRestrictionList": []interface{}{ + map[string]interface{}{"caseNo": "4"}, + map[string]interface{}{"caseNo": "5"}, + }, + }, + }, + } + ra := buildDWBG9FB3RA(data) + score := raAsInt(ra["ra_score"]) + if score > raForcedFScoreCap { + t.Fatalf("forced F score should be capped at %d, got %d", raForcedFScoreCap, score) + } + if raAsString(ra["ra_level"]) != "F" { + t.Fatalf("forced F should have ra_level=F, got %s", raAsString(ra["ra_level"])) + } +} + +func TestCalcRARatingXypModelRiskPoints(t *testing.T) { + cases := []struct { + name string + rating map[string]interface{} + want int + }{ + { + name: "all miss", + rating: map[string]interface{}{ + "xyp_model_score_high": "-1", + "xyp_model_score_mid": "-1", + "xyp_model_score_low": "-1", + }, + want: 0, + }, + { + name: "one low score", + rating: map[string]interface{}{ + "xyp_model_score_high": "600", + "xyp_model_score_mid": "-1", + "xyp_model_score_low": "-1", + }, + want: 50, // 650 - 600 + }, + { + name: "three low scores capped", + rating: map[string]interface{}{ + "xyp_model_score_high": "400", + "xyp_model_score_mid": "400", + "xyp_model_score_low": "400", + }, + want: 291, // (650-400)*3 = 750, capped per field at 97 + }, + { + name: "safe scores no deduction", + rating: map[string]interface{}{ + "xyp_model_score_high": "900", + "xyp_model_score_mid": "800", + "xyp_model_score_low": "700", + }, + want: 0, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := calcRARatingXypModelRiskPoints(tc.rating); got != tc.want { + t.Fatalf("got %d want %d", got, tc.want) + } + }) + } +} + func TestRAScoreWeightedSum(t *testing.T) { verify, judicial, credit := 920, 980, 760 wJ, wC := calcRADynamicWeights(judicial, credit) diff --git a/internal/domains/api/services/processors/dwbg/dwbg9fb3评分规则.md b/internal/domains/api/services/processors/dwbg/dwbg9fb3评分规则.md index 0238fbb..86ed815 100644 --- a/internal/domains/api/services/processors/dwbg/dwbg9fb3评分规则.md +++ b/internal/domains/api/services/processors/dwbg/dwbg9fb3评分规则.md @@ -17,7 +17,7 @@ - **个人身份信息权重固定 10%,不参与动态调整** - **个人司法信息与借贷信息合计 90%,根据风险场景动态分配** -- **司法 + 借贷同时高风险时,直接判定 F 级(不参与加权)** +- **司法 + 借贷同时高风险时,强制归入 F 档(仍参与加权,分数上限 500)** --- @@ -43,14 +43,18 @@ ## 三、等级划分(含 F 级) +千分制下 **每档间隔 100 分**,F 为 **500 分以下**: + | 等级 | 分数区间 | 含义 | | :--- | :--- | :--- | -| A | 800 – 1000 | 最安全 | -| B | 600 – 799 | 较安全 | -| C | 400 – 599 | 一般 | -| D | 200 – 399 | 较不安全 | -| E | 0 – 199 | 高风险 | -| **F** | — | **司法 + 借贷双重高风险(强制)** | +| A | 900 – 1000 | 最安全 | +| B | 800 – 899 | 较安全 | +| C | 700 – 799 | 一般 | +| D | 600 – 699 | 较不安全 | +| E | 500 – 599 | 不安全 | +| **F** | **0 – 499** | **最不安全** | + +> 另:司法 + 借贷双重高风险时 **强制 F 档**(仍按三维度加权计算 `ra_score`,封顶 **500 分**,不置 0)。 --- @@ -116,22 +120,37 @@ ra_score = round( --- -## 五、F 级强判规则(最高优先级) +## 五、F 档强判规则(最高优先级) -同时满足以下条件时: - -```text -ra_level = "F" -ra_score = 0 -``` +同时满足以下条件时,**强制归入 F 档**: | 条件 | 判定标准 | | :--- | :--- | | 司法高风险 | `ra_judicial_score ≤ 400` | | 借贷高风险 | `ra_credit_score ≤ 400` | -| 风险叠加 | 至少存在 2 项中高风险记录 | +| 命中总条数 | 司法 + 借贷 **合计命中 > 5 条** | -> **F 级不参与任何加权计算,直接覆盖结果** +**命中条数统计:** + +| 维度 | 计数方式 | +| :--- | :--- | +| 司法 | `lawsuitStat.*.count.count_total` 累加 + `breachCaseList` 条数 + `consumptionRestrictionList` 条数 | +| 借贷 | 探针命中项(逾期 / 睡眠 / 未履约 / 探针)+ 借贷意向(Reject / Review)+ 借选模型分低(`xyp_model_score_*` 各 1 条) | + +```text +// 第一步:正常三维度动态加权(与常规定价路径相同) +ra_score_raw = round( + ra_verify_score × 0.10 + + ra_judicial_score × w_judicial + + ra_credit_score × w_credit +) + +// 第二步:强制 F 档映射(保留分数,不置 0) +ra_level = "F" +ra_score = min(ra_score_raw, 500) +``` + +> 双高风险场景 **仍参与加权**,仅覆盖等级为 F,并将综合分 **封顶 500**(F 档最高分)。 --- @@ -144,36 +163,44 @@ ra_score = 0 | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | | :--- | :--- | :--- | :--- | -| 三要素不一致 | `triple.state` | 有值且不为 `"1"` | 400 | -| 二要素不一致 | `identity.result` | 不为 `0` | 400 | -| 在网异常 | `presence.desc` | 含停机 / 销号 / 不在网 | 80 | -| 状态码异常 | `presence.status` | 大于 `1` | 50 | +| 三要素不一致 | `triple.state` | 有值且不为 `"1"` | 387 | +| 二要素不一致 | `identity.result` | 不为 `0` | 387 | +| 在网异常 | `presence.desc` | 含停机 / 销号 / 不在网 | 73 | +| 状态码异常 | `presence.status` | 大于 `1` | 47 | -> 身份核验为 **基础准入项**,权重低但不可忽略;不一致时单项扣分仍可达 400–800 分。 +> 身份核验为 **基础准入项**,权重低但不可忽略;不一致时单项扣分仍可达 387–774 分。 --- ### 2️⃣ ra_judicial_score(个人司法信息,基准权重 50%,可动态上调) - **方法链:** `calcRAJudicialScore → calcRAJudicialRiskPoints → toRASafetyScore` -- **数据来源:** `judicial.judicial_data` +- **数据来源:** `judicial.judicial_data`(FLXG7E8F 个人司法数据查询) - **子维度结构:** 涉诉统计 + 失信被执行人 + 限高被执行人(三类独立累加) +- **计分方式:** 按 **命中条数 / 件数** 统计,遍历累加后换算安全分 + +| 子维度 | 命中统计方式 | 代码实现 | +| :--- | :--- | :--- | +| 涉诉统计 | 各类型 `*.count` 字段累加件数 | `lawsuitStat.*.count` 遍历求和 | +| 失信被执行人 | `breachCaseList` 数组长度 = 命中条数 | `len(breachCaseList)` | +| 限高被执行人 | `consumptionRestrictionList` 数组长度 = 命中条数 | `len(consumptionRestrictionList)` | #### 2.1 涉诉统计(lawsuitStat) -遍历 `lawsuitStat` 下各类型节点(如 `civil`、`criminal`、`administrative`、`preservation`、`implement`、`bankrupt` 等),读取 `*.count` 累加: +遍历 `lawsuitStat` 下各类型节点(如 `civil`、`criminal`、`administrative`、`preservation`、`implement`、`bankrupt` 等),读取 `*.count` 累加。 +扣分采用 **非整齐系数**(避免 50/80/100 等整齐倍数),例如涉诉总件数按 **件数 × 47** 折算: | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | 单项上限 | | :--- | :--- | :--- | :--- | :--- | -| 涉诉总件数 | `lawsuitStat.*.count.count_total` | 各类型累加 | 80 / 件 | 400 | -| 未结案数 | `lawsuitStat.*.count.count_wei_total` | 各类型累加 | 60 / 件 | 300 | -| 被告件数 | `lawsuitStat.*.count.count_beigao` | 各类型累加 | 50 / 件 | 250 | +| 涉诉总件数 | `lawsuitStat.*.count.count_total` | 各类型累加 | **件数 × 47** | 387 | +| 未结案数 | `lawsuitStat.*.count.count_wei_total` | 各类型累加 | **件数 × 38** | 293 | +| 被告件数 | `lawsuitStat.*.count.count_beigao` | 各类型累加 | **件数 × 33** | 247 | #### 2.2 失信被执行人(breachCaseList) | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | 单项上限 | | :--- | :--- | :--- | :--- | :--- | -| 失信执行人记录 | `breachCaseList` 长度 | 每条记录 | **150 / 条** | **450** | +| 失信执行人记录 | `breachCaseList` 长度 | 每条记录 | **条数 × 143** | **437** | - 对应最高法 **失信被执行人名单** - 命中即视为 **重度司法风险**,触发动态权重向司法倾斜(`HAS_JUDICIAL_RISK = true`) @@ -183,7 +210,7 @@ ra_score = 0 | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | 单项上限 | | :--- | :--- | :--- | :--- | :--- | -| 限高执行人记录 | `consumptionRestrictionList` 长度 | 每条记录 | **200 / 条** | **400** | +| 限高执行人记录 | `consumptionRestrictionList` 长度 | 每条记录 | **条数 × 187** | **413** | - 对应 **限制高消费被执行人名单** - 单条扣分高于失信记录,体现对消费 / 出行能力的直接限制 @@ -204,17 +231,29 @@ ra_score = 0 ### 3️⃣ ra_credit_score(借贷信息,基准权重 40%,可动态上调) - **方法链:** `calcRACreditScore → calcRACreditRiskPoints → toRASafetyScore` -- **数据来源:** `probe`、`intent`、`rating` +- **数据来源:** `probe`、`intent`、`rating`(JRZQ5E9F / loanRiskTagV21) | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | | :--- | :--- | :--- | :--- | -| 当前逾期 | `probe.currently_overdue` | `"1"` | 300 | -| 睡眠账户 | `probe.acc_sleep` | `"1"` | 150 | -| 未履约 | `probe.currently_performance` | `"0"` | 100 | -| 探针命中 | `probe.result_code` | `"1"` | 100 | -| 借贷意向 | `intent.Rule_final_decision` | `Reject` / `Review` | 400 / 250 | -| 规则权重 | `intent.Rule_final_weight` | 有值 | weight×5,上限 250 | -| 借选指数低 | `rating.score` | 0 < score < 500 | 500−score,上限 300 | +| 当前逾期 | `probe.currently_overdue` | `"1"` | 287 | +| 睡眠账户 | `probe.acc_sleep` | `"1"` | 143 | +| 未履约 | `probe.currently_performance` | `"0"` | 97 | +| 探针命中 | `probe.result_code` | `"1"` | 103 | +| 借贷意向 | `intent.Rule_final_decision` | `Reject` / `Review` | 387 / 237 | +| 规则权重 | `intent.Rule_final_weight` | 有值 | **weight × 4**,上限 243 | +| 小额网贷分低 | `rating.xyp_model_score_high` | 命中且 < 650 | 650−score,上限 97 | +| 小额分期分低 | `rating.xyp_model_score_mid` | 命中且 < 650 | 650−score,上限 97 | +| 中大额分期分低 | `rating.xyp_model_score_low` | 命中且 < 650 | 650−score,上限 97 | + +**借选指数模型分说明(JRZQ5E9F):** + +| 字段 | 含义 | 取值范围 | +| :--- | :--- | :--- | +| `xyp_model_score_high` | 星耀Pro 小额网贷分 V1 | [350, 950],越大逾期率越低 | +| `xyp_model_score_mid` | 星耀Pro 小额分期分 V1 | [350, 950],越大逾期率越低 | +| `xyp_model_score_low` | 星耀Pro 中大额分期分 V1 | [350, 950],越大逾期率越低 | + +> 未命中为 `-1`,不参与扣分;三项独立计分,合计扣分上限 291。 --- @@ -225,12 +264,13 @@ ra_score = 0 | 风险扣分项 | 子字段路径 | 触发条件 | 扣分 | | :--- | :--- | :--- | :--- | -| 行为黑名单 | `behavior.result.black_list` | `"1"` | 500 | -| 行为黑标签 | `behavior.result.black_tag**` | 任意 `"1"` | 每个 80 | -| 投诉风险 | `complaint.result.score` | 有值 | score×10,上限 300 | -| 欺诈黑名单 | `fraud.hit` | `1` | 400 | -| 特殊名单 | `special.Rule_final_decision` | `Reject` / `Review` | 350 / 200 | +| 行为黑名单 | `behavior.result.black_list` | `"1"` | 23 | +| 行为黑标签 | `behavior.result.black_tag**` | 任意 `"1"` | 23 / 项 | +| 投诉风险 | `complaint.result.score` | 有值且 > 0 | 23 | +| 欺诈黑名单 | `fraud.hit` | `1` | 23 | +| 特殊名单 | `special.Rule_final_decision` | `Reject` / `Review` | 23 | +> 欺诈维度采用 **「每命中一项扣 23 分」** 统一计分,多项独立累加,单维上限 1000。 > 欺诈信号仍单独计算并返回 `ra_fraud_score`,供风控人工复核;**不进入 `ra_score` 三维度加权**,避免与借贷维度重复计分。 --- @@ -258,15 +298,15 @@ ra_score = 920×0.10 + 980×0.50 + 760×0.40 | 维度 | 安全分 | 说明 | | :--- | :--- | :--- | | 个人身份 | 900 | 核验正常 | -| 个人司法 | 350 | 失信 1 条 + 限高 1 条,扣分 350 | +| 个人司法 | 670 | 失信 1 条 + 限高 1 条,扣分 330(143+187) | | 借贷 | 680 | 轻度借贷风险 | 动态权重:身份 10% + 司法 65% + 借贷 25% ```text -ra_score = 900×0.10 + 350×0.65 + 680×0.25 - = 90 + 227.5 + 170 - = 488 → D/E +ra_score = 900×0.10 + 670×0.65 + 680×0.25 + = 90 + 435.5 + 170 + = 696 → C ``` --- @@ -289,12 +329,21 @@ ra_score = 880×0.10 + 1000×0.40 + 520×0.50 --- -### ❌ 样例 4:司法 + 借贷双高(F 级) +### ❌ 样例 4:司法 + 借贷双高(强制 F 档) -- `ra_judicial_score ≤ 400`(含失信 / 限高 / 多笔涉诉) -- `ra_credit_score ≤ 400`(当前逾期 + 借贷意向 Reject) +- `ra_judicial_score = 197`(失信 3 条 ×143 + 限高 2 条 ×187,扣分 803,司法命中 **5 条**) +- `ra_credit_score = 0`(逾期 287 + 未履约 97 + 探针 103 + Reject 387 + 权重 40×4,扣分超 1000,借贷命中 **4 条**) +- 合计命中 **9 条 > 5**,满足强制 F 条件 +- `ra_verify_score = 1000` -→ **直接判定:F,`ra_score = 0`** +动态权重:身份 10% + 司法 70% + 借贷 20% + +```text +ra_score_raw = 1000×0.10 + 197×0.70 + 0×0.20 + = 100 + 137.9 + 0 + = 238 +→ 强制 F 档:ra_level = "F",ra_score = min(238, 500) = 238 +``` --- @@ -317,6 +366,9 @@ ra_score = 880×0.10 + 1000×0.40 + 520×0.50 | 身份权重 | 15%–20% 浮动 | **固定 10%** | | 司法 / 借贷权重 | 各自独立浮动 | **基准 50% / 40%,动态互调** | | 司法子维度 | 合并描述 | **涉诉统计 / 失信执行人 / 限高执行人 分项说明** | -| 限高 vs 失信 | 同表罗列 | **限高 200/条、失信 150/条,独立计分** | -| 欺诈维度 | 参与加权 | **单独输出,不参与 `ra_score`** | -| 双高风险 | 仍参与加权 | **直接 F 级** | +| 限高 vs 失信 | 同表罗列 | **限高 ×187/条、失信 ×143/条,独立计分** | +| 欺诈维度 | 参与加权 | **单独输出,不参与 `ra_score`;每命中一项扣 23 分** | +| 扣分系数 | 整齐 5/10 倍数 | **47/38/33/143/187 等非整齐系数** | +| 双高风险 | 强制 0 分 | **加权计分 + 强制 F 档,封顶 500** | +| 等级区间 | A:800+ / B:600+ … | **每档 100 分,F: 0–499** | +| 借选指数 | `rating.score` | **`xyp_model_score_high/mid/low`(JRZQ5E9F)** |