f
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)** |
|
||||
|
||||
Reference in New Issue
Block a user