优化个人贷款流程及模型展示

This commit is contained in:
wkc
2026-08-24 16:07:02 +08:00
parent cb29c38bf1
commit b769180bb1
16 changed files with 198 additions and 56 deletions

View File

@@ -125,7 +125,7 @@ public class ModelInvokeDTO {
/** /**
* 贷款用途(非必填) * 贷款用途(非必填)
* 可选值:consumer/business * 可选值:经营/消费
*/ */
private String loanPurpose; private String loanPurpose;

View File

@@ -41,6 +41,11 @@ public class PersonalLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "申请金额不能为空") @NotBlank(message = "申请金额不能为空")
private String applyAmt; private String applyAmt;
@Schema(description = "贷款用途", requiredMode = Schema.RequiredMode.REQUIRED, example = "business", allowableValues = {"business", "consumer"})
@NotBlank(message = "贷款用途不能为空")
@Pattern(regexp = "^(business|consumer)$", message = "贷款用途必须是business、consumer之一")
private String loanPurpose;
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"}) @Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空") @NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一") @Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")

View File

@@ -159,12 +159,21 @@ public class ModelRetailOutputFields {
// 本金逾期 // 本金逾期
private String prinOverdue; private String prinOverdue;
// BP_本金逾期
private String bpPrinOverdue;
// 利息逾期 // 利息逾期
private String interestOverdue; private String interestOverdue;
// BP_利息逾期
private String bpIinterestOverdue;
// 信用卡逾期 // 信用卡逾期
private String cardOverdue; private String cardOverdue;
// BP_信用卡逾期
private String bpCardOverdue;
// BP_灰名单与逾期 // BP_灰名单与逾期
private String bpGreyOverdue; private String bpGreyOverdue;

View File

@@ -102,11 +102,25 @@ public class LoanPricingModelService {
private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO) private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{ {
modelInvokeDTO.setLoanPurpose(toPersonalModelLoanPurpose(modelInvokeDTO.getLoanPurpose()));
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof())); modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop())); modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop()));
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty())); modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
} }
private String toPersonalModelLoanPurpose(String value)
{
if ("business".equals(value))
{
return "经营";
}
if ("consumer".equals(value))
{
return "消费";
}
return value;
}
private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO) private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{ {
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty())); modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));

View File

@@ -242,6 +242,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
.set(LoanPricingWorkflow::getIdNum, editingWorkflow.getIdNum()) .set(LoanPricingWorkflow::getIdNum, editingWorkflow.getIdNum())
.set(LoanPricingWorkflow::getGuarType, editingWorkflow.getGuarType()) .set(LoanPricingWorkflow::getGuarType, editingWorkflow.getGuarType())
.set(LoanPricingWorkflow::getApplyAmt, editingWorkflow.getApplyAmt()) .set(LoanPricingWorkflow::getApplyAmt, editingWorkflow.getApplyAmt())
.set(LoanPricingWorkflow::getLoanPurpose, editingWorkflow.getLoanPurpose())
.set(LoanPricingWorkflow::getBusinessType, editingWorkflow.getBusinessType()) .set(LoanPricingWorkflow::getBusinessType, editingWorkflow.getBusinessType())
.set(LoanPricingWorkflow::getLoanRateHistory, editingWorkflow.getLoanRateHistory()) .set(LoanPricingWorkflow::getLoanRateHistory, editingWorkflow.getLoanRateHistory())
.set(LoanPricingWorkflow::getCouponRate, editingWorkflow.getCouponRate()) .set(LoanPricingWorkflow::getCouponRate, editingWorkflow.getCouponRate())

View File

@@ -28,6 +28,7 @@ public class LoanPricingConverter {
entity.setIdNum(dto.getIdNum()); entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType()); entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt()); entity.setApplyAmt(dto.getApplyAmt());
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBusinessType(dto.getBusinessType()); entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory()); entity.setLoanRateHistory(dto.getLoanRateHistory());
entity.setCouponRate(dto.getCouponRate()); entity.setCouponRate(dto.getCouponRate());

View File

