15 Commits

38 changed files with 1553 additions and 455 deletions

View File

@@ -25,16 +25,16 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://116.62.17.81:3307/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: Kfcx@1234
url: jdbc:mysql://116.62.17.81:53306/loan_pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: znsj
password: Znsj@123456
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量

View File

@@ -2,6 +2,7 @@ package com.ruoyi.loanpricing.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -139,6 +140,17 @@ public class LoanPricingWorkflowController extends BaseController
return success(loanRateHistoryService.query(custIsn));
}
/**
* 解密历史流程客户名称和证件号码密文字段
*/
@Anonymous
@Operation(summary = "解密历史流程客户敏感字段")
@PostMapping("/anonymous/decrypt-sensitive-fields")
public AjaxResult decryptSensitiveFields()
{
return success(loanPricingWorkflowService.decryptWorkflowSensitiveFields());
}
/**
* 查询利率定价流程列表
*/

View File

@@ -65,8 +65,16 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
@Schema(description = "贸易和建筑业企业标识", example = "0")
private String isTradeBuildEnt;
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押、存单质押、股权质押、其他质押之一")
@Schema(description = "押物性", example = "普通住宅、土地、厂房", allowableValues = {"普通住宅、土地、厂房", "商业性质不动产", "排污权", "其他不动产"})
@Pattern(regexp = "^(普通住宅、土地、厂房|商业性质不动产|排污权|其他不动产)$", message = "抵押物性质必须是:普通住宅、土地、厂房、商业性质不动产、排污权、其他不动产之一")
private String collateralNature;
@Schema(description = "是否属于九地", example = "", allowableValues = {"", ""})
@Pattern(regexp = "^(是|否)$", message = "是否属于九地必须是:是、否之一")
private String isNineArea;
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "排污权抵押", "其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|排污权抵押|其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型必须是:一类、二类、三类、排污权抵押、其他不动产抵押、存单质押、股权质押、其他质押之一")
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "0")

View File

@@ -125,12 +125,12 @@ public class ModelInvokeDTO {
/**
* 贷款用途(非必填)
* 可选值:consumer/business
* 可选值:经营/消费
*/
private String loanPurpose;
/**
* 是否有经营佐证(非必填)
* 是否有营业执照(非必填)
* 可选值0/1
*/
private String bizProof;

View File

@@ -41,6 +41,11 @@ public class PersonalLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "申请金额不能为空")
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 = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
@@ -53,10 +58,22 @@ public class PersonalLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "借款期限不能为空")
private String loanTerm;
@Schema(description = "是否有营业执照", example = "0", allowableValues = {"0", "1"})
@Pattern(regexp = "^[01]$", message = "是否有营业执照必须是0或1")
private String bizProof;
@Schema(description = "循环功能", example = "0")
private String loanLoop;
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一线", "一类", "二类", "三类", "存单质押", "其他质押"})
@Schema(description = "押物性", example = "普通住宅、土地、厂房", allowableValues = {"普通住宅、土地、厂房", "商业性质不动产", "其他抵押财产"})
@Pattern(regexp = "^(普通住宅、土地、厂房|商业性质不动产|其他抵押财产)$", message = "抵押物性质必须是:普通住宅、土地、厂房、商业性质不动产、其他抵押财产之一")
private String collateralNature;
@Schema(description = "是否属于九地", example = "", allowableValues = {"", ""})
@Pattern(regexp = "^(是|否)$", message = "是否属于九地必须是:是、否之一")
private String isNineArea;
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "存单质押", "其他质押"})
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "0")

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@@ -115,7 +116,7 @@ public class LoanPricingWorkflow implements Serializable
/** 余值覆盖: 0/1 */
private String resCover;
/** 是否有经营佐证: true/false */
/** 是否有营业执照: true/false */
private String bizProof;
/** 循环功能: true/false */
@@ -124,6 +125,12 @@ public class LoanPricingWorkflow implements Serializable
/** 抵质押类型: 一类/二类/三类/四类 */
private String collType;
/** 抵押物性质 */
private String collateralNature;
/** 是否属于九地: 是/否 */
private String isNineArea;
/** 抵质押物是否三方所有: true/false */
private String collThirdParty;
@@ -164,6 +171,16 @@ public class LoanPricingWorkflow implements Serializable
@TableField(exist = false)
private Long dataScopeDeptId;
/** 创建时间查询起始值 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(exist = false)
private Date beginCreateTime;
/** 创建时间查询结束值 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(exist = false)
private Date endCreateTime;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)

View File

@@ -106,10 +106,20 @@ public class ModelCorpOutputFields {
private String bpCollateral;
// 灰名单客户
private String greyCust;
// 黑名单客户
private String blackCust;
// 本金逾期
private String prinOverdue;
// BP_本金逾期
private String bpPrinOverdue;
// 利息逾期
private String interestOverdue;
// BP_利息逾期
private String bpIinterestOverdue;
// 信用卡逾期
private String cardOverdue;
// BP_信用卡逾期
private String bpCardOverdue;
// BP_灰名单与逾期
private String bpGreyOverdue;
// TOTAL_BP_风险度
@@ -129,6 +139,8 @@ public class ModelCorpOutputFields {
private String finalCalculateRate;
// 参考利率
private String referenceRate;
// 行社下调BP
private String branchBp;
@TableField(fill = FieldFill.INSERT)
private Date createTime;

View File

@@ -129,7 +129,7 @@ public class ModelRetailOutputFields {
// 贷款用途
private String loanPurpose;
// 是否有经营佐证
// 是否有营业执照
private String bizProof;
// BP_贷款用途
@@ -153,15 +153,27 @@ public class ModelRetailOutputFields {
// 灰名单客户
private String greyCust;
// 黑名单客户
private String blackCust;
// 本金逾期
private String prinOverdue;
// BP_本金逾期
private String bpPrinOverdue;
// 利息逾期
private String interestOverdue;
// BP_利息逾期
private String bpIinterestOverdue;
// 信用卡逾期
private String cardOverdue;
// BP_信用卡逾期
private String bpCardOverdue;
// BP_灰名单与逾期
private String bpGreyOverdue;
@@ -190,6 +202,9 @@ public class ModelRetailOutputFields {
// 参考利率
private String referenceRate;
// 行社下调BP
private String branchBp;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -9,6 +9,8 @@ public class LoanPricingWorkflowListVO
{
private String serialNum;
private Long deptId;
private String custIsn;
private String custName;

View File

@@ -17,4 +17,8 @@ public interface LoanPricingWorkflowMapper extends BaseMapper<LoanPricingWorkflo
{
IPage<LoanPricingWorkflowListVO> selectWorkflowPageWithRates(Page<?> page,
@Param("query") LoanPricingWorkflow query);
int updateSensitiveFieldsById(@Param("id") Long id,
@Param("custName") String custName,
@Param("idNum") String idNum);
}

View File

@@ -9,6 +9,7 @@ import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import java.util.List;
import java.util.Map;
/**
* 利率定价流程Service接口
@@ -93,4 +94,11 @@ public interface ILoanPricingWorkflowService
* @return 是否成功
*/
public boolean setExecuteRate(String serialNum, String executeRate);
/**
* 解密历史流程客户名称和证件号码密文字段
*
* @return 迁移统计
*/
public Map<String, Long> decryptWorkflowSensitiveFields();
}

View File

@@ -36,9 +36,6 @@ public class LoanPricingModelService {
@Resource
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Resource
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
public void invokeModelAsync(Long workflowId) {
invokeModelAndSave(workflowId, false);
}
@@ -53,16 +50,6 @@ public class LoanPricingModelService {
log.error("未找到对应的流程信息,未调用模型服务");
return;
}
try
{
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()));
}
catch (RuntimeException ex)
{
log.error("贷款定价模型调用前敏感字段解密失败", ex);
throw ex;
}
ModelInvokeDTO modelInvokeDTO = new ModelInvokeDTO();
BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO);
if ("个人".equals(loanPricingWorkflow.getCustType()))
@@ -115,11 +102,25 @@ public class LoanPricingModelService {
private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{
modelInvokeDTO.setLoanPurpose(toPersonalModelLoanPurpose(modelInvokeDTO.getLoanPurpose()));
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop()));
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)
{
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));

View File

@@ -31,7 +31,9 @@ import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
@@ -94,8 +96,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
}
loanPricingWorkflow.setDeptId(SecurityUtils.getDeptId());
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
loanPricingModelService.invokeModelAsync(loanPricingWorkflow.getId());
@@ -139,8 +139,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
private void validatePersonalCollateralType(LoanPricingWorkflow workflow) {
if ("抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "线", "", "二类", "三类")) {
throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一");
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类")) {
throw new ServiceException("个人抵押抵质押类型必须是:一类、二类、三类、四类之一");
}
if ("质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) {
@@ -150,8 +150,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
private void validateCorporateCollateralType(LoanPricingWorkflow workflow) {
if ("抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) {
throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一");
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "排污权抵押", "其他不动产抵押")) {
throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、排污权抵押、其他不动产抵押之一");
}
if ("质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "股权质押", "其他质押")) {
@@ -199,8 +199,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
{
LoanPricingWorkflow workflow = selectWorkflowBySerialNum(serialNum);
assertCurrentUserIsCreator(workflow);
workflow.setCustName(sensitiveFieldCryptoService.decrypt(workflow.getCustName()));
workflow.setIdNum(sensitiveFieldCryptoService.decrypt(workflow.getIdNum()));
return workflow;
}
@@ -236,22 +234,24 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
validateBusinessTypeAndHistoryRate(editingWorkflow);
validateCollateralTypeAndCouponRate(editingWorkflow);
String encryptedCustName = sensitiveFieldCryptoService.encrypt(editingWorkflow.getCustName());
String encryptedIdNum = sensitiveFieldCryptoService.encrypt(editingWorkflow.getIdNum());
LambdaUpdateWrapper<LoanPricingWorkflow> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(LoanPricingWorkflow::getId, existingWorkflow.getId())
.set(LoanPricingWorkflow::getCustIsn, editingWorkflow.getCustIsn())
.set(LoanPricingWorkflow::getCustName, encryptedCustName)
.set(LoanPricingWorkflow::getCustName, editingWorkflow.getCustName())
.set(LoanPricingWorkflow::getIdType, editingWorkflow.getIdType())
.set(LoanPricingWorkflow::getIdNum, encryptedIdNum)
.set(LoanPricingWorkflow::getIdNum, editingWorkflow.getIdNum())
.set(LoanPricingWorkflow::getGuarType, editingWorkflow.getGuarType())
.set(LoanPricingWorkflow::getApplyAmt, editingWorkflow.getApplyAmt())
.set(LoanPricingWorkflow::getLoanPurpose, editingWorkflow.getLoanPurpose())
.set(LoanPricingWorkflow::getBusinessType, editingWorkflow.getBusinessType())
.set(LoanPricingWorkflow::getLoanRateHistory, editingWorkflow.getLoanRateHistory())
.set(LoanPricingWorkflow::getCouponRate, editingWorkflow.getCouponRate())
.set(LoanPricingWorkflow::getLoanTerm, editingWorkflow.getLoanTerm())
.set(LoanPricingWorkflow::getCollateralNature, editingWorkflow.getCollateralNature())
.set(LoanPricingWorkflow::getIsNineArea, editingWorkflow.getIsNineArea())
.set(LoanPricingWorkflow::getCollType, editingWorkflow.getCollType())
.set(LoanPricingWorkflow::getCollThirdParty, editingWorkflow.getCollThirdParty())
.set(LoanPricingWorkflow::getBizProof, editingWorkflow.getBizProof())
.set(LoanPricingWorkflow::getLoanLoop, editingWorkflow.getLoanLoop())
.set(LoanPricingWorkflow::getResCover, editingWorkflow.getResCover())
.set(LoanPricingWorkflow::getRepayMethod, editingWorkflow.getRepayMethod())
@@ -312,7 +312,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
{
LoanPricingWorkflow scopedQuery = applyWorkflowListDataScope(loanPricingWorkflow);
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, scopedQuery);
pageResult.getRecords().forEach(row -> row.setCustName(sensitiveFieldCryptoService.decrypt(row.getCustName())));
return pageResult;
}
@@ -330,10 +329,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectOne(wrapper);
String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
loanPricingWorkflow.setCustName(plainCustName);
loanPricingWorkflow.setIdNum(plainIdNum);
loanPricingWorkflowVO.setLoanPricingWorkflow(loanPricingWorkflow);
if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){
@@ -381,10 +376,10 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
}
// 按机构号筛选
if (StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
// 按创建人部门筛选
if (loanPricingWorkflow.getDeptId() != null)
{
wrapper.like(LoanPricingWorkflow::getOrgCode, loanPricingWorkflow.getOrgCode());
wrapper.eq(LoanPricingWorkflow::getDeptId, loanPricingWorkflow.getDeptId());
}
return wrapper;
@@ -468,4 +463,80 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
int result = loanPricingWorkflowMapper.updateById(workflow);
return result > 0;
}
@Override
public Map<String, Long> decryptWorkflowSensitiveFields()
{
long processed = 0L;
long updated = 0L;
long skipped = 0L;
long failed = 0L;
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
wrapper.and(q -> q.isNotNull(LoanPricingWorkflow::getCustName)
.or()
.isNotNull(LoanPricingWorkflow::getIdNum));
List<LoanPricingWorkflow> workflows = loanPricingWorkflowMapper.selectList(wrapper);
for (LoanPricingWorkflow workflow : workflows)
{
processed++;
LoanPricingWorkflow update = new LoanPricingWorkflow();
update.setId(workflow.getId());
boolean changed = false;
String decryptedCustName = tryDecrypt(workflow.getCustName());
if (decryptedCustName != null && !Objects.equals(decryptedCustName, workflow.getCustName()))
{
update.setCustName(decryptedCustName);
changed = true;
}
String decryptedIdNum = tryDecrypt(workflow.getIdNum());
if (decryptedIdNum != null && !Objects.equals(decryptedIdNum, workflow.getIdNum()))
{
update.setIdNum(decryptedIdNum);
changed = true;
}
if (!changed)
{
skipped++;
continue;
}
try
{
loanPricingWorkflowMapper.updateSensitiveFieldsById(update.getId(), update.getCustName(), update.getIdNum());
updated++;
}
catch (RuntimeException ex)
{
failed++;
}
}
Map<String, Long> result = new LinkedHashMap<>();
result.put("processed", processed);
result.put("updated", updated);
result.put("skipped", skipped);
result.put("failed", failed);
return result;
}
private String tryDecrypt(String value)
{
if (!StringUtils.hasText(value))
{
return null;
}
try
{
return sensitiveFieldCryptoService.decrypt(value);
}
catch (RuntimeException ex)
{
return null;
}
}
}

