迁移892-without-redis分支全量功能

This commit is contained in:
wkc
2026-04-15 14:18:56 +08:00
parent 9fe1bffe0d
commit 79c5317414
97 changed files with 10922 additions and 232 deletions

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-loan-pricing</artifactId>
<description>
利率定价模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,100 @@
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.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 利率定价流程Controller
*
* @author ruoyi
* @date 2025-01-19
*/
@RestController
@RequestMapping("/loanPricing/workflow")
public class LoanPricingWorkflowController extends BaseController
{
@Autowired
private ILoanPricingWorkflowService loanPricingWorkflowService;
/**
* 发起个人客户利率定价流程
*/
@Log(title = "个人客户利率定价流程", businessType = BusinessType.INSERT)
@PostMapping("/create/personal")
public AjaxResult createPersonal(@Validated @RequestBody PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow result = loanPricingWorkflowService.createPersonalLoanPricing(dto);
return success(result);
}
/**
* 发起企业客户利率定价流程
*/
@Log(title = "企业客户利率定价流程", businessType = BusinessType.INSERT)
@PostMapping("/create/corporate")
public AjaxResult createCorporate(@Validated @RequestBody CorporateLoanPricingCreateDTO dto)
{
LoanPricingWorkflow result = loanPricingWorkflowService.createCorporateLoanPricing(dto);
return success(result);
}
/**
* 查询利率定价流程列表
*/
@GetMapping("/list")
public TableDataInfo list(LoanPricingWorkflow loanPricingWorkflow)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<LoanPricingWorkflowListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(page, loanPricingWorkflow);
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(200);
rspData.setMsg("查询成功");
rspData.setRows(result.getRecords());
rspData.setTotal(result.getTotal());
return rspData;
}
/**
* 查询利率定价流程详情
*/
@GetMapping("/{serialNum}")
public AjaxResult getInfo(@PathVariable("serialNum") String serialNum)
{
LoanPricingWorkflowVO workflow = loanPricingWorkflowService.selectLoanPricingBySerialNum(serialNum);
if (workflow == null)
{
return error("记录不存在");
}
return success(workflow);
}
/**
* 设定执行利率
*/
@Log(title = "利率定价流程", businessType = BusinessType.UPDATE)
@PutMapping("/{serialNum}/executeRate")
public AjaxResult setExecuteRate(@PathVariable("serialNum") String serialNum,
@RequestBody Map<String, String> request) {
String executeRate = request.get("executeRate");
boolean success = loanPricingWorkflowService.setExecuteRate(serialNum, executeRate);
return success ? success() : error("设定失败");
}
}

View File

@@ -0,0 +1,50 @@
package com.ruoyi.loanpricing.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.*;
import java.io.InputStream;
/**
* @Author 吴凯程
* @Date 2025/11/10
**/
@RestController
@RequestMapping("/rate/pricing/mock")
public class LoanRatePricingMockController extends BaseController {
@Anonymous
@PostMapping("/invokeModel")
public AjaxResult invokeModel( ModelInvokeDTO modelInvokeDTO) {
ObjectNode jsonNodes;
if (modelInvokeDTO.getCustType().equals("个人")) {
jsonNodes = loadJsonFromResource("data/retail_output.json");
} else {
jsonNodes = loadJsonFromResource("data/corp_output.json");
}
return new AjaxResult(10000, "success", jsonNodes);
}
private ObjectNode loadJsonFromResource(String resourcePath){
ClassPathResource classPathResource = new ClassPathResource(resourcePath);
try (InputStream inputStream = classPathResource.getInputStream();){
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(inputStream, ObjectNode.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,48 @@
package com.ruoyi.loanpricing.domain.dto;
import lombok.Data;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* 企业客户利率定价发起DTO
*
* @author ruoyi
* @date 2025-01-19
*/
@Data
public class CorporateLoanPricingCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "客户内码不能为空")
private String custIsn;
private String custName;
private String idType;
private String idNum;
@NotBlank(message = "担保方式不能为空")
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
private String guarType;
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
private String loanTerm;
private String isAgriGuar;
private String isGreenLoan;
private String isTechEnt;
private String isTradeConstruction;
private String collType;
private String collThirdParty;
}

View File

@@ -0,0 +1,170 @@
package com.ruoyi.loanpricing.domain.dto;
import lombok.Data;
/**
* @Author 吴凯程
* @Date 2025/12/23
**/
@Data
public class ModelInvokeDTO {
/**
* 业务方流水号(必填)
* 可使用时间戳,或自定义随机生成
*/
private String serialNum;
/**
* 机构编码(必填)
* 固定值892000
*/
private String orgCode;
/**
* 运行模式(必填)
* 固定值1:同步
*/
private String runType = "1";
/**
* 客户内码(必填)
*/
private String custIsn;
/**
* 客户类型(必填)
* 可选值:个人/企业
*/
private String custType;
/**
* 担保方式(必填)
* 可选值:信用,保证,抵押,质押
*/
private String guarType;
/**
* 中间业务_个人_快捷支付非必填
* 可选值true/false
*/
private String midPerQuickPay;
/**
* 中间业务_个人_电费代扣非必填
* 可选值true/false
*/
private String midPerEleDdc;
/**
* 中间业务_企业_电费代扣非必填
* 可选值true/false
*/
private String midEntEleDdc;
/**
* 中间业务_企业_水费代扣非必填
* 可选值true/false
*/
private String midEntWaterDdc;
/**
* 申请金额(必填)
* 单位:元
*/
private String applyAmt;
/**
* 贷款期限(必填)
* 单位:年
*/
private String loanTerm;
/**
* 净身企业(非必填)
* 可选值true/false
*/
private String isCleanEnt;
/**
* 开立基本结算账户(非必填)
* 可选值true/false
*/
private String hasSettleAcct;
/**
* 制造业企业(非必填)
* 可选值true/false
*/
private String isManufacturing;
/**
* 省农担担保贷款(非必填)
* 可选值true/false
*/
private String isAgriGuar;
/**
* 是否纳税信用等级A级非必填
* 可选值true/false
*/
private String isTaxA;
/**
* 是否县级及以上农业龙头企业(非必填)
* 可选值true/false
*/
private String isAgriLeading;
private String isInclusiveFinance;
/**
* 贷款用途(非必填)
* 可选值consumer/business
*/
private String loanPurpose;
/**
* 是否有经营佐证(非必填)
* 可选值0/1
*/
private String bizProof;
/**
* 循环功能(非必填)
* 可选值0/1
*/
private String loanLoop;
/**
* 抵质押类型(非必填)
* 可选值:一类/二类/三类
*/
private String collType;
/**
* 抵质押物是否三方所有(非必填)
* 可选值0/1
*/
private String collThirdParty;
// /**
// * 贷款利率(必填)
// */
// private String loanRate;
/**
* 客户名称(非必填)
*/
private String custName;
/**
* 证件类型(非必填)
*/
private String idType;
/**
* 证件号码(非必填)
*/
private String idNum;
}

View File

@@ -0,0 +1,49 @@
package com.ruoyi.loanpricing.domain.dto;
import lombok.Data;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* 个人客户利率定价发起DTO
*
* @author ruoyi
* @date 2025-01-19
*/
@Data
public class PersonalLoanPricingCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "客户内码不能为空")
private String custIsn;
private String custName;
private String idType;
private String idNum;
@NotBlank(message = "担保方式不能为空")
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
private String guarType;
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
@NotBlank(message = "贷款用途不能为空")
@Pattern(regexp = "^(consumer|business)$", message = "贷款用途必须是consumer、business之一")
private String loanPurpose;
@NotBlank(message = "借款期限不能为空")
private String loanTerm;
private String bizProof;
private String loanLoop;
private String collType;
private String collThirdParty;
}

