From 9f985ea8c76dfeb5131f1ed545c6e84dee779ce8 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 8 Jul 2026 21:28:08 +0800 Subject: [PATCH] f --- .../processors/dwbg/dwbg6a2c_calc_risk.go | 122 ++++ .../processors/dwbg/dwbg6a2c_collect.go | 142 ++++ .../processors/dwbg/dwbg6a2c_defaults.go | 338 +++++++++ .../processors/dwbg/dwbg6a2c_judicial.go | 471 ++++++++++++ .../services/processors/dwbg/dwbg6a2c_map.go | 682 ++++++++++++++++++ .../processors/dwbg/dwbg6a2c_processor.go | 58 +- .../services/processors/dwbg/dwbg6a2c_test.go | 345 +++++++++ .../processors/dwbg/dwbg6a2c_validate.go | 45 ++ .../services/processors/dwbg/dwbg6a2c_xyp.go | 233 ++++++ .../processors/ivyz/ivyz5e3f_processor.go | 3 + .../processors/jrzq/decode_interval.go | 85 +++ .../processors/jrzq/decode_interval_test.go | 19 + 12 files changed, 2504 insertions(+), 39 deletions(-) create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_calc_risk.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_collect.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_defaults.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_judicial.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_map.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_test.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_validate.go create mode 100644 internal/domains/api/services/processors/dwbg/dwbg6a2c_xyp.go create mode 100644 internal/domains/api/services/processors/jrzq/decode_interval.go create mode 100644 internal/domains/api/services/processors/jrzq/decode_interval_test.go diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_calc_risk.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_calc_risk.go new file mode 100644 index 0000000..6449298 --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_calc_risk.go @@ -0,0 +1,122 @@ +package dwbg + +// calcSinanRisk 基于当前 report 与原始 apiData 计算 riskList / riskPoint。 +func calcSinanRisk(report map[string]interface{}, apiData map[string]interface{}) { + riskList := asMap(report["riskList"]) + if riskList == nil { + riskList = emptyRiskList() + report["riskList"] = riskList + } + + jrzq := jrzqMap(apiData) + jrzq1d09 := jrzq1d09Map(apiData) + baseInfo := asMap(report["baseInfo"]) + + if jrzq1d09 != nil { + if rentalHasApplication(jrzq1d09) { + riskList["creditLeaseRisk"] = 1 + } + if maxTotMons(jrzq1d09) >= 15 { + riskList["vehicleLeaseViolation"] = 1 + } + relationHit := hitRelationRisk(jrzq1d09) + riskList["mediumRelationRisk"] = relationHit + } + + if jrzq != nil { + riskList["bankOverdueRecord"] = hitBankOverdueRecord(jrzq) + if xypString(jrzq, "xyp_cpl0044") == "1" { + riskList["creditOverdueRecord"] = 1 + } + } + + if hasCourtViolator(apiData) { + riskList["courtViolator"] = 1 + } + + if baseInfo != nil { + status := parseIntValue(baseInfo["status"]) + if status != 1 { + riskList["phoneNumberStatus"] = 1 + } + if status == 5 { + riskList["riskPhoneNumber"] = 1 + } + } + + if yysyh6f3 := asMap(getMapValue(apiData, "YYSYH6F3")); yysyh6f3 != nil { + if xypString(yysyh6f3, "result") == "1" { + riskList["identityFake"] = 1 + } + } + + if groupFraudHit(asMap(getMapValue(apiData, "FLXG8B4D"))) { + riskList["groupFraud"] = 1 + } + + if flxgdea9 := asMap(getMapValue(apiData, "FLXGDEA9")); flxgdea9 != nil { + if industryBlacklistHit(xypString(flxgdea9, "level")) { + riskList["industryBlacklist"] = 1 + } + } + + riskList["highRiskArea"] = 0 + riskList["taxDebt"] = 0 + + riskPoint := asMap(report["riskPoint"]) + if riskPoint == nil { + riskPoint = emptyRiskPoint() + report["riskPoint"] = riskPoint + } + + if jrzq != nil { + riskPoint["multiQuery"] = calcMultiQuery(jrzq) + riskPoint["deductFail"] = calcDeductFail(jrzq) + riskPoint["newRiskFeature"] = hitNewRiskFeature(jrzq) + } + + riskPoint["riskList"] = 0 + for _, v := range riskList { + if parseIntValue(v) == 1 { + riskPoint["riskList"] = 1 + break + } + } + + if hasJudicialCases(apiData) { + riskPoint["judicialRisk"] = 1 + } + if hasPreservationCases(apiData) { + riskPoint["hitPreservationReview"] = 1 + } + if hasLegalCases(apiData) { + riskPoint["legalCasesFlag"] = 1 + } + if hasExecutionCases(apiData) { + riskPoint["executionCasesFlag"] = 1 + } + if hasDisinCases(apiData) { + riskPoint["disinCasesFlag"] = 1 + } + if hasLimitCases(apiData) { + riskPoint["limitCasesFlag"] = 1 + } + + if flxgdea9 := asMap(getMapValue(apiData, "FLXGDEA9")); flxgdea9 != nil { + if securityLevelHit(xypString(flxgdea9, "level")) { + riskPoint["securityRisk"] = 1 + } + } + + if antiFraudHit(asMap(getMapValue(apiData, "FLXG8B4D"))) { + riskPoint["antiFraudRisk"] = 1 + } + + relationHit := 0 + if jrzq1d09 != nil { + relationHit = hitRelationRisk(jrzq1d09) + } + riskPoint["relationRisk"] = relationHit + riskPoint["registerRisk"] = 0 + riskPoint["judicialCase"] = 0 +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_collect.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_collect.go new file mode 100644 index 0000000..30398c6 --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_collect.go @@ -0,0 +1,142 @@ +package dwbg + +import ( + "context" + "encoding/json" + "fmt" + "sync" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + + "go.uber.org/zap" +) + +type sinanAPICallInfo struct { + apiCode string + params map[string]interface{} +} + +type sinanProcessorResult struct { + apiCode string + data interface{} + err error +} + +// collectSinanAPIData 并发调用 9 个子接口;单接口失败仅记日志,对应 key 为 nil。 +func collectSinanAPIData(ctx context.Context, params dto.DWBG6A2CReq, deps *processors.ProcessorDependencies, log *zap.Logger) map[string]interface{} { + apiCalls := []sinanAPICallInfo{ + {apiCode: "YYSY35TA", params: map[string]interface{}{"mobile_no": params.MobileNo}}, + {apiCode: "YYSYE7V5", params: map[string]interface{}{"mobile_no": params.MobileNo}}, + { + apiCode: "YYSYH6F3", + params: map[string]interface{}{ + "name": params.Name, + "id_card": params.IDCard, + "mobile_no": params.MobileNo, + }, + }, + {apiCode: "YYSYP0T4", params: map[string]interface{}{"mobile_no": params.MobileNo}}, + { + apiCode: "FLXGDEA9", + params: map[string]interface{}{ + "name": params.Name, + "id_card": params.IDCard, + "authorized": "1", + }, + }, + { + apiCode: "FLXG8B4D", + params: map[string]interface{}{ + "mobile_no": params.MobileNo, + "authorized": "1", + }, + }, + { + apiCode: "FLXG7E8F", + params: map[string]interface{}{ + "name": params.Name, + "id_card": params.IDCard, + "mobile_no": params.MobileNo, + }, + }, + { + apiCode: "JRZQ5E9F", + params: map[string]interface{}{ + "name": params.Name, + "id_card": params.IDCard, + "mobile_no": params.MobileNo, + "authorized": "1", + }, + }, + { + apiCode: "JRZQ1D09", + params: map[string]interface{}{ + "name": params.Name, + "id_card": params.IDCard, + "mobile_no": params.MobileNo, + "authorized": "1", + }, + }, + } + + apiData := make(map[string]interface{}, len(apiCalls)) + results := make(chan sinanProcessorResult, len(apiCalls)) + + var wg sync.WaitGroup + for _, apiCall := range apiCalls { + wg.Add(1) + go func(ac sinanAPICallInfo) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + log.Error("调用司南子接口时发生panic", + zap.String("api_code", ac.apiCode), + zap.Any("panic", r), + ) + results <- sinanProcessorResult{apiCode: ac.apiCode, err: fmt.Errorf("处理器panic: %v", r)} + } + }() + + paramsBytes, err := json.Marshal(ac.params) + if err != nil { + log.Warn("序列化司南子接口参数失败", + zap.String("api_code", ac.apiCode), + zap.Error(err), + ) + results <- sinanProcessorResult{apiCode: ac.apiCode, err: err} + return + } + + data, err := callProcessor(ctx, ac.apiCode, paramsBytes, deps) + results <- sinanProcessorResult{apiCode: ac.apiCode, data: data, err: err} + }(apiCall) + } + + go func() { + wg.Wait() + close(results) + }() + + successCount := 0 + for result := range results { + if result.err != nil { + log.Warn("调用司南子接口失败,将使用默认值", + zap.String("api_code", result.apiCode), + zap.Error(result.err), + ) + apiData[result.apiCode] = nil + continue + } + apiData[result.apiCode] = result.data + successCount++ + } + + log.Info("司南子接口调用完成", + zap.Int("total", len(apiCalls)), + zap.Int("success", successCount), + zap.Int("failed", len(apiCalls)-successCount), + ) + + return apiData +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_defaults.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_defaults.go new file mode 100644 index 0000000..a9323cd --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_defaults.go @@ -0,0 +1,338 @@ +package dwbg + +import "tyapi-server/internal/domains/api/dto" + +var rentalBehaviorFieldNames = []string{ + "rentalApplicationInstitutionsLast3Days", + "rentalApplicationCountLast3Days", + "rentalApplicationInstitutionsLast3DaysWeekend", + "rentalApplicationCountLast3DaysWeekend", + "rentalApplicationInstitutionsLast3DaysNight", + "rentalApplicationCountLast3DaysNight", + "rentalApplicationInstitutionsLast7Days", + "rentalApplicationCountLast7Days", + "rentalApplicationInstitutionsLast7DaysWeekend", + "rentalApplicationCountLast7DaysWeekend", + "rentalApplicationInstitutionsLast7DaysNight", + "rentalApplicationCountLast7DaysNight", + "rentalApplicationInstitutionsLast14Days", + "rentalApplicationCountLast14Days", + "rentalApplicationInstitutionsLast14DaysWeekend", + "rentalApplicationCountLast14DaysWeekend", + "rentalApplicationInstitutionsLast14DaysNight", + "rentalApplicationCountLast14DaysNight", + "rentalApplicationInstitutionsLast1Month", + "rentalApplicationCountLast1Month", + "rentalApplicationInstitutionsLast1MonthWeekend", + "rentalApplicationCountLast1MonthWeekend", + "rentalApplicationInstitutionsLast1MonthNight", + "rentalApplicationCountLast1MonthNight", + "rentalApplicationInstitutionsLast3Months", + "rentalApplicationCountLast3Months", + "rentalApplicationInstitutionsLast3MonthsWeekend", + "rentalApplicationCountLast3MonthsWeekend", + "rentalApplicationInstitutionsLast3MonthsNight", + "rentalApplicationCountLast3MonthsNight", + "rentalApplicationInstitutionsLast6Months", + "rentalApplicationCountLast6Months", + "rentalApplicationInstitutionsLast6MonthsWeekend", + "rentalApplicationCountLast6MonthsWeekend", + "rentalApplicationInstitutionsLast6MonthsNight", + "rentalApplicationCountLast6MonthsNight", + "rentalApplicationInstitutionsLast12Months", + "rentalApplicationCountLast12Months", + "rentalApplicationInstitutionsLast12MonthsWeekend", + "rentalApplicationCountLast12MonthsWeekend", + "rentalApplicationInstitutionsLast12MonthsNight", + "rentalApplicationCountLast12MonthsNight", +} + +var judicialCaseCountsKeys = []string{ + "caseCounts", + "civilCaseCounts", + "criminalCaseCounts", + "administrativeCaseCounts", + "preservationCaseCounts", + "enforcementCaseCounts", + "supervisionCaseCounts", + "compensationCaseCounts", + "bankruptcyCaseCounts", +} + +// emptyCaseCounts 返回司法统计 object 默认骨架(值均为 String)。 +func emptyCaseCounts() map[string]interface{} { + return map[string]interface{}{ + "beigaoTotalCaseAmounts": "0", + "beigaoTotalCasesCounts": "0", + "beigaoTotalJieAmountCases": "0", + "beigaoTotalJieCaseCounts": "0", + "beigaoTotalWeiAmountCases": "0", + "beigaoTotalWeiCaseCounts": "0", + "caseActionDistribution": "-", + "caseCloselDistribution": "-", + "localDistribution": "-", + "otherTotalCaseAmounts": "0", + "otherTotalCasesCounts": "0", + "otherTotalJieAmountCases": "0", + "otherTotalJieCaseCounts": "0", + "otherTotalWeiAmountCases": "0", + "otherTotalWeiCaseCounts": "0", + "timeDistribution": "-", + "totalCaseAmounts": "0", + "totalCasesCounts": "0", + "totalJieAmountCases": "0", + "totalJieCaseCounts": "0", + "totalWeiAmountCases": "0", + "totalWeiCaseCounts": "0", + "yuangaoTotalCaseAmounts": "0", + "yuangaoTotalCasesCounts": "0", + "yuangaoTotalJieAmountCases": "0", + "yuangaoTotalJieCaseCounts": "0", + "yuangaoTotalWeiAmountCases": "0", + "yuangaoTotalWeiCaseCounts": "0", + } +} + +func emptyNewMultCourtInfo() map[string]interface{} { + info := make(map[string]interface{}, len(judicialCaseCountsKeys)) + for _, key := range judicialCaseCountsKeys { + info[key] = emptyCaseCounts() + } + return info +} + +func emptyCaseOverviewInfo() map[string]interface{} { + return map[string]interface{}{ + "disinCaseCounts": 0, + "limitCaseCounts": 0, + "beigaoTotalCasesCounts": 0, + "beigaoTotalWeiCaseCounts": 0, + "executionCaseCounts": 0, + "beigaoTotalCaseAmounts": 0, + "affiliateCompany": 0, + "leastCaseTime": "", + } +} + +// emptyJudicialLeaseReport 返回司法模块默认骨架。 +func emptyJudicialLeaseReport() map[string]interface{} { + return map[string]interface{}{ + "courtInfo": map[string]interface{}{ + "civilCasesCount": 0, + "criminalCasesCount": 0, + "administrativeCasesCount": 0, + "preservationCasesCount": 0, + "enforcementCasesCount": 0, + "supervisionCasesCount": 0, + "compensationCasesCount": 0, + "bankruptcyCasesCount": 0, + "newMultCourtInfo": emptyNewMultCourtInfo(), + }, + "caseOverviewInfo": emptyCaseOverviewInfo(), + } +} + +func emptyRentalBehavior() map[string]interface{} { + behavior := make(map[string]interface{}, len(rentalBehaviorFieldNames)) + for _, field := range rentalBehaviorFieldNames { + behavior[field] = "0/0" + } + return behavior +} + +func emptyBaseInfo(params dto.DWBG6A2CReq) map[string]interface{} { + return map[string]interface{}{ + "name": maskName(params.Name), + "idCard": maskIDCard(params.IDCard), + "phone": maskMobile(params.MobileNo), + "age": 0, + "sex": "", + "location": "", + "phoneArea": "", + "channel": "", + "status": -1, + } +} + +func emptyStandLiveInfo() map[string]interface{} { + return map[string]interface{}{ + "finalAuthResult": "1", + "verification": "-1", + "inTime": "-1", + } +} + +func emptySecurityInfo() map[string]interface{} { + return map[string]interface{}{ + "front": 0, + "drug": 0, + "takeDrug": 0, + "escape": 0, + "icase": 0, + "ikey": 0, + "itrancase": 0, + } +} + +func emptyAntiFraudInfo() map[string]interface{} { + return map[string]interface{}{ + "moneyLaundering": "0", + "deceiver": "0", + "gamblerPlayer": "0", + "gamblerBanker": "0", + } +} + +func emptyRiskList() map[string]interface{} { + return map[string]interface{}{ + "creditLeaseRisk": 0, + "bankOverdueRecord": 0, + "riskPhoneNumber": 0, + "highRiskArea": 0, + "creditOverdueRecord": 0, + "vehicleLeaseViolation": 0, + "courtViolator": 0, + "industryBlacklist": 0, + "identityFake": 0, + "groupFraud": 0, + "taxDebt": 0, + "mediumRelationRisk": 0, + "phoneNumberStatus": 0, + } +} + +func emptyRiskPoint() map[string]interface{} { + return map[string]interface{}{ + "multiQuery": 0, + "riskList": 0, + "newRiskFeature": 0, + "deductFail": 0, + "judicialRisk": 0, + "hitPreservationReview": 0, + "legalCasesFlag": 0, + "executionCasesFlag": 0, + "disinCasesFlag": 0, + "limitCasesFlag": 0, + "securityRisk": 0, + "antiFraudRisk": 0, + "registerRisk": 0, + "relationRisk": 0, + "judicialCase": 0, + } +} + +func emptyApplicationStatistics() map[string]interface{} { + return map[string]interface{}{ + "totalApplicationCount": 0, + "consumptionInstallmentApplicationCount": 0, + "onlineLoanApplicationCount": 0, + "otherApplicationCount": 0, + "lastApplicationDate": "-", + "daysSinceLastApplication": "-", + "applicationCountLastMonth": 0, + "applicationCountLast3Months": 0, + "applicationCountLast6Months": 0, + "applicationCountLast12Months": 0, + } +} + +func emptyLendingStatistics() map[string]interface{} { + return map[string]interface{}{ + "totalLendingInstitutionCount": 0, + "installmentLendingInstitutionCount": 0, + "onlineLendingInstitutionCount": 0, + "lastLendingDate": "-", + "daysSinceLastLending": "-", + "lendingCountLastMonth": 0, + "lendingAmountLastMonth": "0", + "lendingCountLast3Months": 0, + "lendingAmountLast3Months": "0", + "lendingCountLast6Months": 0, + "lendingAmountLast6Months": "0", + "lendingCountLast12Months": 0, + "lendingAmountLast12Months": "0", + "lendingCountLast24Months": 0, + "lendingAmountLast24Months": "0", + } +} + +func emptyPerformanceStatistics() map[string]interface{} { + return map[string]interface{}{ + "normalRepaymentRatio": "0", + "creditLoanDuration": 0, + "settledLoanOrderCount": 0, + "daysSinceLastPerformance": "-", + "performanceCountLastMonth": 0, + "performanceAmountLastMonth": "0", + "repaymentExceptionCountLastMonth": 0, + "performanceCountLast3Months": 0, + "performanceAmountLast3Months": "0", + "repaymentExceptionCountLast3Months": 0, + "performanceCountLast6Months": 0, + "performanceAmountLast6Months": "0", + "repaymentExceptionCountLast6Months": 0, + "performanceCountLast12Months": 0, + "performanceAmountLast12Months": "0", + "repaymentExceptionCountLast12Months": 0, + "performanceCountLast24Months": 0, + "performanceAmountLast24Months": "0", + "repaymentExceptionCountLast24Months": 0, + "serialVersionUID": 1, + } +} + +func emptyOverdueRecord() map[string]interface{} { + return map[string]interface{}{ + "currentOverdueInstitution": "-", + "currentOverdueCount": 0, + "totalOverdueAmount": "0", + "lastOverdueDate": "-", + "m0PlusCountLast6Months": 0, + "m1PlusCountLast6Months": 0, + "totalAmountLast6Months": "0", + "m0PlusCountLast12Months": 0, + "m1PlusCountLast12Months": 0, + "totalAmountLast12Months": "0", + "m0PlusCountLast24Months": 0, + "m1PlusCountLast24Months": 0, + "totalAmountLast24Months": "0", + } +} + +func emptyCreditDetail() map[string]interface{} { + return map[string]interface{}{ + "maxOnlineLoanCredit": "0", + "avgOnlineLoanCredit": "0", + "maxConsumptionInstallmentCredit": "0", + "avgConsumptionInstallmentCredit": "0", + } +} + +func emptyRiskSupervision() map[string]interface{} { + return map[string]interface{}{ + "leastApplicationTime": "", + "rentalRiskListIdCardRelationsPhones": 0, + "rentalRiskListPhoneRelationsIdCards": 0, + "details": "无", + } +} + +// emptySinanReport 返回司南报告全字段默认骨架。 +func emptySinanReport(params dto.DWBG6A2CReq) map[string]interface{} { + return map[string]interface{}{ + "baseInfo": emptyBaseInfo(params), + "standLiveInfo": emptyStandLiveInfo(), + "riskPoint": emptyRiskPoint(), + "securityInfo": emptySecurityInfo(), + "antiFraudInfo": emptyAntiFraudInfo(), + "riskList": emptyRiskList(), + "applicationStatistics": emptyApplicationStatistics(), + "lendingStatistics": emptyLendingStatistics(), + "performanceStatistics": emptyPerformanceStatistics(), + "overdueRecord": emptyOverdueRecord(), + "creditDetail": emptyCreditDetail(), + "rentalBehavior": emptyRentalBehavior(), + "riskSupervision": emptyRiskSupervision(), + "judicialLeaseReport": emptyJudicialLeaseReport(), + } +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_judicial.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_judicial.go new file mode 100644 index 0000000..253cc07 --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_judicial.go @@ -0,0 +1,471 @@ +package dwbg + +import ( + "fmt" + "strings" +) + +// buildJudicialLeaseReport maps FLXG7E8F to Sinan judicialLeaseReport. +func buildJudicialLeaseReport(apiData map[string]interface{}) map[string]interface{} { + report := emptyJudicialLeaseReport() + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return report + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return report + } + + lawsuitStat := asMap(judicialData["lawsuitStat"]) + courtInfo, newMult := judicialCourtMaps(report) + if courtInfo == nil || newMult == nil { + return report + } + + if lawsuitStat != nil { + courtInfo["civilCasesCount"] = lawsuitCasesCount(lawsuitStat, "civil") + courtInfo["criminalCasesCount"] = lawsuitCasesCount(lawsuitStat, "criminal") + courtInfo["administrativeCasesCount"] = lawsuitCasesCount(lawsuitStat, "administrative") + courtInfo["preservationCasesCount"] = lawsuitPreservationCount(lawsuitStat) + courtInfo["enforcementCasesCount"] = lawsuitCasesCount(lawsuitStat, "implement") + courtInfo["bankruptcyCasesCount"] = lawsuitCasesCount(lawsuitStat, "bankrupt") + + if cases := convertSinanCaseList(lawsuitStat, "civil"); len(cases) > 0 { + newMult["civilCases"] = cases + } + if cases := convertSinanCaseList(lawsuitStat, "criminal"); len(cases) > 0 { + newMult["criminalCases"] = cases + } + if cases := convertSinanCaseList(lawsuitStat, "administrative"); len(cases) > 0 { + newMult["administrativeCases"] = cases + } + if cases := convertSinanCaseList(lawsuitStat, "preservation"); len(cases) > 0 { + newMult["preservationCases"] = cases + } + if cases := convertSinanCaseList(lawsuitStat, "implement"); len(cases) > 0 { + newMult["enforcementCases"] = cases + } + if cases := convertSinanCaseList(lawsuitStat, "bankrupt"); len(cases) > 0 { + newMult["bankruptcyCases"] = cases + } + + if globalCount := asMap(lawsuitStat["count"]); globalCount != nil { + newMult["caseCounts"] = convertFLXGCountToSinan(globalCount) + } + for _, pair := range []struct { + flxgKey string + sinanKey string + }{ + {"civil", "civilCaseCounts"}, + {"criminal", "criminalCaseCounts"}, + {"administrative", "administrativeCaseCounts"}, + {"preservation", "preservationCaseCounts"}, + {"implement", "enforcementCaseCounts"}, + {"bankrupt", "bankruptcyCaseCounts"}, + } { + if bucket := asMap(lawsuitStat[pair.flxgKey]); bucket != nil { + if count := asMap(bucket["count"]); count != nil { + newMult[pair.sinanKey] = convertFLXGCountToSinan(count) + } + } + } + } + + if breachList, ok := judicialData["breachCaseList"].([]interface{}); ok && len(breachList) > 0 { + disinCases := make([]interface{}, 0, len(breachList)) + for _, item := range breachList { + if caseMap, ok := item.(map[string]interface{}); ok { + if converted := convertSinanDisinCase(caseMap); converted != nil { + disinCases = append(disinCases, converted) + } + } + } + if len(disinCases) > 0 { + newMult["disinCases"] = disinCases + } + } + + if limitList, ok := judicialData["consumptionRestrictionList"].([]interface{}); ok && len(limitList) > 0 { + limitCases := make([]interface{}, 0, len(limitList)) + for _, item := range limitList { + if caseMap, ok := item.(map[string]interface{}); ok { + if converted := convertSinanLimitCase(caseMap); converted != nil { + limitCases = append(limitCases, converted) + } + } + } + if len(limitCases) > 0 { + newMult["limitCases"] = limitCases + } + } + + report["caseOverviewInfo"] = buildCaseOverviewInfo(lawsuitStat, judicialData, courtInfo) + return report +} + +func judicialCourtMaps(report map[string]interface{}) (courtInfo, newMult map[string]interface{}) { + ci, ok := report["courtInfo"].(map[string]interface{}) + if !ok || ci == nil { + return nil, nil + } + nm, ok := ci["newMultCourtInfo"].(map[string]interface{}) + if !ok || nm == nil { + return nil, nil + } + return ci, nm +} + +func lawsuitCasesCount(lawsuitStat map[string]interface{}, key string) int { + bucket := asMap(lawsuitStat[key]) + if bucket == nil { + return 0 + } + if cases, ok := bucket["cases"].([]interface{}); ok && len(cases) > 0 { + return len(cases) + } + if count := asMap(bucket["count"]); count != nil { + return parseIntValue(count["count_total"]) + } + return 0 +} + +func lawsuitPreservationCount(lawsuitStat map[string]interface{}) int { + bucket := asMap(lawsuitStat["preservation"]) + if bucket == nil { + return 0 + } + if count := asMap(bucket["count"]); count != nil { + if total := parseIntValue(count["count_total"]); total > 0 { + return total + } + } + return lawsuitCasesCount(lawsuitStat, "preservation") +} + +func convertSinanCaseList(lawsuitStat map[string]interface{}, key string) []interface{} { + bucket := asMap(lawsuitStat[key]) + if bucket == nil { + return nil + } + cases, ok := bucket["cases"].([]interface{}) + if !ok || len(cases) == 0 { + return nil + } + out := make([]interface{}, 0, len(cases)) + for _, item := range cases { + caseMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + if converted := convertSinanLegalCase(caseMap); converted != nil { + out = append(out, converted) + } + } + return out +} + +func convertSinanLegalCase(caseMap map[string]interface{}) map[string]interface{} { + caseNumber := xypString(caseMap, "c_ah") + if caseNumber == "" { + return nil + } + info := map[string]interface{}{ + "caseNumber": caseNumber, + "nssdw": xypString(caseMap, "n_ssdw"), + "caseStatus": xypString(caseMap, "n_ajjzjd"), + "najlx": xypString(caseMap, "n_ajlx"), + "cdsrxx": formatCdsrxx(caseMap["c_dsrxx"]), + "nslcx": xypString(caseMap, "n_slcx"), + "zxfy": xypString(caseMap, "n_jbfy"), + "njbfy": xypString(caseMap, "n_jbfy"), + "dlarq": xypString(caseMap, "d_larq"), + "djarq": xypString(caseMap, "d_jarq"), + "njafs": xypString(caseMap, "n_jafs"), + "nqsbdje": formatNumericValue(caseMap["n_qsbdje"]), + "njabdje": formatNumericValue(caseMap["n_jabdje"]), + "nsqzxbdje": formatNumericValue(caseMap["n_sqzxbdje"]), + "nsjdwje": formatNumericValue(caseMap["n_sjdwje"]), + "nwzxje": formatNumericValue(caseMap["n_wzxje"]), + "npjVictory": xypString(caseMap, "n_pj_victory"), + "cgkwsDsr": xypString(caseMap, "c_gkws_dsr"), + "cgkwsPjjg": xypString(caseMap, "c_gkws_pjjg"), + "najbs": xypString(caseMap, "n_ajbs"), + "nlaayTree": firstNonEmpty(xypString(caseMap, "n_laay_tree"), xypString(caseMap, "n_jaay_tree")), + "nfzje": formatNumericValue(caseMap["n_fzje"]), + "npcpcje": formatNumericValue(caseMap["n_pcpcje"]), + "nccxzxje": formatNumericValue(caseMap["n_ccxzxje"]), + "cahHx": "", + "cahHxBs": "", + } + if cAhHx := xypString(caseMap, "c_ah_hx"); cAhHx != "" { + parts := strings.SplitN(cAhHx, ":", 2) + info["cahHx"] = strings.TrimSpace(parts[0]) + if len(parts) == 2 { + info["cahHxBs"] = strings.TrimSpace(parts[1]) + } + } + return info +} + +func formatCdsrxx(raw interface{}) string { + list, ok := raw.([]interface{}) + if !ok || len(list) == 0 { + return "" + } + parts := make([]string, 0, len(list)) + for _, item := range list { + m, ok := item.(map[string]interface{}) + if !ok { + continue + } + role := xypString(m, "n_ssdw") + name := xypString(m, "c_mc") + typ := xypString(m, "n_dsrlx") + if name == "" { + continue + } + parts = append(parts, fmt.Sprintf("%s%s\u3010%s\u3011", role, name, typ)) + } + return strings.Join(parts, "\uff1b") +} + +func convertFLXGCountToSinan(count map[string]interface{}) map[string]interface{} { + out := emptyCaseCounts() + out["totalCasesCounts"] = countToString(count["count_total"]) + out["totalJieCaseCounts"] = countToString(count["count_jie_total"]) + out["totalWeiCaseCounts"] = countToString(count["count_wei_total"]) + out["totalCaseAmounts"] = countToString(count["money_total"]) + out["totalJieAmountCases"] = countToString(count["money_jie_total"]) + out["totalWeiAmountCases"] = countToString(count["money_wei_total"]) + out["beigaoTotalCasesCounts"] = countToString(count["count_beigao"]) + out["beigaoTotalJieCaseCounts"] = countToString(count["count_jie_beigao"]) + out["beigaoTotalWeiCaseCounts"] = countToString(count["count_wei_beigao"]) + out["beigaoTotalCaseAmounts"] = countToString(count["money_beigao"]) + out["beigaoTotalJieAmountCases"] = countToString(count["money_jie_beigao"]) + out["beigaoTotalWeiAmountCases"] = countToString(count["money_wei_beigao"]) + out["yuangaoTotalCasesCounts"] = countToString(count["count_yuangao"]) + out["yuangaoTotalJieCaseCounts"] = countToString(count["count_jie_yuangao"]) + out["yuangaoTotalWeiCaseCounts"] = countToString(count["count_wei_yuangao"]) + out["yuangaoTotalCaseAmounts"] = countToString(count["money_yuangao"]) + out["yuangaoTotalJieAmountCases"] = countToString(count["money_jie_yuangao"]) + out["yuangaoTotalWeiAmountCases"] = countToString(count["money_wei_yuangao"]) + out["otherTotalCasesCounts"] = countToString(count["count_other"]) + out["otherTotalJieCaseCounts"] = countToString(count["count_jie_other"]) + out["otherTotalWeiCaseCounts"] = countToString(count["count_wei_other"]) + out["otherTotalCaseAmounts"] = countToString(count["money_other"]) + out["otherTotalJieAmountCases"] = countToString(count["money_jie_other"]) + out["otherTotalWeiAmountCases"] = countToString(count["money_wei_other"]) + out["caseActionDistribution"] = statOrDash(count["ay_stat"]) + out["localDistribution"] = statOrDash(count["area_stat"]) + out["timeDistribution"] = statOrDash(count["larq_stat"]) + out["caseCloselDistribution"] = statOrDash(count["jafs_stat"]) + return out +} + +func countToString(val interface{}) string { + if val == nil { + return "0" + } + switch v := val.(type) { + case string: + if strings.TrimSpace(v) == "" { + return "0" + } + return v + case float64: + return formatNumericValue(v) + case int: + return formatNumericValue(v) + case int64: + return formatNumericValue(v) + default: + return "0" + } +} + +func statOrDash(val interface{}) string { + s := strings.TrimSpace(fmt.Sprint(val)) + if s == "" || s == "" { + return "-" + } + return s +} + +func buildCaseOverviewInfo(lawsuitStat map[string]interface{}, judicialData map[string]interface{}, courtInfo map[string]interface{}) map[string]interface{} { + overview := emptyCaseOverviewInfo() + + if lawsuitStat != nil { + if globalCount := asMap(lawsuitStat["count"]); globalCount != nil { + overview["beigaoTotalCasesCounts"] = parseIntValue(globalCount["count_beigao"]) + overview["beigaoTotalWeiCaseCounts"] = parseIntValue(globalCount["count_wei_beigao"]) + overview["beigaoTotalCaseAmounts"] = parseIntValue(globalCount["money_beigao"]) + overview["leastCaseTime"] = extractLatestCaseTime(statOrDash(globalCount["larq_stat"])) + } + } + + overview["executionCaseCounts"] = courtInfo["enforcementCasesCount"] + + if breachList, ok := judicialData["breachCaseList"].([]interface{}); ok && len(breachList) > 0 { + overview["disinCaseCounts"] = 1 + } + if limitList, ok := judicialData["consumptionRestrictionList"].([]interface{}); ok && len(limitList) > 0 { + overview["limitCaseCounts"] = 1 + } + + return overview +} + +func extractLatestCaseTime(stat string) string { + if stat == "" || stat == "-" { + return "" + } + parts := strings.Split(stat, ",") + if len(parts) == 0 { + return "" + } + return strings.TrimSpace(parts[len(parts)-1]) +} + +func convertSinanDisinCase(caseMap map[string]interface{}) map[string]interface{} { + caseNumber := xypString(caseMap, "caseNumber") + if caseNumber == "" { + return nil + } + return map[string]interface{}{ + "caseNumber": caseNumber, + "court": xypString(caseMap, "executiveCourt"), + "filingTime": xypString(caseMap, "fileDate"), + "caseStatus": xypString(caseMap, "fulfillStatus"), + } +} + +func convertSinanLimitCase(caseMap map[string]interface{}) map[string]interface{} { + caseNumber := xypString(caseMap, "caseNumber") + if caseNumber == "" { + return nil + } + return map[string]interface{}{ + "caseNumber": caseNumber, + "court": xypString(caseMap, "executiveCourt"), + "filingTime": xypString(caseMap, "fileDate"), + } +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} + +func hasJudicialCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + lawsuitStat := asMap(judicialData["lawsuitStat"]) + if lawsuitStat != nil { + for _, key := range []string{"civil", "criminal", "administrative", "preservation", "implement", "bankrupt"} { + if lawsuitCasesCount(lawsuitStat, key) > 0 { + return true + } + } + } + if list, ok := judicialData["breachCaseList"].([]interface{}); ok && len(list) > 0 { + return true + } + if list, ok := judicialData["consumptionRestrictionList"].([]interface{}); ok && len(list) > 0 { + return true + } + return false +} + +func hasPreservationCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + lawsuitStat := asMap(judicialData["lawsuitStat"]) + if lawsuitStat == nil { + return false + } + return lawsuitPreservationCount(lawsuitStat) > 0 +} + +func hasLegalCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + lawsuitStat := asMap(judicialData["lawsuitStat"]) + if lawsuitStat == nil { + return false + } + for _, key := range []string{"civil", "criminal", "administrative"} { + if lawsuitCasesCount(lawsuitStat, key) > 0 { + return true + } + } + return false +} + +func hasExecutionCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + lawsuitStat := asMap(judicialData["lawsuitStat"]) + if lawsuitStat == nil { + return false + } + return lawsuitCasesCount(lawsuitStat, "implement") > 0 +} + +func hasDisinCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + list, ok := judicialData["breachCaseList"].([]interface{}) + return ok && len(list) > 0 +} + +func hasLimitCases(apiData map[string]interface{}) bool { + flxg := flxg7e8fMap(apiData) + if flxg == nil { + return false + } + judicialData := asMap(flxg["judicial_data"]) + if judicialData == nil { + return false + } + list, ok := judicialData["consumptionRestrictionList"].([]interface{}) + return ok && len(list) > 0 +} + +func hasCourtViolator(apiData map[string]interface{}) bool { + return hasExecutionCases(apiData) || hasDisinCases(apiData) +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_map.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_map.go new file mode 100644 index 0000000..516dbb5 --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_map.go @@ -0,0 +1,682 @@ +package dwbg + +import ( + "fmt" + "strconv" + "strings" + "time" + + "tyapi-server/internal/domains/api/dto" + + "go.uber.org/zap" +) + +// transformToSinanReport 将子接口数据组合为司南报告格式。 +func transformToSinanReport(apiData map[string]interface{}, params dto.DWBG6A2CReq, log *zap.Logger) map[string]interface{} { + _ = log + report := emptySinanReport(params) + + mergeInto(report, "baseInfo", buildSinanBaseInfo(apiData, params)) + mergeInto(report, "standLiveInfo", buildSinanStandLiveInfo(apiData)) + mergeInto(report, "antiFraudInfo", buildSinanAntiFraudInfo(apiData)) + mergeInto(report, "securityInfo", buildSinanSecurityInfo(apiData)) + mergeInto(report, "applicationStatistics", buildApplicationStatistics(apiData)) + mergeInto(report, "lendingStatistics", buildLendingStatistics(apiData)) + mergeInto(report, "performanceStatistics", buildPerformanceStatistics(apiData)) + mergeInto(report, "overdueRecord", buildOverdueRecord(apiData)) + mergeInto(report, "creditDetail", buildCreditDetail(apiData)) + mergeInto(report, "rentalBehavior", buildRentalBehavior(apiData)) + mergeInto(report, "riskSupervision", buildSinanRiskSupervision(apiData)) + report["judicialLeaseReport"] = buildJudicialLeaseReport(apiData) + + calcSinanRisk(report, apiData) + return report +} + +func buildSinanBaseInfo(apiData map[string]interface{}, params dto.DWBG6A2CReq) map[string]interface{} { + base := emptyBaseInfo(params) + + if age, sex := calculateAgeAndSex(params.IDCard); age > 0 { + base["age"] = age + base["sex"] = sex + } + + if yysyh6f3 := asMap(getMapValue(apiData, "YYSYH6F3")); yysyh6f3 != nil { + if address := xypString(yysyh6f3, "address"); address != "" { + base["location"] = address + } + if sex := xypString(yysyh6f3, "sex"); sex != "" { + base["sex"] = sex + } + if birthday := xypString(yysyh6f3, "birthday"); birthday != "" { + if age := ageFromBirthday(birthday); age > 0 { + base["age"] = age + } + } + if channel := xypString(yysyh6f3, "channel"); channel != "" { + base["channel"] = convertChannelName(channel) + } + } + if base["location"] == "" { + if loc := getLocationFromIDCard(params.IDCard); loc != "" { + base["location"] = loc + } + } + + if yysy35ta := asMap(getMapValue(apiData, "YYSY35TA")); yysy35ta != nil { + prov := xypString(yysy35ta, "prov") + city := xypString(yysy35ta, "city") + if prov != "" && city != "" { + base["phoneArea"] = prov + "-" + city + } else if prov != "" { + base["phoneArea"] = prov + } + if base["channel"] == "" { + if name := xypString(yysy35ta, "name"); name != "" { + base["channel"] = convertChannelName(name) + } + } + } + + if yysye7v5 := asMap(getMapValue(apiData, "YYSYE7V5")); yysye7v5 != nil { + base["status"] = convertStatusFromOnlineStatus(yysye7v5) + if base["channel"] == "" { + if ch := xypString(yysye7v5, "channel"); ch != "" { + base["channel"] = convertChannelName(ch) + } + } + } + + return base +} + +func ageFromBirthday(birthday string) int { + birthday = strings.TrimSpace(birthday) + layouts := []string{"2006-01-02", "20060102", "2006/01/02"} + for _, layout := range layouts { + if t, err := time.Parse(layout, birthday); err == nil { + now := time.Now() + age := now.Year() - t.Year() + if now.YearDay() < t.YearDay() { + age-- + } + if age > 0 { + return age + } + } + } + return 0 +} + +func buildSinanStandLiveInfo(apiData map[string]interface{}) map[string]interface{} { + info := emptyStandLiveInfo() + + if yysyh6f3 := asMap(getMapValue(apiData, "YYSYH6F3")); yysyh6f3 != nil { + finalAuth, verification := mapYYSYH6F3StandLive(xypString(yysyh6f3, "result")) + info["finalAuthResult"] = finalAuth + info["verification"] = verification + } + + if yysyp0t4 := asMap(getMapValue(apiData, "YYSYP0T4")); yysyp0t4 != nil { + info["inTime"] = mapYYSYP0T4InTime(xypString(yysyp0t4, "time"), apiData) + } + + return info +} + +func mapYYSYH6F3StandLive(result string) (finalAuthResult, verification string) { + switch result { + case "0": + return "0", "0" + case "1": + return "1", "1" + case "2": + return "1", "-1" + default: + return "1", "-1" + } +} + +func mapYYSYP0T4InTime(timeRange string, apiData map[string]interface{}) string { + timeRange = strings.TrimSpace(timeRange) + if timeRange == "" { + if yysye7v5 := asMap(getMapValue(apiData, "YYSYE7V5")); yysye7v5 != nil { + status := convertStatusFromOnlineStatus(yysye7v5) + if status != 1 { + return "99" + } + } + return "-1" + } + switch timeRange { + case "[0,3)": + return "0" + case "[3,6)": + return "3" + case "[6,12)": + return "6" + case "[12,24)": + return "12" + case "[24,-1)", "[24,+)", "[24,Inf)": + return "24" + default: + return "-1" + } +} + +func buildSinanAntiFraudInfo(apiData map[string]interface{}) map[string]interface{} { + info := emptyAntiFraudInfo() + flxg := asMap(getMapValue(apiData, "FLXG8B4D")) + if flxg == nil { + return info + } + data := flxg + if nested, ok := flxg["data"].(map[string]interface{}); ok { + data = nested + } + for _, field := range []string{"moneyLaundering", "deceiver", "gamblerPlayer", "gamblerBanker"} { + if val := xypString(data, field); val != "" { + info[field] = val + } + } + return info +} + +func buildSinanSecurityInfo(apiData map[string]interface{}) map[string]interface{} { + info := emptySecurityInfo() + flxg := asMap(getMapValue(apiData, "FLXGDEA9")) + if flxg == nil { + return info + } + level := xypString(flxg, "level") + if level == "" || level == "0" { + return info + } + for k, v := range parseSecurityInfoLevel(level) { + info[k] = v + } + return info +} + +func parseSecurityInfoLevel(level string) map[string]interface{} { + out := emptySecurityInfo() + for _, raw := range strings.Split(level, ",") { + t := strings.TrimSpace(raw) + if t == "" { + continue + } + switch { + case t == "A": + out["escape"] = 1 + out["icase"] = 1 + case t == "A1", t == "A2", t == "A3", t == "A4", t == "A5": + out["front"] = 1 + out["icase"] = 1 + case t == "C3": + out["drug"] = 1 + out["takeDrug"] = 1 + out["icase"] = 1 + case t == "D", t == "D1", t == "D2", t == "D3", t == "D4", t == "D5": + out["icase"] = 1 + out["ikey"] = 1 + case strings.HasPrefix(t, "B") || strings.HasPrefix(t, "C"): + out["icase"] = 1 + case t == "E": + out["itrancase"] = 1 + } + } + return out +} + +func buildApplicationStatistics(apiData map[string]interface{}) map[string]interface{} { + stats := emptyApplicationStatistics() + m := asMap(getMapValue(apiData, "JRZQ5E9F")) + if m == nil { + return stats + } + + total := decodeXypInt(m, "xyp_cpl0001") + consumption := decodeXypInt(m, "xyp_cpl0007") + online := decodeXypInt(m, "xyp_cpl0008") + other := total - consumption - online + if other < 0 { + other = 0 + } + + v180 := decodeXypInt(m, "xyp_cpl0013") + v360 := decodeXypInt(m, "xyp_t01dezhbc") + decodeXypInt(m, "xyp_t01dezhba") + last12 := v180 + if v360 > last12 { + last12 = v360 + } + + stats["totalApplicationCount"] = total + stats["consumptionInstallmentApplicationCount"] = consumption + stats["onlineLoanApplicationCount"] = online + stats["otherApplicationCount"] = other + stats["applicationCountLastMonth"] = decodeXypInt(m, "xyp_cpl0011") + stats["applicationCountLast3Months"] = decodeXypInt(m, "xyp_cpl0012") + stats["applicationCountLast6Months"] = v180 + stats["applicationCountLast12Months"] = last12 + stats["lastApplicationDate"] = reverseYYYYMMFromCpl0046(m) + stats["daysSinceLastApplication"] = formatXypInterval(m, "xyp_cpl0046") + if stats["daysSinceLastApplication"] == "0" { + stats["daysSinceLastApplication"] = "-" + } + + return stats +} + +func buildLendingStatistics(apiData map[string]interface{}) map[string]interface{} { + stats := emptyLendingStatistics() + m := asMap(getMapValue(apiData, "JRZQ5E9F")) + if m == nil { + return stats + } + + v360Count := decodeXypInt(m, "xyp_t01cczhzz") + v360Amount := formatXypInterval(m, "xyp_t01achzbz") + + stats["totalLendingInstitutionCount"] = decodeXypInt(m, "xyp_cpl0001") + stats["installmentLendingInstitutionCount"] = decodeXypInt(m, "xyp_cpl0007") + stats["onlineLendingInstitutionCount"] = decodeXypInt(m, "xyp_cpl0008") + stats["lastLendingDate"] = reverseYYYYMMFromCpl0046(m) + stats["daysSinceLastLending"] = formatXypInterval(m, "xyp_cpl0046") + if stats["daysSinceLastLending"] == "0" { + stats["daysSinceLastLending"] = "-" + } + stats["lendingCountLastMonth"] = decodeXypInt(m, "xyp_t01ccezzz") + stats["lendingAmountLastMonth"] = formatXypInterval(m, "xyp_t01acezzz") + stats["lendingCountLast3Months"] = decodeXypInt(m, "xyp_t01ccfzzz") + stats["lendingAmountLast3Months"] = formatXypInterval(m, "xyp_t01acfzzz") + stats["lendingCountLast6Months"] = decodeXypInt(m, "xyp_t01ccgzzz") + stats["lendingAmountLast6Months"] = formatXypInterval(m, "xyp_t01acgzzz") + stats["lendingCountLast12Months"] = v360Count + stats["lendingAmountLast12Months"] = v360Amount + stats["lendingCountLast24Months"] = v360Count + stats["lendingAmountLast24Months"] = v360Amount + + return stats +} + +func buildPerformanceStatistics(apiData map[string]interface{}) map[string]interface{} { + stats := emptyPerformanceStatistics() + m := asMap(getMapValue(apiData, "JRZQ5E9F")) + if m == nil { + return stats + } + + v360Count := decodeXypInt(m, "xyp_t01cchzzc") + v360Amount := formatXypInterval(m, "xyp_t01achzzc") + v180Exception := decodeXypInt(m, "xyp_cpl0026") + v360Exception := decodeXypInt(m, "xyp_t01cczhza") + exception12 := v180Exception + if v360Exception > exception12 { + exception12 = v360Exception + } + + stats["creditLoanDuration"] = decodeXypInt(m, "xyp_cpl0045") + stats["settledLoanOrderCount"] = decodeXypInt(m, "xyp_cpl0002") + stats["daysSinceLastPerformance"] = formatXypInterval(m, "xyp_cpl0068") + if stats["daysSinceLastPerformance"] == "0" { + stats["daysSinceLastPerformance"] = "-" + } + stats["normalRepaymentRatio"] = formatNormalRepaymentRatio(m) + stats["performanceCountLastMonth"] = decodeXypInt(m, "xyp_cpl0023") + stats["performanceAmountLastMonth"] = formatXypInterval(m, "xyp_cpl0039") + stats["repaymentExceptionCountLastMonth"] = decodeXypInt(m, "xyp_cpl0022") + stats["performanceCountLast3Months"] = decodeXypInt(m, "xyp_cpl0025") + stats["performanceAmountLast3Months"] = formatXypInterval(m, "xyp_cpl0041") + stats["repaymentExceptionCountLast3Months"] = decodeXypInt(m, "xyp_cpl0024") + stats["performanceCountLast6Months"] = decodeXypInt(m, "xyp_cpl0027") + stats["performanceAmountLast6Months"] = formatXypInterval(m, "xyp_cpl0043") + stats["repaymentExceptionCountLast6Months"] = v180Exception + stats["performanceCountLast12Months"] = v360Count + stats["performanceAmountLast12Months"] = v360Amount + stats["repaymentExceptionCountLast12Months"] = exception12 + stats["performanceCountLast24Months"] = v360Count + stats["performanceAmountLast24Months"] = v360Amount + stats["repaymentExceptionCountLast24Months"] = exception12 + + return stats +} + +func buildOverdueRecord(apiData map[string]interface{}) map[string]interface{} { + record := emptyOverdueRecord() + m := asMap(getMapValue(apiData, "JRZQ5E9F")) + if m == nil { + return record + } + + currentOverdue := decodeXypInt(m, "xyp_cpl0071") + if xypString(m, "xyp_cpl0044") == "1" { + if currentOverdue > 0 { + record["currentOverdueInstitution"] = fmt.Sprintf("%d(未结清)", currentOverdue) + } else { + record["currentOverdueInstitution"] = "1(未结清)" + } + } else if currentOverdue > 0 { + record["currentOverdueInstitution"] = formatXypInterval(m, "xyp_cpl0071") + } + + record["currentOverdueCount"] = currentOverdue + record["totalOverdueAmount"] = formatOverdueAmountHyphen(m, "xyp_cpl0072") + record["lastOverdueDate"] = "-" + + m1Signal := 0 + for _, key := range []string{"xyp_cpl0029", "xyp_cpl0030", "xyp_cpl0031"} { + if xypString(m, key) == "1" { + m1Signal++ + } + } + m1Plus6 := m1Signal + if m1Plus6 > 6 { + m1Plus6 = 6 + } + m1Plus12 := m1Signal + if m1Plus12 > 12 { + m1Plus12 = 12 + } + record["m1PlusCountLast6Months"] = m1Plus6 + record["m1PlusCountLast12Months"] = m1Plus12 + record["m1PlusCountLast24Months"] = m1Plus12 + + repay360 := decodeXypInt(m, "xyp_t01cchzzc") + if xypString(m, "xyp_cpl0044") != "1" && repay360 > 0 { + record["m0PlusCountLast6Months"] = decodeXypInt(m, "xyp_cpl0027") + record["m0PlusCountLast12Months"] = repay360 + record["m0PlusCountLast24Months"] = repay360 + } + + record["totalAmountLast6Months"] = formatXypInterval(m, "xyp_cpl0042") + record["totalAmountLast12Months"] = formatXypInterval(m, "xyp_t01aahzza") + record["totalAmountLast24Months"] = record["totalAmountLast12Months"] + + return record +} + +func buildCreditDetail(apiData map[string]interface{}) map[string]interface{} { + detail := emptyCreditDetail() + m := asMap(getMapValue(apiData, "JRZQ5E9F")) + if m == nil { + return detail + } + detail["maxOnlineLoanCredit"] = formatXypNumericString(m, "xyp_t01aahzbz") + detail["avgOnlineLoanCredit"] = formatXypNumericString(m, "xyp_t01adhzbc") + detail["maxConsumptionInstallmentCredit"] = formatXypNumericString(m, "xyp_t01aazhaz") + detail["avgConsumptionInstallmentCredit"] = formatXypNumericString(m, "xyp_t01adgzzc") + return detail +} + +var rentalPeriodMap = map[string]string{ + "3Days": "d3", + "7Days": "d7", + "14Days": "d14", + "1Month": "m1", + "3Months": "m3", + "6Months": "m6", + "12Months": "m12", +} + +func buildRentalBehavior(apiData map[string]interface{}) map[string]interface{} { + behavior := emptyRentalBehavior() + jrzq := asMap(getMapValue(apiData, "JRZQ1D09")) + if jrzq == nil { + return behavior + } + + for periodLabel, apiPeriod := range rentalPeriodMap { + behavior[fmt.Sprintf("rentalApplicationCountLast%s", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "allnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "allnum"), + ) + behavior[fmt.Sprintf("rentalApplicationInstitutionsLast%s", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "orgnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "orgnum"), + ) + behavior[fmt.Sprintf("rentalApplicationCountLast%sWeekend", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "weekend_allnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "weekend_allnum"), + ) + behavior[fmt.Sprintf("rentalApplicationInstitutionsLast%sWeekend", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "weekend_orgnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "weekend_orgnum"), + ) + behavior[fmt.Sprintf("rentalApplicationCountLast%sNight", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "night_allnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "night_allnum"), + ) + behavior[fmt.Sprintf("rentalApplicationInstitutionsLast%sNight", periodLabel)] = formatRentalPair( + rentalNestedValue(jrzq, apiPeriod, "id", "night_orgnum"), + rentalNestedValue(jrzq, apiPeriod, "cell", "night_orgnum"), + ) + } + + return behavior +} + +func rentalNestedValue(jrzq map[string]interface{}, period, dim, field string) string { + if periodData := asMap(jrzq[period]); periodData != nil { + if dimData := asMap(periodData[dim]); dimData != nil { + if val := xypString(dimData, field); val != "" { + return val + } + } + } + flatKey := fmt.Sprintf("alc_%s_%s_%s", period, dim, field) + if val := getStringValue(jrzq, flatKey); val != "" { + return val + } + return "0" +} + +func formatRentalPair(idVal, cellVal string) string { + if idVal == "" { + idVal = "0" + } + if cellVal == "" { + cellVal = "0" + } + return idVal + "/" + cellVal +} + +func buildSinanRiskSupervision(apiData map[string]interface{}) map[string]interface{} { + supervision := buildRiskSupervision(apiData, zap.NewNop()) + idCount := parseIntValue(supervision["rentalRiskListIdCardRelationsPhones"]) + phoneCount := parseIntValue(supervision["rentalRiskListPhoneRelationsIdCards"]) + if idCount > 1 || phoneCount > 1 { + supervision["details"] = "存在中介关联风险" + } else { + supervision["details"] = "无" + } + return supervision +} + +func asMap(val interface{}) map[string]interface{} { + if val == nil { + return nil + } + m, ok := val.(map[string]interface{}) + if !ok { + return nil + } + return m +} + +func jrzqMap(apiData map[string]interface{}) map[string]interface{} { + return asMap(getMapValue(apiData, "JRZQ5E9F")) +} + +func jrzq1d09Map(apiData map[string]interface{}) map[string]interface{} { + return asMap(getMapValue(apiData, "JRZQ1D09")) +} + +func flxg7e8fMap(apiData map[string]interface{}) map[string]interface{} { + return asMap(getMapValue(apiData, "FLXG7E8F")) +} + +func maxTotMons(jrzq map[string]interface{}) int { + maxVal := 0 + for _, period := range []string{"m6", "m12"} { + for _, dim := range []string{"id", "cell"} { + if periodData := asMap(jrzq[period]); periodData != nil { + if dimData := asMap(periodData[dim]); dimData != nil { + maxVal = maxInt(maxVal, parseIntValue(dimData["tot_mons"])) + } + } + flatKey := fmt.Sprintf("alc_%s_%s_tot_mons", period, dim) + maxVal = maxInt(maxVal, parseIntValue(jrzq[flatKey])) + } + } + return maxVal +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func rentalHasApplication(jrzq map[string]interface{}) bool { + for _, period := range []string{"m12"} { + idVal := parseIntValue(rentalNestedValue(jrzq, period, "id", "allnum")) + cellVal := parseIntValue(rentalNestedValue(jrzq, period, "cell", "allnum")) + if idVal > 0 || cellVal > 0 { + return true + } + } + return false +} + +func hitBankOverdueRecord(m map[string]interface{}) int { + if xypString(m, "xyp_cpl0044") == "1" { + return 1 + } + if decodeXypInt(m, "xyp_cpl0071") > 0 { + return 1 + } + if decodeXypInt(m, "xyp_cpl0072") > 0 { + return 1 + } + return 0 +} + +func hitRelationRisk(jrzq map[string]interface{}) int { + if jrzq == nil { + return 0 + } + idCount := parseIntValue(jrzq["idcard_relation_phone"]) + phoneCount := parseIntValue(jrzq["phone_relation_idard"]) + if idCount > 1 || phoneCount > 1 { + return 1 + } + return 0 +} + +func hitNewRiskFeature(m map[string]interface{}) int { + raw := xypString(m, "xyp_model_score_mid") + if raw == "" || raw == "-1" { + return 0 + } + score, err := strconv.ParseFloat(raw, 64) + if err != nil { + return 0 + } + const threshold = 350.0 + (950.0-350.0)/3.0 + if score < threshold { + return 1 + } + return 0 +} + +func calcMultiQuery(m map[string]interface{}) int { + windows := []struct { + months int + key string + }{ + {6, "xyp_cpl0013"}, + {3, "xyp_cpl0012"}, + {1, "xyp_cpl0011"}, + } + for _, w := range windows { + if decodeXypInt(m, w.key) > 0 { + return w.months + } + } + return 0 +} + +func calcDeductFail(m map[string]interface{}) int { + windows := []struct { + months int + key string + }{ + {6, "xyp_cpl0026"}, + {3, "xyp_cpl0024"}, + {1, "xyp_cpl0022"}, + } + for _, w := range windows { + if decodeXypInt(m, w.key) > 0 { + return w.months + } + } + return 0 +} + +func antiFraudHit(data map[string]interface{}) bool { + if data == nil { + return false + } + nested := data + if d, ok := data["data"].(map[string]interface{}); ok { + nested = d + } + for _, field := range []string{"moneyLaundering", "deceiver", "gamblerPlayer", "gamblerBanker"} { + val := xypString(nested, field) + if val != "" && val != "0" { + return true + } + } + return false +} + +func securityLevelHit(level string) bool { + level = strings.TrimSpace(level) + return level != "" && level != "0" +} + +func industryBlacklistHit(level string) bool { + for _, raw := range strings.Split(level, ",") { + if isLevelDClass(strings.TrimSpace(raw)) { + return true + } + } + return false +} + +func isLevelDClass(token string) bool { + switch token { + case "D", "D1", "D2", "D3", "D4", "D5": + return true + default: + return false + } +} + +func groupFraudHit(data map[string]interface{}) bool { + if data == nil { + return false + } + nested := data + if d, ok := data["data"].(map[string]interface{}); ok { + nested = d + } + for _, field := range []string{"deceiver", "gamblerBanker"} { + val := strings.ToUpper(xypString(nested, field)) + if val == "B" || val == "C" || val == "D" { + return true + } + } + return false +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_processor.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_processor.go index e74b2df..ca39d2d 100644 --- a/internal/domains/api/services/processors/dwbg/dwbg6a2c_processor.go +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_processor.go @@ -7,7 +7,9 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/zhicha" + "tyapi-server/internal/shared/logger" + + "go.uber.org/zap" ) // ProcessDWBG6A2CRequest DWBG6A2C API处理方法 - 司南报告 @@ -21,47 +23,25 @@ func ProcessDWBG6A2CRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - - encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - reqData := map[string]interface{}{ - "name": encryptedName, - "idCard": encryptedIDCard, - "phone": encryptedMobileNo, - "accessoryUrl": paramsDto.AuthorizationURL, - } - - respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI102", reqData) - if err != nil { - if errors.Is(err, zhicha.ErrDatasource) { - return nil, errors.Join(processors.ErrDatasource, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } - } - - // 过滤响应数据,删除指定字段 - if respMap, ok := respData.(map[string]interface{}); ok { - delete(respMap, "reportUrl") - delete(respMap, "multCourtInfo") - // delete(respMap, "judiciaRiskInfos") - } - - // 将响应数据转换为JSON字节 - respBytes, err := json.Marshal(respData) + log := logger.GetGlobalLogger() + log.Info("开始处理司南报告请求", + zap.String("name", paramsDto.Name), + zap.String("id_card", maskIDCard(paramsDto.IDCard)), + zap.String("mobile_no", maskMobile(paramsDto.MobileNo)), + ) + + apiData := collectSinanAPIData(ctx, paramsDto, deps, log) + report := transformToSinanReport(apiData, paramsDto, log) + if cleaned, ok := stripNullValues(report).(map[string]interface{}); ok { + report = cleaned + } + + respBytes, err := json.Marshal(report) if err != nil { + log.Error("序列化司南报告响应失败", zap.Error(err)) return nil, errors.Join(processors.ErrSystem, err) } + log.Info("司南报告处理完成") return respBytes, nil } diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_test.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_test.go new file mode 100644 index 0000000..3b5c1de --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_test.go @@ -0,0 +1,345 @@ +package dwbg + +import ( + "encoding/json" + "strings" + "testing" + + "tyapi-server/internal/domains/api/dto" + + "go.uber.org/zap" +) + +func TestEmptySinanReportMarshal(t *testing.T) { + params := dto.DWBG6A2CReq{ + Name: "张三", + IDCard: "320321199102011152", + MobileNo: "13812345678", + } + + report := emptySinanReport(params) + data, err := json.Marshal(report) + if err != nil { + t.Fatalf("序列化失败: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + t.Fatalf("反序列化失败: %v", err) + } + + requiredModules := []string{ + "baseInfo", "standLiveInfo", "riskPoint", "securityInfo", "antiFraudInfo", + "riskList", "applicationStatistics", "lendingStatistics", "performanceStatistics", + "overdueRecord", "creditDetail", "rentalBehavior", "riskSupervision", "judicialLeaseReport", + } + for _, module := range requiredModules { + if _, ok := parsed[module]; !ok { + t.Errorf("缺少顶层模块: %s", module) + } + } + + antiFraud, ok := parsed["antiFraudInfo"].(map[string]interface{}) + if !ok { + t.Fatal("antiFraudInfo 类型错误") + } + for _, key := range []string{"moneyLaundering", "deceiver", "gamblerPlayer", "gamblerBanker"} { + if _, exists := antiFraud[key]; !exists { + t.Errorf("antiFraudInfo 缺少键: %s", key) + } + } + + rental, ok := parsed["rentalBehavior"].(map[string]interface{}) + if !ok { + t.Fatal("rentalBehavior 类型错误") + } + if len(rental) != len(rentalBehaviorFieldNames) { + t.Errorf("rentalBehavior 字段数 = %d, want %d", len(rental), len(rentalBehaviorFieldNames)) + } +} + +func TestTransformToSinanReportAllNil(t *testing.T) { + params := dto.DWBG6A2CReq{ + Name: "张三", + IDCard: "320321199102011152", + MobileNo: "13812345678", + } + + apiData := map[string]interface{}{ + "YYSY35TA": nil, + "YYSYE7V5": nil, + "YYSYH6F3": nil, + "YYSYP0T4": nil, + "FLXGDEA9": nil, + "FLXG8B4D": nil, + "FLXG7E8F": nil, + "JRZQ5E9F": nil, + "JRZQ1D09": nil, + } + + report := transformToSinanReport(apiData, params, zap.NewNop()) + if _, err := json.Marshal(report); err != nil { + t.Fatalf("全 nil 数据源仍应可序列化: %v", err) + } +} + +func TestTransformToSinanReportWithMockData(t *testing.T) { + params := dto.DWBG6A2CReq{ + Name: "黄春丽", + IDCard: "452621197903185522", + MobileNo: "13907764404", + } + + apiData := map[string]interface{}{ + "YYSYH6F3": map[string]interface{}{ + "result": "0", + "sex": "女", + "address": "广西壮族自治区百色地区百色市", + "channel": "cmcc", + }, + "YYSYE7V5": map[string]interface{}{ + "status": 0, + "channel": "cmcc", + }, + "YYSY35TA": map[string]interface{}{ + "prov": "广西壮族自治区", + "city": "南宁", + "name": "中国移动", + }, + "YYSYP0T4": map[string]interface{}{ + "time": "[24,-1)", + }, + "FLXG8B4D": map[string]interface{}{ + "gamblerBanker": "A", + }, + "FLXGDEA9": map[string]interface{}{ + "level": "A1,C3", + }, + "JRZQ5E9F": map[string]interface{}{ + "xyp_cpl0001": "2", + "xyp_cpl0007": "2", + "xyp_cpl0008": "1", + "xyp_cpl0011": "2", + "xyp_cpl0012": "2", + "xyp_cpl0013": "3", + "xyp_cpl0044": "1", + "xyp_cpl0074": "0.56", + "xyp_model_score_mid": "500", + }, + "JRZQ1D09": map[string]interface{}{ + "m12": map[string]interface{}{ + "id": map[string]interface{}{"allnum": "7", "orgnum": "1", "tot_mons": "16"}, + "cell": map[string]interface{}{"allnum": "7", "orgnum": "1", "tot_mons": "16"}, + }, + "idcard_relation_phone": 1, + "phone_relation_idard": 0, + }, + } + + report := transformToSinanReport(apiData, params, zap.NewNop()) + + standLive := report["standLiveInfo"].(map[string]interface{}) + if standLive["finalAuthResult"] != "0" || standLive["verification"] != "0" { + t.Errorf("standLiveInfo 三要素映射错误: %+v", standLive) + } + if standLive["inTime"] != "24" { + t.Errorf("inTime = %v, want 24", standLive["inTime"]) + } + + security := report["securityInfo"].(map[string]interface{}) + if security["front"] != 1 || security["escape"] != 0 || security["drug"] != 1 { + t.Errorf("securityInfo 解析错误: %+v", security) + } + + antiFraud := report["antiFraudInfo"].(map[string]interface{}) + if antiFraud["gamblerBanker"] != "A" { + t.Errorf("antiFraudInfo.gamblerBanker = %v", antiFraud["gamblerBanker"]) + } + + riskList := report["riskList"].(map[string]interface{}) + if riskList["creditLeaseRisk"] != 1 { + t.Errorf("creditLeaseRisk = %v, want 1", riskList["creditLeaseRisk"]) + } + if riskList["vehicleLeaseViolation"] != 1 { + t.Errorf("vehicleLeaseViolation = %v, want 1", riskList["vehicleLeaseViolation"]) + } + if riskList["highRiskArea"] != 0 { + t.Errorf("highRiskArea = %v, want 0", riskList["highRiskArea"]) + } + + riskPoint := report["riskPoint"].(map[string]interface{}) + if riskPoint["antiFraudRisk"] != 1 { + t.Errorf("antiFraudRisk = %v, want 1", riskPoint["antiFraudRisk"]) + } + if riskPoint["judicialCase"] != 0 { + t.Errorf("judicialCase = %v, want 0", riskPoint["judicialCase"]) + } + + perf := report["performanceStatistics"].(map[string]interface{}) + if perf["serialVersionUID"] != 1 { + t.Errorf("serialVersionUID = %v, want 1", perf["serialVersionUID"]) + } +} + +func TestMapYYSYH6F3StandLive(t *testing.T) { + cases := []struct { + result, wantFinal, wantVerify string + }{ + {"0", "0", "0"}, + {"1", "1", "1"}, + {"2", "1", "-1"}, + {"", "1", "-1"}, + } + for _, tc := range cases { + final, verify := mapYYSYH6F3StandLive(tc.result) + if final != tc.wantFinal || verify != tc.wantVerify { + t.Errorf("result=%q got (%s,%s) want (%s,%s)", tc.result, final, verify, tc.wantFinal, tc.wantVerify) + } + } +} + +func TestParseSecurityInfoLevel(t *testing.T) { + info := parseSecurityInfoLevel("A,C3") + if info["escape"] != 1 || info["front"] != 0 || info["drug"] != 1 { + t.Fatalf("A,C3 => %+v", info) + } + info = parseSecurityInfoLevel("A1") + if info["front"] != 1 || info["escape"] != 0 { + t.Fatalf("A1 => %+v", info) + } +} + +func TestIndustryBlacklistHit(t *testing.T) { + if !industryBlacklistHit("D1,B") { + t.Fatal("D1 should hit industry blacklist") + } + if industryBlacklistHit("A1,C3") { + t.Fatal("A1,C3 should not hit industry blacklist") + } +} + +func TestReportNoNullValues(t *testing.T) { + params := dto.DWBG6A2CReq{ + Name: "张三", + IDCard: "320321199102011152", + MobileNo: "13812345678", + } + apiData := map[string]interface{}{ + "FLXG7E8F": map[string]interface{}{ + "judicial_data": map[string]interface{}{ + "breachCaseList": []interface{}{}, + "consumptionRestrictionList": []interface{}{}, + "lawsuitStat": map[string]interface{}{ + "count": map[string]interface{}{ + "count_beigao": 0, + "larq_stat": "", + }, + "civil": map[string]interface{}{ + "cases": []interface{}{}, + "count": map[string]interface{}{"count_total": 0}, + }, + }, + }, + }, + } + report := transformToSinanReport(apiData, params, zap.NewNop()) + cleaned, ok := stripNullValues(report).(map[string]interface{}) + if !ok { + t.Fatal("stripNullValues failed") + } + if reportContainsNull(cleaned) { + t.Fatal("report still contains null after strip") + } + data, err := json.Marshal(cleaned) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + if strings.Contains(string(data), "null") { + t.Fatalf("JSON contains null: %s", string(data)) + } +} + +func TestBuildJudicialLeaseReportWithCases(t *testing.T) { + apiData := map[string]interface{}{ + "FLXG7E8F": map[string]interface{}{ + "judicial_data": map[string]interface{}{ + "breachCaseList": []interface{}{}, + "consumptionRestrictionList": []interface{}{}, + "lawsuitStat": map[string]interface{}{ + "count": map[string]interface{}{ + "count_total": 1, + "count_beigao": 1, + "larq_stat": "2026(1)", + "ay_stat": "民事(1)", + }, + "civil": map[string]interface{}{ + "cases": []interface{}{ + map[string]interface{}{ + "c_ah": "(2019)桂0103民初5129号", + "n_ssdw": "原告", + "n_ajjzjd": "已结案", + "n_ajlx": "民事一审", + "c_dsrxx": []interface{}{ + map[string]interface{}{ + "c_mc": "黄春丽", + "n_ssdw": "原告", + "n_dsrlx": "自然人", + }, + }, + }, + }, + "count": map[string]interface{}{ + "count_total": 1, + "count_beigao": 1, + }, + }, + "preservation": map[string]interface{}{ + "cases": []interface{}{}, + "count": map[string]interface{}{"count_total": 1}, + }, + "implement": map[string]interface{}{ + "cases": []interface{}{}, + "count": map[string]interface{}{"count_total": 0}, + }, + }, + }, + }, + } + jlr := buildJudicialLeaseReport(apiData) + courtInfo := jlr["courtInfo"].(map[string]interface{}) + if courtInfo["civilCasesCount"] != 1 { + t.Fatalf("civilCasesCount = %v", courtInfo["civilCasesCount"]) + } + if courtInfo["preservationCasesCount"] != 1 { + t.Fatalf("preservationCasesCount = %v", courtInfo["preservationCasesCount"]) + } + newMult := courtInfo["newMultCourtInfo"].(map[string]interface{}) + civilCases, ok := newMult["civilCases"].([]interface{}) + if !ok || len(civilCases) != 1 { + t.Fatalf("civilCases missing or empty: %+v", newMult["civilCases"]) + } + if _, has := newMult["preservationCases"]; has { + t.Fatal("preservationCases list should be omitted when empty") + } + overview := jlr["caseOverviewInfo"].(map[string]interface{}) + if overview["beigaoTotalCasesCounts"] != 1 { + t.Fatalf("beigaoTotalCasesCounts = %v", overview["beigaoTotalCasesCounts"]) + } +} + +func TestM1PlusCountCaps(t *testing.T) { + apiData := map[string]interface{}{ + "JRZQ5E9F": map[string]interface{}{ + "xyp_cpl0029": "1", + "xyp_cpl0030": "1", + "xyp_cpl0031": "1", + }, + } + record := buildOverdueRecord(apiData) + if record["m1PlusCountLast6Months"] != 3 { + t.Fatalf("6m cap = %v", record["m1PlusCountLast6Months"]) + } + if record["m1PlusCountLast12Months"] != 3 { + t.Fatalf("12m cap = %v", record["m1PlusCountLast12Months"]) + } +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_validate.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_validate.go new file mode 100644 index 0000000..4861f9e --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_validate.go @@ -0,0 +1,45 @@ +package dwbg + +// stripNullValues 递归移除 map/slice 中的 nil,避免 JSON 输出 null。 +func stripNullValues(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(val)) + for k, item := range val { + if item == nil { + continue + } + out[k] = stripNullValues(item) + } + return out + case []interface{}: + out := make([]interface{}, 0, len(val)) + for _, item := range val { + if item == nil { + continue + } + out = append(out, stripNullValues(item)) + } + return out + default: + return v + } +} + +func reportContainsNull(v interface{}) bool { + switch val := v.(type) { + case map[string]interface{}: + for _, item := range val { + if item == nil || reportContainsNull(item) { + return true + } + } + case []interface{}: + for _, item := range val { + if item == nil || reportContainsNull(item) { + return true + } + } + } + return false +} diff --git a/internal/domains/api/services/processors/dwbg/dwbg6a2c_xyp.go b/internal/domains/api/services/processors/dwbg/dwbg6a2c_xyp.go new file mode 100644 index 0000000..86afd6d --- /dev/null +++ b/internal/domains/api/services/processors/dwbg/dwbg6a2c_xyp.go @@ -0,0 +1,233 @@ +package dwbg + +import ( + "fmt" + "math" + "strconv" + "strings" + "time" + + "tyapi-server/internal/domains/api/services/processors/jrzq" +) + +func xypString(m map[string]interface{}, key string) string { + if m == nil { + return "" + } + val, ok := m[key] + if !ok || val == nil { + return "" + } + switch v := val.(type) { + case string: + return strings.TrimSpace(v) + case float64: + if math.IsNaN(v) || math.IsInf(v, 0) { + return "" + } + if v == float64(int64(v)) { + return strconv.FormatInt(int64(v), 10) + } + return strconv.FormatFloat(v, 'f', -1, 64) + case int: + return strconv.Itoa(v) + case int64: + return strconv.FormatInt(v, 10) + default: + return fmt.Sprint(v) + } +} + +func decodeXypInt(m map[string]interface{}, key string) int { + code := xypString(m, key) + if code == "" || code == "0" { + return 0 + } + return jrzq.DecodeIntervalMidpoint(key, code) +} + +func formatXypInterval(m map[string]interface{}, key string) string { + code := xypString(m, key) + if code == "" || code == "0" { + return "0" + } + if conv := xypIntervalConverter(key); conv != nil { + return conv(code) + } + return jrzq.FormatLoanRiskInterval(key, code) +} + +func xypIntervalConverter(key string) func(string) string { + switch key { + case "xyp_t01aazzzc": + return convertXypT01aazzzcToInterval + case "xyp_cpl0068": + return convertXypCpl0068ToInterval + case "xyp_cpl0001": + return convertXypCpl0001ToInterval + case "xyp_cpl0002": + return convertXypCpl0002ToInterval + case "xyp_cpl0071": + return convertXypCpl0071ToInterval + case "xyp_cpl0072": + return convertXypCpl0072ToInterval + case "xyp_cpl0018", "xyp_cpl0019", "xyp_cpl0020", "xyp_cpl0021", "xyp_cpl0022", "xyp_cpl0023", "xyp_cpl0024", "xyp_cpl0025", "xyp_cpl0026": + return func(code string) string { + return callCplCountConverter(key, code) + } + case "xyp_cpl0034", "xyp_cpl0035", "xyp_cpl0036", "xyp_cpl0037", "xyp_cpl0038", "xyp_cpl0039", "xyp_cpl0040", "xyp_cpl0041", "xyp_cpl0042": + return func(code string) string { + return callCplAmountConverter(key, code) + } + default: + return nil + } +} + +func callCplCountConverter(key, code string) string { + switch key { + case "xyp_cpl0018": + return convertXypCpl0018ToInterval(code) + case "xyp_cpl0019": + return convertXypCpl0019ToInterval(code) + case "xyp_cpl0020": + return convertXypCpl0020ToInterval(code) + case "xyp_cpl0021": + return convertXypCpl0021ToInterval(code) + case "xyp_cpl0022": + return convertXypCpl0022ToInterval(code) + case "xyp_cpl0023": + return convertXypCpl0023ToInterval(code) + case "xyp_cpl0024": + return convertXypCpl0024ToInterval(code) + case "xyp_cpl0025": + return convertXypCpl0025ToInterval(code) + case "xyp_cpl0026": + return convertXypCpl0026ToInterval(code) + default: + return jrzq.FormatLoanRiskInterval(key, code) + } +} + +func callCplAmountConverter(key, code string) string { + switch key { + case "xyp_cpl0034": + return convertXypCpl0034ToInterval(code) + case "xyp_cpl0035": + return convertXypCpl0035ToInterval(code) + case "xyp_cpl0036": + return convertXypCpl0036ToInterval(code) + case "xyp_cpl0037": + return convertXypCpl0037ToInterval(code) + case "xyp_cpl0038": + return convertXypCpl0038ToInterval(code) + case "xyp_cpl0039": + return convertXypCpl0039ToInterval(code) + case "xyp_cpl0040": + return convertXypCpl0040ToInterval(code) + case "xyp_cpl0041": + return convertXypCpl0041ToInterval(code) + case "xyp_cpl0042": + return convertXypCpl0042ToInterval(code) + default: + return jrzq.FormatLoanRiskInterval(key, code) + } +} + +func formatXypNumericString(m map[string]interface{}, key string) string { + mid := decodeXypInt(m, key) + if mid <= 0 { + return "0" + } + return strconv.Itoa(mid) +} + +func formatOverdueAmountHyphen(m map[string]interface{}, key string) string { + interval := formatXypInterval(m, key) + if interval == "0" || interval == "-" { + return "0" + } + return intervalToHyphen(interval) +} + +func intervalToHyphen(interval string) string { + interval = strings.TrimSpace(interval) + if interval == "0" || interval == "-" { + return interval + } + inner := strings.TrimPrefix(strings.TrimPrefix(interval, "("), "[") + inner = strings.TrimSuffix(strings.TrimSuffix(inner, ")"), "]") + parts := strings.SplitN(inner, ",", 2) + if len(parts) != 2 { + return interval + } + left := strings.TrimSpace(parts[0]) + right := strings.TrimSpace(parts[1]) + right = strings.TrimSuffix(right, "+)") + right = strings.TrimSuffix(right, "+") + if left == "" || right == "" { + return interval + } + return left + "-" + right +} + +func reverseYYYYMMFromCpl0046(m map[string]interface{}) string { + days := decodeXypInt(m, "xyp_cpl0046") + if days <= 0 { + return "-" + } + return time.Now().AddDate(0, 0, -days).Format("2006-01") +} + +func formatNormalRepaymentRatio(m map[string]interface{}) string { + raw := xypString(m, "xyp_cpl0074") + if raw == "" || raw == "0" || raw == "-1" { + return "0" + } + if val, err := strconv.ParseFloat(raw, 64); err == nil { + if val <= 1 && val >= 0 { + return fmt.Sprintf("%.0f%%", val*100) + } + if val > 1 && val <= 100 { + return fmt.Sprintf("%.0f%%", val) + } + } + mid := decodeXypInt(m, "xyp_cpl0074") + if mid <= 0 { + return "0" + } + return fmt.Sprintf("%d%%", mid) +} + +func parseIntValue(val interface{}) int { + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + case string: + if v == "" { + return 0 + } + if n, err := strconv.Atoi(v); err == nil { + return n + } + } + return 0 +} + +func mergeInto(base map[string]interface{}, key string, patch map[string]interface{}) { + if patch == nil { + return + } + existing, ok := base[key].(map[string]interface{}) + if !ok { + base[key] = patch + return + } + for k, v := range patch { + existing[k] = v + } +} diff --git a/internal/domains/api/services/processors/ivyz/ivyz5e3f_processor.go b/internal/domains/api/services/processors/ivyz/ivyz5e3f_processor.go index 5cb22ec..03e735e 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz5e3f_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz5e3f_processor.go @@ -53,3 +53,6 @@ func ProcessIVYZ5E3FRequest(ctx context.Context, params []byte, deps *processors return respBytes, nil } + + + diff --git a/internal/domains/api/services/processors/jrzq/decode_interval.go b/internal/domains/api/services/processors/jrzq/decode_interval.go new file mode 100644 index 0000000..0e53010 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/decode_interval.go @@ -0,0 +1,85 @@ +package jrzq + +import ( + "fmt" + "math" + "strconv" +) + +// DecodeIntervalMidpoint 将 xyp 区间码解码为区间中位整数。 +func DecodeIntervalMidpoint(field, code string) int { + if code == "" || code == "0" { + return 0 + } + rules, ok := loanRiskIntervalMappings[field] + if !ok { + return 0 + } + for _, rule := range rules { + if rule.output != code { + continue + } + min := rule.min + max := rule.max + if max >= math.MaxFloat64/4 { + if rule.minInclusive { + return int(math.Round(min + 1)) + } + return int(math.Round(min + 2)) + } + lo := min + hi := max + if !rule.minInclusive { + lo += 0.5 + } + if !rule.maxInclusive { + hi -= 0.5 + } + return int(math.Round((lo + hi) / 2)) + } + return 0 +} + +// FormatLoanRiskInterval 将 xyp 区间码格式化为可读区间字符串。 +func FormatLoanRiskInterval(field, code string) string { + if code == "" || code == "0" { + return "0" + } + rules, ok := loanRiskIntervalMappings[field] + if !ok { + return "0" + } + for _, rule := range rules { + if rule.output != code { + continue + } + return formatIntervalBounds(rule.min, rule.max, rule.minInclusive, rule.maxInclusive) + } + return "0" +} + +func formatIntervalBounds(min, max float64, minInclusive, maxInclusive bool) string { + if max >= math.MaxFloat64/4 { + left := "(" + if minInclusive { + left = "[" + } + return fmt.Sprintf("%s%s,+)", left, formatBound(min)) + } + left := "(" + if minInclusive { + left = "[" + } + right := ")" + if maxInclusive { + right = "]" + } + return fmt.Sprintf("%s%s,%s%s", left, formatBound(min), formatBound(max), right) +} + +func formatBound(v float64) string { + if v == float64(int64(v)) { + return strconv.FormatInt(int64(v), 10) + } + return strconv.FormatFloat(v, 'f', -1, 64) +} diff --git a/internal/domains/api/services/processors/jrzq/decode_interval_test.go b/internal/domains/api/services/processors/jrzq/decode_interval_test.go new file mode 100644 index 0000000..b2666c5 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/decode_interval_test.go @@ -0,0 +1,19 @@ +package jrzq + +import "testing" + +func TestDecodeIntervalMidpoint(t *testing.T) { + if got := DecodeIntervalMidpoint("xyp_cpl0011", "2"); got <= 0 { + t.Fatalf("xyp_cpl0011 code 2 midpoint = %d, want > 0", got) + } + if got := DecodeIntervalMidpoint("xyp_cpl0011", "0"); got != 0 { + t.Fatalf("empty code midpoint = %d, want 0", got) + } +} + +func TestFormatLoanRiskInterval(t *testing.T) { + got := FormatLoanRiskInterval("xyp_cpl0046", "2") + if got == "0" || got == "" { + t.Fatalf("xyp_cpl0046 code 2 interval = %q", got) + } +}