package dwbg import ( "math" "strconv" "strings" ) const raScoreMax = 1000 const ( 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 ) // buildDWBG3BF9RA 构建顶层 RA 总体安全评估(千分制,分值越高越安全) // // 输出字段(对外): // - ra_score / ra_level → 综合安全分与等级 // - ra_credit_score → 借贷维度安全分(参与加权) // - ra_verify_score → 个人身份区安全分(参与加权) // - ra_credit_risk_index → 信用风险指数(越高风险越大,与 ra_score 同向) // - ra_performance_amt_index → 履约金额综合指数(越高越好) // - ra_performance_cnt_index → 履约笔数综合指数(越高越好) // 内部仍计算 fraud/judicial 分用于 ra_score 加权,但不对外返回。 func buildDWBG3BF9RA(data map[string]interface{}) map[string]interface{} { creditScore := calcRACreditScore(data) judicialScore := calcRAJudicialScore(data) verifyScore := calcRAVerifyScore(data) total := calcRAWightedScore(verifyScore, judicialScore, creditScore) level := raLevelFromScore(total) if isRAForcedFGrade(data, judicialScore, creditScore) { level = "F" total = clampRAInt(total, 0, raForcedFScoreCap) } return map[string]interface{}{ "ra_score": total, "ra_level": level, "ra_credit_score": creditScore, "ra_verify_score": verifyScore, "ra_credit_risk_index": calcRACreditRiskIndex(data, creditScore), "ra_performance_amt_index": calcRAPerformanceAmtIndex(data), "ra_performance_cnt_index": calcRAPerformanceCntIndex(data), } } // 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 hasCreditRisk := creditScore < raScoreMax switch { case hasJudicialRisk && hasCreditRisk: return 0.70, 0.20 case hasJudicialRisk: return 0.65, 0.25 case hasCreditRisk: return 0.40, 0.50 default: return raWeightJudicialBase, raWeightCreditBase } } // 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 } // 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 >= 900: return "A" case score >= 800: return "B" case score >= 700: return "C" case score >= 600: return "D" case score >= 500: return "E" default: return "F" } } // toRASafetyScore 将风险扣分转为安全分:安全分 = 1000 - 风险扣分 func toRASafetyScore(riskPoints int) int { return clampRAInt(raScoreMax-riskPoints, 0, raScoreMax) } // calcRAFraudScore 欺诈/黑名单子维度安全分(来源:calcRAFraudRiskPoints) // 统计方式:满分 1000,根据 behavior/complaint/fraud/special 风险信号扣分后取补集;同时计入 ra_verify_score func calcRAFraudScore(data map[string]interface{}) int { return toRASafetyScore(calcRAFraudRiskPoints(data)) } // calcRAFraudRiskPoints 统计欺诈维度风险扣分(计入个人身份区,分值越高代表越不安全) // 每命中一项扣 raFraudPointsPerHit(23)分,多项独立累加 func calcRAFraudRiskPoints(data map[string]interface{}) int { hits := 0 // 来源子字段 behavior(JRZQV0MD 行为黑名单) if behavior := raAsMap(data["behavior"]); behavior != nil { if result := raAsMap(behavior["result"]); result != nil { if raAsString(result["black_list"]) == "1" { hits++ } for k, v := range result { if strings.HasPrefix(k, "black_tag") && raAsString(v) == "1" { hits++ } } } } // 来源子字段 complaint(JRZQVT43 投诉风险筛查) if complaint := raAsMap(data["complaint"]); complaint != nil { if result := raAsMap(complaint["result"]); result != nil { if raAsInt(result["score"]) > 0 { hits++ } } } // 来源子字段 fraud(JRZQV3HM 债务欺诈黑名单) if fraud := raAsMap(data["fraud"]); fraud != nil { if raAsInt(fraud["hit"]) == 1 || raAsString(fraud["hit"]) == "1" { hits++ } } // 来源子字段 special(JRZQV7MD 特殊名单) if special := raAsMap(data["special"]); special != nil && len(special) > 0 { switch raAsString(special["Rule_final_decision"]) { case "Reject", "Review": hits++ } } return clampRAInt(hits*raFraudPointsPerHit, 0, raScoreMax) } // calcRACreditScore 借贷/逾期维度安全分(来源:calcRACreditRiskPoints) // 统计方式:满分 1000,根据 probe/intent/rating 风险信号扣分后取补集 func calcRACreditScore(data map[string]interface{}) int { return toRASafetyScore(calcRACreditRiskPoints(data)) } // calcRACreditRiskPoints 统计借贷维度风险扣分 func calcRACreditRiskPoints(data map[string]interface{}) int { risk := 0 // 来源子字段 probe(JRZQ4B6C 探针C) if probe := raAsMap(data["probe"]); probe != nil { if raAsString(probe["currently_overdue"]) == "1" { risk += raCreditOverduePoints } if raAsString(probe["acc_sleep"]) == "1" { risk += raCreditSleepPoints } if raAsString(probe["currently_performance"]) == "0" { risk += raCreditPerformancePoints } if raAsString(probe["result_code"]) == "1" { risk += raCreditProbeHitPoints } } // 来源子字段 intent(JRZQ3C7B 借贷意向验证) if intent := raAsMap(data["intent"]); intent != nil { switch raAsString(intent["Rule_final_decision"]) { case "Reject": risk += raCreditIntentReject case "Review": risk += raCreditIntentReview } weight := raAsInt(intent["Rule_final_weight"]) if weight > 0 { risk += clampRAInt(weight*raCreditIntentWeightMult, 0, raCreditIntentWeightCap) } } // 来源子字段 rating(JRZQ5E9F / loanRiskTagV21 借选指数) if rating := raAsMap(data["rating"]); rating != nil { 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 { return toRASafetyScore(calcRAJudicialRiskPoints(data)) } // calcRAJudicialRiskPoints 统计司法维度风险扣分 func calcRAJudicialRiskPoints(data map[string]interface{}) int { risk := 0 // 来源子字段 judicial(FLXG7E8F 个人司法数据查询) judicial := raAsMap(data["judicial"]) if judicial == nil { return 0 } judicialData := raAsMap(judicial["judicial_data"]) if judicialData == nil { return 0 } // lawsuitStat 下 civil/criminal/administrative/preservation 等节点累加 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 } 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"]))*raJudicialBreachPerCase, 0, raJudicialBreachCap) risk += clampRAInt(len(raAsSlice(judicialData["consumptionRestrictionList"]))*raJudicialConsumptionPerCase, 0, raJudicialConsumptionCap) return clampRAInt(risk, 0, raScoreMax) } // calcRAVerifyScore 个人身份区安全分(来源:calcRAVerifyRiskPoints) // 统计方式:满分 1000,核验异常 + 欺诈/黑名单信号扣分后取补集,参与 ra_score 10% 加权 func calcRAVerifyScore(data map[string]interface{}) int { return toRASafetyScore(calcRAVerifyRiskPoints(data)) } // calcRAVerifyRiskPoints 统计个人身份区风险扣分(核验 + 欺诈/黑名单) func calcRAVerifyRiskPoints(data map[string]interface{}) int { risk := 0 // 来源子字段 triple(YYSYK9R4 三要素验证) if triple := raAsMap(data["triple"]); triple != nil { if state := raAsString(triple["state"]); state != "" && state != "1" { risk += raVerifyMismatchPoints } } // 来源子字段 identity(IVYZN2P8 二要素认证) if identity := raAsMap(data["identity"]); identity != nil { if result := raAsInt(identity["result"]); result != 0 { risk += raVerifyMismatchPoints } } // 来源子字段 presence(YYSYE7V5 在网状态) if presence := raAsMap(data["presence"]); presence != nil { desc := raAsString(presence["desc"]) if strings.Contains(desc, "停机") || strings.Contains(desc, "销号") || strings.Contains(desc, "不在网") { risk += raVerifyPresenceDesc } if status := raAsInt(presence["status"]); status > 1 { risk += raVerifyPresenceStatus } } // 来源子字段 behavior / complaint / fraud / special(欺诈/黑名单,与 calcRAFraudRiskPoints 一致) risk += calcRAFraudRiskPoints(data) return clampRAInt(risk, 0, raScoreMax) } func raAsMap(v interface{}) map[string]interface{} { m, ok := v.(map[string]interface{}) if !ok || m == nil { return nil } return m } func raAsSlice(v interface{}) []interface{} { s, ok := v.([]interface{}) if !ok { return nil } return s } func raAsString(v interface{}) string { switch val := v.(type) { case string: return strings.TrimSpace(val) case float64: return strconv.FormatInt(int64(val), 10) case int: return strconv.Itoa(val) case int64: return strconv.FormatInt(val, 10) case bool: if val { return "1" } return "0" default: return "" } } func raAsInt(v interface{}) int { switch val := v.(type) { case int: return val case int64: return int(val) case float64: return int(val) case string: n, err := strconv.Atoi(strings.TrimSpace(val)) if err == nil { return n } case bool: if val { return 1 } } return 0 } func clampRAInt(v, min, max int) int { if v < min { return min } if v > max { return max } return v }