View File

@@ -0,0 +1,159 @@
package com.ruoyi.loanpricing.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Date;
/**
* 利率定价流程对象 loan_pricing_workflow
*
* @author ruoyi
* @date 2025-01-19
*/
@Data
@TableName("loan_pricing_workflow")
public class LoanPricingWorkflow implements Serializable
{
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 模型输出ID */
private Long modelOutputId;
/** 业务方流水号 */
private String serialNum;
/** 机构编码 */
private String orgCode;
/** 运行模式: 1-同步 */
private String runType;
/** 客户内码 */
@NotBlank(message = "客户内码不能为空")
private String custIsn;
/** 客户类型: 个人/企业 */
@NotBlank(message = "客户类型不能为空")
private String custType;
/** 担保方式: 信用/保证/抵押/质押 */
@NotBlank(message = "担保方式不能为空")
private String guarType;
/** 中间业务_个人_快捷支付: true/false */
private String midPerQuickPay;
/** 中间业务_个人_电费代扣: true/false */
private String midPerEleDdc;
/** 中间业务_企业_电费代扣: true/false */
private String midEntEleDdc;
/** 中间业务_企业_水费代扣: true/false */
private String midEntWaterDdc;
/** 申请金额(元) */
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
/**
* 贷款期限
*/
private String loanTerm;
/** 净身企业: true/false */
private String isCleanEnt;
/** 开立基本结算账户: true/false */
private String hasSettleAcct;
/** 制造业企业: true/false */
private String isManufacturing;
/** 省农担担保贷款: true/false */
private String isAgriGuar;
/**
* 贸易和建筑业企业标识: true/false
*/
private String isTradeConstruction;
/**
* 绿色贷款: true/false
*/
private String isGreenLoan;
/**
* 科技型企业: true/false
*/
private String isTechEnt;
/** 是否纳税信用等级A级: true/false */
private String isTaxA;
/** 是否县级及以上农业龙头企业: true/false */
private String isAgriLeading;
/** 贷款用途: consumer-消费/business-经营 */
private String loanPurpose;
/** 是否有经营佐证: true/false */
private String bizProof;
/** 循环功能: true/false */
private String loanLoop;
/** 抵质押类型: 一线/一类/二类 */
private String collType;
/** 抵质押物是否三方所有: true/false */
private String collThirdParty;
/** 贷款利率 */
private String loanRate;
/**
* 执行利率(%)
*/
private String executeRate;
/** 客户名称 */
private String custName;
/**
* 证件类型
*/
private String idType;
/** 证件号码 */
private String idNum;
/** 是否普惠小微借款人: true/false */
private String isInclusiveFinance;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}

View File

