Files
tyapi-server/internal/domains/api/services/processors/dwbg/dwbg3bf9_ra.go
2026-06-10 21:12:37 +08:00

487 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 统计欺诈维度风险扣分(计入个人身份区,分值越高代表越不安全)
// 每命中一项扣 raFraudPointsPerHit23多项独立累加
func calcRAFraudRiskPoints(data map[string]interface{}) int {
hits := 0
// 来源子字段 behaviorJRZQV0MD 行为黑名单)
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++
}
}
}
}
// 来源子字段 complaintJRZQVT43 投诉风险筛查)
if complaint := raAsMap(data["complaint"]); complaint != nil {
if result := raAsMap(complaint["result"]); result != nil {
if raAsInt(result["score"]) > 0 {
hits++
}
}
}
// 来源子字段 fraudJRZQV3HM 债务欺诈黑名单)
if fraud := raAsMap(data["fraud"]); fraud != nil {
if raAsInt(fraud["hit"]) == 1 || raAsString(fraud["hit"]) == "1" {
hits++
}
}
// 来源子字段 specialJRZQV7MD 特殊名单)
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
// 来源子字段 probeJRZQ4B6C 探针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
}
}
// 来源子字段 intentJRZQ3C7B 借贷意向验证)
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)
}
}
// 来源子字段 ratingJRZQ5E9F / 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
// 来源子字段 judicialFLXG7E8F 个人司法数据查询)
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
// 来源子字段 tripleYYSYK9R4 三要素验证)
if triple := raAsMap(data["triple"]); triple != nil {
if state := raAsString(triple["state"]); state != "" && state != "1" {
risk += raVerifyMismatchPoints
}
}
// 来源子字段 identityIVYZN2P8 二要素认证)
if identity := raAsMap(data["identity"]); identity != nil {
if result := raAsInt(identity["result"]); result != 0 {
risk += raVerifyMismatchPoints
}
}
// 来源子字段 presenceYYSYE7V5 在网状态)
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
}