@@ -49,8 +49,11 @@
"greyCust": "否", "greyCust": "否",
"blackCust": "否", "blackCust": "否",
"prinOverdue": "否", "prinOverdue": "否",
"bpPrinOverdue": "0",
"interestOverdue": "否", "interestOverdue": "否",
"bpIinterestOverdue": "0",
"cardOverdue": "否", "cardOverdue": "否",
"bpCardOverdue": "0",
"bpGreyOverdue": "98", "bpGreyOverdue": "98",
"totalBpRisk": "95", "totalBpRisk": "95",
"totalBp": "350", "totalBp": "350",

View File

@@ -25,5 +25,8 @@ class ModelRetailOutputFieldsTest
assertTrue(fieldNames.contains("greyBlackCust"), "缺少字段 greyBlackCust"); assertTrue(fieldNames.contains("greyBlackCust"), "缺少字段 greyBlackCust");
assertTrue(fieldNames.contains("blackCust"), "缺少字段 blackCust"); assertTrue(fieldNames.contains("blackCust"), "缺少字段 blackCust");
assertTrue(fieldNames.contains("branchBp"), "缺少字段 branchBp"); assertTrue(fieldNames.contains("branchBp"), "缺少字段 branchBp");
assertTrue(fieldNames.contains("bpPrinOverdue"), "缺少字段 bpPrinOverdue");
assertTrue(fieldNames.contains("bpIinterestOverdue"), "缺少字段 bpIinterestOverdue");
assertTrue(fieldNames.contains("bpCardOverdue"), "缺少字段 bpCardOverdue");
} }
} }

View File

@@ -18,8 +18,6 @@ import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -44,27 +42,27 @@ class LoanPricingModelServicePersonalParamsTest {
private LoanPricingModelService loanPricingModelService; private LoanPricingModelService loanPricingModelService;
@Test @Test
void shouldRemoveLoanPurposeAndKeepPersonalModelFieldsInCreateDto() throws NoSuchFieldException { void shouldContainLoanPurposeAndPersonalModelFieldsInCreateDto() throws NoSuchFieldException {
assertThrows(NoSuchFieldException.class, assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof")); assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm")); assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
} }
@Test @Test
void shouldMapBizProofButNotLoanPurposeFromPersonalDto() { void shouldMapLoanPurposeAndBizProofFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001"); dto.setCustIsn("CUST001");
dto.setCustName("张三"); dto.setCustName("张三");
dto.setGuarType("信用"); dto.setGuarType("信用");
dto.setApplyAmt("100000"); dto.setApplyAmt("100000");
dto.setLoanPurpose("business");
dto.setBusinessType("新增"); dto.setBusinessType("新增");
dto.setLoanTerm("3"); dto.setLoanTerm("3");
dto.setBizProof("1"); dto.setBizProof("1");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto); LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertNull(workflow.getLoanPurpose()); assertEquals("business", workflow.getLoanPurpose());
assertEquals("1", workflow.getBizProof()); assertEquals("1", workflow.getBizProof());
assertEquals("3", workflow.getLoanTerm()); assertEquals("3", workflow.getLoanTerm());
} }
@@ -89,6 +87,7 @@ class LoanPricingModelServicePersonalParamsTest {
workflow.setIdNum("110101199001011234"); workflow.setIdNum("110101199001011234");
workflow.setGuarType("信用"); workflow.setGuarType("信用");
workflow.setApplyAmt("100000"); workflow.setApplyAmt("100000");
workflow.setLoanPurpose("consumer");
workflow.setLoanTerm("3"); workflow.setLoanTerm("3");
workflow.setBizProof("true"); workflow.setBizProof("true");
workflow.setLoanLoop("false"); workflow.setLoanLoop("false");
@@ -114,10 +113,27 @@ class LoanPricingModelServicePersonalParamsTest {
&& Objects.equals("110101199001011234", dto.getIdNum()) && Objects.equals("110101199001011234", dto.getIdNum())
&& Objects.equals("信用", dto.getGuarType()) && Objects.equals("信用", dto.getGuarType())
&& Objects.equals("100000", dto.getApplyAmt()) && Objects.equals("100000", dto.getApplyAmt())
&& Objects.equals("消费", dto.getLoanPurpose())
&& Objects.equals("3", dto.getLoanTerm()) && Objects.equals("3", dto.getLoanTerm())
&& Objects.equals("1", dto.getBizProof()) && Objects.equals("1", dto.getBizProof())
&& Objects.equals("0", dto.getLoanLoop()) && Objects.equals("0", dto.getLoanLoop())
&& Objects.equals("1", dto.getCollThirdParty()) && Objects.equals("1", dto.getCollThirdParty())
&& Objects.equals("一类", dto.getCollType()))); && Objects.equals("一类", dto.getCollType())));
} }
@Test
void shouldConvertBusinessLoanPurposeForPersonalModel() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(2L);
workflow.setCustType("个人");
workflow.setLoanPurpose("business");
when(loanPricingWorkflowMapper.selectById(2L)).thenReturn(workflow);
when(modelService.invokePersonalModel(any())).thenReturn(new ModelRetailOutputFields());
loanPricingModelService.invokeModelAsync(2L);
verify(modelService).invokePersonalModel(argThat((ModelInvokeDTO dto) ->
Objects.equals("经营", dto.getLoanPurpose())));
}
} }