@@ -0,0 +1,125 @@
package com.ruoyi.loanpricing.domain.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
/**
* 贷款定价模型输入参数对象
*
* @author ruoyi
* @date 2025-01-21
*/
@Data
public class ModelCorpOutputFields {
@TableId(type = IdType.AUTO)
private Long id;
// 客户内码
private String custIsn;
// 客户类型
private String custType;
// 担保方式
private String guarType;
// 客户名称
private String custName;
// 证件类型
private String idType;
// 证件号码
private String idNum;
// 基准利率
private String baseLoanRate;
// 我行首贷客户
private String isFirstLoan;
// 用信天数
private String faithDay;
// BP_首贷
private String bpFirstLoan;
// BP_贷龄
private String bpAgeLoan;
// TOTAL_BP_忠诚度
private String totalBpLoyalty;
// 存款年日均
private String balanceAvg;
// 贷款年日均
private String loanAvg;
// 派生率
private String derivationRate;
// TOTAL_BP_贡献度
private String totalBpContribution;
// 中间业务_企业_企业互联
private String midEntConnect;
// 中间业务_企业_有效价值客户
private String midEntEffect;
// 中间业务_企业_国际业务
private String midEntInter;
// 中间业务_企业_承兑
private String midEntAccept;
// 中间业务_企业_贴现
private String midEntDiscount;
// 中间业务_企业_电费代扣
private String midEntEleDdc;
// 中间业务_企业_水费代扣
private String midEntWaterDdc;
// 中间业务_企业_税务代扣
private String midEntTax;
// BP_中间业务
private String bpMid;
// 代发工资户数
private String payroll;
// 存量贷款余额
private String invLoanAmount;
// BP_代发工资
private String bpPayroll;
// 净身企业
private String isCleanEnt;
// 开立基本结算账户
private String hasSettleAcct;
// 省农担担保贷款
private String isAgriGuar;
// 绿色贷款
private String isGreenLoan;
// 科技型企业
private String isTechEnt;
// BP_企业客户类别
private String bpEntType;
// TOTAL_BP_关联度
private String totoalBpRelevance;
// 贷款期限
private String loanTerm;
// BP_贷款期限
private String bpLoanTerm;
// 申请金额
private String applyAmt;
// BP_贷款额度
private String bpLoanAmount;
// 抵质押类型
private String collType;
// 抵质押物是否三方所有
private String collThirdParty;
// BP_抵押物
private String bpCollateral;
// 灰名单客户
private String greyCust;
// 本金逾期
private String prinOverdue;
// 利息逾期
private String interestOverdue;
// 信用卡逾期
private String cardOverdue;
// BP_灰名单与逾期
private String bpGreyOverdue;
// TOTAL_BP_风险度
private String totoalBpRisk;
// 浮动BP
private String totalBp;
// 测算利率
private String calculateRate;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,190 @@
package com.ruoyi.loanpricing.domain.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
/**
* 贷款定价模型输入参数对象
*
* @author ruoyi
* @date 2025-01-21
*/
@Data
public class ModelRetailOutputFields {
@TableId(type = IdType.AUTO)
private Long id;
// 客户内码
private String custIsn;
// 客户类型
private String custType;
// 担保方式
private String guarType;
// 客户名称
private String custName;
// 证件类型
private String idType;
// 证件号码
private String idNum;
// 基准利率
private String baseLoanRate;
// 我行首贷客户
private String isFirstLoan;
// 用信天数
private String faithDay;
// 客户年龄
private String custAge;
// BP_首贷
private String bpFirstLoan;
// BP_贷龄
private String bpAgeLoan;
// BP_年龄
private String bpAge;
// TOTAL_BP_忠诚度
private String totalBpLoyalty;
// 存款年日均
private String balanceAvg;
// 贷款年日均
private String loanAvg;
// 派生率
private String derivationRate;
// TOTAL_BP_贡献度
private String totalBpContribution;
// 中间业务_个人_信用卡
private String midPerCard;
// 中间业务_个人_一码通
private String midPerPass;
// 中间业务_个人_丰收互联
private String midPerHarvest;
// 中间业务_个人_有效客户
private String midPerEffect;
// 中间业务_个人_快捷支付
private String midPerQuickPay;
// 中间业务_个人_电费代扣
private String midPerEleDdc;
// 中间业务_个人_水费代扣
private String midPerWaterDdc;
// 中间业务_个人_华数费代扣
private String midPerHuashuDdc;
// 中间业务_个人_煤气费代扣
private String MidPerGasDdc;
// 中间业务_个人_市民卡
private String midPerCitizencard;
// 中间业务_个人_理财业务
private String midPerFinMan;
// 中间业务_个人_etc
private String midPerEtc;
// BP_中间业务
private String bpMid;
// TOTAL_BP_关联度注意原字段名拼写错误totoalBpRelevance已保留原拼写
private String totoalBpRelevance;
// 申请金额
private String applyAmt;
// BP_贷款额度
private String bpLoanAmount;
// 贷款用途
private String loanPurpose;
// 是否有经营佐证
private String bizProof;
// BP_贷款用途
private String bpLoanUse;
// 循环功能
private String loanLoop;
// BP_循环功能
private String bpLoanLoop;
// 抵质押类型
private String collType;
// 抵质押物是否三方所有
private String collThirdParty;
// BP_抵押物
private String bpCollateral;
// 灰名单客户
private String greyCust;
// 本金逾期
private String prinOverdue;
// 利息逾期
private String interestOverdue;
// 信用卡逾期
private String cardOverdue;
// BP_灰名单与逾期
private String bpGreyOverdue;
// TOTAL_BP_风险度注意原字段名拼写错误totoalBpRisk已保留原拼写
private String totoalBpRisk;
// 浮动BP
private String totalBp;
// 测算利率
private String calculateRate;
// 历史利率
private String loanRateHistory;
// 产品最低利率下限
private String minRateProduct;
// 平滑幅度
private String smoothRange;
// 最终测算利率
private String finalCalculateRate;
// 参考利率
private String referenceRate;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.loanpricing.domain.vo;
import lombok.Data;
import java.util.Date;
@Data
public class LoanPricingWorkflowListVO
{
private String serialNum;
private String custName;
private String custType;
private String guarType;
private String applyAmt;
private String calculateRate;
private String executeRate;
private Date createTime;
private String createBy;
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.loanpricing.domain.vo;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import lombok.Data;
/**
* @Author: wkc
* @CreateTime: 2026-01-21
*/
@Data
public class LoanPricingWorkflowVO {
private LoanPricingWorkflow loanPricingWorkflow;
private ModelRetailOutputFields modelRetailOutputFields;
private ModelCorpOutputFields modelCorpOutputFields;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.loanpricing.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import org.apache.ibatis.annotations.Param;
/**
* 利率定价流程Mapper接口
*
* @author ruoyi
* @date 2025-01-19
*/
public interface LoanPricingWorkflowMapper extends BaseMapper<LoanPricingWorkflow>
{
IPage<LoanPricingWorkflowListVO> selectWorkflowPageWithRates(Page<?> page,
@Param("query") LoanPricingWorkflow query);
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.loanpricing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
/**
* 对公贷款定价模型输出字段Mapper接口
*
* @author ruoyi
* @date 2025-01-21
*/
public interface ModelCorpOutputFieldsMapper extends BaseMapper<ModelCorpOutputFields>
{
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.loanpricing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
/**
* 零售贷款定价模型输出字段Mapper接口
*
* @author ruoyi
* @date 2025-01-21
*/
public interface ModelRetailOutputFieldsMapper extends BaseMapper<ModelRetailOutputFields>
{
}

View File

@@ -0,0 +1,70 @@
package com.ruoyi.loanpricing.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import java.util.List;
/**
* 利率定价流程Service接口
*
* @author ruoyi
* @date 2025-01-19
*/
public interface ILoanPricingWorkflowService
{
/**
* 发起个人客户利率定价流程
*
* @param dto 个人客户发起DTO
* @return 结果
*/
public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto);
/**
* 发起企业客户利率定价流程
*
* @param dto 企业客户发起DTO
* @return 结果
*/
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto);
/**
* 查询利率定价流程列表
*
* @param loanPricingWorkflow 利率定价流程信息
* @return 利率定价流程集合
*/
public List<LoanPricingWorkflow> selectLoanPricingList(LoanPricingWorkflow loanPricingWorkflow);
/**
* 分页查询利率定价流程列表
*
* @param page 分页参数
* @param loanPricingWorkflow 利率定价流程信息
* @return 分页结果
*/
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow);
/**
* 查询利率定价流程详情
*
* @param serialNum 业务方流水号
* @return 利率定价流程
*/
public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum);
/**
* 设定执行利率
*
* @param serialNum 业务方流水号
* @param executeRate 执行利率
* @return 是否成功
*/
public boolean setExecuteRate(String serialNum, String executeRate);
}

View File

@@ -0,0 +1,109 @@
package com.ruoyi.loanpricing.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.util.Objects;
import javax.annotation.Resource;
/**
* @Author: wkc
* @CreateTime: 2026-01-21
*/
@Service
@Slf4j
@EnableAsync
public class LoanPricingModelService {
@Resource
private ModelService modelService;
@Resource
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Resource
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Resource
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Resource
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
public void invokeModelAsync(Long workflowId) {
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectById(workflowId);
if (Objects.isNull(loanPricingWorkflow)){
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()))
{
normalizePersonalModelInvokeDTO(modelInvokeDTO);
}
JSONObject response = modelService.invokeModel(modelInvokeDTO);
if (loanPricingWorkflow.getCustType().equals("个人")){
// 个人模型
ModelRetailOutputFields modelRetailOutputFields = JSON.parseObject(response.toJSONString(), ModelRetailOutputFields.class);
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
log.info("个人模型调用成功");
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
workflowToUpdate.setId(loanPricingWorkflow.getId());
workflowToUpdate.setModelOutputId(modelRetailOutputFields.getId());
loanPricingWorkflowMapper.updateById(workflowToUpdate);
log.info("更新流程信息成功");
}else if (loanPricingWorkflow.getCustType().equals("企业")){
// 企业模型
ModelCorpOutputFields modelCorpOutputFields = JSON.parseObject(response.toJSONString(), ModelCorpOutputFields.class);
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
log.info("企业模型调用成功");
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
workflowToUpdate.setId(loanPricingWorkflow.getId());
workflowToUpdate.setModelOutputId(modelCorpOutputFields.getId());
loanPricingWorkflowMapper.updateById(workflowToUpdate);
log.info("更新流程信息成功");
}
}
private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop()));
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
}
private String toZeroOne(String value)
{
if ("true".equals(value) || "1".equals(value))
{
return "1";
}
if ("false".equals(value) || "0".equals(value))
{
return "0";
}
return value;
}
}