View File

@@ -28,13 +28,17 @@ public class LoanPricingConverter {
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory());
entity.setCouponRate(dto.getCouponRate());
entity.setLoanTerm(dto.getLoanTerm());
entity.setCollateralNature(dto.getCollateralNature());
entity.setIsNineArea(dto.getIsNineArea());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射个人特有字段
entity.setBizProof(dto.getBizProof());
entity.setLoanLoop(dto.getLoanLoop());
return entity;
}
@@ -60,6 +64,8 @@ public class LoanPricingConverter {
entity.setCouponRate(dto.getCouponRate());
entity.setResCover(dto.getResCover());
entity.setRepayMethod(dto.getRepayMethod());
entity.setCollateralNature(dto.getCollateralNature());
entity.setIsNineArea(dto.getIsNineArea());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
entity.setIsGreenLoan(dto.getIsGreenLoan());

View File

@@ -20,13 +20,13 @@
"loanAvg": "3000000.00",
"derivationRate": "1.8",
"totalBpContribution": "12.3",
"midEntConnect": "1",
"midEntEffect": "1",
"midEntInter": "1",
"midEntAccept": "1",
"midEntDiscount": "1",
"midEntEleDdc": "1",
"midEntWaterDdc": "1",
"midEntConnect": "-2",
"midEntEffect": "0",
"midEntInter": "-3",
"midEntAccept": "0",
"midEntDiscount": "-1",
"midEntEleDdc": "0",
"midEntWaterDdc": "-1",
"bpMid": "6.8",
"payroll": "200",
"invLoanAmount": "2500000.00",
@@ -45,8 +45,13 @@
"collThirdParty": "0",
"bpCollateral": "4.5",
"greyCust": "0",
"blackCust": "1",
"prinOverdue": "0",
"interestOverdue": "0",
"bpPrinOverdue": "0",
"interestOverdue": "3",
"bpIinterestOverdue": "0",
"cardOverdue": "0",
"bpCardOverdue": "0",
"bpGreyOverdue": "0",
"totalBpRisk": "1.2",
"totalBp": "48.2",
@@ -56,7 +61,8 @@
"minRateProduct": "3.10",
"smoothRange": "-0.20",
"finalCalculateRate": "3.732",
"referenceRate": "3.432"
"referenceRate": "3.432",
"branchBp": "-5"
},
"extensionMap": {},
"reasonMessage": "Running successfully",

View File

@@ -22,18 +22,18 @@
"loanAvg": "100000.00",
"derivationRate": "1.2",
"totalBpContribution": "88",
"midPerCard": "1000.50",
"midPerPass": "500.00",
"midPerHarvest": "800.20",
"midPerEffect": "",
"midPerQuickPay": "300.00",
"midPerEleDdc": "150.00",
"midPerWaterDdc": "80.00",
"midPerHuashuDdc": "120.00",
"MidPerGasDdc": "90.00",
"midPerCitizencard": "200.00",
"midPerFinMan": "5000.00",
"midPerEtc": "180.00",
"midPerCard": "-2",
"midPerPass": "0",
"midPerHarvest": "-3",
"midPerEffect": "0",
"midPerQuickPay": "-1",
"midPerEleDdc": "0",
"midPerWaterDdc": "-1",
"midPerHuashuDdc": "0",
"MidPerGasDdc": "-1",
"midPerCitizencard": "0",
"midPerFinMan": "-4",
"midPerEtc": "0",
"bpMid": "45",
"totalBpRelevance": "90",
"applyAmt": "200000.00",
@@ -47,9 +47,13 @@
"collThirdParty": "否",
"bpCollateral": "0",
"greyCust": "否",
"blackCust": "否",
"prinOverdue": "否",
"bpPrinOverdue": "0",
"interestOverdue": "否",
"bpIinterestOverdue": "0",
"cardOverdue": "否",
"bpCardOverdue": "0",
"bpGreyOverdue": "98",
"totalBpRisk": "95",
"totalBp": "350",
@@ -58,7 +62,8 @@
"minRateProduct": "5.50",
"smoothRange": "-0.10",
"finalCalculateRate": "6.05",
"referenceRate": "5.95"
"referenceRate": "5.95",
"branchBp": "-8"
},
"extensionMap": {},
"reasonMessage": "Running successfully",

View File

