Files
tyapi-server/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go
2026-06-10 20:27:18 +08:00

483 lines
14 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
)
// 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 统计欺诈维度风险扣分(仅内部使用,分值越高代表越不安全)
// 每命中一项扣 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根据 triple/identity/presence 核验异常扣分后取补集
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
}
}
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
}