View File

@@ -0,0 +1,46 @@
package com.ruoyi.loanpricing.service;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class LoanPricingSensitiveDisplayService
{
public String maskCustName(String custName)
{
if (!StringUtils.hasText(custName))
{
return custName;
}
if (custName.contains("公司") && custName.length() > 4)
{
return custName.substring(0, 2) + "*".repeat(custName.length() - 4) + custName.substring(custName.length() - 2);
}
if (custName.length() == 1)
{
return custName;
}
return custName.substring(0, 1) + "*".repeat(custName.length() - 1);
}
public String maskIdNum(String idNum)
{
if (!StringUtils.hasText(idNum))
{
return idNum;
}
if (idNum.startsWith("91") && idNum.length() == 18)
{
return idNum.substring(0, 2) + "*".repeat(13) + idNum.substring(idNum.length() - 3);
}
if (idNum.matches("\\d{17}[\\dXx]"))
{
return idNum.substring(0, 4) + "*".repeat(8) + idNum.substring(idNum.length() - 4);
}
if (idNum.length() > 5)
{
return idNum.substring(0, 2) + "*".repeat(idNum.length() - 5) + idNum.substring(idNum.length() - 3);
}
return "*".repeat(idNum.length());
}
}

View File

@@ -0,0 +1,70 @@
package com.ruoyi.loanpricing.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import org.springframework.beans.factory.annotation.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
/**
* @Author 吴凯程
* @Date 2025/12/11
**/
@Service
@Slf4j
@EnableAsync
public class ModelService {
@Value("${model.url}")
private String modelUrl;
public JSONObject invokeModel(ModelInvokeDTO modelInvokeDTO) {
Map<String, String> requestBody = entityToMap(modelInvokeDTO);
JSONObject response = HttpUtils.doPostFormUrlEncoded(modelUrl, requestBody, null, JSONObject.class);
log.info("------------------->调用模型返回结果:" + JSON.toJSONString(response));
if(Objects.nonNull(response) && response.containsKey("code") && response.getInteger("code") == 10000){
JSONObject mappingOutputFields = response.getJSONObject("data").getJSONObject("mappingOutputFields");
// return JSON.parseObject(mappingOutputFields.toJSONString(), ModelOutputFields.class);
return mappingOutputFields;
}else{
log.error("------------------->调用模型失败,失败原因为:" + response.getString("message"));
throw new ServiceException("调用模型失败");
}
}
/**
* 使用FastJSON将实体类转换为Map<String, String>
* @param obj 待转换的实体类对象
* @return 转换后的Map
*/
public static Map<String, String> entityToMap(Object obj) {
if (obj == null) {
return null;
}
// 先转为JSON字符串再转换为指定类型的Map
String jsonStr = JSON.toJSONString(obj);
return JSON.parseObject(jsonStr, new TypeReference<Map<String, String>>() {});
}
}

View File

@@ -0,0 +1,68 @@
package com.ruoyi.loanpricing.service;
import com.ruoyi.common.exception.ServiceException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Service
public class SensitiveFieldCryptoService
{
private final String key;
public SensitiveFieldCryptoService(@Value("${loan-pricing.sensitive.key:}") String key)
{
this.key = key;
}
public String encrypt(String plainText)
{
validateKey();
if (!StringUtils.hasText(plainText))
{
return plainText;
}
try
{
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)));
}
catch (Exception ex)
{
throw new ServiceException("贷款定价敏感字段加密失败");
}
}
public String decrypt(String cipherText)
{
validateKey();
if (!StringUtils.hasText(cipherText))
{
return cipherText;
}
try
{
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8);
}
catch (Exception ex)
{
throw new ServiceException("贷款定价敏感字段解密失败");
}
}
private void validateKey()
{
if (!StringUtils.hasText(key))
{
throw new IllegalStateException("loan-pricing.sensitive.key 未配置");
}
}
}