View File

@@ -496,6 +496,7 @@ class LoanPricingWorkflowServiceImplTest
dto.setIdNum("110101199001019999"); dto.setIdNum("110101199001019999");
dto.setGuarType("抵押"); dto.setGuarType("抵押");
dto.setApplyAmt("200000"); dto.setApplyAmt("200000");
dto.setLoanPurpose("business");
dto.setBusinessType("新增"); dto.setBusinessType("新增");
dto.setLoanTerm("3"); dto.setLoanTerm("3");
dto.setLoanLoop("1"); dto.setLoanLoop("1");
@@ -510,6 +511,7 @@ class LoanPricingWorkflowServiceImplTest
verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture()); verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture());
String sqlSet = updateWrapperCaptor.getValue().getSqlSet(); String sqlSet = updateWrapperCaptor.getValue().getSqlSet();
assertTrue(sqlSet.contains("apply_amt")); assertTrue(sqlSet.contains("apply_amt"));
assertTrue(sqlSet.contains("loan_purpose"));
assertTrue(sqlSet.contains("cust_name")); assertTrue(sqlSet.contains("cust_name"));
assertTrue(sqlSet.contains("id_num")); assertTrue(sqlSet.contains("id_num"));
assertTrue(sqlSet.contains("biz_proof")); assertTrue(sqlSet.contains("biz_proof"));
@@ -518,6 +520,7 @@ class LoanPricingWorkflowServiceImplTest
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("张三")); assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("张三"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("110101199001019999")); assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("110101199001019999"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("普通住宅、土地、厂房")); assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("普通住宅、土地、厂房"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("business"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("")); assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue(""));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("1")); assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("1"));
verify(sensitiveFieldCryptoService, never()).encrypt(any()); verify(sensitiveFieldCryptoService, never()).encrypt(any());

View File