@@ -7,6 +7,7 @@
<select id="selectWorkflowPageWithRates" resultType="com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO">
SELECT
lpw.serial_num AS serialNum,
lpw.dept_id AS deptId,
lpw.cust_isn AS custIsn,
lpw.cust_name AS custName,
lpw.cust_type AS custType,
@@ -40,13 +41,42 @@
AND SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')
</if>
<if test="query != null and query.custIsn != null and query.custIsn != ''">
AND lpw.cust_isn LIKE CONCAT('%', #{query.custIsn}, '%')
AND (
lpw.cust_isn LIKE CONCAT('%', #{query.custIsn}, '%')
OR lpw.id_num LIKE CONCAT('%', #{query.custIsn}, '%')
OR lpw.cust_name LIKE CONCAT('%', #{query.custIsn}, '%')
)
</if>
<if test="query != null and query.orgCode != null and query.orgCode != ''">
AND lpw.org_code LIKE CONCAT('%', #{query.orgCode}, '%')
<if test="query != null and query.deptId != null">
AND lpw.dept_id = #{query.deptId}
</if>
<if test="query != null and query.custType != null and query.custType != ''">
AND lpw.cust_type = #{query.custType}
</if>
<if test="query != null and query.guarType != null and query.guarType != ''">
AND lpw.guar_type = #{query.guarType}
</if>
<if test="query != null and query.beginCreateTime != null">
AND lpw.create_time &gt;= #{query.beginCreateTime}
</if>
<if test="query != null and query.endCreateTime != null">
AND lpw.create_time &lt;= #{query.endCreateTime}
</if>
</where>
ORDER BY lpw.update_time DESC
</select>
<update id="updateSensitiveFieldsById">
UPDATE loan_pricing_workflow
<set>
<if test="custName != null">
cust_name = #{custName},
</if>
<if test="idNum != null">
id_num = #{idNum},
</if>
</set>
WHERE id = #{id}
</update>
</mapper>

View File

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

View File

@@ -10,10 +10,12 @@ class LoanPricingWorkflowListVOTest
void shouldExposeCalculateRateAndExecuteRateFields()
{
LoanPricingWorkflowListVO vo = new LoanPricingWorkflowListVO();
vo.setDeptId(100L);
vo.setCustIsn("CUST001");
vo.setCalculateRate("6.15");
vo.setExecuteRate("5.80");
assertEquals(100L, vo.getDeptId());
assertEquals("CUST001", vo.getCustIsn());
assertEquals("6.15", vo.getCalculateRate());
assertEquals("5.80", vo.getExecuteRate());

View File

@@ -1,6 +1,7 @@
package com.ruoyi.loanpricing.mapper;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -18,9 +19,33 @@ class LoanPricingWorkflowMapperXmlTest
assertTrue(xml.contains("WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate"));
assertTrue(xml.contains("WHEN lpw.cust_type = '企业' THEN mc.final_calculate_rate"));
assertTrue(xml.contains("lpw.dept_id AS deptId"));
assertTrue(xml.contains("lpw.create_by = #{query.dataScopeCreateBy}"));
assertTrue(xml.contains("lpw.dept_id IN"));
assertTrue(xml.contains("find_in_set(#{query.dataScopeDeptId}, ancestors)"));
assertTrue(xml.contains("lpw.dept_id = #{query.deptId}"));
assertTrue(xml.contains("SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')"));
assertTrue(xml.contains("lpw.cust_isn LIKE CONCAT('%', #{query.custIsn}, '%')"));
assertTrue(xml.contains("lpw.id_num LIKE CONCAT('%', #{query.custIsn}, '%')"));
assertTrue(xml.contains("lpw.cust_name LIKE CONCAT('%', #{query.custIsn}, '%')"));
assertTrue(xml.contains("lpw.cust_type = #{query.custType}"));
assertTrue(xml.contains("lpw.guar_type = #{query.guarType}"));
assertTrue(xml.contains("lpw.create_time &gt;= #{query.beginCreateTime}"));
assertTrue(xml.contains("lpw.create_time &lt;= #{query.endCreateTime}"));
}
@Test
void shouldUpdateOnlySensitiveFieldsForAnonymousMigration() throws IOException
{
ClassPathResource resource = new ClassPathResource("mapper/loanpricing/LoanPricingWorkflowMapper.xml");
String xml = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
String updateSql = xml.substring(xml.indexOf("<update id=\"updateSensitiveFieldsById\">"),
xml.indexOf("</update>", xml.indexOf("<update id=\"updateSensitiveFieldsById\">")));
assertTrue(updateSql.contains("cust_name = #{custName}"));
assertTrue(updateSql.contains("id_num = #{idNum}"));
assertTrue(updateSql.contains("WHERE id = #{id}"));
assertFalse(updateSql.contains("update_by"));
assertFalse(updateSql.contains("update_time"));
}
}

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.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.argThat;
import static org.mockito.Mockito.verify;
@@ -40,35 +38,32 @@ class LoanPricingModelServicePersonalParamsTest {
@Mock
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Mock
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@InjectMocks
private LoanPricingModelService loanPricingModelService;
@Test
void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof"));
void shouldContainLoanPurposeAndPersonalModelFieldsInCreateDto() throws NoSuchFieldException {
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
}
@Test
void shouldNotMapLoanPurposeAndBizProofFromPersonalDto() {
void shouldMapLoanPurposeAndBizProofFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setLoanPurpose("business");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setBizProof("1");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
assertEquals("business", workflow.getLoanPurpose());
assertEquals("1", workflow.getBizProof());
assertEquals("3", workflow.getLoanTerm());
}
@@ -87,12 +82,14 @@ class LoanPricingModelServicePersonalParamsTest {
workflow.setRunType("1");
workflow.setCustIsn("CUST001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setCustName("张三");
workflow.setIdType("身份证");
workflow.setIdNum("cipher-id");
workflow.setIdNum("110101199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setLoanPurpose("consumer");
workflow.setLoanTerm("3");
workflow.setBizProof("true");
workflow.setLoanLoop("false");
workflow.setCollThirdParty("true");
workflow.setCollType("一类");
@@ -101,8 +98,6 @@ class LoanPricingModelServicePersonalParamsTest {
response.setCalculateRate("6.15");
when(loanPricingWorkflowMapper.selectById(1L)).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(modelService.invokePersonalModel(any())).thenReturn(response);
loanPricingModelService.invokeModelAsync(1L);
@@ -118,9 +113,27 @@ class LoanPricingModelServicePersonalParamsTest {
&& Objects.equals("110101199001011234", dto.getIdNum())
&& Objects.equals("信用", dto.getGuarType())
&& Objects.equals("100000", dto.getApplyAmt())
&& Objects.equals("消费", dto.getLoanPurpose())
&& Objects.equals("3", dto.getLoanTerm())
&& Objects.equals("1", dto.getBizProof())
&& Objects.equals("0", dto.getLoanLoop())
&& Objects.equals("1", dto.getCollThirdParty())
&& 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

@@ -1,7 +1,6 @@
package com.ruoyi.loanpricing.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
@@ -21,7 +20,7 @@ import org.junit.jupiter.api.Test;
class LoanPricingModelServiceTest
{
@Test
void shouldDecryptCustNameAndIdNumBeforeInvokePersonalModel() throws Exception
void shouldPassPlainCustNameAndIdNumBeforeInvokePersonalModel() throws Exception
{
TestContext context = createContext(personalWorkflow(1L));
@@ -30,6 +29,7 @@ class LoanPricingModelServiceTest
assertEquals("张三", context.modelService.personalRequest.getCustName());
assertEquals("110101199001011234", context.modelService.personalRequest.getIdNum());
assertEquals("2.15", context.modelService.personalRequest.getCouponRate());
assertEquals("1", context.modelService.personalRequest.getBizProof());
assertEquals(1, context.modelService.personalCalls.get());
assertEquals(0, context.modelService.corporateCalls.get());
assertEquals(1, context.retailInsertCount.get());
@@ -37,15 +37,16 @@ class LoanPricingModelServiceTest
}
@Test
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow() throws Exception
void shouldOnlyWriteModelOutputIdBackWhenUpdatingWorkflow() throws Exception
{
TestContext context = createContext(personalWorkflow(2L));
context.service.invokeModelAsync(2L);
LoanPricingWorkflow updatedWorkflow = context.updatedWorkflow.get();
assertNotEquals("张三", updatedWorkflow.getCustName());
assertNotEquals("110101199001011234", updatedWorkflow.getIdNum());
assertEquals(2L, updatedWorkflow.getId());
assertEquals(null, updatedWorkflow.getCustName());
assertEquals(null, updatedWorkflow.getIdNum());
}
@Test
@@ -54,8 +55,8 @@ class LoanPricingModelServiceTest
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(3L);
workflow.setCustType("企业");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("测试科技有限公司");
workflow.setIdNum("91110000100000000X");
workflow.setRepayMethod("分期");
workflow.setResCover("true");
workflow.setIsGreenLoan("true");
@@ -86,8 +87,8 @@ class LoanPricingModelServiceTest
workflow.setId(4L);
workflow.setModelOutputId(88L);
workflow.setCustType("企业");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("测试科技有限公司");
workflow.setIdNum("91110000100000000X");
TestContext context = createContext(workflow);
context.service.reinvokeModelAndOverwrite(4L);
@@ -102,9 +103,10 @@ class LoanPricingModelServiceTest
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(id);
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setCouponRate("2.15");
workflow.setBizProof("true");
return workflow;
}
@@ -127,7 +129,6 @@ class LoanPricingModelServiceTest
savingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount, context.retailUpdateCount));
setField(context.service, "modelCorpOutputFieldsMapper",
savingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount, context.corpUpdateCount));
setField(context.service, "sensitiveFieldCryptoService", new TestSensitiveFieldCryptoService());
return context;
}
@@ -228,25 +229,4 @@ class LoanPricingModelServiceTest
}
}
private static class TestSensitiveFieldCryptoService extends SensitiveFieldCryptoService
{
private TestSensitiveFieldCryptoService()
{
super("1234567890abcdef");
}
@Override
public String decrypt(String cipherText)
{
if ("cipher-name".equals(cipherText))
{
return "张三";
}
if ("cipher-id".equals(cipherText))
{
return "110101199001011234";
}
return cipherText;
}
}
}

View File

@@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -45,6 +46,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ExtendWith(MockitoExtension.class)
@@ -81,7 +84,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldEncryptCustNameAndIdNumBeforeInsert()
void shouldKeepCustNameAndIdNumPlainBeforeInsert()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustName("张三");
@@ -89,16 +92,14 @@ class LoanPricingWorkflowServiceImplTest
workflow.setCustIsn("CUST001");
workflow.setBusinessType("新增");
when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name");
when(sensitiveFieldCryptoService.encrypt("110101199001011234")).thenReturn("cipher-id");
loanPricingWorkflowService.createLoanPricing(workflow);
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("cipher-name", entity.getCustName())
&& Objects.equals("cipher-id", entity.getIdNum())
Objects.equals("张三", entity.getCustName())
&& Objects.equals("110101199001011234", entity.getIdNum())
&& Objects.equals(100L, entity.getDeptId())
&& Objects.equals("CUST001", entity.getCustIsn())));
verify(sensitiveFieldCryptoService, never()).encrypt(any());
}
@Test
@@ -121,17 +122,17 @@ class LoanPricingWorkflowServiceImplTest
void shouldReturnPlainCustNameWhenReturningPagedWorkflowList()
{
LoanPricingWorkflowListVO row = new LoanPricingWorkflowListVO();
row.setCustName("cipher-name");
row.setCustName("张三");
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
pageResult.setRecords(Collections.singletonList(row));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(pageResult);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
assertEquals("张三", result.getRecords().get(0).getCustName());
verify(sensitiveFieldCryptoService, never()).decrypt(any());
}
@Test
@@ -229,7 +230,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition()
void shouldUseCustIsnAsQueryConditionInLegacyList()
{
LoanPricingWorkflow query = new LoanPricingWorkflow();
query.setCustIsn("CUST001");
@@ -289,36 +290,67 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldAllowPersonalMortgageOneLineAndRejectOldOther()
void shouldAllowPersonalMortgageFourthCategoryAndRejectOldFirstLine()
{
LoanPricingWorkflow allowedWorkflow = validWorkflow();
allowedWorkflow.setCustType("个人");
allowedWorkflow.setGuarType("抵押");
allowedWorkflow.setCollType("一线");
allowedWorkflow.setCollType("四类");
loanPricingWorkflowService.createLoanPricing(allowedWorkflow);
LoanPricingWorkflow rejectedWorkflow = validWorkflow();
rejectedWorkflow.setCustType("个人");
rejectedWorkflow.setGuarType("抵押");
rejectedWorkflow.setCollType("其他");
rejectedWorkflow.setCollType("一线");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(rejectedWorkflow));
assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", exception.getMessage());
assertEquals("个人抵押抵质押类型必须是:一类、二类、三类、四类之一", exception.getMessage());
}
@Test
void shouldAllowCorporateMortgagePollutionRightAndPledgeEquity()
void shouldAllowCorporateNewMortgageTypesAndRejectOldMortgageTypes()
{
LoanPricingWorkflow mortgageWorkflow = validWorkflow();
mortgageWorkflow.setCustType("企业");
mortgageWorkflow.setGuarType("抵押");
mortgageWorkflow.setCollType("排污权抵押");
LoanPricingWorkflow pollutionRightWorkflow = validWorkflow();
pollutionRightWorkflow.setCustType("企业");
pollutionRightWorkflow.setGuarType("抵押");
pollutionRightWorkflow.setCollType("排污权抵押");
loanPricingWorkflowService.createLoanPricing(mortgageWorkflow);
loanPricingWorkflowService.createLoanPricing(pollutionRightWorkflow);
LoanPricingWorkflow otherRealEstateWorkflow = validWorkflow();
otherRealEstateWorkflow.setCustType("企业");
otherRealEstateWorkflow.setGuarType("抵押");
otherRealEstateWorkflow.setCollType("其他不动产抵押");
loanPricingWorkflowService.createLoanPricing(otherRealEstateWorkflow);
LoanPricingWorkflow oldFourthCategoryWorkflow = validWorkflow();
oldFourthCategoryWorkflow.setCustType("企业");
oldFourthCategoryWorkflow.setGuarType("抵押");
oldFourthCategoryWorkflow.setCollType("四类");
ServiceException fourthCategoryException = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(oldFourthCategoryWorkflow));
LoanPricingWorkflow oldOtherRealEstateWorkflow = validWorkflow();
oldOtherRealEstateWorkflow.setCustType("企业");
oldOtherRealEstateWorkflow.setGuarType("抵押");
oldOtherRealEstateWorkflow.setCollType("设备等其他不动产抵押");
ServiceException otherRealEstateException = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(oldOtherRealEstateWorkflow));
String expectedMessage = "企业抵押抵质押类型必须是:一类、二类、三类、排污权抵押、其他不动产抵押之一";
assertEquals(expectedMessage, fourthCategoryException.getMessage());
assertEquals(expectedMessage, otherRealEstateException.getMessage());
}
@Test
void shouldKeepCorporatePledgeTypesUnchanged()
{
LoanPricingWorkflow pledgeWorkflow = validWorkflow();
pledgeWorkflow.setCustType("企业");
pledgeWorkflow.setGuarType("质押");
@@ -338,7 +370,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldCreatePersonalWorkflowWithCouponRateAndWithoutRemovedFields()
void shouldCreatePersonalWorkflowWithCouponRateAndBizProof()
{
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
@@ -348,6 +380,7 @@ class LoanPricingWorkflowServiceImplTest
dto.setLoanTerm("3");
dto.setCollType("存单质押");
dto.setCouponRate("2.15");
dto.setBizProof("1");
loanPricingWorkflowService.createPersonalLoanPricing(dto);
@@ -355,7 +388,7 @@ class LoanPricingWorkflowServiceImplTest
Objects.equals("个人", entity.getCustType())
&& Objects.equals("2.15", entity.getCouponRate())
&& Objects.isNull(entity.getLoanPurpose())
&& Objects.isNull(entity.getBizProof())));
&& Objects.equals("1", entity.getBizProof())));
}
@Test
@@ -380,21 +413,62 @@ class LoanPricingWorkflowServiceImplTest
&& Objects.equals("2.35", entity.getCouponRate())));
}
@Test
void shouldCreatePersonalWorkflowWithMortgageSourceFields()
{
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setGuarType("抵押");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setCollateralNature("普通住宅、土地、厂房");
dto.setIsNineArea("");
dto.setCollType("一类");
loanPricingWorkflowService.createPersonalLoanPricing(dto);
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("普通住宅、土地、厂房", entity.getCollateralNature())
&& Objects.equals("", entity.getIsNineArea())
&& Objects.equals("一类", entity.getCollType())));
}
@Test
void shouldCreateCorporateWorkflowWithMortgageSourceFields()
{
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("CORP001");
dto.setGuarType("抵押");
dto.setApplyAmt("1000000");
dto.setBusinessType("新增");
dto.setLoanTerm("5");
dto.setCollateralNature("商业性质不动产");
dto.setIsNineArea("");
dto.setCollType("三类");
loanPricingWorkflowService.createCorporateLoanPricing(dto);
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("商业性质不动产", entity.getCollateralNature())
&& Objects.equals("", entity.getIsNineArea())
&& Objects.equals("三类", entity.getCollType())));
}
@Test
void shouldReturnEditableWorkflowWithPlainSensitiveFieldsForCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
LoanPricingWorkflow result = loanPricingWorkflowService.selectEditableLoanPricingBySerialNum("P20260525001");
assertEquals("张三", result.getCustName());
assertEquals("110101199001011234", result.getIdNum());
verify(sensitiveFieldCryptoService, never()).decrypt(any());
}
@Test
@@ -414,27 +488,42 @@ class LoanPricingWorkflowServiceImplTest
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name-new");
when(sensitiveFieldCryptoService.encrypt("110101199001019999")).thenReturn("cipher-id-new");
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001019999");
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setIdType("身份证");
dto.setIdNum("110101199001019999");
dto.setGuarType("信用");
dto.setGuarType("抵押");
dto.setApplyAmt("200000");
dto.setLoanPurpose("business");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setLoanLoop("1");
dto.setBizProof("1");
dto.setCollateralNature("普通住宅、土地、厂房");
dto.setIsNineArea("");
dto.setCollType("一类");
loanPricingWorkflowService.updatePersonalLoanPricing("P20260525001", dto);
ArgumentCaptor<LambdaUpdateWrapper> updateWrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture());
assertTrue(updateWrapperCaptor.getValue().getSqlSet().contains("apply_amt"));
String sqlSet = updateWrapperCaptor.getValue().getSqlSet();
assertTrue(sqlSet.contains("apply_amt"));
assertTrue(sqlSet.contains("loan_purpose"));
assertTrue(sqlSet.contains("cust_name"));
assertTrue(sqlSet.contains("id_num"));
assertTrue(sqlSet.contains("biz_proof"));
assertTrue(sqlSet.contains("collateral_nature"));
assertTrue(sqlSet.contains("is_nine_area"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("张三"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("110101199001019999"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("普通住宅、土地、厂房"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("business"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue(""));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("1"));
verify(sensitiveFieldCryptoService, never()).encrypt(any());
verify(loanPricingModelService).reinvokeModelAndOverwrite(101L);
}
@@ -443,10 +532,6 @@ class LoanPricingWorkflowServiceImplTest
{
LoanPricingWorkflow workflow = editableWorkflow("企业", "若依-admin");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.encrypt("测试科技有限公司")).thenReturn("cipher-corp-name");
when(sensitiveFieldCryptoService.encrypt("91110000100000000X")).thenReturn("cipher-corp-id");
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试科技有限公司");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91110000100000000X");
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("CORP001");
@@ -473,6 +558,9 @@ class LoanPricingWorkflowServiceImplTest
assertTrue(sqlSet.contains("is_green_loan"), sqlSet);
assertTrue(sqlSet.contains("is_trade_construction"), sqlSet);
assertTrue(sqlSet.contains("coll_third_party"), sqlSet);
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("测试科技有限公司"));
assertTrue(updateWrapperCaptor.getValue().getParamNameValuePairs().containsValue("91110000100000000X"));
verify(sensitiveFieldCryptoService, never()).encrypt(any());
verify(loanPricingModelService).reinvokeModelAndOverwrite(101L);
}
@@ -543,17 +631,16 @@ class LoanPricingWorkflowServiceImplTest
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
assertEquals("张三", result.getLoanPricingWorkflow().getCustName());
assertEquals("110101199001011234", result.getLoanPricingWorkflow().getIdNum());
verify(sensitiveFieldCryptoService, never()).decrypt(any());
}
@Test
@@ -562,8 +649,8 @@ class LoanPricingWorkflowServiceImplTest
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setModelOutputId(11L);
ModelRetailOutputFields retailOutputFields = new ModelRetailOutputFields();
@@ -573,8 +660,6 @@ class LoanPricingWorkflowServiceImplTest
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelRetailOutputFieldsMapper.selectById(11L)).thenReturn(retailOutputFields);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
@@ -611,8 +696,8 @@ class LoanPricingWorkflowServiceImplTest
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("C20260328001");
workflow.setCustType("企业");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCustName("测试科技有限公司");
workflow.setIdNum("91110000100000000X");
workflow.setModelOutputId(22L);
ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields();
@@ -622,8 +707,6 @@ class LoanPricingWorkflowServiceImplTest
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelCorpOutputFieldsMapper.selectById(22L)).thenReturn(corpOutputFields);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试科技有限公司");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91110000100000000X");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001");
@@ -631,6 +714,74 @@ class LoanPricingWorkflowServiceImplTest
assertEquals("91110000100000000X", result.getModelCorpOutputFields().getIdNum());
}
@Test
void shouldDecryptEncryptedWorkflowFieldsAndSkipPlainFields()
{
LoanPricingWorkflow encrypted = new LoanPricingWorkflow();
encrypted.setId(1L);
encrypted.setCustName("cipher-name");
encrypted.setIdNum("cipher-id");
LoanPricingWorkflow plain = new LoanPricingWorkflow();
plain.setId(2L);
plain.setCustName("张三");
plain.setIdNum("");
when(loanPricingWorkflowMapper.selectList(any())).thenReturn(List.of(encrypted, plain));
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(sensitiveFieldCryptoService.decrypt("张三")).thenThrow(new ServiceException("贷款定价敏感字段解密失败"));
Map<String, Long> result = loanPricingWorkflowService.decryptWorkflowSensitiveFields();
assertEquals(2L, result.get("processed"));
assertEquals(1L, result.get("updated"));
assertEquals(1L, result.get("skipped"));
assertEquals(0L, result.get("failed"));
verify(loanPricingWorkflowMapper).updateSensitiveFieldsById(1L, "张三", "110101199001011234");
verify(loanPricingWorkflowMapper, never()).updateById(any(LoanPricingWorkflow.class));
}
@Test
void shouldUpdateOnlySuccessfullyDecryptedWorkflowField()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(3L);
workflow.setCustName("cipher-name");
workflow.setIdNum("plain-id");
when(loanPricingWorkflowMapper.selectList(any())).thenReturn(List.of(workflow));
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("plain-id")).thenThrow(new ServiceException("贷款定价敏感字段解密失败"));
Map<String, Long> result = loanPricingWorkflowService.decryptWorkflowSensitiveFields();
assertEquals(1L, result.get("processed"));
assertEquals(1L, result.get("updated"));
assertEquals(0L, result.get("skipped"));
assertEquals(0L, result.get("failed"));
verify(loanPricingWorkflowMapper).updateSensitiveFieldsById(3L, "张三", null);
verify(loanPricingWorkflowMapper, never()).updateById(any(LoanPricingWorkflow.class));
}
@Test
void shouldCountDatabaseUpdateExceptionAsMigrationFailure()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(4L);
workflow.setCustName("cipher-name");
when(loanPricingWorkflowMapper.selectList(any())).thenReturn(List.of(workflow));
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(loanPricingWorkflowMapper.updateSensitiveFieldsById(4L, "张三", null)).thenThrow(new RuntimeException("db error"));
Map<String, Long> result = loanPricingWorkflowService.decryptWorkflowSensitiveFields();
assertEquals(1L, result.get("processed"));
assertEquals(0L, result.get("updated"));
assertEquals(0L, result.get("skipped"));
assertEquals(1L, result.get("failed"));
}
private LoanPricingWorkflow validWorkflow()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
@@ -650,9 +801,9 @@ class LoanPricingWorkflowServiceImplTest
workflow.setModelOutputId(201L);
workflow.setCustIsn("CUST001");
workflow.setCustType(custType);
workflow.setCustName("cipher-name");
workflow.setCustName("张三");
workflow.setIdType("身份证");
workflow.setIdNum("cipher-id");
workflow.setIdNum("110101199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新增");