View File

@@ -0,0 +1,262 @@
package com.ruoyi.loanpricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
import com.ruoyi.loanpricing.service.LoanPricingModelService;
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
import com.ruoyi.loanpricing.util.LoanPricingConverter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.annotation.Resource;
/**
* 利率定价流程Service业务层处理
*
* @author ruoyi
* @date 2025-01-19
*/
@Service
public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowService
{
@Resource
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Resource
private LoanPricingModelService loanPricingModelService;
@Resource
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Resource
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Resource
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@Resource
private LoanPricingSensitiveDisplayService loanPricingSensitiveDisplayService;
/**
* 发起利率定价流程
*
* @param loanPricingWorkflow 利率定价流程信息
* @return 结果
*/
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow)
{
// 自动生成业务方流水号(时间戳)
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String serialNum = sdf.format(new Date());
loanPricingWorkflow.setSerialNum(serialNum);
// 设置默认值
if (!StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
{
loanPricingWorkflow.setOrgCode("892000");
}
if (!StringUtils.hasText(loanPricingWorkflow.getRunType()))
{
loanPricingWorkflow.setRunType("1");
}
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
loanPricingModelService.invokeModelAsync(loanPricingWorkflow.getId());
return loanPricingWorkflow;
}
/**
* 发起个人客户利率定价流程
*
* @param dto 个人客户发起DTO
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
return createLoanPricing(entity);
}
/**
* 发起企业客户利率定价流程
*
* @param dto 企业客户发起DTO
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
return createLoanPricing(entity);
}
/**
* 查询利率定价流程列表
*
* @param loanPricingWorkflow 利率定价流程信息
* @return 利率定价流程
*/
@Override
public List<LoanPricingWorkflow> selectLoanPricingList(LoanPricingWorkflow loanPricingWorkflow)
{
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = buildQueryWrapper(loanPricingWorkflow);
// 按更新时间倒序
wrapper.orderByDesc(LoanPricingWorkflow::getUpdateTime);
return loanPricingWorkflowMapper.selectList(wrapper);
}
/**
* 分页查询利率定价流程列表
*
* @param page 分页参数
* @param loanPricingWorkflow 利率定价流程信息
* @return 利率定价流程
*/
@Override
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
{
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
pageResult.getRecords().forEach(row -> row.setCustName(
loanPricingSensitiveDisplayService.maskCustName(
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
return pageResult;
}
/**
* 查询利率定价流程详情
*
* @param serialNum 业务方流水号
* @return 利率定价流程
*/
@Override
public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum)
{
LoanPricingWorkflowVO loanPricingWorkflowVO = new LoanPricingWorkflowVO();
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(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));
loanPricingWorkflowVO.setLoanPricingWorkflow(loanPricingWorkflow);
if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){
if (loanPricingWorkflow.getCustType().equals("个人")){
ModelRetailOutputFields modelRetailOutputFields = modelRetailOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelRetailOutputFields))
{
maskModelRetailOutputBasicInfo(modelRetailOutputFields);
loanPricingWorkflow.setLoanRate(modelRetailOutputFields.getFinalCalculateRate());
}
loanPricingWorkflowVO.setModelRetailOutputFields(modelRetailOutputFields);
}
if (loanPricingWorkflow.getCustType().equals("企业")){
ModelCorpOutputFields modelCorpOutputFields = modelCorpOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelCorpOutputFields))
{
maskModelCorpOutputBasicInfo(modelCorpOutputFields);
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate());
}
loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields);
}
}
return loanPricingWorkflowVO;
}
/**
* 构建查询条件
*
* @param loanPricingWorkflow 利率定价流程信息
* @return LambdaQueryWrapper
*/
private LambdaQueryWrapper<LoanPricingWorkflow> buildQueryWrapper(LoanPricingWorkflow loanPricingWorkflow)
{
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
// 按创建者筛选
if (StringUtils.hasText(loanPricingWorkflow.getCreateBy()))
{
wrapper.like(LoanPricingWorkflow::getCreateBy, loanPricingWorkflow.getCreateBy());
}
// 按客户内码模糊查询
if (StringUtils.hasText(loanPricingWorkflow.getCustIsn()))
{
wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
}
// 按机构号筛选
if (StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
{
wrapper.like(LoanPricingWorkflow::getOrgCode, loanPricingWorkflow.getOrgCode());
}
return wrapper;
}
private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields)
{
modelRetailOutputFields.setCustName(
loanPricingSensitiveDisplayService.maskCustName(modelRetailOutputFields.getCustName()));
modelRetailOutputFields.setIdNum(
loanPricingSensitiveDisplayService.maskIdNum(modelRetailOutputFields.getIdNum()));
}
private void maskModelCorpOutputBasicInfo(ModelCorpOutputFields modelCorpOutputFields)
{
modelCorpOutputFields.setCustName(
loanPricingSensitiveDisplayService.maskCustName(modelCorpOutputFields.getCustName()));
modelCorpOutputFields.setIdNum(
loanPricingSensitiveDisplayService.maskIdNum(modelCorpOutputFields.getIdNum()));
}
/**
* 设定执行利率
*
* @param serialNum 业务方流水号
* @param executeRate 执行利率
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean setExecuteRate(String serialNum, String executeRate) {
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
LoanPricingWorkflow workflow = loanPricingWorkflowMapper.selectOne(wrapper);
if (workflow == null) {
return false;
}
workflow.setExecuteRate(executeRate);
int result = loanPricingWorkflowMapper.updateById(workflow);
return result > 0;
}
}

View File

@@ -0,0 +1,67 @@
package com.ruoyi.loanpricing.util;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
/**
* 利率定价转换器
*
* @author ruoyi
* @date 2025-01-19
*/
public class LoanPricingConverter {
/**
* 个人客户DTO转Entity
*
* @param dto 个人客户发起DTO
* @return 利率定价流程实体
*/
public static LoanPricingWorkflow toEntity(PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("个人");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setLoanTerm(dto.getLoanTerm());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射个人特有字段
entity.setBizProof(dto.getBizProof());
entity.setLoanLoop(dto.getLoanLoop());
return entity;
}
/**
* 企业客户DTO转Entity
*
* @param dto 企业客户发起DTO
* @return 利率定价流程实体
*/
public static LoanPricingWorkflow toEntity(CorporateLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("企业");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射企业特有字段
entity.setIsAgriGuar(dto.getIsAgriGuar());
entity.setIsGreenLoan(dto.getIsGreenLoan());
entity.setIsTechEnt(dto.getIsTechEnt());
entity.setIsTradeConstruction(dto.getIsTradeConstruction());
entity.setLoanTerm(dto.getLoanTerm());
return entity;
}
}

