From 3745a3768f5a7e5e8bae500f7ff029d531d4becc Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 12 Nov 2025 23:16:37 +0800 Subject: [PATCH] add COMBHZY2 --- internal/domains/api/dto/api_request_dto.go | 7 + .../api/services/api_request_service.go | 1 + .../api/services/form_config_service.go | 1 + .../processors/comb/combhzy2_processor.go | 64 + .../processors/comb/combhzy2_transform.go | 1783 +++++++++++++++++ 5 files changed, 1856 insertions(+) create mode 100644 internal/domains/api/services/processors/comb/combhzy2_processor.go create mode 100644 internal/domains/api/services/processors/comb/combhzy2_transform.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index dcbc040..444bce9 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -213,6 +213,13 @@ type COMB298YReq struct { TimeRange string `json:"time_range" validate:"omitempty,validTimeRange"` // 非必填字段 } +type COMBHZY2Req struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` + MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` + AuthorizationURL string `json:"authorization_url" validate:"required,authorization_url"` +} + type COMB86PMReq struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 7e73f4c..0244ac2 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -185,6 +185,7 @@ func registerAllProcessors(combService *comb.CombService) { // COMB系列处理器 - 只注册有自定义逻辑的组合包 "COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode + "COMBHZY2": comb.ProcessCOMBHZY2Request, // 自定义处理:生成合规报告 // QCXG系列处理器 "QCXG7A2B": qcxg.ProcessQCXG7A2BRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 0f3d474..ba57ae7 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -171,6 +171,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZ6G7H": &dto.IVYZ6G7HReq{}, "IVYZ8I9J": &dto.IVYZ8I9JReq{}, "JRZQ0L85": &dto.JRZQ0L85Req{}, + "COMBHZY2": &dto.COMBHZY2Req{}, } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/comb/combhzy2_processor.go b/internal/domains/api/services/processors/comb/combhzy2_processor.go new file mode 100644 index 0000000..9a58267 --- /dev/null +++ b/internal/domains/api/services/processors/comb/combhzy2_processor.go @@ -0,0 +1,64 @@ +package comb + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" +) + +// ProcessCOMBHZY2Request 处理 COMBHZY2 组合包请求 +func ProcessCOMBHZY2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var req dto.COMBHZY2Req + if err := json.Unmarshal(params, &req); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(req); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + + combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMBHZY2") + if err != nil { + return nil, err + } + + sourceCtx, err := buildSourceContextFromCombined(combinedResult) + if err != nil { + return nil, err + } + + report := buildTargetReport(sourceCtx) + return json.Marshal(report) +} + +func buildSourceContextFromCombined(result *processors.CombinedResult) (*sourceContext, error) { + if result == nil { + return nil, errors.New("组合包响应为空") + } + + src := sourceFile{Responses: make([]sourceResponse, 0, len(result.Responses))} + for _, resp := range result.Responses { + if !resp.Success { + continue + } + raw, err := json.Marshal(resp.Data) + if err != nil { + return nil, fmt.Errorf("序列化子产品(%s)数据失败: %w", resp.ApiCode, err) + } + src.Responses = append(src.Responses, sourceResponse{ + ApiCode: resp.ApiCode, + Data: raw, + Success: resp.Success, + }) + } + + if len(src.Responses) == 0 { + return nil, errors.New("组合包子产品全部调用失败") + } + + return buildSourceContext(src) +} diff --git a/internal/domains/api/services/processors/comb/combhzy2_transform.go b/internal/domains/api/services/processors/comb/combhzy2_transform.go new file mode 100644 index 0000000..a9b7fda --- /dev/null +++ b/internal/domains/api/services/processors/comb/combhzy2_transform.go @@ -0,0 +1,1783 @@ +package comb + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +// ======================= +// 原始数据结构 +// ======================= + +// sourceFile 对应 source.json 顶层结构,包含组合包所有子产品响应 +type sourceFile struct { + Responses []sourceResponse `json:"responses"` +} + +// sourceResponse 描述每个子产品在源数据中的响应格式 +type sourceResponse struct { + ApiCode string `json:"api_code"` + Data json.RawMessage `json:"data"` + Success bool `json:"success"` +} + +// --- DWBG8B4D --- + +// baseProductData 映射 DWBG8B4D 产品的 data 节点,用于基础信息与风险指标 +type baseProductData struct { + BaseInfo baseInfo `json:"baseInfo"` + CheckSuggest string `json:"checkSuggest"` + CreditScore int `json:"creditScore"` + ElementVerificationDetail elementVerificationDetail `json:"elementVerificationDetail"` + FraudRule string `json:"fraudRule"` + FraudScore int `json:"fraudScore"` + LeasingRiskAssessment baseLeasingRiskAssessment `json:"leasingRiskAssessment"` + LoanEvaluationVerification loanEvaluationVerificationDetail `json:"loanEvaluationVerificationDetail"` + OverdueRiskProduct overdueRiskProduct `json:"overdueRiskProduct"` + RiskSupervision riskSupervision `json:"riskSupervision"` + RiskWarning riskWarning `json:"riskWarning"` + StandLiveInfo standLiveInfo `json:"standLiveInfo"` + VerifyRule string `json:"verifyRule"` +} + +// baseInfo 存放被查询人的基础身份信息 +type baseInfo struct { + Age int `json:"age"` + Channel string `json:"channel"` + IdCard string `json:"idCard"` + Location string `json:"location"` + Name string `json:"name"` + Phone string `json:"phone"` + PhoneArea string `json:"phoneArea"` + Sex string `json:"sex"` + Status int `json:"status"` +} + +// elementVerificationDetail 用于要素及运营商核验结果 +type elementVerificationDetail struct { + PersonCheckDetails simpleCheckDetail `json:"personCheckDetails"` + PhoneCheckDetails simpleCheckDetail `json:"phoneCheckDetails"` + OnlineRiskFlag int `json:"onlineRiskFlag"` + PhoneVailRiskFlag int `json:"phoneVailRiskFlag"` +} + +// simpleCheckDetail 表示某项核验结果的简要描述 +type simpleCheckDetail struct { + Ele string `json:"ele"` + Result string `json:"result"` +} + +// baseLeasingRiskAssessment 3C 租赁多头风险统计字段 +type baseLeasingRiskAssessment struct { + RiskFlag int `json:"riskFlag"` + ThreeCInstitutionApplicationCountLast12M string `json:"threeCInstitutionApplicationCountLast12Months"` + ThreeCPlatformApplicationCountLast12M string `json:"threeCPlatformApplicationCountLast12Months"` + ThreeCInstitutionApplicationCountLast6M string `json:"threeCInstitutionApplicationCountLast6Months"` + ThreeCPlatformApplicationCountLast6M string `json:"threeCPlatformApplicationCountLast6Months"` + ThreeCInstitutionApplicationCountLast3M string `json:"threeCInstitutionApplicationCountLast3Months"` + ThreeCPlatformApplicationCountLast3M string `json:"threeCPlatformApplicationCountLast3Months"` + ThreeCInstitutionApplicationCountLast1M string `json:"threeCInstitutionApplicationCountLastMonth"` + ThreeCPlatformApplicationCountLast1M string `json:"threeCPlatformApplicationCountLastMonth"` + ThreeCInstitutionApplicationCountLast7D string `json:"threeCInstitutionApplicationCountLast7Days"` + ThreeCPlatformApplicationCountLast7D string `json:"threeCPlatformApplicationCountLast7Days"` + ThreeCInstitutionApplicationCountLast3D string `json:"threeCInstitutionApplicationCountLast3Days"` + ThreeCPlatformApplicationCountLast3D string `json:"threeCPlatformApplicationCountLast3Days"` + ThreeCInstitutionApplicationCountLast14D string `json:"threeCInstitutionApplicationCountLast14Days"` + ThreeCPlatformApplicationCountLast14D string `json:"threeCPlatformApplicationCountLast14Days"` + ThreeCInstitutionApplicationCountLast12MNig string `json:"threeCInstitutionApplicationCountLast12MonthsNight"` + ThreeCPlatformApplicationCountLast12MNig string `json:"threeCPlatformApplicationCountLast12MonthsNight"` +} + +// loanEvaluationVerificationDetail 信贷意向与时间段风险统计 +type loanEvaluationVerificationDetail struct { + BusinessLoanPerformances []loanPerformance `json:"businessLoanPerformances"` + CustomerLoanPerformances []loanPerformance `json:"customerLoanPerformances"` + TimeLoanPerformances []loanPerformance `json:"timeLoanPerformances"` + RiskFlag int `json:"riskFlag"` +} + +// loanPerformance 描述某类别在不同统计窗口内的申请次数 +type loanPerformance struct { + Type string `json:"type"` + Last12Month string `json:"last12Month"` + Last12MonthCount string `json:"last12MonthCount"` + Last6Month string `json:"last6Month"` + Last6MonthCount string `json:"last6MonthCount"` + Last3Month string `json:"last3Month"` + Last3MonthCount string `json:"last3MonthCount"` + Last1Month string `json:"last1Month"` + Last1MonthCount string `json:"last1MonthCount"` + Last7Day string `json:"last7Day"` + Last7DayCount string `json:"last7DayCount"` + Last15Day string `json:"last15Day"` + Last15DayCount string `json:"last15DayCount"` +} + +// overdueRiskProduct 当前逾期相关信息(本报告暂未直接使用) +type overdueRiskProduct struct { + CurrentOverdueAmount string `json:"currentOverdueAmount"` +} + +// riskSupervision 租赁监管相关字段(本报告暂未直接使用) +type riskSupervision struct { + LeastApplicationTime string `json:"leastApplicationTime"` +} + +// riskWarning 各类风险命中指标集合 +type riskWarning struct { + FrequentApplicationRecent int `json:"frequentApplicationRecent"` + FrequentBankApplications int `json:"frequentBankApplications"` + FrequentNonBankApplications int `json:"frequentNonBankApplications"` + HasCriminalRecord int `json:"hasCriminalRecord"` + HighDebtPressure int `json:"highDebtPressure"` + IsAntiFraudInfo int `json:"isAntiFraudInfo"` + IsEconomyFront int `json:"isEconomyFront"` + IsDisrupSocial int `json:"isDisrupSocial"` + IsKeyPerson int `json:"isKeyPerson"` + IsTrafficRelated int `json:"isTrafficRelated"` + IdCardTwoElementMismatch int `json:"idCardTwoElementMismatch"` + PhoneThreeElementMismatch int `json:"phoneThreeElementMismatch"` + IdCardPhoneProvinceMismatch int `json:"idCardPhoneProvinceMismatch"` + TotalRiskCounts int `json:"totalRiskCounts"` + Level string `json:"level"` + ShortPhoneDuration int `json:"shortPhoneDuration"` + ShortPhoneDurationSlight int `json:"shortPhoneDurationSlight"` + NoPhoneDuration int `json:"noPhoneDuration"` + MoreFrequentBankApplications int `json:"moreFrequentBankApplications"` + MoreFrequentNonBankApplications int `json:"moreFrequentNonBankApplications"` + FrequentRentalApplications int `json:"frequentRentalApplications"` + VeryFrequentRentalApplications int `json:"veryFrequentRentalApplications"` + HitCriminalRisk int `json:"hitCriminalRisk"` + HitExecutionCase int `json:"hitExecutionCase"` + RiskLevel string `json:"riskLevel"` + HitCurrentOverdue int `json:"hitCurrentOverdue"` + HitHighRiskNonBankLastTwoYears int `json:"hitHighRiskNonBankLastTwoYears"` + HitHighRiskBankLastTwoYears int `json:"hitHighRiskBankLastTwoYears"` + HitHighRiskBank int `json:"hitHighRiskBank"` +} + +// standLiveInfo 活体核验结果(本报告暂未直接使用) +type standLiveInfo struct { + FinalAuthResult string `json:"finalAuthResult"` +} + +// --- FLXG7E8F --- + +// judicialProductData 对应 FLXG7E8F 司法产品数据 +type judicialProductData struct { + JudicialData judicialWrapper `json:"judicial_data"` +} + +// judicialWrapper 聚合司法产品下的各类列表信息 +type judicialWrapper struct { + LawsuitStat lawsuitStat `json:"lawsuitStat"` + ConsumptionRestrictionList []consumptionRestriction `json:"consumptionRestrictionList"` + BreachCaseList []breachCase `json:"breachCaseList"` +} + +// lawsuitStat 按案件类型、执行情况等维度统计的结构化数据 +type lawsuitStat struct { + CasesTree casesTree `json:"cases_tree"` + Civil lawCategory `json:"civil"` + Criminal lawCategory `json:"criminal"` + Administrative lawCategory `json:"administrative"` + Preservation lawCategory `json:"preservation"` + Bankrupt lawCategory `json:"bankrupt"` + Implement lawCategory `json:"implement"` +} + +// lawCategory 包装同类案件的集合 +type lawCategory struct { + Cases []judicialCase `json:"cases"` +} + +// casesTree 旧格式的案件分类(当前主要使用 lawCategory) +type casesTree struct { + Criminal []judicialCase `json:"criminal"` + Civil []judicialCase `json:"civil"` +} + +// judicialCase 司法案件公共字段 +type judicialCase struct { + CaseNumber string `json:"c_ah"` + CaseType int `json:"case_type"` + StageType int `json:"stage_type"` + Najbs string `json:"n_ajbs"` + Court string `json:"n_jbfy"` + FilingDate string `json:"d_larq"` + JudgeResult string `json:"n_jafs"` + CaseStatus string `json:"n_ajjzjd"` + ApplyAmount float64 `json:"n_sqzxbdje"` + CaseCategory string `json:"n_jaay"` +} + +// consumptionRestriction 限制高消费记录 +type consumptionRestriction struct { + CaseNumber string `json:"caseNumber"` + IssueDate string `json:"issueDate"` + ExecutiveCourt string `json:"executiveCourt"` +} + +// breachCase 失信被执行人记录 +type breachCase struct { + CaseNumber string `json:"caseNumber"` + FileDate string `json:"fileDate"` + IssueDate string `json:"issueDate"` + ExecutiveCourt string `json:"executiveCourt"` + FulfillStatus string `json:"fulfillStatus"` + EstimatedJudgementAmount float64 `json:"estimatedJudgementAmount"` + EnforcementBasisNumber string `json:"enforcementBasisNumber"` + EnforcementBasisOrganization string `json:"enforcementBasisOrganization"` +} + +// --- JRZQ9D4E --- + +// contentsProductData 对应 JRZQ9D4E 产品的数据内容 +type contentsProductData struct { + Contents map[string]string `json:"contents"` + ResponseID string `json:"responseId"` + Score string `json:"score"` + Reason string `json:"reason"` +} + +// --- JRZQ6F2A --- + +// riskScreenProductData 对应 JRZQ6F2A 产品的风控决策结果 +type riskScreenProductData struct { + RiskScreenV2 riskScreenV2 `json:"risk_screen_v2"` +} + +// riskScreenV2 风险引擎输出的决策、模型等信息 +type riskScreenV2 struct { + Code string `json:"code"` + Decision string `json:"decision"` + Message string `json:"message"` + FulinFlag int `json:"fulinHitFlag"` + Id string `json:"id"` + Knowledge riskScreenKnowledge `json:"knowledge"` + Models []riskScreenModel `json:"models"` + Variables []riskScreenVariable `json:"variables"` +} + +// riskScreenKnowledge 风险知识库命中的规则编码 +type riskScreenKnowledge struct { + Code string `json:"code"` +} + +// riskScreenModel 风控模型得分信息 +type riskScreenModel struct { + SceneCode string `json:"sceneCode"` + Score string `json:"score"` +} + +type riskScreenVariable struct { + VariableName string `json:"variableName"` + VariableValue map[string]string `json:"variableValue"` +} + +// ======================= +// 目标数据结构 +// ======================= + +// targetReport 与 target.json 结构一一对应的报告格式 +type targetReport struct { + ReportSummary reportSummary `json:"reportSummary"` + BasicInfo reportBasicInfo `json:"basicInfo"` + RiskIdentification riskIdentification `json:"riskIdentification"` + CreditAssessment creditAssessment `json:"creditAssessment"` + LeasingRiskAssessment leasingRiskAssessment `json:"leasingRiskAssessment"` + ComprehensiveAnalysis []string `json:"comprehensiveAnalysis"` + ReportFooter reportFooter `json:"reportFooter"` +} + +// reportSummary 对应 target.json 中 reportSummary 节点 +type reportSummary struct { + RuleValidation summaryItem `json:"ruleValidation"` + AntiFraudScore summaryItem `json:"antiFraudScore"` + AntiFraudRule summaryItem `json:"antiFraudRule"` + AbnormalRulesHit abnormalHit `json:"abnormalRulesHit"` +} + +// summaryItem 统一描述包含 code/result/level 的子项 +type summaryItem struct { + Code string `json:"code,omitempty"` + Result string `json:"result,omitempty"` + Level string `json:"level,omitempty"` +} + +// abnormalHit 表示异常规则汇总 +type abnormalHit struct { + Count int `json:"count"` + Alert string `json:"alert"` +} + +// reportBasicInfo 对应 target.json 中 basicInfo 节点 +type reportBasicInfo struct { + Name string `json:"name"` + Phone string `json:"phone"` + IdCard string `json:"idCard"` + ReportID string `json:"reportId"` + Verifications []verificationItem `json:"verifications"` +} + +// verificationItem 描述核验项的展示信息 +type verificationItem struct { + Item string `json:"item"` + Description string `json:"description"` + Result string `json:"result"` + Details string `json:"details,omitempty"` +} + +// riskIdentification 对应 target.json 中 riskIdentification 节点 +type riskIdentification struct { + Title string `json:"title"` + CaseAnnouncements announcementSection `json:"caseAnnouncements"` + EnforcementAnnouncements announcementSection `json:"enforcementAnnouncements"` + DishonestAnnouncements announcementSection `json:"dishonestAnnouncements"` + HighConsumptionRestrictionAnn announcementSection `json:"highConsumptionRestrictionAnnouncements"` +} + +// announcementSection 统一封装列表标题与记录 +type announcementSection struct { + Title string `json:"title"` + Records []map[string]string `json:"records"` +} + +// creditAssessment 对应 target.json 中 creditAssessment 节点 +type creditAssessment struct { + Title string `json:"title"` + LoanIntentionByCustomerType assessmentSection[loanIntentionRecord] `json:"loanIntentionByCustomerType"` + LoanIntentionAbnormalTimes assessmentSection[abnormalTimeRecord] `json:"loanIntentionAbnormalTimes"` +} + +// leasingRiskAssessment 对应 target.json 中 leasingRiskAssessment 节点 +type leasingRiskAssessment struct { + Title string `json:"title"` + MultiLender3C assessmentSection[multiLenderRecord] `json:"multiLenderRisk3C"` +} + +// assessmentSection 泛型结构,封装任意记录列表及标题 +type assessmentSection[T any] struct { + Title string `json:"title"` + Records []T `json:"records"` +} + +// loanIntentionRecord 对应 loanIntentionByCustomerType.records 项 +type loanIntentionRecord struct { + CustomerType string `json:"customerType"` + ApplicationCount int `json:"applicationCount"` + RiskLevel string `json:"riskLevel"` +} + +// abnormalTimeRecord 对应 loanIntentionAbnormalTimes.records 项 +type abnormalTimeRecord struct { + TimePeriod string `json:"timePeriod"` + MainInstitutionType string `json:"mainInstitutionType"` + RiskLevel string `json:"riskLevel"` +} + +// multiLenderRecord 对应 multiLenderRisk3C.records 项 +type multiLenderRecord struct { + InstitutionType string `json:"institutionType"` + AppliedCount int `json:"appliedCount"` + InUseCount int `json:"inUseCount"` + TotalCreditLimit float64 `json:"totalCreditLimit"` + TotalDebtBalance float64 `json:"totalDebtBalance"` + RiskLevel string `json:"riskLevel"` +} + +// reportFooter 对应 target.json 中 reportFooter 节点 +type reportFooter struct { + DataSource string `json:"dataSource"` + GenerationTime string `json:"generationTime"` + Disclaimer string `json:"disclaimer"` +} + +// ======================= +// 上下文结构 +// ======================= + +// sourceContext 缓存各子产品解析后的结构,方便后续加工 +type sourceContext struct { + BaseData *baseProductData + JudicialData *judicialProductData + ContentsData *contentsProductData + RiskScreen *riskScreenProductData +} + +// ======================= +// 主流程 +// ======================= + +// buildSourceContext 根据 source.json 解析出各子产品的结构化数据 +func buildSourceContext(src sourceFile) (*sourceContext, error) { + ctx := &sourceContext{} + for _, resp := range src.Responses { + if !resp.Success { + continue + } + + switch strings.ToUpper(resp.ApiCode) { + case "DWBG8B4D": + var data baseProductData + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, fmt.Errorf("解析DWBG8B4D数据失败: %w", err) + } + ctx.BaseData = &data + case "FLXG7E8F": + var data judicialProductData + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, fmt.Errorf("解析FLXG7E8F数据失败: %w", err) + } + ctx.JudicialData = &data + case "JRZQ9D4E": + var data contentsProductData + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, fmt.Errorf("解析JRZQ9D4E数据失败: %w", err) + } + ctx.ContentsData = &data + case "JRZQ6F2A": + var data riskScreenProductData + if err := json.Unmarshal(resp.Data, &data); err != nil { + return nil, fmt.Errorf("解析JRZQ6F2A数据失败: %w", err) + } + ctx.RiskScreen = &data + } + } + + if ctx.BaseData == nil { + return nil, errors.New("未获取到DWBG8B4D核心数据") + } + + return ctx, nil +} + +// ======================= +// 构建目标报告 +// ======================= + +// buildTargetReport 将上下文数据映射为 target.json 完整结构 +func buildTargetReport(ctx *sourceContext) targetReport { + report := targetReport{ + ReportSummary: buildReportSummary(ctx), + BasicInfo: buildBasicInfo(ctx), + RiskIdentification: buildRiskIdentification(ctx), + CreditAssessment: buildCreditAssessment(ctx), + LeasingRiskAssessment: buildLeasingRiskAssessment(ctx), + ComprehensiveAnalysis: buildComprehensiveAnalysis(ctx), + ReportFooter: buildReportFooter(ctx), + } + + return report +} + +// buildReportSummary 组装 reportSummary,包括规则、反欺诈信息 +func buildReportSummary(ctx *sourceContext) reportSummary { + const strategyCode = "STR0042314/贷前-经营性租赁全量策略" + + summary := reportSummary{ + RuleValidation: summaryItem{ + Code: strategyCode, + Result: "未命中", + }, + AntiFraudScore: summaryItem{ + Level: "未命中", + }, + AntiFraudRule: summaryItem{ + Code: strategyCode, + Level: "未命中", + }, + AbnormalRulesHit: abnormalHit{ + Count: 0, + Alert: "无风险", + }, + } + + if ctx.BaseData == nil { + return summary + } + + verifyResult := strings.TrimSpace(ctx.BaseData.VerifyRule) + if verifyResult == "" { + verifyResult = "未命中" + } + summary.RuleValidation.Result = verifyResult + + scoreLevel := riskLevelFromScore(ctx.BaseData.FraudScore) + summary.AntiFraudScore.Level = scoreLevel + fraudRuleLevel := strings.TrimSpace(ctx.BaseData.FraudRule) + if fraudRuleLevel == "" || fraudRuleLevel == "-" { + fraudRuleLevel = "未命中" + } + summary.AntiFraudRule.Level = fraudRuleLevel + + riskCount := ctx.BaseData.RiskWarning.TotalRiskCounts + summary.AbnormalRulesHit.Count = riskCount + summary.AbnormalRulesHit.Alert = buildRiskWarningAlert(ctx.BaseData.RiskWarning) + + return summary +} + +// buildBasicInfo 组装 basicInfo,包含基础信息与核验列表 +func buildBasicInfo(ctx *sourceContext) reportBasicInfo { + base := ctx.BaseData.BaseInfo + + reportID := generateReportID() + + verifications := make([]verificationItem, 0, 5) + + elementResult, elementDetails := buildElementVerificationResult(ctx) + verifications = append(verifications, verificationItem{ + Item: "要素核查", + Description: "使用姓名、手机号、身份证信息进行三要素核验", + Result: elementResult, + Details: elementDetails, + }) + + carrierResult, carrierDetails := buildCarrierVerificationResult(ctx) + verifications = append(verifications, verificationItem{ + Item: "运营商检验", + Description: "检查手机号在运营商处的状态及在线时长", + Result: carrierResult, + Details: carrierDetails, + }) + + var stat *lawsuitStat + if ctx.JudicialData != nil { + stat = &ctx.JudicialData.JudicialData.LawsuitStat + } + + totalCaseCount := 0 + totalCriminal := 0 + totalExecution := 0 + if stat != nil { + totalCriminal = len(stat.Criminal.Cases) + totalCaseCount = totalCriminal + len(stat.Civil.Cases) + len(stat.Administrative.Cases) + len(stat.Preservation.Cases) + len(stat.Bankrupt.Cases) + totalExecution = len(stat.Implement.Cases) + } + + totalDishonest := 0 + totalRestriction := 0 + if ctx.JudicialData != nil { + totalDishonest = len(ctx.JudicialData.JudicialData.BreachCaseList) + totalRestriction = len(ctx.JudicialData.JudicialData.ConsumptionRestrictionList) + } + + if totalCaseCount > 0 || totalExecution > 0 || totalDishonest > 0 || totalRestriction > 0 { + detailParts := make([]string, 0, 5) + if stat != nil { + addCaseDetail := func(label string, count int) { + if count > 0 { + detailParts = append(detailParts, fmt.Sprintf("%s%d条", label, count)) + } + } + addCaseDetail("刑事案件", len(stat.Criminal.Cases)) + addCaseDetail("民事案件", len(stat.Civil.Cases)) + addCaseDetail("行政案件", len(stat.Administrative.Cases)) + addCaseDetail("非诉保全审查案件", len(stat.Preservation.Cases)) + addCaseDetail("强制清算与破产案件", len(stat.Bankrupt.Cases)) + } + if totalExecution > 0 { + detailParts = append(detailParts, fmt.Sprintf("执行案件%d条", totalExecution)) + } + if totalDishonest > 0 { + detailParts = append(detailParts, fmt.Sprintf("失信案件%d条", totalDishonest)) + } + if totalRestriction > 0 { + detailParts = append(detailParts, fmt.Sprintf("限高案件%d条", totalRestriction)) + } + details := buildCaseDetails(detailParts) + verifications = append(verifications, verificationItem{ + Item: "法院信息", + Description: "检测被查询人的借贷风险情况,及在司法体系中是否存在行为风险", + Result: "高风险", + Details: details, + }) + } else { + verifications = append(verifications, verificationItem{ + Item: "法院信息", + Description: "检测被查询人的借贷风险情况,及在司法体系中是否存在行为风险", + Result: "未命中", + }) + } + + loanRiskResult, loanRiskDetails := buildLoanRiskResult(ctx) + verifications = append(verifications, verificationItem{ + Item: "借贷评估", + Description: "综合近12个月借贷申请情况评估风险", + Result: loanRiskResult, + Details: loanRiskDetails, + }) + + otherDetails := gatherOtherRiskDetails(ctx) + if otherDetails != "" { + verifications = append(verifications, verificationItem{ + Item: "其他", + Description: "其它规则风险", + Result: otherDetails, + }) + } + + return reportBasicInfo{ + Name: base.Name, + Phone: base.Phone, + IdCard: base.IdCard, + ReportID: reportID, + Verifications: verifications, + } +} + +// buildRiskIdentification 组装 riskIdentification,各列表对标 target.json +func buildRiskIdentification(ctx *sourceContext) riskIdentification { + identification := riskIdentification{ + Title: "风险识别产品", + CaseAnnouncements: announcementSection{ + Title: "涉案公告列表", + Records: []map[string]string{}, + }, + EnforcementAnnouncements: announcementSection{ + Title: "执行公告列表", + Records: []map[string]string{}, + }, + DishonestAnnouncements: announcementSection{ + Title: "失信公告列表", + Records: []map[string]string{}, + }, + HighConsumptionRestrictionAnn: announcementSection{ + Title: "限高公告列表", + Records: []map[string]string{}, + }, + } + + if ctx.JudicialData == nil { + return identification + } + + stat := ctx.JudicialData.JudicialData.LawsuitStat + baseName := "" + baseID := "" + if ctx.BaseData != nil { + baseName = ctx.BaseData.BaseInfo.Name + baseID = ctx.BaseData.BaseInfo.IdCard + } + + caseRecords := make([]map[string]string, 0) + caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Civil.Cases, "民事案件")...) + caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Criminal.Cases, "刑事案件")...) + caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Administrative.Cases, "行政案件")...) + caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Preservation.Cases, "非诉保全审查")...) + caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Bankrupt.Cases, "强制清算与破产")...) + identification.CaseAnnouncements.Records = caseRecords + + identification.EnforcementAnnouncements.Records = convertEnforcementAnnouncements(stat.Implement.Cases) + identification.DishonestAnnouncements.Records = convertDishonestAnnouncements(ctx.JudicialData.JudicialData.BreachCaseList, baseName, baseID) + identification.HighConsumptionRestrictionAnn.Records = convertConsumptionRestrictions(ctx.JudicialData.JudicialData.ConsumptionRestrictionList, baseName, baseID) + + return identification +} + +// buildCreditAssessment 组装 creditAssessment,包含客户类型与异常时间段 +func buildCreditAssessment(ctx *sourceContext) creditAssessment { + assessment := creditAssessment{ + Title: "信贷评估产品", + LoanIntentionByCustomerType: assessmentSection[loanIntentionRecord]{ + Title: "本人在各类机构的借贷意向表现", + Records: []loanIntentionRecord{}, + }, + LoanIntentionAbnormalTimes: assessmentSection[abnormalTimeRecord]{ + Title: "异常时间段借贷申请情况", + Records: []abnormalTimeRecord{}, + }, + } + + metrics := extractApplyLoanMetrics(ctx) + if len(metrics) == 0 { + return assessment + } + + totalBankKeys := []string{"als_m12_id_bank_allnum", "als_m12_cell_bank_allnum"} + totalNonBankKeys := []string{"als_m12_id_nbank_allnum", "als_m12_cell_nbank_allnum"} + bankNightKeys := []string{"als_m12_id_bank_night_allnum", "als_m12_cell_bank_night_allnum"} + nonBankNightKeys := []string{"als_m12_id_nbank_night_allnum", "als_m12_cell_nbank_night_allnum"} + bankWeekKeys := []string{"als_m12_id_bank_week_allnum", "als_m12_cell_bank_week_allnum"} + nonBankWeekKeys := []string{"als_m12_id_nbank_week_allnum", "als_m12_cell_nbank_week_allnum"} + + customerMappings := []struct { + CustomerType string + Keys []string + }{ + {"持牌网络小贷", []string{"als_m12_id_nbank_nsloan_allnum", "als_m12_cell_nbank_nsloan_allnum"}}, + {"持牌消费金融", []string{"als_m12_id_nbank_cons_allnum", "als_m12_cell_nbank_cons_allnum"}}, + {"持牌融资租赁机构", []string{"als_m12_id_nbank_finlea_allnum", "als_m12_cell_nbank_finlea_allnum"}}, + {"持牌汽车金融", []string{"als_m12_id_nbank_autofin_allnum", "als_m12_cell_nbank_autofin_allnum"}}, + {"其他非银机构", []string{"als_m12_id_nbank_else_allnum", "als_m12_id_nbank_oth_allnum", "als_m12_cell_nbank_else_allnum", "als_m12_cell_nbank_oth_allnum"}}, + } + + for _, mapping := range customerMappings { + count := sumMetrics(metrics, mapping.Keys...) + record := loanIntentionRecord{ + CustomerType: mapping.CustomerType, + ApplicationCount: count, + RiskLevel: riskLevelFromCount(count), + } + assessment.LoanIntentionByCustomerType.Records = append(assessment.LoanIntentionByCustomerType.Records, record) + } + + timeMappings := []struct { + TimePeriod string + Pairs []struct { + Label string + Count int + } + }{ + { + TimePeriod: "夜间(22:00-06:00)", + Pairs: []struct { + Label string + Count int + }{ + {"银行类机构", sumMetrics(metrics, bankNightKeys...)}, + {"非银金融机构", sumMetrics(metrics, nonBankNightKeys...)}, + }, + }, + { + TimePeriod: "周末", + Pairs: []struct { + Label string + Count int + }{ + {"银行类机构", sumMetrics(metrics, bankWeekKeys...)}, + {"非银金融机构", sumMetrics(metrics, nonBankWeekKeys...)}, + }, + }, + { + TimePeriod: "工作日工作时间", + Pairs: []struct { + Label string + Count int + }{ + {"银行类机构", sumMetrics(metrics, totalBankKeys...) - sumMetrics(metrics, append(bankNightKeys, bankWeekKeys...)...)}, + {"非银金融机构", sumMetrics(metrics, totalNonBankKeys...) - sumMetrics(metrics, append(nonBankNightKeys, nonBankWeekKeys...)...)}, + }, + }, + } + + for _, mapping := range timeMappings { + labels := make([]string, 0, len(mapping.Pairs)) + totalCount := 0 + for _, pair := range mapping.Pairs { + count := pair.Count + if count < 0 { + count = 0 + } + totalCount += count + if count > 0 && !containsLabel(labels, pair.Label) { + labels = append(labels, pair.Label) + } + } + + if len(labels) == 0 { + labels = append(labels, "无机构命中") + } + + record := abnormalTimeRecord{ + TimePeriod: mapping.TimePeriod, + MainInstitutionType: joinWithChineseComma(labels), + } + if mapping.TimePeriod == "工作日工作时间" { + record.RiskLevel = riskLevelFromCount(totalCount) + } else { + record.RiskLevel = riskLevelFromStrictCount(totalCount) + } + assessment.LoanIntentionAbnormalTimes.Records = append(assessment.LoanIntentionAbnormalTimes.Records, record) + } + + return assessment +} + +// buildLeasingRiskAssessment 组装 leasingRiskAssessment 中的 3C 多头信息 +func buildLeasingRiskAssessment(ctx *sourceContext) leasingRiskAssessment { + assessment := leasingRiskAssessment{ + Title: "租赁风险评估产品", + MultiLender3C: assessmentSection[multiLenderRecord]{ + Title: "3C机构多头借贷风险", + Records: []multiLenderRecord{}, + }, + } + + if ctx.ContentsData == nil || len(ctx.ContentsData.Contents) == 0 { + return assessment + } + + contents := ctx.ContentsData.Contents + + // 消费金融机构指标,优先使用近12个月的机构数,其次退化到近一年内的其它统计 + consumerApplied := pickFirstInt(contents, "BH_A074", "BH_A065", "BH_A055") + consumerInUse := pickFirstInt(contents, "BH_F004") + consumerCredit := pickFirstFloat(contents, "BH_E044", "BH_E034", "BH_E014") + consumerDebt := pickFirstFloat(contents, "BH_F014", "BH_B264", "BH_B238") + assessment.MultiLender3C.Records = append(assessment.MultiLender3C.Records, multiLenderRecord{ + InstitutionType: "消费金融", + AppliedCount: consumerApplied, + InUseCount: consumerInUse, + TotalCreditLimit: consumerCredit, + TotalDebtBalance: consumerDebt, + RiskLevel: riskLevelFromCount(consumerApplied), + }) + + // 小贷公司指标,同样按优先级取值 + smallApplied := pickFirstInt(contents, "BH_A093", "BH_A084", "BH_A075") + smallInUse := pickFirstInt(contents, "BH_F005") + smallCredit := pickFirstFloat(contents, "BH_E045", "BH_E035", "BH_E015") + smallDebt := pickFirstFloat(contents, "BH_F015", "BH_B266", "BH_B239") + assessment.MultiLender3C.Records = append(assessment.MultiLender3C.Records, multiLenderRecord{ + InstitutionType: "小贷公司", + AppliedCount: smallApplied, + InUseCount: smallInUse, + TotalCreditLimit: smallCredit, + TotalDebtBalance: smallDebt, + RiskLevel: riskLevelFromCount(smallApplied), + }) + + // 其它非银机构通过"非银机构"总量减去已统计的两类机构,避免缺失字段导致的重复 + totalNonBankApplied := pickFirstInt(contents, "BH_A355", "BH_A344", "BH_A339") + otherApplied := totalNonBankApplied - consumerApplied - smallApplied + if otherApplied < 0 { + otherApplied = 0 + } + + totalInUse := pickFirstInt(contents, "BH_F003") + otherInUse := totalInUse - consumerInUse - smallInUse + if otherInUse < 0 { + otherInUse = 0 + } + + totalCredit := pickFirstFloat(contents, "BH_E046", "BH_E032") + otherCredit := totalCredit - consumerCredit - smallCredit + if otherCredit < 0 { + otherCredit = 0 + } + + totalDebt := pickFirstFloat(contents, "BH_F013", "BH_B262", "BH_B274") + otherDebt := totalDebt - consumerDebt - smallDebt + if otherDebt < 0 { + otherDebt = 0 + } + + assessment.MultiLender3C.Records = append(assessment.MultiLender3C.Records, multiLenderRecord{ + InstitutionType: "其他非银机构", + AppliedCount: otherApplied, + InUseCount: otherInUse, + TotalCreditLimit: otherCredit, + TotalDebtBalance: otherDebt, + RiskLevel: riskLevelFromCount(otherApplied), + }) + + return assessment +} + +// buildComprehensiveAnalysis 汇总最终的文字结论(仅输出存在风险的要点) +func buildComprehensiveAnalysis(ctx *sourceContext) []string { + analysis := make([]string, 0, 8) + if ctx.BaseData == nil { + return analysis + } + + summary := buildReportSummary(ctx) + highRiskDetected := false + mediumRiskDetected := false + + if msg, high, medium := buildRuleSummaryBullet(summary); msg != "" { + analysis = append(analysis, msg) + highRiskDetected = highRiskDetected || high + mediumRiskDetected = mediumRiskDetected || medium + } + + if msg, high, medium := buildRuleHitBullet(summary); msg != "" { + analysis = append(analysis, msg) + highRiskDetected = highRiskDetected || high + mediumRiskDetected = mediumRiskDetected || medium + } + + judicialBullet, judicialHigh, judicialMedium := buildJudicialBullet(ctx) + if judicialBullet != "" { + analysis = append(analysis, judicialBullet) + highRiskDetected = highRiskDetected || judicialHigh + mediumRiskDetected = mediumRiskDetected || judicialMedium + } + + if msg, high, medium := buildLoanAssessmentBullet(ctx); msg != "" { + analysis = append(analysis, msg) + highRiskDetected = highRiskDetected || high + mediumRiskDetected = mediumRiskDetected || medium + } + + if msg, high, medium := buildOtherRiskBullet(ctx, judicialBullet != ""); msg != "" { + analysis = append(analysis, msg) + highRiskDetected = highRiskDetected || high + mediumRiskDetected = mediumRiskDetected || medium + } + + if msg, high, medium := buildMultiLenderBullet(ctx); msg != "" { + analysis = append(analysis, msg) + highRiskDetected = highRiskDetected || high + mediumRiskDetected = mediumRiskDetected || medium + } + + risk := ctx.BaseData.RiskWarning + if hasHighRiskHit(risk) { + highRiskDetected = true + } else if hasMediumRiskHit(risk) { + mediumRiskDetected = true + } + + if highRiskDetected { + analysis = append(analysis, "风险提示:系统识别出该用户存在多项高风险因素,建议谨慎评估信用状况并加强风险管控措施。") + } else if mediumRiskDetected { + analysis = append(analysis, "风险提示:系统识别出该用户存在一定风险因素,建议保持关注并做好人工复核。") + } + + return analysis +} + +// buildReportFooter 填充数据来源与免责声明 +func buildReportFooter(ctx *sourceContext) reportFooter { + return reportFooter{ + DataSource: "天远数据报告", + GenerationTime: time.Now().Format("2006-01-02"), + Disclaimer: fmt.Sprintf("本报告基于%s数据生成,仅供参考,具体审批以最终审核为准。", time.Now().Format("2006-01-02")), + } +} + +// ======================= +// 工具函数 +// ======================= + +// mapDecision 翻译风控决策枚举值 +func mapDecision(decision string) string { + switch strings.ToLower(decision) { + case "accept": + return "通过" + case "reject": + return "拒绝" + case "review": + return "需复核" + } + return "未知" +} + +// mapDecisionLevel 将决策映射到风险等级描述 +func mapDecisionLevel(decision string) string { + switch strings.ToLower(decision) { + case "accept": + return "正常" + case "reject": + return "高风险" + case "review": + return "需人工复核" + } + return "" +} + +// firstModelSceneCode 获取模型列表中的首个场景编码 +func firstModelSceneCode(models []riskScreenModel) string { + if len(models) == 0 { + return "" + } + return models[0].SceneCode +} + +// riskLevelFromCount 根据命中次数给出风险等级 +func riskLevelFromCount(count int) string { + switch { + case count <= 0: + return "无风险" + case count < 6: + return "低风险" + case count <= 12: + return "中风险" + default: + return "高风险" + } +} + +// riskLevelFromScore 根据 fraudScore 数值映射风险等级 +func riskLevelFromScore(score int) string { + switch { + case score < 0: + return "未命中" + case score <= 59: + return "低风险" + case score <= 79: + return "中风险" + case score <= 100: + return "高风险" + default: + return "未命中" + } +} + +// buildRiskWarningAlert 依据 riskWarning 中的命中情况给出提示级别 +func buildRiskWarningAlert(r riskWarning) string { + if hasHighRiskHit(r) { + return "高风险提示" + } + if hasMediumRiskHit(r) { + return "中风险提示" + } + return "无风险" +} + +// defaultIfEmpty 当原值为空时回退至默认值 +func defaultIfEmpty(val, fallback string) string { + if strings.TrimSpace(val) == "" { + return fallback + } + return val +} + +// mapRiskFlag 将风险标记的数值型结果转换为文本 +func mapRiskFlag(flag int) string { + switch flag { + case 0: + return "未命中" + case 1: + return "异常" + case 2: + return "正常" + default: + return "未知" + } +} + +// gatherOtherRiskDetails 汇总其它风险命中项的说明文本 +func gatherOtherRiskDetails(ctx *sourceContext) string { + if ctx.BaseData == nil { + return "" + } + hits := make([]string, 0, 4) + risk := ctx.BaseData.RiskWarning + if risk.IsAntiFraudInfo > 0 { + hits = append(hits, "涉赌涉诈风险") + } + if risk.PhoneThreeElementMismatch > 0 { + hits = append(hits, "手机号三要素不一致") + } + if risk.IdCardPhoneProvinceMismatch > 0 { + hits = append(hits, "身份证与手机号归属地不一致") + } + if risk.HasCriminalRecord > 0 { + hits = append(hits, "历史前科记录") + } + if risk.IsEconomyFront > 0 { + hits = append(hits, "经济类前科风险") + } + if risk.IsDisrupSocial > 0 { + hits = append(hits, "妨害社会管理秩序风险") + } + if risk.IsKeyPerson > 0 { + hits = append(hits, "重点人员风险") + } + if risk.IsTrafficRelated > 0 { + hits = append(hits, "涉交通案件风险") + } + if risk.FrequentRentalApplications > 0 { + hits = append(hits, "租赁机构申请极为频繁") + } + if risk.VeryFrequentRentalApplications > 0 { + hits = append(hits, "租赁机构申请次数极多") + } + if ctx.JudicialData != nil { + stat := ctx.JudicialData.JudicialData.LawsuitStat + totalCase := len(stat.Civil.Cases) + len(stat.Criminal.Cases) + len(stat.Administrative.Cases) + len(stat.Preservation.Cases) + len(stat.Bankrupt.Cases) + totalExecution := len(stat.Implement.Cases) + totalDishonest := len(ctx.JudicialData.JudicialData.BreachCaseList) + totalRestriction := len(ctx.JudicialData.JudicialData.ConsumptionRestrictionList) + if totalCase > 0 || totalExecution > 0 || totalDishonest > 0 || totalRestriction > 0 { + hits = append(hits, "存在司法风险记录") + } + } + if len(hits) == 0 { + return "" + } + return strings.Join(hits, "、") +} + +func buildRuleSummaryBullet(summary reportSummary) (string, bool, bool) { + type item struct { + label string + level string + } + + items := []item{ + {label: "规则验证", level: strings.TrimSpace(summary.RuleValidation.Result)}, + {label: "反欺诈得分", level: strings.TrimSpace(summary.AntiFraudScore.Level)}, + {label: "反欺诈规则", level: strings.TrimSpace(summary.AntiFraudRule.Level)}, + } + + phrases := make([]string, 0, len(items)) + highLabels := make([]string, 0, len(items)) + riskCount := 0 + highCount := 0 + mediumCount := 0 + + for _, item := range items { + if !isMeaningfulRiskValue(item.level) { + continue + } + riskCount++ + + sentence := fmt.Sprintf("%s结果为%s", item.label, item.level) + + switch { + case strings.Contains(item.level, "高风险"): + highCount++ + highLabels = append(highLabels, item.label) + sentence = fmt.Sprintf("%s判定为高风险", item.label) + case strings.Contains(item.level, "中风险"): + mediumCount++ + sentence = fmt.Sprintf("%s判定为中风险", item.label) + case strings.Contains(item.level, "低风险"): + sentence = fmt.Sprintf("%s判定为低风险", item.label) + default: + sentence = fmt.Sprintf("%s结果为%s", item.label, item.level) + } + + phrases = append(phrases, sentence) + } + + if riskCount == 0 { + return "", false, false + } + + if highCount == riskCount { + subject := joinWithChineseComma(highLabels) + sentence := fmt.Sprintf("该用户%s均为高风险,表明该用户存在较高的信用风险。", subject) + return sentence, true, false + } + + sentence := joinWithChineseComma(phrases) + "。" + high := highCount > 0 + medium := !high && mediumCount > 0 + return sentence, high, medium +} + +func buildRuleHitBullet(summary reportSummary) (string, bool, bool) { + if summary.AbnormalRulesHit.Count <= 0 { + return "", false, false + } + + alert := strings.TrimSpace(summary.AbnormalRulesHit.Alert) + if alert == "" || alert == "无风险" { + return "", false, false + } + + sentence := fmt.Sprintf("系统共识别%d项规则命中(%s)。", summary.AbnormalRulesHit.Count, alert) + if strings.Contains(alert, "高风险") { + return sentence, true, false + } + return sentence, false, true +} + +func buildJudicialBullet(ctx *sourceContext) (string, bool, bool) { + if ctx.JudicialData == nil { + return "", false, false + } + + stat := ctx.JudicialData.JudicialData.LawsuitStat + parts := make([]string, 0, 6) + + addPart := func(label string, count int) { + if count > 0 { + parts = append(parts, fmt.Sprintf("%s%d条", label, count)) + } + } + + addPart("刑事案件", len(stat.Criminal.Cases)) + addPart("民事案件", len(stat.Civil.Cases)) + addPart("行政案件", len(stat.Administrative.Cases)) + addPart("执行案件", len(stat.Implement.Cases)) + addPart("失信记录", len(ctx.JudicialData.JudicialData.BreachCaseList)) + addPart("限高记录", len(ctx.JudicialData.JudicialData.ConsumptionRestrictionList)) + + if len(parts) == 0 { + return "", false, false + } + + sentence := "法院信息显示" + joinWithChineseComma(parts) + ",属于司法高风险因素。" + return sentence, true, false +} + +func buildLoanAssessmentBullet(ctx *sourceContext) (string, bool, bool) { + assessment := buildCreditAssessment(ctx) + records := assessment.LoanIntentionByCustomerType.Records + if len(records) == 0 { + return "", false, false + } + + type phrase struct { + text string + level string + } + + phrases := make([]phrase, 0, len(records)) + high := false + medium := false + + for _, record := range records { + level := strings.TrimSpace(record.RiskLevel) + if level == "" || level == "无风险" || level == "低风险" { + continue + } + + txt := fmt.Sprintf("%s近12个月申请机构数%d家,风险等级为%s", record.CustomerType, record.ApplicationCount, level) + phrases = append(phrases, phrase{text: txt, level: level}) + + if level == "高风险" { + high = true + } else { + medium = true + } + } + + if len(phrases) == 0 { + return "", false, false + } + + parts := make([]string, len(phrases)) + for i, p := range phrases { + parts[i] = p.text + } + + sentence := "借贷评估显示" + joinWithChineseComma(parts) + "。" + return sentence, high, medium +} + +func buildMultiLenderBullet(ctx *sourceContext) (string, bool, bool) { + assessment := buildCreditAssessment(ctx) + records := assessment.LoanIntentionAbnormalTimes.Records + if len(records) == 0 { + return "", false, false + } + + phrases := make([]string, 0, len(records)) + high := false + medium := false + + for _, record := range records { + level := strings.TrimSpace(record.RiskLevel) + if level == "" || level == "无风险" || strings.Contains(record.MainInstitutionType, "无机构命中") { + continue + } + + phrases = append(phrases, fmt.Sprintf("%s阶段主要由%s发起,风险等级为%s", record.TimePeriod, record.MainInstitutionType, level)) + + if level == "高风险" { + high = true + } else { + medium = true + } + } + + if len(phrases) == 0 { + return "", false, false + } + + sentence := "多头借贷风险在" + joinWithChineseComma(phrases) + "。" + return sentence, high, medium +} + +func buildOtherRiskBullet(ctx *sourceContext, skipJudicial bool) (string, bool, bool) { + otherDetails := gatherOtherRiskDetails(ctx) + if otherDetails == "" { + return "", false, false + } + + items := strings.Split(otherDetails, "、") + filtered := make([]string, 0, len(items)) + high := false + medium := false + + for _, item := range items { + trimmed := strings.TrimSpace(item) + if trimmed == "" { + continue + } + if skipJudicial && trimmed == "存在司法风险记录" { + continue + } + filtered = append(filtered, trimmed) + + if strings.Contains(trimmed, "涉赌") || strings.Contains(trimmed, "前科") || strings.Contains(trimmed, "重点人员") { + high = true + } else { + medium = true + } + } + + if len(filtered) == 0 { + return "", high, medium + } + + sentence := "其他风险因素包括:" + joinWithChineseComma(filtered) + "。" + return sentence, high, medium +} + +func isMeaningfulRiskValue(value string) bool { + if value == "" { + return false + } + normalized := strings.ReplaceAll(value, " ", "") + switch normalized { + case "-", "未命中", "无风险", "正常": + return false + } + return true +} + +func updateSeverityFlags(value string, high, medium bool) (bool, bool) { + switch { + case strings.Contains(value, "高风险"): + high = true + case strings.Contains(value, "中风险"): + medium = true + case strings.Contains(value, "低风险"), strings.Contains(value, "命中"): + medium = true + } + return high, medium +} + +// parseRatio 解析类似 "3/2" 的分子分母格式 +func parseRatio(value string) (int, int) { + parts := strings.Split(value, "/") + if len(parts) != 2 { + return parseIntSafe(value), 0 + } + return parseIntSafe(parts[0]), parseIntSafe(parts[1]) +} + +// parseIntSafe 安全地将字符串转为整数 +func parseIntSafe(value string) int { + value = strings.TrimSpace(value) + if value == "" || value == "-" { + return 0 + } + result, err := strconv.Atoi(value) + if err != nil { + return 0 + } + return result +} + +// parseFloatSafe 安全地解析字符串为浮点数 +func parseFloatSafe(value string) float64 { + value = strings.TrimSpace(value) + if value == "" || value == "-" { + return 0 + } + result, err := strconv.ParseFloat(value, 64) + if err != nil { + return 0 + } + return result +} + +// getContentInt 从 JRZQ9D4E contents 中读取整数值 +func getContentInt(contents map[string]string, key string) int { + if contents == nil { + return 0 + } + if val, ok := contents[key]; ok { + return parseIntSafe(val) + } + return 0 +} + +// getContentFloat 从 JRZQ9D4E contents 中读取浮点数字段 +func getContentFloat(contents map[string]string, key string) float64 { + if contents == nil { + return 0 + } + if val, ok := contents[key]; ok { + return parseFloatSafe(val) + } + return 0 +} + +// pickFirstInt 依次尝试多个 key,返回首个非零整数 +func pickFirstInt(contents map[string]string, keys ...string) int { + for _, key := range keys { + if v := getContentInt(contents, key); v != 0 { + return v + } + } + return 0 +} + +// pickFirstFloat 依次尝试多个 key,返回首个非零浮点数 +func pickFirstFloat(contents map[string]string, keys ...string) float64 { + for _, key := range keys { + if v := getContentFloat(contents, key); v != 0 { + return v + } + } + return 0 +} + +// convertCaseAnnouncements 转换各类案件记录为 target 所需的涉案公告结构 +func convertCaseAnnouncements(cases []judicialCase, defaultType string) []map[string]string { + if len(cases) == 0 { + return nil + } + records := make([]map[string]string, 0, len(cases)) + for _, c := range cases { + caseType := defaultType + if caseType == "" { + caseType = caseTypeName(c.CaseType) + } + record := map[string]string{ + "caseNumber": c.CaseNumber, + "authority": c.Court, + "filingDate": c.FilingDate, + "caseType": caseType, + } + records = append(records, record) + } + return records +} + +// convertEnforcementAnnouncements 转换执行案件数据为执行公告列表 +func convertEnforcementAnnouncements(cases []judicialCase) []map[string]string { + if len(cases) == 0 { + return nil + } + records := make([]map[string]string, 0, len(cases)) + for _, c := range cases { + record := map[string]string{ + "caseNumber": c.CaseNumber, + "targetAmount": formatCurrencyYuan(c.ApplyAmount), + "filingDate": c.FilingDate, + "court": c.Court, + "status": defaultIfEmpty(c.CaseStatus, "-"), + } + records = append(records, record) + } + return records +} + +// convertDishonestAnnouncements 将失信记录转为失信公告列表 +func convertDishonestAnnouncements(items []breachCase, name, id string) []map[string]string { + if len(items) == 0 { + return nil + } + records := make([]map[string]string, 0, len(items)) + for _, item := range items { + record := map[string]string{ + "dishonestPerson": name, + "idCard": id, + "court": item.ExecutiveCourt, + "filingDate": defaultIfEmpty(item.FileDate, item.IssueDate), + "performanceStatus": defaultIfEmpty(item.FulfillStatus, "-"), + } + records = append(records, record) + } + return records +} + +// convertConsumptionRestrictions 将限高记录转为限高公告列表 +func convertConsumptionRestrictions(items []consumptionRestriction, name, id string) []map[string]string { + if len(items) == 0 { + return nil + } + records := make([]map[string]string, 0, len(items)) + for _, item := range items { + record := map[string]string{ + "restrictedPerson": name, + "idCard": id, + "court": item.ExecutiveCourt, + "startDate": item.IssueDate, + "measure": "限制高消费", + } + records = append(records, record) + } + return records +} + +// caseTypeName 根据案件类型编码给出默认名称 +func caseTypeName(code int) string { + switch code { + case 100: + return "民事案件" + case 200: + return "刑事案件" + case 300: + return "行政案件" + default: + return "" + } +} + +// formatCurrencyYuan 将金额格式化为"千分位+元",为空时返回 "-" +func formatCurrencyYuan(amount float64) string { + if amount <= 0 { + return "-" + } + val := strconv.FormatFloat(amount, 'f', 2, 64) + parts := strings.Split(val, ".") + intPart := addThousandsSeparator(parts[0]) + if len(parts) == 2 && strings.TrimRight(parts[1], "0") != "" { + return intPart + "." + strings.TrimRight(parts[1], "0") + "元" + } + return intPart + "元" +} + +// addThousandsSeparator 为整数部分添加千分位分隔 +func addThousandsSeparator(value string) string { + if len(value) <= 3 { + return value + } + var builder strings.Builder + mod := len(value) % 3 + if mod == 0 { + mod = 3 + } + builder.WriteString(value[:mod]) + for i := mod; i < len(value); i += 3 { + builder.WriteString(",") + builder.WriteString(value[i : i+3]) + } + return builder.String() +} + +// buildLoanRiskResult 根据 riskWarning 命中情况生成借贷评估结果 +func buildLoanRiskResult(ctx *sourceContext) (string, string) { + if ctx.BaseData == nil { + return "正常", "" + } + + risk := ctx.BaseData.RiskWarning + hits := make([]string, 0, 3) + + if risk.HitHighRiskBankLastTwoYears > 0 { + hits = append(hits, "命中近两年银行高风险") + } + if risk.HitHighRiskNonBankLastTwoYears > 0 { + hits = append(hits, "命中近两年非银高风险") + } + if risk.HitCurrentOverdue > 0 { + hits = append(hits, "命中当前逾期") + } + + if len(hits) == 0 { + return "正常", "" + } + + result := "命中" + details := strings.Join(hits, "、") + return result, details +} + +// joinWithChineseComma 使用中文顿号串联文本 +func joinWithChineseComma(parts []string) string { + if len(parts) == 0 { + return "" + } + joined := strings.Join(parts, ",") + sep := fmt.Sprintf("%c", rune(0x3001)) + return strings.ReplaceAll(joined, ",", sep) +} + +// buildCaseDetails 兼容旧调用,复用中文顿号拼接逻辑 +func buildCaseDetails(parts []string) string { + return joinWithChineseComma(parts) +} + +// buildElementVerificationResult 根据 riskWarning 的要素相关项生成结果 +func buildElementVerificationResult(ctx *sourceContext) (string, string) { + if ctx.BaseData == nil { + return "正常", "" + } + risk := ctx.BaseData.RiskWarning + hits := make([]string, 0, 2) + if risk.IdCardTwoElementMismatch > 0 { + hits = append(hits, "身份证二要素不一致") + } + if risk.PhoneThreeElementMismatch > 0 { + hits = append(hits, "手机号三要素不一致") + } + if len(hits) == 0 { + return "正常", "" + } + return "命中", joinWithChineseComma(hits) +} + +// buildCarrierVerificationResult 根据 riskWarning 的运营商相关项生成结果 +func buildCarrierVerificationResult(ctx *sourceContext) (string, string) { + if ctx.BaseData == nil { + return "正常", "" + } + risk := ctx.BaseData.RiskWarning + hits := make([]string, 0, 4) + if risk.ShortPhoneDuration > 0 { + hits = append(hits, "手机在网时长极短") + } + if risk.ShortPhoneDurationSlight > 0 { + hits = append(hits, "手机在网时长较短") + } + if risk.IdCardPhoneProvinceMismatch > 0 { + hits = append(hits, "身份证号手机号归属地不一致") + } + if risk.NoPhoneDuration > 0 { + hits = append(hits, "手机号在网状态异常") + } + if len(hits) == 0 { + return "正常", "" + } + return "命中", joinWithChineseComma(hits) +} + +// generateReportID 生成报告ID(RPT-前缀 + 随机十六进制) +func generateReportID() string { + buf := make([]byte, 4) + if _, err := rand.Read(buf); err != nil { + return time.Now().Format("20060102") + fmt.Sprintf("%010x", time.Now().UnixNano()) + } + datePart := time.Now().Format("20060102") + return datePart + strings.ToUpper(hex.EncodeToString(buf)) +} + +// mapTimeType 将时间段编码映射为展示文案与机构类型 +func mapTimeType(value string) (string, string) { + switch { + case strings.Contains(value, "夜间"): + return "夜间(22:00-06:00)", mapInstitutionType(value) + case strings.Contains(value, "周末"): + return "周末", mapInstitutionType(value) + case strings.Contains(value, "节假日"): + return "节假日", mapInstitutionType(value) + default: + return "工作日", mapInstitutionType(value) + } +} + +// mapInstitutionType 根据描述判断机构大类 +func mapInstitutionType(value string) string { + if strings.Contains(value, "银行") { + return "银行类机构" + } + return "非银金融机构" +} + +// hasHighRiskHit 判断 riskWarning 是否命中任一高风险项 +func hasHighRiskHit(r riskWarning) bool { + return r.FrequentApplicationRecent > 0 || + r.FrequentBankApplications > 0 || + r.FrequentNonBankApplications > 0 || + r.HasCriminalRecord > 0 || + r.HighDebtPressure > 0 || + r.PhoneThreeElementMismatch > 0 || + r.ShortPhoneDuration > 0 || + r.ShortPhoneDurationSlight > 0 || + r.VeryFrequentRentalApplications > 0 || + r.FrequentRentalApplications > 0 || + r.HitCriminalRisk > 0 || + r.HitExecutionCase > 0 || + r.HitHighRiskBankLastTwoYears > 0 || + r.HitHighRiskNonBankLastTwoYears > 0 || + r.HitHighRiskBank > 0 +} + +// hasMediumRiskHit 判断 riskWarning 是否命中任一中风险项 +func hasMediumRiskHit(r riskWarning) bool { + return r.IdCardPhoneProvinceMismatch > 0 || + r.IsAntiFraudInfo > 0 || + r.HitCurrentOverdue > 0 || + r.MoreFrequentBankApplications > 0 || + r.MoreFrequentNonBankApplications > 0 +} + +func extractApplyLoanMetrics(ctx *sourceContext) map[string]int { + if ctx.RiskScreen == nil { + return nil + } + + for _, variable := range ctx.RiskScreen.RiskScreenV2.Variables { + if strings.EqualFold(variable.VariableName, "bairong_applyloan_extend") { + results := make(map[string]int, len(variable.VariableValue)) + for key, val := range variable.VariableValue { + results[key] = parseMetricValue(val) + } + return results + } + } + + return nil +} + +func parseMetricValue(raw string) int { + trimmed := strings.TrimSpace(raw) + if trimmed == "" || trimmed == "空" || trimmed == "N" { + return 0 + } + value, err := strconv.ParseFloat(trimmed, 64) + if err != nil { + return 0 + } + return int(value + 0.5) +} + +func sumMetrics(metrics map[string]int, keys ...string) int { + idVal := 0 + cellVal := 0 + + for _, key := range keys { + v := metrics[key] + if strings.Contains(key, "_id_") { + if v > idVal { + idVal = v + } + } else if strings.Contains(key, "_cell_") { + if v > cellVal { + cellVal = v + } + } else { + if v > idVal { + idVal = v + } + } + } + + if cellVal > idVal { + return cellVal + } + return idVal +} + +func containsLabel(labels []string, target string) bool { + for _, label := range labels { + if label == target { + return true + } + } + return false +} + +// riskLevelFromStrictCount 特殊时段风险等级(夜间/周末) +func riskLevelFromStrictCount(count int) string { + switch { + case count <= 0: + return "无风险" + case count < 3: + return "低风险" + case count <= 6: + return "中风险" + default: + return "高风险" + } +}