View File

@@ -100,7 +100,33 @@
<!-- 抵质押信息 -->
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
<el-row v-if="isCollateralGuarantee">
<el-col :span="12">
<template v-if="isMortgage">
<el-col :span="12">
<el-form-item label="抵押物性质" prop="collateralNature">
<el-select v-model="form.collateralNature" placeholder="请选择抵押物性质" style="width: 100%">
<el-option v-for="item in collateralNatureOptions" :key="item" :label="item" :value="item"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否属于九地" prop="isNineArea">
<div class="nine-area-control">
<el-select
v-model="form.isNineArea"
:disabled="!requiresNineArea"
:placeholder="requiresNineArea ? '请选择是否属于九地' : '无需选择'"
>
<el-option label="是" value="是"/>
<el-option label="否" value="否"/>
</el-select>
<el-tooltip content="九地是指绍兴、杭州、宁波、上海、北京、广州、深圳、南京、苏州" placement="top">
<i class="el-icon-question nine-area-help" tabindex="0" aria-label="查看九地说明"></i>
</el-tooltip>
</div>
</el-form-item>
</el-col>
</template>
<el-col :span="12" v-if="isPledge">
<el-form-item label="抵质押类型" prop="collType">
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
@@ -181,6 +207,30 @@ export default {
callback()
}
const validateCollateralType = (rule, value, callback) => {
if (this.isPledge && !value) {
callback(new Error('请选择抵质押类型'))
return
}
callback()
}
const validateCollateralNature = (rule, value, callback) => {
if (this.isMortgage && !value) {
callback(new Error('请选择抵押物性质'))
return
}
callback()
}
const validateNineArea = (rule, value, callback) => {
if (this.requiresNineArea && !value) {
callback(new Error('请选择是否属于九地'))
return
}
callback()
}
// 贷款期限验证
const validateLoanTerm = (rule, value, callback) => {
if (!value && value !== 0) {
@@ -221,6 +271,8 @@ export default {
isGreenLoan: false,
isTradeBuildEnt: false,
collType: undefined,
collateralNature: undefined,
isNineArea: undefined,
collThirdParty: false,
couponRate: undefined
},
@@ -255,7 +307,13 @@ export default {
{required: true, message: "请选择历史贷款合同", trigger: "change"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
{validator: validateCollateralType, trigger: "change"}
],
collateralNature: [
{validator: validateCollateralNature, trigger: "change"}
],
isNineArea: [
{validator: validateNineArea, trigger: "change"}
],
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
@@ -275,6 +333,15 @@ export default {
isCollateralGuarantee() {
return this.form.guarType === '抵押' || this.form.guarType === '质押'
},
isMortgage() {
return this.form.guarType === '抵押'
},
isPledge() {
return this.form.guarType === '质押'
},
requiresNineArea() {
return this.isMortgage && ['普通住宅、土地、厂房', '商业性质不动产'].includes(this.form.collateralNature)
},
isStockTransfer() {
return this.form.businessType === '存量转贷'
},
@@ -288,13 +355,13 @@ export default {
return this.isEdit ? '编辑企业利率定价流程' : '新增企业利率定价流程'
},
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']
}
if (this.form.guarType === '质押') {
if (this.isPledge) {
return ['存单质押', '股权质押', '其他质押']
}
return []
},
collateralNatureOptions() {
return ['普通住宅、土地、厂房', '商业性质不动产', '排污权', '其他不动产']
}
},
watch: {
@@ -316,6 +383,12 @@ export default {
return
}
this.resetCouponRateIfNotCertificatePledge()
},
'form.collateralNature'(val, oldVal) {
if (this.resettingForm || val === oldVal) {
return
}
this.handleCollateralNatureChange()
}
},
methods: {
@@ -353,7 +426,9 @@ export default {
resCover: this.isOne(this.editData.resCover),
isGreenLoan: this.isOne(this.editData.isGreenLoan),
isTradeBuildEnt: this.isOne(this.editData.isTradeBuildEnt),
collType: this.editData.collType,
collType: this.editData.guarType === '质押' ? this.editData.collType : undefined,
collateralNature: this.editData.collateralNature,
isNineArea: this.editData.isNineArea,
collThirdParty: this.isOne(this.editData.collThirdParty),
couponRate: this.editData.couponRate
}
@@ -374,6 +449,8 @@ export default {
isGreenLoan: false,
isTradeBuildEnt: false,
collType: undefined,
collateralNature: undefined,
isNineArea: undefined,
collThirdParty: false,
couponRate: undefined
}
@@ -393,14 +470,38 @@ export default {
/** 清空抵质押字段 */
resetCollateralFields() {
this.form.collType = undefined
this.form.collateralNature = undefined
this.form.isNineArea = undefined
this.form.collThirdParty = false
this.resetCouponRateIfNotCertificatePledge()
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate'])
this.$refs.form.clearValidate(['collType', 'collateralNature', 'isNineArea', 'collThirdParty', 'couponRate'])
}
})
},
handleCollateralNatureChange() {
this.form.isNineArea = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['isNineArea'])
}
})
},
resolveMortgageCollateralType() {
if (this.form.collateralNature === '普通住宅、土地、厂房') {
return this.form.isNineArea === '是' ? '一类' : '二类'
}
if (this.form.collateralNature === '商业性质不动产') {
return this.form.isNineArea === '是' ? '二类' : '三类'
}
if (this.form.collateralNature === '排污权') {
return '排污权抵押'
}
if (this.form.collateralNature === '其他不动产') {
return '其他不动产抵押'
}
},
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
@@ -472,6 +573,12 @@ export default {
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
}
if (this.isMortgage) {
data.collType = this.resolveMortgageCollateralType()
} else {
delete data.collateralNature
delete data.isNineArea
}
if (this.isCollateralGuarantee) {
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
} else {
@@ -512,6 +619,24 @@ export default {
font-size: 14px;
}
.nine-area-control {
display: flex;
align-items: center;
gap: 8px;
}
.nine-area-control .el-select {
flex: 1;
min-width: 0;
}
.nine-area-help {
flex: 0 0 auto;
color: #909399;
cursor: help;
font-size: 16px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;

View File

@@ -13,14 +13,10 @@
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="最终测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
<el-descriptions-item label="期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="担保方式">{{ detailData.guarType || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">
<span class="rate-value">{{ formatRate(corpOutput && corpOutput.minRateProduct) }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
@@ -46,13 +42,6 @@
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="null"
:corp-output="corpOutput"
/>
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
@@ -64,11 +53,10 @@
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</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.idNum }}</el-descriptions-item>
<el-descriptions-item label="贷款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions>
@@ -81,10 +69,17 @@
<el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ formatRate(detailData.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="余值覆盖">{{ formatBoolean(detailData.resCover) }}</el-descriptions-item>
<el-descriptions-item label="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="贸易和建筑业企业">{{ formatBoolean(detailData.isTradeBuildEnt) }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.guarType === '抵押'" label="抵押物性质">
{{ detailData.collateralNature || '-' }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.guarType === '抵押'" label="是否属于九地">
{{ detailData.isNineArea || '-' }}
</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{
formatBoolean(detailData.collThirdParty)
@@ -94,6 +89,14 @@
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:guar-type="detailData.guarType"
:retail-output="null"
:corp-output="corpOutput"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
@@ -155,18 +158,6 @@ export default {
if (value === 'false' || value === false || value === '0' || value === 0) return '否'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.formatRate(this.corpOutput?.baseLoanRate)
},
/** 获取浮动BP */
getTotalBp() {
return this.corpOutput?.totalBp || '-'
},
/** 获取最终测算利率 */
getCalculateRate() {
return this.formatRate(this.corpOutput?.finalCalculateRate)
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
@@ -248,16 +239,6 @@ export default {
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}

View File

@@ -3,7 +3,8 @@
:close-on-click-modal="false" @close="handleClose">
<el-form :model="queryForm" inline size="small">
<el-form-item label="客户号">
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable
@keydown.enter.native.prevent="handleQuery"/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>

View File

@@ -6,203 +6,176 @@
<div class="output-sections">
<!-- 个人客户模型输出 -->
<template v-if="custType === '个人' && retailOutput">
<div class="output-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户内码">{{ retailOutput.custIsn || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ retailOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ retailOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ retailOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(retailOutput.baseLoanRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="灰黑名单客户">{{ formatOutputValue(retailOutput.greyBlackCust) }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">测算结果</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(retailOutput.baseLoanRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ retailOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ formatRate(retailOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="模型测算利率"><span class="calculate-rate">{{ formatRate(retailOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ formatRate(retailOutput.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ formatRate(retailOutput.minRateProduct) }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ formatRate(retailOutput.smoothRange) }}</el-descriptions-item>
<el-descriptions-item label="平滑利率"><span class="calculate-rate">{{ formatRate(retailOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="行社下调BP">{{ formatOutputValue(retailOutput.branchBp) }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ formatRate(retailOutput.referenceRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ formatRate(retailOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">忠诚度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(retailOutput.isFirstLoan) }}</el-descriptions-item>
<el-descriptions-item label="用信天数">{{ retailOutput.faithDay || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户年龄">{{ retailOutput.custAge || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_首贷"><span class="bp-value">{{ retailOutput.bpFirstLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_贷龄"><span class="bp-value">{{ retailOutput.bpAgeLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_年龄"><span class="bp-value">{{ retailOutput.bpAge || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ retailOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in retailLoyaltyRows" :key="`retail-loyalty-${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 :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">贡献度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ retailOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ retailOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ formatRate(retailOutput.derivationRate) }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ retailOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<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.bpLabel" :class="row.total ? 'total-bp-value' : 'bp-value'">
{{ row.bpLabel }}{{ row.bpValue }}
<el-tooltip v-if="row.bpTooltip" :content="row.bpTooltip" placement="top">
<i class="el-icon-question analysis-help" tabindex="0" :aria-label="row.bpTooltipAriaLabel"></i>
</el-tooltip>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">关联度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="中间业务_个人_信用卡">{{ formatBoolean(retailOutput.midPerCard) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_一码通">{{ formatBoolean(retailOutput.midPerPass) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_丰收互联">{{ formatBoolean(retailOutput.midPerHarvest) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_有效客户">{{ formatBoolean(retailOutput.midPerEffect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_快捷支付">{{ formatBoolean(retailOutput.midPerQuickPay) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_电费代扣">{{ formatBoolean(retailOutput.midPerEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_水费代扣">{{ formatBoolean(retailOutput.midPerWaterDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_华数费代扣">{{ formatBoolean(retailOutput.midPerHuashuDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_煤气费代扣">{{ formatBoolean(retailOutput.MidPerGasDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_市民卡">{{ formatBoolean(retailOutput.midPerCitizencard) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_理财业务">{{ formatBoolean(retailOutput.midPerFinMan) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_etc">{{ formatBoolean(retailOutput.midPerEtc) }}</el-descriptions-item>
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ retailOutput.bpMid || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ retailOutput.totalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">贷款特征</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="申请金额">{{ retailOutput.applyAmt || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ retailOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="贷款用途">{{ formatLoanPurpose(retailOutput.loanPurpose) }}</el-descriptions-item>
<el-descriptions-item label="是否有经营佐证">{{ formatBoolean(retailOutput.bizProof) }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款用途"><span class="bp-value">{{ retailOutput.bpLoanUse || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="循环功能">{{ formatBoolean(retailOutput.loanLoop) }}</el-descriptions-item>
<el-descriptions-item label="BP_循环功能"><span class="bp-value">{{ retailOutput.bpLoanLoop || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ retailOutput.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(retailOutput.collThirdParty) }}</el-descriptions-item>
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ retailOutput.bpCollateral || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<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 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 }}
</template>{{ row.bpValue }}
<el-tooltip v-if="row.bpTooltip" :content="row.bpTooltip" placement="top">
<i class="el-icon-question analysis-help" tabindex="0" :aria-label="row.bpTooltipAriaLabel"></i>
</el-tooltip>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">风险度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="灰名单客户">{{ formatBoolean(retailOutput.greyCust) }}</el-descriptions-item>
<el-descriptions-item label="本金逾期">{{ formatBoolean(retailOutput.prinOverdue) }}</el-descriptions-item>
<el-descriptions-item label="利息逾期">{{ formatBoolean(retailOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="信用卡逾期">{{ formatBoolean(retailOutput.cardOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ retailOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ retailOutput.totalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in retailRiskRows" :key="`retail-risk-${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 :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr>
</tbody>
</table>
</div>
</template>
<!-- 企业客户模型输出 -->
<template v-else-if="custType === '企业' && corpOutput">
<div class="output-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户内码">{{ corpOutput.custIsn || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ corpOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ corpOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ corpOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(corpOutput.baseLoanRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">测算结果</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(corpOutput.baseLoanRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ corpOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ formatRate(corpOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="模型测算利率"><span class="calculate-rate">{{ formatRate(corpOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ formatRate(corpOutput.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ formatRate(corpOutput.minRateProduct) }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ formatRate(corpOutput.smoothRange) }}</el-descriptions-item>
<el-descriptions-item label="平滑利率"><span class="calculate-rate">{{ formatRate(corpOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="行社下调BP">{{ formatOutputValue(corpOutput.branchBp) }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ formatRate(corpOutput.referenceRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ formatRate(corpOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">忠诚度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(corpOutput.isFirstLoan) }}</el-descriptions-item>
<el-descriptions-item label="用信天数">{{ corpOutput.faithDay || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_首贷"><span class="bp-value">{{ corpOutput.bpFirstLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_贷龄"><span class="bp-value">{{ corpOutput.bpAgeLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ corpOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in corporateLoyaltyRows" :key="`corporate-loyalty-${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 :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">贡献度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ corpOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ corpOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ formatRate(corpOutput.derivationRate) }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ corpOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in corporateContributionRows" :key="`corporate-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.bpLabel" :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">关联度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="中间业务_企业_企业互联">{{ formatBoolean(corpOutput.midEntConnect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_有效价值客户">{{ formatBoolean(corpOutput.midEntEffect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_国际业务">{{ formatBoolean(corpOutput.midEntInter) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_承兑">{{ formatBoolean(corpOutput.midEntAccept) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_贴现">{{ formatBoolean(corpOutput.midEntDiscount) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_电费代扣">{{ formatBoolean(corpOutput.midEntEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_水费代扣">{{ formatBoolean(corpOutput.midEntWaterDdc) }}</el-descriptions-item>
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ corpOutput.bpMid || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="代发工资户数">{{ corpOutput.payroll || '-' }}</el-descriptions-item>
<el-descriptions-item label="存量贷款余额">{{ corpOutput.invLoanAmount || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_代发工资"><span class="bp-value">{{ corpOutput.bpPayroll || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ corpOutput.totalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">企业类别</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="净身企业">{{ formatBoolean(corpOutput.isCleanEnt) }}</el-descriptions-item>
<el-descriptions-item label="开立基本结算账户">{{ formatBoolean(corpOutput.hasSettleAcct) }}</el-descriptions-item>
<el-descriptions-item label="绿色贷款">{{ formatBoolean(corpOutput.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="科技型企业">{{ formatBoolean(corpOutput.isTechEnt) }}</el-descriptions-item>
<el-descriptions-item label="贸易和建筑业企业">{{ formatBoolean(corpOutput.isTradeBuildEnt) }}</el-descriptions-item>
<el-descriptions-item label="BP_企业客户类别"><span class="bp-value">{{ corpOutput.bpEntType || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
<div class="output-section">
<h4 class="section-title">贷款特征</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="借款期限">{{ corpOutput.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款期限"><span class="bp-value">{{ corpOutput.bpLoanTerm || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="申请金额">{{ corpOutput.applyAmt || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ corpOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ corpOutput.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(corpOutput.collThirdParty) }}</el-descriptions-item>
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ corpOutput.bpCollateral || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in corporateRelevanceRows" :key="`corporate-relevance-${index}`" :class="{ 'is-total-row': row.total }">
<td><span v-if="row.summary">汇总</span><template v-else><div v-if="row.category" class="result-category">{{ row.category }}</div><div v-for="item in row.results" :key="item.label" class="result-item"><span class="result-label">{{ item.label }}</span><span v-if="item.showValue !== false" class="result-value">{{ item.value }}</span></div></template></td>
<td>
<span :class="row.total ? 'total-bp-value' : 'bp-value'">
<template v-if="row.bpLabel">{{ row.bpLabel }}</template>{{ row.bpValue }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="output-section">
<h4 class="section-title">风险度分析</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="灰名单客户">{{ formatBoolean(corpOutput.greyCust) }}</el-descriptions-item>
<el-descriptions-item label="本金逾期">{{ formatBoolean(corpOutput.prinOverdue) }}</el-descriptions-item>
<el-descriptions-item label="利息逾期">{{ formatBoolean(corpOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ corpOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ corpOutput.totalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
<table class="output-analysis-table">
<colgroup><col class="result-column"><col class="bp-column"></colgroup>
<thead><tr><th>模型输出结果</th><th>影响BP</th></tr></thead>
<tbody>
<tr v-for="(row, index) in corporateRiskRows" :key="`corporate-risk-${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 :class="row.total ? 'total-bp-value' : 'bp-value'">{{ row.bpLabel }}{{ row.bpValue }}</span></td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
</el-card>
@@ -225,10 +198,156 @@ export default {
corpOutput: {
type: Object,
default: null
},
guarType: {
type: String,
default: null
}
},
computed: {
retailLoyaltyRows() {
return [
this.analysisRow('我行首贷客户', this.formatBoolean(this.retailOutput.isFirstLoan), 'BP_首贷', this.retailOutput.bpFirstLoan),
this.analysisRow('用信天数', this.retailOutput.faithDay, 'BP_贷龄', this.retailOutput.bpAgeLoan),
this.analysisRow('客户年龄', this.retailOutput.custAge, 'BP_年龄', this.retailOutput.bpAge),
this.totalRow('TOTAL_BP_忠诚度', this.retailOutput.totalBpLoyalty)
]
},
retailContributionRows() {
return [
this.groupedAnalysisRow([
this.resultItem('存款年日均', this.retailOutput.balanceAvg),
this.resultItem('贷款年日均', this.retailOutput.loanAvg),
this.resultItem('派生率', this.formatRate(this.retailOutput.derivationRate))
]),
this.totalRow('TOTAL_BP_贡献度', this.retailOutput.totalBpContribution, '最高上限100BP', '查看贡献度汇总上限')
]
},
retailRelevanceRows() {
return [
this.middleBusinessRow('中间业务_个人_信用卡', this.retailOutput.midPerCard, '办理减5个BP信用卡额度1万元及以上的再减10个BP'),
this.middleBusinessRow('中间业务_个人_一码通', this.retailOutput.midPerPass),
this.middleBusinessRow('中间业务_个人_丰收互联', this.retailOutput.midPerHarvest),
this.middleBusinessRow('中间业务_个人_有效客户', this.retailOutput.midPerEffect),
this.middleBusinessRow('中间业务_个人_快捷支付', this.retailOutput.midPerQuickPay),
this.middleBusinessRow('中间业务_个人_电费代扣', this.retailOutput.midPerEleDdc),
this.middleBusinessRow('中间业务_个人_水费代扣', this.retailOutput.midPerWaterDdc),
this.middleBusinessRow('中间业务_个人_华数费代扣', this.retailOutput.midPerHuashuDdc),
this.middleBusinessRow('中间业务_个人_煤气费代扣', this.retailOutput.midPerGasDdc),
this.middleBusinessRow('中间业务_个人_市民卡', this.retailOutput.midPerCitizencard),
this.middleBusinessRow('中间业务_个人_理财业务', this.retailOutput.midPerFinMan),
this.middleBusinessRow('中间业务_个人_etc', this.retailOutput.midPerEtc),
this.totalRow('TOTAL_BP_关联度', this.retailOutput.totalBpRelevance, '最高上限-30BP', '查看中间业务BP上限')
]
},
retailRiskRows() {
const rows = [
this.analysisRow('申请金额', this.retailOutput.applyAmt, 'BP_贷款额度', this.retailOutput.bpLoanAmount),
this.groupedAnalysisRow([
this.resultItem('贷款用途', this.formatLoanPurpose(this.retailOutput.loanPurpose)),
this.resultItem('营业执照', this.formatBoolean(this.retailOutput.bizProof))
], 'BP_贷款用途', this.retailOutput.bpLoanUse),
this.analysisRow('循环功能', this.formatBoolean(this.retailOutput.loanLoop), 'BP_循环功能', this.retailOutput.bpLoanLoop)
]
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() {
return [
this.analysisRow('我行首贷客户', this.formatBoolean(this.corpOutput.isFirstLoan), 'BP_首贷', this.corpOutput.bpFirstLoan),
this.analysisRow('用信天数', this.corpOutput.faithDay, 'BP_贷龄', this.corpOutput.bpAgeLoan),
this.totalRow('TOTAL_BP_忠诚度', this.corpOutput.totalBpLoyalty)
]
},
corporateContributionRows() {
return [
this.groupedAnalysisRow([
this.resultItem('存款年日均', this.corpOutput.balanceAvg),
this.resultItem('贷款年日均', this.corpOutput.loanAvg),
this.resultItem('派生率', this.formatRate(this.corpOutput.derivationRate))
]),
this.totalRow('TOTAL_BP_贡献度', this.corpOutput.totalBpContribution)
]
},
corporateRelevanceRows() {
return [
this.middleBusinessRow('中间业务_企业_企业互联', this.corpOutput.midEntConnect),
this.middleBusinessRow('中间业务_企业_有效价值客户', this.corpOutput.midEntEffect),
this.middleBusinessRow('中间业务_企业_国际业务', this.corpOutput.midEntInter),
this.middleBusinessRow('中间业务_企业_承兑', this.corpOutput.midEntAccept),
this.middleBusinessRow('中间业务_企业_贴现', this.corpOutput.midEntDiscount),
this.middleBusinessRow('中间业务_企业_电费代扣', this.corpOutput.midEntEleDdc),
this.middleBusinessRow('中间业务_企业_水费代扣', this.corpOutput.midEntWaterDdc),
this.groupedAnalysisRow([
this.resultItem('代发工资户数', this.corpOutput.payroll),
this.resultItem('存量贷款余额', this.corpOutput.invLoanAmount)
], 'BP_代发工资', this.corpOutput.bpPayroll, false, '代发工资'),
this.groupedAnalysisRow([
this.resultItem('净身企业', this.formatBoolean(this.corpOutput.isCleanEnt)),
this.resultItem('开立基本结算账户', this.formatBoolean(this.corpOutput.hasSettleAcct)),
this.resultItem('绿色贷款', this.formatBoolean(this.corpOutput.isGreenLoan)),
this.resultItem('科技型企业', this.formatBoolean(this.corpOutput.isTechEnt)),
this.resultItem('贸易和建筑业企业', this.formatBoolean(this.corpOutput.isTradeBuildEnt))
], 'BP_企业客户类别', this.corpOutput.bpEntType, false, '客户类型'),
this.totalRow('TOTAL_BP_关联度', this.corpOutput.totalBpRelevance)
]
},
corporateRiskRows() {
const rows = [
this.analysisRow('借款期限', this.corpOutput.loanTerm, 'BP_贷款期限', this.corpOutput.bpLoanTerm),
this.analysisRow('申请金额', this.corpOutput.applyAmt, 'BP_贷款额度', this.corpOutput.bpLoanAmount)
]
if (this.guarType === '抵押' || this.guarType === '质押') {
rows.push(this.groupedAnalysisRow([
this.resultItem('抵质押类型', this.corpOutput.collType),
this.resultItem('抵质押物三方所有', this.formatBoolean(this.corpOutput.collThirdParty))
], 'BP_抵押物', this.corpOutput.bpCollateral))
}
rows.push(
this.groupedAnalysisRow([
this.resultItem('灰名单客户', this.formatBoolean(this.corpOutput.greyCust)),
this.resultItem('黑名单客户', this.formatBoolean(this.corpOutput.blackCust))
], 'BP_灰名单与逾期', this.corpOutput.bpGreyOverdue),
this.analysisRow('本金逾期', this.formatBoolean(this.corpOutput.prinOverdue), 'BP_本金逾期', this.corpOutput.bpPrinOverdue),
this.analysisRow('利息逾期', this.corpOutput.interestOverdue, 'BP_利息逾期', this.corpOutput.bpIinterestOverdue),
this.analysisRow('信用卡逾期', this.formatBoolean(this.corpOutput.cardOverdue), 'BP_信用卡逾期', this.corpOutput.bpCardOverdue),
this.totalRow('TOTAL_BP_风险度', this.corpOutput.totalBpRisk)
)
return rows
}
},
methods: {
formatRate,
resultItem(label, value, showValue = true) {
return {label, value: this.formatOutputValue(value), showValue}
},
groupedAnalysisRow(results, bpLabel, bpValue, total = false, category = null) {
return {results, bpLabel, bpValue: this.formatOutputValue(bpValue), total, category}
},
analysisRow(resultLabel, resultValue, bpLabel, bpValue) {
return this.groupedAnalysisRow([this.resultItem(resultLabel, resultValue)], bpLabel, bpValue)
},
middleBusinessRow(resultLabel, bpValue, tooltip = null) {
return {
results: [this.resultItem(resultLabel, null, false)],
bpLabel: null,
bpValue: this.formatOutputValue(bpValue),
total: false,
bpTooltip: tooltip,
bpTooltipAriaLabel: tooltip ? `${resultLabel}提示` : null
}
},
totalRow(bpLabel, bpValue, bpTooltip = null, bpTooltipAriaLabel = null) {
return {summary: true, bpLabel, bpValue: this.formatOutputValue(bpValue), total: true, bpTooltip, bpTooltipAriaLabel}
},
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true || value === '1' || value === 1) return '是'
@@ -283,6 +402,98 @@ export default {
color: #303133;
}
.analysis-help {
margin-left: 4px;
color: #909399;
cursor: help;
font-size: 14px;
vertical-align: middle;
}
.output-analysis-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #ebeef5;
color: #606266;
font-size: 13px;
.result-column {
width: 60%;
}
.bp-column {
width: 40%;
}
th,
td {
padding: 10px 12px;
border-right: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
text-align: left;
vertical-align: top;
overflow-wrap: anywhere;
word-break: break-word;
}
th:last-child,
td:last-child {
border-right: 0;
}
tbody tr:last-child td {
border-bottom: 0;
}
th {
background: #f5f7fa;
color: #909399;
font-weight: 500;
}
.result-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 4px 12px;
}
.result-item + .result-item {
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed #ebeef5;
}
.result-category {
margin-bottom: 8px;
color: #303133;
font-weight: 600;
}
.result-label {
min-width: 0;
color: #606266;
}
.result-value {
margin-left: auto;
color: #303133;
font-weight: 500;
text-align: right;
}
.is-total-row {
background: #fffaf0;
td:first-child {
color: #606266;
font-weight: 500;
}
}
}
// BP 值样式
.bp-value {
color: #409eff;
@@ -308,4 +519,44 @@ export default {
font-weight: 500;
}
}
@media screen and (max-width: 768px) {
.model-output-card {
::v-deep .el-card__body {
padding: 16px;
}
.output-analysis-table {
th,
td {
padding: 8px;
}
}
}
}
@media print {
.model-output-card {
break-inside: auto !important;
page-break-inside: auto !important;
.output-section {
break-inside: auto;
page-break-inside: auto;
}
.output-analysis-table {
color: #000;
thead {
display: table-header-group;
}
tr {
break-inside: avoid !important;
page-break-inside: avoid !important;
}
}
}
}
</style>

View File

@@ -52,6 +52,14 @@
</el-col>
</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-form-item label="借款期限(年)" prop="loanTerm">
<el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%">
@@ -77,6 +85,11 @@
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否有营业执照" prop="bizProof">
<el-switch v-model="form.bizProof"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="循环功能" prop="loanLoop">
<el-switch v-model="form.loanLoop"/>
@@ -87,7 +100,33 @@
<!-- 抵质押信息 -->
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
<el-row v-if="isCollateralGuarantee">
<el-col :span="12">
<template v-if="isMortgage">
<el-col :span="12">
<el-form-item label="抵押物性质" prop="collateralNature">
<el-select v-model="form.collateralNature" placeholder="请选择抵押物性质" style="width: 100%">
<el-option v-for="item in collateralNatureOptions" :key="item" :label="item" :value="item"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否属于九地" prop="isNineArea">
<div class="nine-area-control">
<el-select
v-model="form.isNineArea"
:disabled="!requiresNineArea"
:placeholder="requiresNineArea ? '请选择是否属于九地' : '无需选择'"
>
<el-option label="是" value="是"/>
<el-option label="否" value="否"/>
</el-select>
<el-tooltip content="九地是指绍兴、杭州、宁波、上海、北京、广州、深圳、南京、苏州" placement="top">
<i class="el-icon-question nine-area-help" tabindex="0" aria-label="查看九地说明"></i>
</el-tooltip>
</div>
</el-form-item>
</el-col>
</template>
<el-col :span="12" v-if="isPledge">
<el-form-item label="抵质押类型" prop="collType">
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
@@ -168,6 +207,30 @@ export default {
callback()
}
const validateCollateralType = (rule, value, callback) => {
if (this.isPledge && !value) {
callback(new Error('请选择抵质押类型'))
return
}
callback()
}
const validateCollateralNature = (rule, value, callback) => {
if (this.isMortgage && !value) {
callback(new Error('请选择抵押物性质'))
return
}
callback()
}
const validateNineArea = (rule, value, callback) => {
if (this.requiresNineArea && !value) {
callback(new Error('请选择是否属于九地'))
return
}
callback()
}
return {
loanTermOptions: [
'1', '2', '3', '4', '5', '6'
@@ -187,11 +250,15 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collateralNature: undefined,
isNineArea: undefined,
collThirdParty: false,
couponRate: undefined
},
@@ -216,6 +283,9 @@ export default {
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
loanPurpose: [
{required: true, message: "请选择贷款用途", trigger: "change"}
],
loanTerm: [
{required: true, message: "请选择借款期限", trigger: "change"}
],
@@ -226,7 +296,13 @@ export default {
{required: true, message: "请选择历史贷款合同", trigger: "change"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
{validator: validateCollateralType, trigger: "change"}
],
collateralNature: [
{validator: validateCollateralNature, trigger: "change"}
],
isNineArea: [
{validator: validateNineArea, trigger: "change"}
],
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
@@ -246,6 +322,15 @@ export default {
isCollateralGuarantee() {
return this.form.guarType === '抵押' || this.form.guarType === '质押'
},
isMortgage() {
return this.form.guarType === '抵押'
},
isPledge() {
return this.form.guarType === '质押'
},
requiresNineArea() {
return this.isMortgage && ['普通住宅、土地、厂房', '商业性质不动产'].includes(this.form.collateralNature)
},
isStockTransfer() {
return this.form.businessType === '存量转贷'
},
@@ -259,13 +344,13 @@ export default {
return this.isEdit ? '编辑个人利率定价流程' : '新增个人利率定价流程'
},
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一线', '一类', '二类', '三类']
}
if (this.form.guarType === '质押') {
if (this.isPledge) {
return ['存单质押', '其他质押']
}
return []
},
collateralNatureOptions() {
return ['普通住宅、土地、厂房', '商业性质不动产', '其他抵押财产']
}
},
watch: {
@@ -287,6 +372,12 @@ export default {
return
}
this.resetCouponRateIfNotCertificatePledge()
},
'form.collateralNature'(val, oldVal) {
if (this.resettingForm || val === oldVal) {
return
}
this.handleCollateralNatureChange()
}
},
methods: {
@@ -318,11 +409,15 @@ export default {
idNum: this.editData.idNum,
guarType: this.editData.guarType,
applyAmt: this.editData.applyAmt,
loanPurpose: this.editData.loanPurpose,
loanTerm: this.editData.loanTerm,
businessType: this.editData.businessType,
loanRateHistory: this.editData.loanRateHistory,
bizProof: this.isOne(this.editData.bizProof),
loanLoop: this.isOne(this.editData.loanLoop),
collType: this.editData.collType,
collType: this.editData.guarType === '质押' ? this.editData.collType : undefined,
collateralNature: this.editData.collateralNature,
isNineArea: this.editData.isNineArea,
collThirdParty: this.isOne(this.editData.collThirdParty),
couponRate: this.editData.couponRate
}
@@ -336,11 +431,15 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collateralNature: undefined,
isNineArea: undefined,
collThirdParty: false,
couponRate: undefined
}
@@ -360,14 +459,35 @@ export default {
/** 清空抵质押字段 */
resetCollateralFields() {
this.form.collType = undefined
this.form.collateralNature = undefined
this.form.isNineArea = undefined
this.form.collThirdParty = false
this.resetCouponRateIfNotCertificatePledge()
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate'])
this.$refs.form.clearValidate(['collType', 'collateralNature', 'isNineArea', 'collThirdParty', 'couponRate'])
}
})
},
handleCollateralNatureChange() {
this.form.isNineArea = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['isNineArea'])
}
})
},
resolveMortgageCollateralType() {
if (this.form.collateralNature === '普通住宅、土地、厂房') {
return this.form.isNineArea === '是' ? '一类' : '二类'
}
if (this.form.collateralNature === '商业性质不动产') {
return this.form.isNineArea === '是' ? '二类' : '三类'
}
if (this.form.collateralNature === '其他抵押财产') {
return '四类'
}
},
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
@@ -435,8 +555,15 @@ export default {
// 转换开关值为字符串
const data = {
...this.form,
bizProof: this.form.bizProof ? '1' : '0',
loanLoop: this.form.loanLoop ? '1' : '0'
}
if (this.isMortgage) {
data.collType = this.resolveMortgageCollateralType()
} else {
delete data.collateralNature
delete data.isNineArea
}
if (this.isCollateralGuarantee) {
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
} else {
@@ -477,6 +604,24 @@ export default {
font-size: 14px;
}
.nine-area-control {
display: flex;
align-items: center;
gap: 8px;
}
.nine-area-control .el-select {
flex: 1;
min-width: 0;
}
.nine-area-help {
flex: 0 0 auto;
color: #909399;
cursor: help;
font-size: 16px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;

View File

@@ -13,14 +13,10 @@
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="最终测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
<el-descriptions-item label="期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="担保方式">{{ detailData.guarType || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">
<span class="rate-value">{{ formatRate(retailOutput && retailOutput.minRateProduct) }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
@@ -46,13 +42,6 @@
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="retailOutput"
:corp-output="null"
/>
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
@@ -64,10 +53,18 @@
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</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.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.createBy }}</el-descriptions-item>
</el-descriptions>
@@ -83,11 +80,17 @@
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ formatRate(detailData.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="借款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否有经营佐证">{{
<el-descriptions-item label="是否有营业执照">{{
formatBoolean(detailData.bizProof)
}}
</el-descriptions-item>
<el-descriptions-item label="循环功能">{{ formatBoolean(detailData.loanLoop) }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.guarType === '抵押'" label="抵押物性质">
{{ detailData.collateralNature || '-' }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.guarType === '抵押'" label="是否属于九地">
{{ detailData.isNineArea || '-' }}
</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{
formatBoolean(detailData.collThirdParty)
@@ -97,6 +100,14 @@
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:guar-type="detailData.guarType"
:retail-output="retailOutput"
:corp-output="null"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
@@ -164,18 +175,6 @@ export default {
if (value === 'business') return '经营'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.formatRate(this.retailOutput?.baseLoanRate)
},
/** 获取浮动BP */
getTotalBp() {
return this.retailOutput?.totalBp || '-'
},
/** 获取最终测算利率 */
getCalculateRate() {
return this.formatRate(this.retailOutput?.finalCalculateRate)
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
@@ -257,16 +256,6 @@ export default {
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}

View File

@@ -1,32 +1,67 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="客户内码" prop="custIsn">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
v-show="showSearch"
label-width="68px"
@submit.native.prevent="handleQuery"
>
<el-form-item label="客户信息" prop="custIsn">
<el-input
v-model="queryParams.custIsn"
placeholder="请输入客户内码"
placeholder="请输入客户内码/证件号/客户名"
clearable
@keyup.enter.native="handleQuery"
@keydown.enter.native.prevent="handleQuery"
/>
</el-form-item>
<el-form-item label="创建者" prop="createBy">
<el-form-item v-if="showAllSearchItems" label="客户类型" prop="custType">
<el-select v-model="queryParams.custType" placeholder="请选择客户类型" clearable style="width: 160px">
<el-option label="个人" value="个人" />
<el-option label="企业" value="企业" />
</el-select>
</el-form-item>
<el-form-item v-if="showAllSearchItems" label="担保方式" prop="guarType">
<el-select v-model="queryParams.guarType" placeholder="请选择担保方式" clearable style="width: 160px">
<el-option label="信用" value="信用" />
<el-option label="保证" value="保证" />
<el-option label="抵押" value="抵押" />
<el-option label="质押" value="质押" />
</el-select>
</el-form-item>
<el-form-item v-if="showAllSearchItems" label="创建者" prop="createBy">
<el-input
v-model="queryParams.createBy"
placeholder="请输入创建者"
clearable
@keyup.enter.native="handleQuery"
@keydown.enter.native.prevent="handleQuery"
/>
</el-form-item>
<el-form-item label="机构号" prop="orgCode">
<el-input
v-model="queryParams.orgCode"
placeholder="请输入机构号"
<el-form-item v-if="showAllSearchItems" label="机构号" prop="deptId">
<treeselect
v-model="queryParams.deptId"
:options="deptOptions"
:show-count="true"
placeholder="请选择机构号"
clearable
@keyup.enter.native="handleQuery"
style="width: 220px"
/>
</el-form-item>
<el-form-item v-if="showAllSearchItems" label="创建时间">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button type="primary" icon="el-icon-search" size="mini" native-type="submit">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
@@ -67,6 +102,7 @@
</template>
</el-table-column>
<el-table-column label="创建者" align="center" prop="createBy" min-width="220" class-name="workflow-important-column" />
<el-table-column label="机构号" align="center" prop="deptId" min-width="110" />
<el-table-column label="操作" align="center" fixed="right" min-width="150" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
@@ -124,16 +160,20 @@
<script>
import {getWorkflowEdit, listWorkflow} from "@/api/loanPricing/workflow"
import {deptTreeSelect} from "@/api/system/user"
import {mapGetters} from "vuex"
import CustomerTypeSelector from "./components/CustomerTypeSelector"
import CustomerMapSelector from "./components/CustomerMapSelector"
import PersonalCreateDialog from "./components/PersonalCreateDialog"
import CorporateCreateDialog from "./components/CorporateCreateDialog"
import {formatRate} from "@/utils/rate"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
export default {
name: "LoanPricingWorkflow",
components: {
Treeselect,
CustomerTypeSelector,
CustomerMapSelector,
PersonalCreateDialog,
@@ -149,6 +189,10 @@ export default {
total: 0,
// 利率定价流程表格数据
workflowList: [],
// 机构树选项
deptOptions: [],
// 创建时间范围
dateRange: [],
// 是否显示客户类型选择弹出层
showTypeSelector: false,
// 是否显示客户号查询选择弹出层
@@ -168,19 +212,26 @@ export default {
pageNum: 1,
pageSize: 10,
custIsn: undefined,
custType: undefined,
guarType: undefined,
createBy: undefined,
orgCode: undefined
deptId: undefined
}
}
},
created() {
this.getList()
this.getDeptTree()
},
computed: {
...mapGetters([
'name',
'nickName'
'nickName',
'roles'
]),
showAllSearchItems() {
return !this.roles.includes('common')
},
currentCreateBy() {
return `${this.nickName}-${this.name}`
}
@@ -202,10 +253,22 @@ export default {
},
methods: {
formatRate,
/** 查询机构下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data
})
},
/** 查询利率定价流程列表 */
getList() {
this.loading = true
listWorkflow(this.queryParams).then(response => {
const hasDateRange = Array.isArray(this.dateRange) && this.dateRange.length === 2
const query = {
...this.queryParams,
beginCreateTime: hasDateRange ? `${this.dateRange[0]} 00:00:00` : undefined,
endCreateTime: hasDateRange ? `${this.dateRange[1]} 23:59:59` : undefined
}
listWorkflow(query).then(response => {
this.workflowList = response.rows
this.total = response.total
this.loading = false
@@ -218,6 +281,7 @@ export default {
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.handleQuery()
},

View File

@@ -19,10 +19,12 @@ assert(
)
assert(
!personalCreateDialog.includes('label="贷款用途"') &&
!personalCreateDialog.includes('prop="loanPurpose"') &&
!personalCreateDialog.includes('loanPurpose:'),
'个人新增弹窗不应继续保留贷款用途字段'
personalCreateDialog.includes('label="贷款用途"') &&
personalCreateDialog.includes('prop="loanPurpose"') &&
personalCreateDialog.includes('loanPurpose: undefined') &&
personalCreateDialog.includes('loanPurpose: this.editData.loanPurpose') &&
personalCreateDialog.includes('请选择贷款用途'),
'个人新增弹窗缺少贷款用途、必填校验或编辑回显'
)
assert(
@@ -31,8 +33,9 @@ assert(
)
assert(
!personalCreateDialog.includes("value=\"consumer\"") && !personalCreateDialog.includes("value=\"business\""),
'个人新增弹窗不应继续保留贷款用途选项'
personalCreateDialog.includes('label="经营" value="business"') &&
personalCreateDialog.includes('label="消费" value="consumer"'),
'个人新增弹窗贷款用途选项或协议值不正确'
)
assert(
@@ -52,25 +55,49 @@ assert(
assert(
personalCreateDialog.includes('collateralTypeOptions') &&
personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") &&
personalCreateDialog.includes("return ['存单质押', '其他质押']"),
'个人新增弹窗质押类型选项未按担保方式动态切换'
'个人新增弹窗应保留质押类型选项'
)
assert(
personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
'个人新增弹窗抵质押类型应为必填'
personalCreateDialog.includes('label="抵押物性质"') &&
personalCreateDialog.includes('prop="collateralNature"') &&
personalCreateDialog.includes("return ['普通住宅、土地、厂房', '商业性质不动产', '其他抵押财产']") &&
personalCreateDialog.includes('label="是否属于九地"') &&
personalCreateDialog.includes('九地是指绍兴、杭州、宁波、上海、北京、广州、深圳、南京、苏州') &&
personalCreateDialog.includes(':disabled="!requiresNineArea"') &&
personalCreateDialog.includes("'无需选择'"),
'个人新增弹窗缺少抵押物性质、九地选择或九地说明'
)
assert(
!personalCreateDialog.includes('label="是否有经营佐证"') &&
!personalCreateDialog.includes('prop="bizProof"') &&
!personalCreateDialog.includes('bizProof:') &&
personalCreateDialog.includes("return this.form.isNineArea === '是' ? '一类' : '二类'") &&
personalCreateDialog.includes("return this.form.isNineArea === '是' ? '二类' : '三类'") &&
personalCreateDialog.includes("return '四类'"),
'个人新增弹窗抵押物类型映射不完整'
)
assert(
personalCreateDialog.includes("if (this.isPledge && !value)") &&
personalCreateDialog.includes("if (this.isMortgage && !value)") &&
personalCreateDialog.includes("if (this.requiresNineArea && !value)"),
'个人新增弹窗缺少抵押和质押的条件校验'
)
assert(
personalCreateDialog.includes('label="是否有营业执照"') &&
personalCreateDialog.includes('prop="bizProof"') &&
personalCreateDialog.includes('bizProof: false') &&
personalCreateDialog.includes('bizProof: this.isOne(this.editData.bizProof)') &&
personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") &&
personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") &&
personalCreateDialog.includes("data.collThirdParty = this.form.collThirdParty ? '1' : '0'") &&
personalCreateDialog.includes('data.collType = this.resolveMortgageCollateralType()') &&
personalCreateDialog.includes('collateralNature: this.editData.collateralNature') &&
personalCreateDialog.includes('isNineArea: this.editData.isNineArea') &&
personalCreateDialog.includes('delete data.collType') &&
personalCreateDialog.includes('delete data.collThirdParty'),
'个人新增弹窗开关字段或非抵质押提交字段处理不正确'
'个人新增弹窗营业执照、抵押来源字段、开关字段或非抵质押提交字段处理不正确'
)
assert(
@@ -78,6 +105,19 @@ assert(
'个人详情页缺少贷款用途展示'
)
assert(
personalDetail.includes('label="是否有营业执照"') && personalDetail.includes('detailData.bizProof'),
'个人详情页缺少营业执照展示'
)
assert(
personalDetail.includes('label="抵押物性质"') &&
personalDetail.includes('detailData.collateralNature') &&
personalDetail.includes('label="是否属于九地"') &&
personalDetail.includes('detailData.isNineArea'),
'个人详情页缺少抵押物性质或九地展示'
)
assert(
personalDetail.includes("value === '1'") && personalDetail.includes("value === '0'"),
'个人详情页布尔格式化未兼容 1/0'

View File

@@ -7,15 +7,22 @@ function read(relativePath) {
}
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const summaryStart = personalDetail.indexOf('<el-card class="summary-card">')
const summaryEnd = personalDetail.indexOf('</el-card>', summaryStart)
const summaryCard = personalDetail.slice(summaryStart, summaryEnd)
assert(summaryStart !== -1 && summaryEnd !== -1, '个人流程详情缺少关键信息卡片')
assert(
/label="最终测算利率"/.test(personalDetail),
'个人流程详情左侧缺少“最终测算利率”标签'
!/label="(?:基准利率|浮动BP|最终测算利率)"/.test(summaryCard),
'个人流程详情关键信息不应展示基准利率、浮动BP或最终测算利率'
)
assert(
/return this\.retailOutput\?\.finalCalculateRate \|\| '-'/.test(personalDetail),
'个人流程详情没有使用 finalCalculateRate 展示最终测算利率'
/label="期限"/.test(summaryCard)
&& /label="担保方式"/.test(summaryCard)
&& /label="产品最低利率下限"/.test(summaryCard),
'个人流程详情关键信息缺少期限、担保方式或产品最低利率下限'
)
console.log('personal final calculate rate display assertions passed')
console.log('personal key information display assertions passed')

View File

@@ -9,18 +9,55 @@ function read(relativePath) {
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const modelOutput = read('src/views/loanPricing/workflow/components/ModelOutputDisplay.vue')
function extractCard(source, className) {
const start = source.indexOf(`<el-card class="${className}">`)
const end = source.indexOf('</el-card>', start)
assert(start !== -1 && end !== -1, `缺少卡片: ${className}`)
return source.slice(start, end)
}
const summaryCard = extractCard(personalDetail, 'summary-card')
const detailCard = extractCard(personalDetail, 'detail-card')
const businessInfoStart = detailCard.indexOf('<h4 class="section-title">业务信息</h4>')
const basicInfo = detailCard.slice(0, businessInfoStart)
const businessInfo = detailCard.slice(businessInfoStart)
assert(
personalDetail.includes('label="借款期限"') && personalDetail.includes('detailData.loanTerm'),
'个人详情页缺少借款期限展示'
!basicInfo.includes('label="借款期限"')
&& businessInfo.includes('label="借款期限"')
&& businessInfo.includes('detailData.loanTerm'),
'个人详情页借款期限未展示在业务信息中'
)
;['期限', '担保方式', '产品最低利率下限'].forEach((label) => {
assert(summaryCard.includes(`label="${label}"`), `个人流程详情关键信息缺少字段: ${label}`)
})
;['基准利率', '浮动BP', '最终测算利率'].forEach((label) => {
assert(!summaryCard.includes(`label="${label}"`), `个人流程详情关键信息不应展示字段: ${label}`)
})
assert(
personalDetail.includes('formatRate(retailOutput && retailOutput.minRateProduct)'),
'个人流程详情关键信息未展示产品最低利率下限'
)
assert(
personalDetail.includes(':guar-type="detailData.guarType"'),
'个人模型输出未使用流程担保方式控制抵押物行'
)
const requiredRetailFields = [
'retailOutput.greyBlackCust',
'retailOutput.loanRateHistory',
'retailOutput.minRateProduct',
'retailOutput.smoothRange',
'retailOutput.finalCalculateRate',
'retailOutput.referenceRate'
'retailOutput.referenceRate',
'retailOutput.branchBp',
'retailOutput.bpLoanUse',
'retailOutput.midPerGasDdc',
'retailOutput.bpPrinOverdue',
'retailOutput.bpIinterestOverdue',
'retailOutput.bpCardOverdue'
]
requiredRetailFields.forEach((field) => {
@@ -28,9 +65,34 @@ requiredRetailFields.forEach((field) => {
})
assert(
modelOutput.indexOf('label="灰黑名单客户"') > modelOutput.indexOf('<h4 class="section-title">基本信息</h4>')
&& modelOutput.indexOf('label="灰黑名单客户"') < modelOutput.indexOf('<h4 class="section-title">测算结果</h4>'),
'灰黑名单客户未展示在个人模型输出基础信息中'
modelOutput.includes("this.resultItem('营业执照', this.formatBoolean(this.retailOutput.bizProof))"),
'个人风险度缺少营业执照或 BP_贷款用途关联展示'
)
assert(
basicInfo.includes('label="灰名单客户"')
&& basicInfo.includes('retailOutput && retailOutput.greyCust')
&& 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(
!basicInfo.includes('label="基准利率"')
&& !basicInfo.includes('retailOutput && retailOutput.baseLoanRate'),
'个人流程详情基本信息不应展示基准利率'
)
assert(
!detailCard.includes('label="运行模式"') && !modelOutput.includes('<h4 class="section-title">基本信息</h4>'),
'个人详情仍展示运行模式或模型输出基本信息'
)
console.log('retail display fields assertions passed')

View File

@@ -6,22 +6,22 @@ function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
function assertModelOutputBeforeDetailCard(source, label) {
function assertDetailCardBeforeModelOutput(source, label) {
const modelOutputIndex = source.indexOf('<ModelOutputDisplay')
const detailCardIndex = source.indexOf('<el-card class="detail-card">')
assert(modelOutputIndex !== -1, `${label} 缺少模型输出卡片`)
assert(detailCardIndex !== -1, `${label} 缺少流程详情卡片`)
assert(
modelOutputIndex < detailCardIndex,
`${label}模型输出卡片应位于流程详情卡片上方`
detailCardIndex < modelOutputIndex,
`${label}流程详情卡片应位于模型输出卡片上方`
)
}
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')
assertModelOutputBeforeDetailCard(personalDetail, '个人流程详情')
assertModelOutputBeforeDetailCard(corporateDetail, '企业流程详情')
assertDetailCardBeforeModelOutput(personalDetail, '个人流程详情')
assertDetailCardBeforeModelOutput(corporateDetail, '企业流程详情')
console.log('workflow detail card order assertions passed')

View File

@@ -55,6 +55,23 @@ assert(source.includes('icon="el-icon-edit"') && source.includes('>编辑</el-bu
assert(source.includes('v-if="canEdit(scope.row)"'), '流程列表编辑按钮应只对可编辑行显示')
assert(source.includes('row.createBy === this.currentCreateBy'), '流程列表应按创建者判断编辑按钮权限')
assert(source.includes('getWorkflowEdit(row.serialNum)'), '编辑操作应先查询编辑数据')
assert(source.includes('v-model="queryParams.custType"'), '流程列表应包含客户类型筛选')
assert(source.includes('v-model="queryParams.guarType"'), '流程列表应包含担保方式筛选')
assert(source.includes('v-model="dateRange"'), '流程列表应包含创建时间范围筛选')
assert(source.includes('value-format="yyyy-MM-dd"'), '流程列表创建时间筛选应使用日期值')
assert(source.includes('beginCreateTime: hasDateRange ? `${this.dateRange[0]} 00:00:00` : undefined'), '流程列表应传递当天零点的顶层创建时间起始参数')
assert(source.includes('endCreateTime: hasDateRange ? `${this.dateRange[1]} 23:59:59` : undefined'), '流程列表应传递当天末秒的顶层创建时间结束参数')
assert(source.includes('label="机构号"') && source.includes('prop="deptId"'), '流程列表应展示机构号')
assert(source.indexOf('label="机构号"') > source.indexOf('label="创建者"'), '机构号列应位于创建者列之后')
assert.strictEqual((source.match(/v-if="showAllSearchItems"/g) || []).length, 5, '五个高级搜索项均应根据角色显示')
assert.strictEqual(component.computed.showAllSearchItems.call({
roles: ['common']
}), false, '客户经理只应展示客户信息搜索项')
assert.strictEqual(component.computed.showAllSearchItems.call({
roles: ['headAdmin']
}), true, '其他角色应展示全部搜索项')
let refreshCount = 0
component.activated.call({

View File

@@ -0,0 +1,16 @@
-- 2026-08-14 production database changes
-- Personal model output fields
ALTER TABLE `model_retail_output_fields`
ADD COLUMN `black_cust` varchar(100) DEFAULT NULL COMMENT '黑名单客户' AFTER `grey_cust`,
ADD COLUMN `branch_bp` varchar(100) DEFAULT NULL COMMENT '行社下调BP' AFTER `reference_rate`;
-- Corporate model output fields
ALTER TABLE `model_corp_output_fields`
ADD COLUMN `black_cust` varchar(100) DEFAULT NULL COMMENT '黑名单客户' AFTER `grey_cust`,
ADD COLUMN `branch_bp` varchar(100) DEFAULT NULL COMMENT '行社下调BP' AFTER `reference_rate`;
-- Workflow collateral source fields
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `collateral_nature` varchar(50) DEFAULT NULL COMMENT '抵押物性质' AFTER `loan_loop`,
ADD COLUMN `is_nine_area` varchar(10) DEFAULT NULL COMMENT '是否属于九地: 是/否' AFTER `collateral_nature`;

View File

@@ -0,0 +1,5 @@
-- 2026-08-24 corporate model output overdue BP fields
ALTER TABLE `model_corp_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`;

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`;