View File

@@ -0,0 +1,68 @@
{
"traceId": "350626558347246735E7F4722CUZRWOMNRR53O0",
"cost": 2267,
"tokenId": "17364055486305E7F4722M8IPFWNL8TOBEB",
"mappingOutputFields": {
"custIsn": "CUST20260121001",
"custType": "企业客户",
"guarType": "抵押担保",
"custName": "北京智联科技有限公司",
"idType": "营业执照",
"idNum": "91110108MA00XXXXXX",
"baseLoanRate": "3.45",
"isFirstLoan": "N",
"faithDay": "730",
"bpFirstLoan": "0",
"bpAgeLoan": "5.2",
"totalBpLoyalty": "8.5",
"balanceAvg": "5000000.00",
"loanAvg": "3000000.00",
"derivationRate": "1.8",
"totalBpContribution": "12.3",
"midEntConnect": "100000.00",
"midEntEffect": "50000.00",
"midEntInter": "80000.00",
"midEntAccept": "200000.00",
"midEntDiscount": "150000.00",
"midEntEleDdc": "30000.00",
"midEntWaterDdc": "10000.00",
"midEntTax": "40000.00",
"bpMid": "6.8",
"payroll": "200",
"invLoanAmount": "2500000.00",
"bpPayroll": "4.1",
"isCleanEnt": "Y",
"hasSettleAcct": "Y",
"isAgriGuar": "N",
"isGreenLoan": "Y",
"isTechEnt": "Y",
"bpEntType": "7.5",
"totoalBpRelevance": "9.2",
"loanTerm": "36",
"bpLoanTerm": "3.3",
"applyAmt": "5000000.00",
"bpLoanAmount": "5.8",
"collType": "房产抵押",
"collThirdParty": "N",
"bpCollateral": "4.5",
"greyCust": "N",
"prinOverdue": "N",
"interestOverdue": "N",
"cardOverdue": "N",
"bpGreyOverdue": "0",
"totoalBpRisk": "1.2",
"totalBp": "48.2",
"calculateRate": "3.932"
},
"extensionMap": {},
"reasonMessage": "Running successfully",
"bizTime": 1736405548630,
"outputFields": {},
"workflowCode": "TBKH",
"orgCode": "802000",
"bizId": "2025010914345",
"reasonCode": 200,
"workflowVersion": 14,
"callTime": 1736405548630,
"status": 1
}

View File

