Files
tyapi-server/internal/domains/api/services/processors/dwbg/dwbg9fb3_ra.go

483 lines
14 KiB
Go
Raw Normal View History

2026-06-10 17:47:36 +08:00
package dwbg
import (
"math"
"strconv"
"strings"
)
const raScoreMax = 1000
const (
2026-06-10 20:27:18 +08:00
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
2026-06-10 17:47:36 +08:00
)
// buildDWBG9FB3RA 构建顶层 RA 总体安全评估(千分制,分值越高越安全)
//
// 输出字段来源:
// - ra_fraud_score → calcRAFraudScore辅助输出不参与 ra_score 加权)
// - ra_credit_score → calcRACreditScore
// - ra_judicial_score → calcRAJudicialScore
// - ra_verify_score → calcRAVerifyScore
// - ra_score → 本方法,身份 10% 固定 + 司法/借贷动态加权
2026-06-10 20:27:18 +08:00
// - ra_level → raLevelFromScore(ra_score),司法+借贷双高时强制 F 档(分数仍加权,上限 500
2026-06-10 17:47:36 +08:00
func buildDWBG9FB3RA(data map[string]interface{}) map[string]interface{} {
fraudScore := calcRAFraudScore(data)
creditScore := calcRACreditScore(data)
judicialScore := calcRAJudicialScore(data)
verifyScore := calcRAVerifyScore(data)
2026-06-10 20:27:18 +08:00
total := calcRAWightedScore(verifyScore, judicialScore, creditScore)
level := raLevelFromScore(total)
if isRAForcedFGrade(data, judicialScore, creditScore) {
2026-06-10 17:47:36 +08:00
level = "F"
2026-06-10 20:27:18 +08:00
total = clampRAInt(total, 0, raForcedFScoreCap)
2026-06-10 17:47:36 +08:00
}
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,
}
}
2026-06-10 20:27:18 +08:00
// 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)
}
2026-06-10 17:47:36 +08:00
// 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
}
}
2026-06-10 20:27:18 +08:00
// 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
2026-06-10 17:47:36 +08:00
}
2026-06-10 20:27:18 +08:00
// raLevelFromScore 由 ra_score 映射等级(千分制,越高越安全,每档 100 分)
// A: 900-1000 B: 800-899 C: 700-799 D: 600-699 E: 500-599 F: 0-499
2026-06-10 17:47:36 +08:00
func raLevelFromScore(score int) string {
switch {
2026-06-10 20:27:18 +08:00
case score >= 900:
2026-06-10 17:47:36 +08:00
return "A"
2026-06-10 20:27:18 +08:00
case score >= 800:
2026-06-10 17:47:36 +08:00
return "B"
2026-06-10 20:27:18 +08:00
case score >= 700:
2026-06-10 17:47:36 +08:00
return "C"
2026-06-10 20:27:18 +08:00
case score >= 600:
2026-06-10 17:47:36 +08:00
return "D"
2026-06-10 20:27:18 +08:00
case score >= 500:
2026-06-10 17:47:36 +08:00
return "E"
2026-06-10 20:27:18 +08:00
default:
return "F"
2026-06-10 17:47:36 +08:00
}
}
// 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 统计欺诈维度风险扣分(仅内部使用,分值越高代表越不安全)
2026-06-10 20:27:18 +08:00
// 每命中一项扣 raFraudPointsPerHit23多项独立累加
2026-06-10 17:47:36 +08:00
func calcRAFraudRiskPoints(data map[string]interface{}) int {
2026-06-10 20:27:18 +08:00
hits := 0
2026-06-10 17:47:36 +08:00
// 来源子字段 behaviorJRZQV0MD 行为黑名单)
if behavior := raAsMap(data["behavior"]); behavior != nil {
if result := raAsMap(behavior["result"]); result != nil {
if raAsString(result["black_list"]) == "1" {
2026-06-10 20:27:18 +08:00
hits++
2026-06-10 17:47:36 +08:00
}
for k, v := range result {
if strings.HasPrefix(k, "black_tag") && raAsString(v) == "1" {
2026-06-10 20:27:18 +08:00
hits++
2026-06-10 17:47:36 +08:00
}
}
}
}
// 来源子字段 complaintJRZQVT43 投诉风险筛查)
if complaint := raAsMap(data["complaint"]); complaint != nil {
if result := raAsMap(complaint["result"]); result != nil {
2026-06-10 20:27:18 +08:00
if raAsInt(result["score"]) > 0 {
hits++
}
2026-06-10 17:47:36 +08:00
}
}
// 来源子字段 fraudJRZQV3HM 债务欺诈黑名单)
if fraud := raAsMap(data["fraud"]); fraud != nil {
if raAsInt(fraud["hit"]) == 1 || raAsString(fraud["hit"]) == "1" {
2026-06-10 20:27:18 +08:00
hits++
2026-06-10 17:47:36 +08:00
}
}
// 来源子字段 specialJRZQV7MD 特殊名单)
if special := raAsMap(data["special"]); special != nil && len(special) > 0 {
switch raAsString(special["Rule_final_decision"]) {
2026-06-10 20:27:18 +08:00
case "Reject", "Review":
hits++
2026-06-10 17:47:36 +08:00
}
}
2026-06-10 20:27:18 +08:00
return clampRAInt(hits*raFraudPointsPerHit, 0, raScoreMax)
2026-06-10 17:47:36 +08:00
}
// 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" {
2026-06-10 20:27:18 +08:00
risk += raCreditOverduePoints
2026-06-10 17:47:36 +08:00
}
if raAsString(probe["acc_sleep"]) == "1" {
2026-06-10 20:27:18 +08:00
risk += raCreditSleepPoints
2026-06-10 17:47:36 +08:00
}
if raAsString(probe["currently_performance"]) == "0" {
2026-06-10 20:27:18 +08:00
risk += raCreditPerformancePoints
2026-06-10 17:47:36 +08:00
}
if raAsString(probe["result_code"]) == "1" {
2026-06-10 20:27:18 +08:00
risk += raCreditProbeHitPoints
2026-06-10 17:47:36 +08:00
}
}
// 来源子字段 intentJRZQ3C7B 借贷意向验证)
if intent := raAsMap(data["intent"]); intent != nil {
switch raAsString(intent["Rule_final_decision"]) {
case "Reject":
2026-06-10 20:27:18 +08:00
risk += raCreditIntentReject
2026-06-10 17:47:36 +08:00
case "Review":
2026-06-10 20:27:18 +08:00
risk += raCreditIntentReview
2026-06-10 17:47:36 +08:00
}
weight := raAsInt(intent["Rule_final_weight"])
if weight > 0 {
2026-06-10 20:27:18 +08:00
risk += clampRAInt(weight*raCreditIntentWeightMult, 0, raCreditIntentWeightCap)
2026-06-10 17:47:36 +08:00
}
}
2026-06-10 20:27:18 +08:00
// 来源子字段 ratingJRZQ5E9F / loanRiskTagV21 借选指数)
2026-06-10 17:47:36 +08:00
if rating := raAsMap(data["rating"]); rating != nil {
2026-06-10 20:27:18 +08:00
risk += calcRARatingXypModelRiskPoints(rating)
2026-06-10 17:47:36 +08:00
}
return clampRAInt(risk, 0, raScoreMax)
}
2026-06-10 20:27:18 +08:00
// 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)
}
2026-06-10 17:47:36 +08:00
// 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
}
2026-06-10 20:27:18 +08:00
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)
2026-06-10 17:47:36 +08:00
}
}
2026-06-10 20:27:18 +08:00
risk += clampRAInt(len(raAsSlice(judicialData["breachCaseList"]))*raJudicialBreachPerCase, 0, raJudicialBreachCap)
risk += clampRAInt(len(raAsSlice(judicialData["consumptionRestrictionList"]))*raJudicialConsumptionPerCase, 0, raJudicialConsumptionCap)
2026-06-10 17:47:36 +08:00
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" {
2026-06-10 20:27:18 +08:00
risk += raVerifyMismatchPoints
2026-06-10 17:47:36 +08:00
}
}
// 来源子字段 identityIVYZN2P8 二要素认证)
if identity := raAsMap(data["identity"]); identity != nil {
if result := raAsInt(identity["result"]); result != 0 {
2026-06-10 20:27:18 +08:00
risk += raVerifyMismatchPoints
2026-06-10 17:47:36 +08:00
}
}
// 来源子字段 presenceYYSYE7V5 在网状态)
if presence := raAsMap(data["presence"]); presence != nil {
desc := raAsString(presence["desc"])
if strings.Contains(desc, "停机") || strings.Contains(desc, "销号") || strings.Contains(desc, "不在网") {
2026-06-10 20:27:18 +08:00
risk += raVerifyPresenceDesc
2026-06-10 17:47:36 +08:00
}
if status := raAsInt(presence["status"]); status > 1 {
2026-06-10 20:27:18 +08:00
risk += raVerifyPresenceStatus
2026-06-10 17:47:36 +08:00
}
}
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
}