483 lines
14 KiB
Go
483 lines
14 KiB
Go
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
|
||
)
|
||
|
||
// buildDWBG9FB3RA 构建顶层 RA 总体安全评估(千分制,分值越高越安全)
|
||
//
|
||
// 输出字段来源:
|
||
// - ra_fraud_score → calcRAFraudScore(辅助输出,不参与 ra_score 加权)
|
||
// - ra_credit_score → calcRACreditScore
|
||
// - ra_judicial_score → calcRAJudicialScore
|
||
// - ra_verify_score → calcRAVerifyScore
|
||
// - ra_score → 本方法,身份 10% 固定 + 司法/借贷动态加权
|
||
// - 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 := 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_fraud_score": fraudScore,
|
||
"ra_credit_score": creditScore,
|
||
"ra_judicial_score": judicialScore,
|
||
"ra_verify_score": verifyScore,
|
||
}
|
||
}
|
||
|
||
// 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 风险信号扣分后取补集
|
||
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,根据 triple/identity/presence 核验异常扣分后取补集
|
||
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
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|