@@ -0,0 +1,73 @@
{
"traceId": "350626558347246735E7F4722CUZRWOMNRR53O0",
"cost": 2267,
"tokenId": "17364055486305E7F4722M8IPFWNL8TOBEB",
"mappingOutputFields": {
"custIsn": "CUST20260121001",
"custType": "个人",
"guarType": "信用担保",
"custName": "张三",
"idType": "身份证",
"idNum": "330106199001011234",
"baseLoanRate": "4.35",
"isFirstLoan": "是",
"faithDay": "365",
"custAge": "36",
"bpFirstLoan": "50",
"bpAgeLoan": "30",
"bpAge": "20",
"totalBpLoyalty": "95",
"balanceAvg": "50000.00",
"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",
"bpMid": "45",
"totoalBpRelevance": "90",
"applyAmt": "200000.00",
"bpLoanAmount": "60",
"loanPurpose": "个人消费",
"bizProof": "有",
"bpLoanUse": "55",
"loanLoop": "支持",
"bpLoanLoop": "40",
"collType": "无抵质押",
"collThirdParty": "否",
"bpCollateral": "0",
"greyCust": "否",
"prinOverdue": "否",
"interestOverdue": "否",
"cardOverdue": "否",
"bpGreyOverdue": "98",
"totoalBpRisk": "95",
"totalBp": "350",
"calculateRate": "6.15",
"loanRateHistory": "6.40",
"minRateProduct": "5.50",
"smoothRange": "-0.10",
"finalCalculateRate": "6.05",
"referenceRate": "5.95"
},
"extensionMap": {},
"reasonMessage": "Running successfully",
"bizTime": 1736405548630,
"outputFields": {},
"workflowCode": "TBKH",
"orgCode": "802000",
"bizId": "2025010914345",
"reasonCode": 200,
"workflowVersion": 14,
"callTime": 1736405548630,
"status": 1
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper">
<select id="selectWorkflowPageWithRates" resultType="com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO">
SELECT
lpw.serial_num AS serialNum,
lpw.cust_name AS custName,
lpw.cust_type AS custType,
lpw.guar_type AS guarType,
lpw.apply_amt AS applyAmt,
CASE
WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate
WHEN lpw.cust_type = '企业' THEN mc.calculate_rate
ELSE NULL
END AS calculateRate,
lpw.execute_rate AS executeRate,
lpw.create_time AS createTime,
lpw.create_by AS createBy
FROM loan_pricing_workflow lpw
LEFT JOIN model_retail_output_fields mr ON lpw.model_output_id = mr.id
LEFT JOIN model_corp_output_fields mc ON lpw.model_output_id = mc.id
<where>
<if test="query != null and query.createBy != null and query.createBy != ''">
AND lpw.create_by LIKE CONCAT('%', #{query.createBy}, '%')
</if>
<if test="query != null and query.custIsn != null and query.custIsn != ''">
AND lpw.cust_isn 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>
</where>
ORDER BY lpw.update_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,26 @@
package com.ruoyi.loanpricing.domain.entity;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
class ModelRetailOutputFieldsTest
{
@Test
void shouldContainLatestRetailDisplayRateFields()
{
Set<String> fieldNames = Arrays.stream(ModelRetailOutputFields.class.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toSet());
assertTrue(fieldNames.contains("loanRateHistory"), "缺少字段 loanRateHistory");
assertTrue(fieldNames.contains("minRateProduct"), "缺少字段 minRateProduct");
assertTrue(fieldNames.contains("smoothRange"), "缺少字段 smoothRange");
assertTrue(fieldNames.contains("finalCalculateRate"), "缺少字段 finalCalculateRate");
assertTrue(fieldNames.contains("referenceRate"), "缺少字段 referenceRate");
}
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.loanpricing.domain.vo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LoanPricingWorkflowListVOTest
{
@Test
void shouldExposeCalculateRateAndExecuteRateFields()
{
LoanPricingWorkflowListVO vo = new LoanPricingWorkflowListVO();
vo.setCalculateRate("6.15");
vo.setExecuteRate("5.80");
assertEquals("6.15", vo.getCalculateRate());
assertEquals("5.80", vo.getExecuteRate());
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.loanpricing.mapper;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
class LoanPricingWorkflowMapperXmlTest
{
@Test
void shouldUseRetailFinalCalculateRateInWorkflowListQuery() throws IOException
{
ClassPathResource resource = new ClassPathResource("mapper/loanpricing/LoanPricingWorkflowMapper.xml");
String xml = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
assertTrue(xml.contains("WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate"));
assertTrue(xml.contains("WHEN lpw.cust_type = '企业' THEN mc.calculate_rate"));
}
}

View File

@@ -0,0 +1,125 @@
package com.ruoyi.loanpricing.service;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.util.LoanPricingConverter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
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.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LoanPricingModelServicePersonalParamsTest {
@Mock
private ModelService modelService;
@Mock
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Mock
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Mock
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Mock
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@InjectMocks
private LoanPricingModelService loanPricingModelService;
@Test
void shouldContainLoanPurposeAndLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
}
@Test
void shouldMapLoanPurposeAndLoanTermFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setLoanPurpose("business");
dto.setLoanTerm("3");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("business", workflow.getLoanPurpose());
assertEquals("3", workflow.getLoanTerm());
}
@Test
void shouldContainLoanTermAndLoanLoopInModelInvokeDto() throws NoSuchFieldException {
assertNotNull(ModelInvokeDTO.class.getDeclaredField("loanTerm"));
assertNotNull(ModelInvokeDTO.class.getDeclaredField("loanLoop"));
}
@Test
void shouldInvokePersonalModelWithExpectedParams() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(1L);
workflow.setSerialNum("202604090001");
workflow.setOrgCode("892000");
workflow.setRunType("1");
workflow.setCustIsn("CUST001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdType("身份证");
workflow.setIdNum("cipher-id");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setLoanPurpose("business");
workflow.setLoanTerm("3");
workflow.setBizProof("true");
workflow.setLoanLoop("false");
workflow.setCollThirdParty("true");
workflow.setCollType("一类");
JSONObject response = new JSONObject();
response.put("calculateRate", "6.15");
when(loanPricingWorkflowMapper.selectById(1L)).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(modelService.invokeModel(any())).thenReturn(response);
loanPricingModelService.invokeModelAsync(1L);
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
Objects.equals("202604090001", dto.getSerialNum())
&& Objects.equals("892000", dto.getOrgCode())
&& Objects.equals("1", dto.getRunType())
&& Objects.equals("CUST001", dto.getCustIsn())
&& Objects.equals("个人", dto.getCustType())
&& Objects.equals("张三", dto.getCustName())
&& Objects.equals("身份证", dto.getIdType())
&& Objects.equals("110101199001011234", dto.getIdNum())
&& Objects.equals("信用", dto.getGuarType())
&& Objects.equals("100000", dto.getApplyAmt())
&& Objects.equals("business", 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())));
}
}

View File

@@ -0,0 +1,90 @@
package com.ruoyi.loanpricing.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Objects;
@ExtendWith(MockitoExtension.class)
class LoanPricingModelServiceTest
{
@Mock
private ModelService modelService;
@Mock
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Mock
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Mock
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Mock
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@InjectMocks
private LoanPricingModelService loanPricingModelService;
@Test
void shouldDecryptCustNameAndIdNumBeforeInvokeModel()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(1L);
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
JSONObject response = new JSONObject();
response.put("calculateRate", "6.15");
when(loanPricingWorkflowMapper.selectById(1L)).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(modelService.invokeModel(any())).thenReturn(response);
loanPricingModelService.invokeModelAsync(1L);
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
Objects.equals("张三", dto.getCustName())
&& Objects.equals("110101199001011234", dto.getIdNum())));
}
@Test
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(2L);
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
JSONObject response = new JSONObject();
response.put("calculateRate", "6.15");
when(loanPricingWorkflowMapper.selectById(2L)).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(modelService.invokeModel(any())).thenReturn(response);
loanPricingModelService.invokeModelAsync(2L);
verify(loanPricingWorkflowMapper).updateById(argThat((LoanPricingWorkflow entity) ->
!Objects.equals("张三", entity.getCustName())
&& !Objects.equals("110101199001011234", entity.getIdNum())));
}
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.loanpricing.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LoanPricingSensitiveDisplayServiceTest
{
private final LoanPricingSensitiveDisplayService displayService = new LoanPricingSensitiveDisplayService();
@Test
void shouldMaskPersonalNameAndIdNum()
{
assertEquals("张*", displayService.maskCustName("张三"));
assertEquals("1101********1234", displayService.maskIdNum("110101199001011234"));
}
@Test
void shouldMaskCorporateNameAndCreditCode()
{
assertEquals("测试****公司", displayService.maskCustName("测试科技有限公司"));
assertEquals("91*************00X", displayService.maskIdNum("91110000100000000X"));
}
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.loanpricing.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class SensitiveFieldCryptoServiceTest
{
@Test
void shouldEncryptAndDecryptCustNameAndIdNum()
{
SensitiveFieldCryptoService service = new SensitiveFieldCryptoService("1234567890abcdef");
String nameCipher = service.encrypt("张三");
String idNumCipher = service.encrypt("110101199001011234");
assertNotEquals("张三", nameCipher);
assertNotEquals("110101199001011234", idNumCipher);
assertEquals("张三", service.decrypt(nameCipher));
assertEquals("110101199001011234", service.decrypt(idNumCipher));
}
@Test
void shouldRejectBlankKeyConfiguration()
{
SensitiveFieldCryptoService service = new SensitiveFieldCryptoService("");
assertThrows(IllegalStateException.class, () -> service.encrypt("张三"));
}
}

View File

@@ -0,0 +1,256 @@
package com.ruoyi.loanpricing.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.verify;
import static org.mockito.Mockito.when;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
import com.ruoyi.loanpricing.service.LoanPricingModelService;
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.Objects;
@ExtendWith(MockitoExtension.class)
class LoanPricingWorkflowServiceImplTest
{
@Mock
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@Mock
private LoanPricingModelService loanPricingModelService;
@Mock
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
@Mock
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
@Mock
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@Mock
private LoanPricingSensitiveDisplayService loanPricingSensitiveDisplayService;
@InjectMocks
private LoanPricingWorkflowServiceImpl loanPricingWorkflowService;
@Test
void shouldEncryptCustNameAndIdNumBeforeInsert()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setCustIsn("CUST001");
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("CUST001", entity.getCustIsn())));
}
@Test
void shouldReturnPagedWorkflowListWithCalculateRate()
{
LoanPricingWorkflowListVO row = new LoanPricingWorkflowListVO();
row.setCalculateRate("6.15");
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
pageResult.setRecords(java.util.List.of(row));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(pageResult);
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
assertEquals("6.15", result.getRecords().get(0).getCalculateRate());
}
@Test
void shouldMaskCustNameWhenReturningPagedWorkflowList()
{
LoanPricingWorkflowListVO row = new LoanPricingWorkflowListVO();
row.setCustName("cipher-name");
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("张三");
when(loanPricingSensitiveDisplayService.maskCustName("张三")).thenReturn("张*");
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
assertEquals("张*", result.getRecords().get(0).getCustName());
}
@Test
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition()
{
LoanPricingWorkflow query = new LoanPricingWorkflow();
query.setCustIsn("CUST001");
query.setCustName("张三");
when(loanPricingWorkflowMapper.selectList(any())).thenReturn(Collections.emptyList());
loanPricingWorkflowService.selectLoanPricingList(query);
ArgumentCaptor<LambdaQueryWrapper<LoanPricingWorkflow>> wrapperCaptor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(loanPricingWorkflowMapper).selectList(wrapperCaptor.capture());
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = wrapperCaptor.getValue();
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), LoanPricingWorkflow.class);
String sqlSegment = wrapper.getSqlSegment();
assertTrue(sqlSegment.contains("cust_isn"), sqlSegment);
assertTrue(!sqlSegment.contains("cust_name"), sqlSegment);
}
@Test
void shouldUseRetailModelOutputFinalCalculateRateForWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setModelOutputId(11L);
workflow.setLoanRate("4.35");
ModelRetailOutputFields retailOutputFields = new ModelRetailOutputFields();
retailOutputFields.setCalculateRate("6.15");
retailOutputFields.setFinalCalculateRate("6.05");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelRetailOutputFieldsMapper.selectById(11L)).thenReturn(retailOutputFields);
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
assertEquals("6.05", result.getLoanPricingWorkflow().getLoanRate());
assertEquals("6.15", result.getModelRetailOutputFields().getCalculateRate());
assertEquals("6.05", result.getModelRetailOutputFields().getFinalCalculateRate());
}
@Test
void shouldMaskCustNameAndIdNumWhenReturningWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
when(loanPricingSensitiveDisplayService.maskCustName("张三")).thenReturn("张*");
when(loanPricingSensitiveDisplayService.maskIdNum("110101199001011234")).thenReturn("1101********1234");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
assertEquals("张*", result.getLoanPricingWorkflow().getCustName());
assertEquals("1101********1234", result.getLoanPricingWorkflow().getIdNum());
}
@Test
void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setModelOutputId(11L);
ModelRetailOutputFields retailOutputFields = new ModelRetailOutputFields();
retailOutputFields.setCustName("张三");
retailOutputFields.setIdNum("110101199001011234");
retailOutputFields.setCalculateRate("6.15");
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");
when(loanPricingSensitiveDisplayService.maskCustName("张三")).thenReturn("张*");
when(loanPricingSensitiveDisplayService.maskIdNum("110101199001011234")).thenReturn("1101********1234");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001");
assertEquals("张*", result.getModelRetailOutputFields().getCustName());
assertEquals("1101********1234", result.getModelRetailOutputFields().getIdNum());
}
@Test
void shouldUseCorporateModelOutputCalculateRateForWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("C20260328001");
workflow.setCustType("企业");
workflow.setModelOutputId(22L);
workflow.setLoanRate("3.80");
ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields();
corpOutputFields.setCalculateRate("3.932");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(modelCorpOutputFieldsMapper.selectById(22L)).thenReturn(corpOutputFields);
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001");
assertEquals("3.932", result.getLoanPricingWorkflow().getLoanRate());
assertEquals("3.932", result.getModelCorpOutputFields().getCalculateRate());
}
@Test
void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("C20260328001");
workflow.setCustType("企业");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setModelOutputId(22L);
ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields();
corpOutputFields.setCustName("测试科技有限公司");
corpOutputFields.setIdNum("91110000100000000X");
corpOutputFields.setCalculateRate("3.932");
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");
when(loanPricingSensitiveDisplayService.maskCustName("测试科技有限公司")).thenReturn("测试****公司");
when(loanPricingSensitiveDisplayService.maskIdNum("91110000100000000X")).thenReturn("91*************00X");
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001");
assertEquals("测试****公司", result.getModelCorpOutputFields().getCustName());
assertEquals("91*************00X", result.getModelCorpOutputFields().getIdNum());
}
}