@@ -41,7 +41,15 @@
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead> <thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody> <tbody>
<tr v-for="(row, index) in retailContributionRows" :key="`retail-contribution-${index}`" :class="{ 'is-total-row': row.total }"> <tr v-for="(row, index) in retailContributionRows" :key="`retail-contribution-${index}`" :class="{ 'is-total-row': row.total }">
<td><span v-if="row.summary">汇总</span><div v-else v-for="item in row.results" :key="item.label" class="result-item"><span class="result-label">{{ item.label }}</span><span class="result-value">{{ item.value }}</span></div></td> <td>
<span v-if="row.summary">
汇总
<el-tooltip v-if="row.summaryTooltip" :content="row.summaryTooltip" placement="top">
<i class="el-icon-question analysis-help" tabindex="0" aria-label="查看贡献度汇总上限"></i>
</el-tooltip>
</span>
<div v-else v-for="item in row.results" :key="item.label" class="result-item"><span class="result-label">{{ item.label }}</span><span class="result-value">{{ item.value }}</span></div>
</td>
<td><span v-if="row.bpLabel" :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td> <td><span v-if="row.bpLabel" :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr> </tr>
</tbody> </tbody>
@@ -55,8 +63,26 @@
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead> <thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody> <tbody>
<tr v-for="(row, index) in retailRelevanceRows" :key="`retail-relevance-${index}`" :class="{ 'is-total-row': row.total }"> <tr v-for="(row, index) in retailRelevanceRows" :key="`retail-relevance-${index}`" :class="{ 'is-total-row': row.total }">
<td><span v-if="row.summary">汇总</span><div v-else v-for="item in row.results" :key="item.label" class="result-item"><span class="result-label">{{ item.label }}</span><span class="result-value">{{ item.value }}</span></div></td> <td>
<td><span :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td> <span v-if="row.summary">汇总</span>
<div v-else v-for="item in row.results" :key="item.label" class="result-item">
<span class="result-label">{{ item.label }}</span>
<el-tooltip v-if="item.tooltip" :content="item.tooltip" placement="top">
<i class="el-icon-question analysis-help" tabindex="0" :aria-label="`${item.label}提示`"></i>
</el-tooltip>
<span v-if="item.showValue !== false" class="result-value">{{ item.value }}</span>
</div>
</td>
<td>
<span :class="row.total ? 'total-bp-value' : 'bp-value'">
<template v-if="row.bpLabel">
{{ row.bpLabel }}
<el-tooltip v-if="row.bpTooltip" :content="row.bpTooltip" placement="top">
<i class="el-icon-question analysis-help" tabindex="0" aria-label="查看中间业务BP上限"></i>
</el-tooltip>
</template>{{ row.bpValue }}
</span>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -172,6 +198,10 @@ export default {
corpOutput: { corpOutput: {
type: Object, type: Object,
default: null default: null
},
guarType: {
type: String,
default: null
} }
}, },
computed: { computed: {
@@ -190,49 +220,46 @@ export default {
this.resultItem('贷款年日均', this.retailOutput.loanAvg), this.resultItem('贷款年日均', this.retailOutput.loanAvg),
this.resultItem('派生率', this.formatRate(this.retailOutput.derivationRate)) this.resultItem('派生率', this.formatRate(this.retailOutput.derivationRate))
]), ]),
this.totalRow('TOTAL_BP_贡献度', this.retailOutput.totalBpContribution) this.totalRow('TOTAL_BP_贡献度', this.retailOutput.totalBpContribution, '最高上限100BP')
] ]
}, },
retailRelevanceRows() { retailRelevanceRows() {
return [ return [
this.groupedAnalysisRow([ this.middleBusinessRow('中间业务_个人_信用卡', this.retailOutput.midPerCard, '办理减5个BP信用卡额度1万元及以上的再减10个BP'),
this.resultItem('中间业务_个人_信用卡', this.retailOutput.midPerCard), this.middleBusinessRow('中间业务_个人_一码通', this.retailOutput.midPerPass),
this.resultItem('中间业务_个人_一码通', this.retailOutput.midPerPass), this.middleBusinessRow('中间业务_个人_丰收互联', this.retailOutput.midPerHarvest),
this.resultItem('中间业务_个人_丰收互联', this.retailOutput.midPerHarvest), this.middleBusinessRow('中间业务_个人_有效客户', this.retailOutput.midPerEffect),
this.resultItem('中间业务_个人_有效客户', this.retailOutput.midPerEffect), this.middleBusinessRow('中间业务_个人_快捷支付', this.retailOutput.midPerQuickPay),
this.resultItem('中间业务_个人_快捷支付', this.retailOutput.midPerQuickPay), this.middleBusinessRow('中间业务_个人_电费代扣', this.retailOutput.midPerEleDdc),
this.resultItem('中间业务_个人_费代扣', this.retailOutput.midPerEleDdc), this.middleBusinessRow('中间业务_个人_费代扣', this.retailOutput.midPerWaterDdc),
this.resultItem('中间业务_个人_费代扣', this.retailOutput.midPerWaterDdc), this.middleBusinessRow('中间业务_个人_华数费代扣', this.retailOutput.midPerHuashuDdc),
this.resultItem('中间业务_个人_华数费代扣', this.retailOutput.midPerHuashuDdc), this.middleBusinessRow('中间业务_个人_煤气费代扣', this.retailOutput.midPerGasDdc),
this.resultItem('中间业务_个人_煤气费代扣', this.retailOutput.midPerGasDdc), this.middleBusinessRow('中间业务_个人_市民卡', this.retailOutput.midPerCitizencard),
this.resultItem('中间业务_个人_市民卡', this.retailOutput.midPerCitizencard), this.middleBusinessRow('中间业务_个人_理财业务', this.retailOutput.midPerFinMan),
this.resultItem('中间业务_个人_理财业务', this.retailOutput.midPerFinMan), this.middleBusinessRow('中间业务_个人_etc', this.retailOutput.midPerEtc),
this.resultItem('中间业务_个人_etc', this.retailOutput.midPerEtc) this.summaryRow('BP_中间业务', this.retailOutput.bpMid, '最高上限-30BP'),
], 'BP_中间业务', this.retailOutput.bpMid),
this.totalRow('TOTAL_BP_关联度', this.retailOutput.totalBpRelevance) this.totalRow('TOTAL_BP_关联度', this.retailOutput.totalBpRelevance)
] ]
}, },
retailRiskRows() { retailRiskRows() {
return [ const rows = [
this.analysisRow('申请金额', this.retailOutput.applyAmt, 'BP_贷款额度', this.retailOutput.bpLoanAmount), this.analysisRow('申请金额', this.retailOutput.applyAmt, 'BP_贷款额度', this.retailOutput.bpLoanAmount),
this.groupedAnalysisRow([ this.groupedAnalysisRow([
this.resultItem('贷款用途', this.formatLoanPurpose(this.retailOutput.loanPurpose)), this.resultItem('贷款用途', this.formatLoanPurpose(this.retailOutput.loanPurpose)),
this.resultItem('营业执照', this.formatBoolean(this.retailOutput.bizProof)) this.resultItem('营业执照', this.formatBoolean(this.retailOutput.bizProof))
], 'BP_贷款用途', this.retailOutput.bpLoanUse), ], 'BP_贷款用途', this.retailOutput.bpLoanUse),
this.analysisRow('循环功能', this.formatBoolean(this.retailOutput.loanLoop), 'BP_循环功能', this.retailOutput.bpLoanLoop), this.analysisRow('循环功能', this.formatBoolean(this.retailOutput.loanLoop), 'BP_循环功能', this.retailOutput.bpLoanLoop)
this.groupedAnalysisRow([
this.resultItem('抵质押类型', this.retailOutput.collType),
this.resultItem('抵质押物三方所有', this.formatBoolean(this.retailOutput.collThirdParty))
], 'BP_抵押物', this.retailOutput.bpCollateral),
this.groupedAnalysisRow([
this.resultItem('灰名单客户', this.formatBoolean(this.retailOutput.greyCust)),
this.resultItem('黑名单客户', this.formatBoolean(this.retailOutput.blackCust)),
this.resultItem('本金逾期', this.formatBoolean(this.retailOutput.prinOverdue)),
this.resultItem('利息逾期', this.formatBoolean(this.retailOutput.interestOverdue)),
this.resultItem('信用卡逾期', this.formatBoolean(this.retailOutput.cardOverdue))
], 'BP_灰名单与逾期', this.retailOutput.bpGreyOverdue),
this.totalRow('TOTAL_BP_风险度', this.retailOutput.totalBpRisk)
] ]
if (this.guarType === '抵押' || this.guarType === '质押') {
rows.push(this.analysisRow('抵押物第三方所有', this.formatBoolean(this.retailOutput.collThirdParty), 'BP_抵押物', this.retailOutput.bpCollateral))
}
rows.push(
this.analysisRow('本金逾期', this.formatBoolean(this.retailOutput.prinOverdue), 'BP_本金逾期', this.retailOutput.bpPrinOverdue),
this.analysisRow('利息逾期', this.formatBoolean(this.retailOutput.interestOverdue), 'BP_利息逾期', this.retailOutput.bpIinterestOverdue),
this.analysisRow('信用卡逾期', this.formatBoolean(this.retailOutput.cardOverdue), 'BP_信用卡逾期', this.retailOutput.bpCardOverdue),
this.totalRow('TOTAL_BP_风险度', this.retailOutput.totalBpRisk)
)
return rows
}, },
corporateLoyaltyRows() { corporateLoyaltyRows() {
return [ return [
@@ -296,8 +323,8 @@ export default {
}, },
methods: { methods: {
formatRate, formatRate,
resultItem(label, value) { resultItem(label, value, tooltip = null, showValue = true) {
return {label, value: this.formatOutputValue(value)} return {label, value: this.formatOutputValue(value), tooltip, showValue}
}, },
groupedAnalysisRow(results, bpLabel, bpValue, total = false, category = null) { groupedAnalysisRow(results, bpLabel, bpValue, total = false, category = null) {
return {results, bpLabel, bpValue: this.formatOutputValue(bpValue), total, category} return {results, bpLabel, bpValue: this.formatOutputValue(bpValue), total, category}
@@ -305,8 +332,14 @@ export default {
analysisRow(resultLabel, resultValue, bpLabel, bpValue) { analysisRow(resultLabel, resultValue, bpLabel, bpValue) {
return this.groupedAnalysisRow([this.resultItem(resultLabel, resultValue)], bpLabel, bpValue) return this.groupedAnalysisRow([this.resultItem(resultLabel, resultValue)], bpLabel, bpValue)
}, },
totalRow(bpLabel, bpValue) { middleBusinessRow(resultLabel, bpValue, tooltip = null) {
return {summary: true, bpLabel, bpValue: this.formatOutputValue(bpValue), total: true} return this.groupedAnalysisRow([this.resultItem(resultLabel, null, tooltip, false)], null, bpValue)
},
summaryRow(bpLabel, bpValue, bpTooltip) {
return {summary: true, bpLabel, bpValue: this.formatOutputValue(bpValue), bpTooltip, total: false}
},
totalRow(bpLabel, bpValue, summaryTooltip = null) {
return {summary: true, bpLabel, bpValue: this.formatOutputValue(bpValue), total: true, summaryTooltip}
}, },
/** 格式化布尔值为中文 */ /** 格式化布尔值为中文 */
formatBoolean(value) { formatBoolean(value) {
@@ -362,6 +395,14 @@ export default {
color: #303133; color: #303133;
} }
.analysis-help {
margin-left: 4px;
color: #909399;
cursor: help;
font-size: 14px;
vertical-align: middle;
}
.output-analysis-table { .output-analysis-table {
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;

View File

@@ -52,6 +52,14 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="12">
<el-form-item label="贷款用途" prop="loanPurpose">
<el-select v-model="form.loanPurpose" placeholder="请选择贷款用途" style="width: 100%">
<el-option label="经营" value="business"/>
<el-option label="消费" value="consumer"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="借款期限(年)" prop="loanTerm"> <el-form-item label="借款期限(年)" prop="loanTerm">
<el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%"> <el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%">
@@ -242,6 +250,7 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined, idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined, guarType: undefined,
applyAmt: undefined, applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined, loanTerm: undefined,
businessType: undefined, businessType: undefined,
loanRateHistory: undefined, loanRateHistory: undefined,
@@ -274,6 +283,9 @@ export default {
applyAmt: [ applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"} {required: true, validator: validateApplyAmt, trigger: "blur"}
], ],
loanPurpose: [
{required: true, message: "请选择贷款用途", trigger: "change"}
],
loanTerm: [ loanTerm: [
{required: true, message: "请选择借款期限", trigger: "change"} {required: true, message: "请选择借款期限", trigger: "change"}
], ],
@@ -397,6 +409,7 @@ export default {
idNum: this.editData.idNum, idNum: this.editData.idNum,
guarType: this.editData.guarType, guarType: this.editData.guarType,
applyAmt: this.editData.applyAmt, applyAmt: this.editData.applyAmt,
loanPurpose: this.editData.loanPurpose,
loanTerm: this.editData.loanTerm, loanTerm: this.editData.loanTerm,
businessType: this.editData.businessType, businessType: this.editData.businessType,
loanRateHistory: this.editData.loanRateHistory, loanRateHistory: this.editData.loanRateHistory,
@@ -418,6 +431,7 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined, idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined, guarType: undefined,
applyAmt: undefined, applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined, loanTerm: undefined,
businessType: undefined, businessType: undefined,
loanRateHistory: undefined, loanRateHistory: undefined,

View File

@@ -57,6 +57,14 @@
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item> <el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item> <el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item> <el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
<el-descriptions-item label="灰名单客户">{{
formatBoolean(retailOutput && retailOutput.greyCust)
}}
</el-descriptions-item>
<el-descriptions-item label="黑名单客户">{{
formatBoolean(retailOutput && retailOutput.blackCust)
}}
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item> <el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item> <el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -95,6 +103,7 @@
<!-- 模型输出卡片 --> <!-- 模型输出卡片 -->
<ModelOutputDisplay <ModelOutputDisplay
:cust-type="detailData.custType" :cust-type="detailData.custType"
:guar-type="detailData.guarType"
:retail-output="retailOutput" :retail-output="retailOutput"
:corp-output="null" :corp-output="null"
/> />

View File

@@ -19,10 +19,12 @@ assert(
) )
assert( assert(
!personalCreateDialog.includes('label="贷款用途"') && personalCreateDialog.includes('label="贷款用途"') &&
!personalCreateDialog.includes('prop="loanPurpose"') && personalCreateDialog.includes('prop="loanPurpose"') &&
!personalCreateDialog.includes('loanPurpose:'), personalCreateDialog.includes('loanPurpose: undefined') &&
'个人新增弹窗不应继续保留贷款用途字段' personalCreateDialog.includes('loanPurpose: this.editData.loanPurpose') &&
personalCreateDialog.includes('请选择贷款用途'),
'个人新增弹窗缺少贷款用途、必填校验或编辑回显'
) )
assert( assert(
@@ -31,8 +33,9 @@ assert(
) )
assert( assert(
!personalCreateDialog.includes("value=\"consumer\"") && !personalCreateDialog.includes("value=\"business\""), personalCreateDialog.includes('label="经营" value="business"') &&
'个人新增弹窗不应继续保留贷款用途选项' personalCreateDialog.includes('label="消费" value="consumer"'),
'个人新增弹窗贷款用途选项或协议值不正确'
) )
assert( assert(

View File

@@ -42,15 +42,22 @@ assert(
'个人流程详情关键信息未展示产品最低利率下限' '个人流程详情关键信息未展示产品最低利率下限'
) )
assert(
personalDetail.includes(':guar-type="detailData.guarType"'),
'个人模型输出未使用流程担保方式控制抵押物行'
)
const requiredRetailFields = [ const requiredRetailFields = [
'retailOutput.loanRateHistory', 'retailOutput.loanRateHistory',
'retailOutput.smoothRange', 'retailOutput.smoothRange',
'retailOutput.finalCalculateRate', 'retailOutput.finalCalculateRate',
'retailOutput.referenceRate', 'retailOutput.referenceRate',
'retailOutput.blackCust',
'retailOutput.branchBp', 'retailOutput.branchBp',
'retailOutput.bpLoanUse', 'retailOutput.bpLoanUse',
'retailOutput.midPerGasDdc' 'retailOutput.midPerGasDdc',
'retailOutput.bpPrinOverdue',
'retailOutput.bpIinterestOverdue',
'retailOutput.bpCardOverdue'
] ]
requiredRetailFields.forEach((field) => { requiredRetailFields.forEach((field) => {
@@ -63,10 +70,18 @@ assert(
) )
assert( assert(
detailCard.includes('label="客户名称"') basicInfo.includes('label="灰名单客户"')
&& !detailCard.includes('label="灰黑名单客户"') && basicInfo.includes('retailOutput && retailOutput.greyCust')
&& !detailCard.includes('retailOutput && retailOutput.greyBlackCust'), && basicInfo.includes('label="黑名单客户"')
'个人流程详情基本信息不应展示灰黑名单客户' && basicInfo.includes('retailOutput && retailOutput.blackCust'),
'个人流程详情基本信息缺少灰名单客户或黑名单客户'
)
assert(
!modelOutput.includes("this.resultItem('灰名单客户', this.formatBoolean(this.retailOutput.greyCust))")
&& !modelOutput.includes("this.resultItem('黑名单客户', this.formatBoolean(this.retailOutput.blackCust))")
&& !modelOutput.includes("'BP_灰名单与逾期', this.retailOutput.bpGreyOverdue"),
'个人风险度不应继续展示灰黑客户或旧聚合BP'
) )
assert( assert(

View File

@@ -0,0 +1,5 @@
-- 2026-08-24 personal model output overdue BP fields
ALTER TABLE `model_retail_output_fields`
ADD COLUMN `bp_prin_overdue` varchar(100) DEFAULT NULL COMMENT 'BP_本金逾期' AFTER `prin_overdue`,
ADD COLUMN `bp_iinterest_overdue` varchar(100) DEFAULT NULL COMMENT 'BP_利息逾期' AFTER `interest_overdue`,
ADD COLUMN `bp_card_overdue` varchar(100) DEFAULT NULL COMMENT 'BP_信用卡逾期' AFTER `card_overdue`;