Split model URLs for personal and corporate clients
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -54,4 +54,7 @@ ruoyi-ui/tests
|
||||
.playwright-cli
|
||||
|
||||
tongweb_63310.properties
|
||||
audit.log
|
||||
audit.log
|
||||
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
@@ -0,0 +1,35 @@
|
||||
# 对公还款方式移除与抵质押字段联动实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 对公新增弹窗移除 `还款方式` 输入项、初始化字段、重置字段、必填校验和提交字段。
|
||||
- 对公详情页与模型输出展示移除 `还款方式`。
|
||||
- 对公、对私新增弹窗中,`担保方式` 为 `抵押` 或 `质押` 时才展示 `抵质押类型`、`抵质押物是否第三方所有`。
|
||||
- `抵质押类型` 根据担保方式动态切换:
|
||||
- `抵押`:`一类`、`二类`、`三类`、`四类`、`其他`
|
||||
- `质押`:`存单质押`、`其他`
|
||||
- 担保方式切换时清空已选抵质押类型和第三方所有标识,隐藏抵质押字段时不向后端提交。
|
||||
- 对公创建 DTO 取消 `repayMethod` 必填与枚举校验;`collType` 不再全局必填,合法值调整为 `一类/二类/三类/四类/其他/存单质押`。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 前端静态测试通过:
|
||||
- `npm run test:corporate-create-input-params`
|
||||
- `npm run test:corporate-display-fields`
|
||||
- `npm run test:personal-create-input-params`
|
||||
- 后端编译与单测通过:
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 后端接口验证通过:
|
||||
- `信用` 不传 `repayMethod`、不传抵质押字段可创建。
|
||||
- `抵押` 传 `一类` 且不传 `repayMethod` 可创建。
|
||||
- `质押` 传 `存单质押` 且不传 `repayMethod` 可创建。
|
||||
- 缺少 `custIsn`、缺少 `guarType`、非法 `guarType` 仍返回参数错误。
|
||||
- 真实前端页面验证通过:
|
||||
- 对公新增弹窗不显示 `还款方式`。
|
||||
- 对公、对私新增弹窗在 `信用/保证` 下隐藏抵质押字段。
|
||||
- 对公、对私新增弹窗在 `抵押/质押` 下显示抵质押字段,且选项分别符合规则。
|
||||
- 对公详情页与模型输出区域不再显示 `还款方式`。
|
||||
|
||||
## 说明
|
||||
|
||||
- 本次不删除数据库字段和实体字段,仅停止创建入口要求和页面展示,保留历史数据结构。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 个人/企业模型接口拆分实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将统一模型接口配置 `model.url` 拆分为 `model.personal-url` 和 `model.corporate-url`。
|
||||
- `dev`、`uat` 环境分别指向本地个人/企业 mock:
|
||||
- `http://localhost:63310/rate/pricing/mock/invokeModel/personal`
|
||||
- `http://localhost:63310/rate/pricing/mock/invokeModel/corporate`
|
||||
- `pro` 环境改为从 `MODEL_PERSONAL_URL`、`MODEL_CORPORATE_URL` 读取真实接口地址。
|
||||
- `ModelService` 拆分为 `invokePersonalModel` 和 `invokeCorporateModel`,分别返回 `ModelRetailOutputFields`、`ModelCorpOutputFields`。
|
||||
- `LoanPricingModelService` 根据 `custType` 调用对应模型接口,个人只写个人模型输出表,企业只写企业模型输出表。
|
||||
- mock 控制器拆分为个人、企业两个入口,不再保留统一 mock 路径作为业务调用入口。
|
||||
|
||||
## 字段管理
|
||||
|
||||
- 个人模型返回字段继续由 `ModelRetailOutputFields` 与 `model_retail_output_fields` 管理。
|
||||
- 企业模型返回字段继续由 `ModelCorpOutputFields` 与 `model_corp_output_fields` 管理。
|
||||
- 未新增统一返回对象,避免个人/企业字段混在同一套结构中。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 后端单测:
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,ModelRetailOutputFieldsTest,ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:通过,`Tests run: 5, Failures: 0, Errors: 0`
|
||||
- 后端打包与启动:
|
||||
- `./bin/restart_java_backend.sh restart`
|
||||
- 结果:打包成功,提升权限后启动成功,后端监听 `63310`。
|
||||
- 真实接口验证:
|
||||
- `/login/test` 获取 token 成功。
|
||||
- 调用 `/loanPricing/workflow/create/personal` 创建个人流程,流水号 `20260427150819677`。
|
||||
- 查询个人详情,返回 `modelRetailOutputFields.finalCalculateRate=6.05`,`modelCorpOutputFields=null`。
|
||||
- 调用 `/loanPricing/workflow/create/corporate` 创建企业流程,流水号 `20260427150820494`。
|
||||
- 查询企业详情,返回 `modelCorpOutputFields.finalCalculateRate=3.732`,`modelRetailOutputFields=null`。
|
||||
- 缺少 `custIsn` 的个人创建请求返回 `客户内码不能为空`。
|
||||
- 后端日志确认个人命中 `/rate/pricing/mock/invokeModel/personal`,企业命中 `/rate/pricing/mock/invokeModel/corporate`。
|
||||
- 测试结束后已执行 `./bin/restart_java_backend.sh stop` 停止本次启动的后端进程。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 生产环境启动前必须提供 `MODEL_PERSONAL_URL`、`MODEL_CORPORATE_URL`。
|
||||
- 本次不改前端页面和现有业务接口路径。
|
||||
Binary file not shown.
@@ -79,7 +79,8 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
personal-url: http://localhost:63310/rate/pricing/mock/invokeModel/personal
|
||||
corporate-url: http://localhost:63310/rate/pricing/mock/invokeModel/corporate
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
|
||||
@@ -79,8 +79,8 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
|
||||
|
||||
personal-url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
|
||||
corporate-url: http://64.202.32.40:8083/api/service/interface/invokeService/sydgllcs
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
|
||||
@@ -79,7 +79,8 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
personal-url: http://localhost:63310/rate/pricing/mock/invokeModel/personal
|
||||
corporate-url: http://localhost:63310/rate/pricing/mock/invokeModel/corporate
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
|
||||
@@ -27,17 +27,17 @@ import java.io.InputStream;
|
||||
public class LoanRatePricingMockController extends BaseController {
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "调用模型获取测算利率")
|
||||
@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");
|
||||
}
|
||||
@Operation(summary = "调用个人模型获取测算利率")
|
||||
@PostMapping("/invokeModel/personal")
|
||||
public AjaxResult invokePersonalModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
return new AjaxResult(10000, "success", loadJsonFromResource("data/retail_output.json"));
|
||||
}
|
||||
|
||||
return new AjaxResult(10000, "success", jsonNodes);
|
||||
@Anonymous
|
||||
@Operation(summary = "调用企业模型获取测算利率")
|
||||
@PostMapping("/invokeModel/corporate")
|
||||
public AjaxResult invokeCorporateModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
return new AjaxResult(10000, "success", loadJsonFromResource("data/corp_output.json"));
|
||||
}
|
||||
|
||||
private ObjectNode loadJsonFromResource(String resourcePath){
|
||||
|
||||
@@ -41,9 +41,7 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
|
||||
@NotBlank(message = "申请金额不能为空")
|
||||
private String applyAmt;
|
||||
|
||||
@Schema(description = "还款方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "分期", allowableValues = {"分期", "不分期"})
|
||||
@NotBlank(message = "还款方式不能为空")
|
||||
@Pattern(regexp = "^(分期|不分期)$", message = "还款方式必须是:分期、不分期之一")
|
||||
@Schema(description = "还款方式", example = "分期", allowableValues = {"分期", "不分期"})
|
||||
private String repayMethod;
|
||||
|
||||
@Schema(description = "借款期限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
@@ -56,9 +54,8 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
|
||||
@Schema(description = "贸易和建筑业企业标识", example = "0")
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
@Schema(description = "抵质押类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "一类", allowableValues = {"一类", "二类", "三类", "四类"})
|
||||
@NotBlank(message = "抵质押类型不能为空")
|
||||
@Pattern(regexp = "^(一类|二类|三类|四类)$", message = "抵质押类型必须是:一类、二类、三类、四类之一")
|
||||
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "其他", "存单质押"})
|
||||
@Pattern(regexp = "^(一类|二类|三类|四类|其他|存单质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、其他、存单质押之一")
|
||||
private String collType;
|
||||
|
||||
@Schema(description = "抵质押物是否三方所有", example = "0")
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -12,7 +10,6 @@ import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -68,10 +65,9 @@ public class LoanPricingModelService {
|
||||
{
|
||||
normalizeCorporateModelInvokeDTO(modelInvokeDTO);
|
||||
}
|
||||
JSONObject response = modelService.invokeModel(modelInvokeDTO);
|
||||
if (loanPricingWorkflow.getCustType().equals("个人")){
|
||||
// 个人模型
|
||||
ModelRetailOutputFields modelRetailOutputFields = JSON.parseObject(response.toJSONString(), ModelRetailOutputFields.class);
|
||||
ModelRetailOutputFields modelRetailOutputFields = modelService.invokePersonalModel(modelInvokeDTO);
|
||||
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
|
||||
log.info("个人模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
@@ -81,7 +77,7 @@ public class LoanPricingModelService {
|
||||
log.info("更新流程信息成功");
|
||||
}else if (loanPricingWorkflow.getCustType().equals("企业")){
|
||||
// 企业模型
|
||||
ModelCorpOutputFields modelCorpOutputFields = JSON.parseObject(response.toJSONString(), ModelCorpOutputFields.class);
|
||||
ModelCorpOutputFields modelCorpOutputFields = modelService.invokeCorporateModel(modelInvokeDTO);
|
||||
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
|
||||
log.info("企业模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
|
||||
@@ -3,12 +3,11 @@ 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 com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
|
||||
@@ -28,19 +27,29 @@ import java.util.Objects;
|
||||
@EnableAsync
|
||||
public class ModelService {
|
||||
|
||||
@Value("${model.url}")
|
||||
private String modelUrl;
|
||||
@Value("${model.personal-url}")
|
||||
private String personalModelUrl;
|
||||
|
||||
@Value("${model.corporate-url}")
|
||||
private String corporateModelUrl;
|
||||
|
||||
|
||||
public ModelRetailOutputFields invokePersonalModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
JSONObject mappingOutputFields = invokeModel(personalModelUrl, modelInvokeDTO);
|
||||
return JSON.parseObject(mappingOutputFields.toJSONString(), ModelRetailOutputFields.class);
|
||||
}
|
||||
|
||||
public JSONObject invokeModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
public ModelCorpOutputFields invokeCorporateModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
JSONObject mappingOutputFields = invokeModel(corporateModelUrl, modelInvokeDTO);
|
||||
return JSON.parseObject(mappingOutputFields.toJSONString(), ModelCorpOutputFields.class);
|
||||
}
|
||||
|
||||
private JSONObject invokeModel(String modelUrl, 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;
|
||||
return response.getJSONObject("data").getJSONObject("mappingOutputFields");
|
||||
}else{
|
||||
log.error("------------------->调用模型失败,失败原因为:" + response.getString("message"));
|
||||
throw new ServiceException("调用模型失败");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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.domain.entity.ModelRetailOutputFields;
|
||||
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
@@ -94,17 +94,17 @@ class LoanPricingModelServicePersonalParamsTest {
|
||||
workflow.setCollThirdParty("true");
|
||||
workflow.setCollType("一类");
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
ModelRetailOutputFields response = new ModelRetailOutputFields();
|
||||
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.invokeModel(any())).thenReturn(response);
|
||||
when(modelService.invokePersonalModel(any())).thenReturn(response);
|
||||
|
||||
loanPricingModelService.invokeModelAsync(1L);
|
||||
|
||||
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
|
||||
verify(modelService).invokePersonalModel(argThat((ModelInvokeDTO dto) ->
|
||||
Objects.equals("202604090001", dto.getSerialNum())
|
||||
&& Objects.equals("892000", dto.getOrgCode())
|
||||
&& Objects.equals("1", dto.getRunType())
|
||||
|
||||
@@ -1,95 +1,54 @@
|
||||
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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
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 java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
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()
|
||||
void shouldDecryptCustNameAndIdNumBeforeInvokePersonalModel() throws Exception
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(1L);
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
TestContext context = createContext(personalWorkflow(1L));
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
context.service.invokeModelAsync(1L);
|
||||
|
||||
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())));
|
||||
assertEquals("张三", context.modelService.personalRequest.getCustName());
|
||||
assertEquals("110101199001011234", context.modelService.personalRequest.getIdNum());
|
||||
assertEquals(1, context.modelService.personalCalls.get());
|
||||
assertEquals(0, context.modelService.corporateCalls.get());
|
||||
assertEquals(1, context.retailInsertCount.get());
|
||||
assertEquals(0, context.corpInsertCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow()
|
||||
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow() throws Exception
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(2L);
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
TestContext context = createContext(personalWorkflow(2L));
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
context.service.invokeModelAsync(2L);
|
||||
|
||||
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())));
|
||||
LoanPricingWorkflow updatedWorkflow = context.updatedWorkflow.get();
|
||||
assertNotEquals("张三", updatedWorkflow.getCustName());
|
||||
assertNotEquals("110101199001011234", updatedWorkflow.getIdNum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeCorporateUploadParamsBeforeInvokeModel()
|
||||
void shouldNormalizeCorporateUploadParamsBeforeInvokeCorporateModel() throws Exception
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(3L);
|
||||
@@ -100,21 +59,161 @@ class LoanPricingModelServiceTest
|
||||
workflow.setIsGreenLoan("true");
|
||||
workflow.setIsTradeBuildEnt("false");
|
||||
workflow.setCollThirdParty("true");
|
||||
TestContext context = createContext(workflow);
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "4.15");
|
||||
context.service.invokeModelAsync(3L);
|
||||
|
||||
when(loanPricingWorkflowMapper.selectById(3L)).thenReturn(workflow);
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试公司");
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91330000123456789X");
|
||||
when(modelService.invokeModel(any())).thenReturn(response);
|
||||
ModelInvokeDTO request = context.modelService.corporateRequest;
|
||||
assertEquals("分期", request.getRepayMethod());
|
||||
assertEquals("1", request.getIsGreenLoan());
|
||||
assertEquals("0", request.getIsTradeBuildEnt());
|
||||
assertEquals("1", request.getCollThirdParty());
|
||||
assertEquals(0, context.modelService.personalCalls.get());
|
||||
assertEquals(1, context.modelService.corporateCalls.get());
|
||||
assertEquals(0, context.retailInsertCount.get());
|
||||
assertEquals(1, context.corpInsertCount.get());
|
||||
}
|
||||
|
||||
loanPricingModelService.invokeModelAsync(3L);
|
||||
private static LoanPricingWorkflow personalWorkflow(Long id)
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(id);
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
return workflow;
|
||||
}
|
||||
|
||||
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
|
||||
Objects.equals("分期", dto.getRepayMethod())
|
||||
&& Objects.equals("1", dto.getIsGreenLoan())
|
||||
&& Objects.equals("0", dto.getIsTradeBuildEnt())
|
||||
&& Objects.equals("1", dto.getCollThirdParty())));
|
||||
private static TestContext createContext(LoanPricingWorkflow workflow) throws Exception
|
||||
{
|
||||
TestContext context = new TestContext();
|
||||
context.service = new LoanPricingModelService();
|
||||
context.modelService = new CapturingModelService();
|
||||
context.workflow = workflow;
|
||||
context.updatedWorkflow = new AtomicReference<>();
|
||||
context.retailInsertCount = new AtomicInteger();
|
||||
context.corpInsertCount = new AtomicInteger();
|
||||
|
||||
setField(context.service, "modelService", context.modelService);
|
||||
setField(context.service, "loanPricingWorkflowMapper",
|
||||
workflowMapper(context.workflow, context.updatedWorkflow));
|
||||
setField(context.service, "modelRetailOutputFieldsMapper",
|
||||
insertCountingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount));
|
||||
setField(context.service, "modelCorpOutputFieldsMapper",
|
||||
insertCountingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount));
|
||||
setField(context.service, "sensitiveFieldCryptoService", new TestSensitiveFieldCryptoService());
|
||||
return context;
|
||||
}
|
||||
|
||||
private static LoanPricingWorkflowMapper workflowMapper(
|
||||
LoanPricingWorkflow workflow, AtomicReference<LoanPricingWorkflow> updatedWorkflow)
|
||||
{
|
||||
return proxy(LoanPricingWorkflowMapper.class, (proxy, method, args) -> {
|
||||
if ("selectById".equals(method.getName()))
|
||||
{
|
||||
return workflow;
|
||||
}
|
||||
if ("updateById".equals(method.getName()))
|
||||
{
|
||||
updatedWorkflow.set((LoanPricingWorkflow) args[0]);
|
||||
return 1;
|
||||
}
|
||||
return defaultValue(method);
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> T insertCountingMapper(Class<T> mapperClass, AtomicInteger insertCount)
|
||||
{
|
||||
return proxy(mapperClass, (proxy, method, args) -> {
|
||||
if ("insert".equals(method.getName()))
|
||||
{
|
||||
insertCount.incrementAndGet();
|
||||
return 1;
|
||||
}
|
||||
return defaultValue(method);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T proxy(Class<T> mapperClass, InvocationHandler invocationHandler)
|
||||
{
|
||||
return (T) Proxy.newProxyInstance(
|
||||
mapperClass.getClassLoader(), new Class<?>[] { mapperClass }, invocationHandler);
|
||||
}
|
||||
|
||||
private static Object defaultValue(Method method)
|
||||
{
|
||||
if (method.getReturnType().equals(boolean.class))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.getReturnType().isPrimitive())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = LoanPricingModelService.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private static class TestContext
|
||||
{
|
||||
private LoanPricingModelService service;
|
||||
private CapturingModelService modelService;
|
||||
private LoanPricingWorkflow workflow;
|
||||
private AtomicReference<LoanPricingWorkflow> updatedWorkflow;
|
||||
private AtomicInteger retailInsertCount;
|
||||
private AtomicInteger corpInsertCount;
|
||||
}
|
||||
|
||||
private static class CapturingModelService extends ModelService
|
||||
{
|
||||
private final AtomicInteger personalCalls = new AtomicInteger();
|
||||
private final AtomicInteger corporateCalls = new AtomicInteger();
|
||||
private ModelInvokeDTO personalRequest;
|
||||
private ModelInvokeDTO corporateRequest;
|
||||
|
||||
@Override
|
||||
public ModelRetailOutputFields invokePersonalModel(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
personalCalls.incrementAndGet();
|
||||
personalRequest = modelInvokeDTO;
|
||||
return new ModelRetailOutputFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCorpOutputFields invokeCorporateModel(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
corporateCalls.incrementAndGet();
|
||||
corporateRequest = modelInvokeDTO;
|
||||
return new ModelCorpOutputFields();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -59,14 +59,6 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="还款方式" prop="repayMethod">
|
||||
<el-select v-model="form.repayMethod" placeholder="请选择还款方式" style="width: 100%">
|
||||
<el-option label="分期" value="分期"/>
|
||||
<el-option label="不分期" value="不分期"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 企业标识 -->
|
||||
@@ -85,20 +77,17 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 抵质押信息 -->
|
||||
<el-divider content-position="left">抵质押信息</el-divider>
|
||||
<el-row>
|
||||
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
|
||||
<el-row v-if="isCollateralGuarantee">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押类型" prop="collType">
|
||||
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
|
||||
<el-option label="一类" value="一类"/>
|
||||
<el-option label="二类" value="二类"/>
|
||||
<el-option label="三类" value="三类"/>
|
||||
<el-option label="四类" value="四类"/>
|
||||
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
|
||||
<el-form-item label="抵质押物是否第三方所有" prop="collThirdParty">
|
||||
<el-switch v-model="form.collThirdParty"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -168,7 +157,6 @@ export default {
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanTerm: undefined,
|
||||
repayMethod: undefined,
|
||||
isGreenLoan: false,
|
||||
isTradeBuildEnt: false,
|
||||
collType: undefined,
|
||||
@@ -198,9 +186,6 @@ export default {
|
||||
loanTerm: [
|
||||
{required: true, validator: validateLoanTerm, trigger: "change"}
|
||||
],
|
||||
repayMethod: [
|
||||
{required: true, message: "请选择还款方式", trigger: "change"}
|
||||
],
|
||||
collType: [
|
||||
{required: true, message: "请选择抵质押类型", trigger: "change"}
|
||||
]
|
||||
@@ -215,6 +200,18 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
isCollateralGuarantee() {
|
||||
return this.form.guarType === '抵押' || this.form.guarType === '质押'
|
||||
},
|
||||
collateralTypeOptions() {
|
||||
if (this.form.guarType === '抵押') {
|
||||
return ['一类', '二类', '三类', '四类', '其他']
|
||||
}
|
||||
if (this.form.guarType === '质押') {
|
||||
return ['存单质押', '其他']
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -222,6 +219,11 @@ export default {
|
||||
if (val) {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
'form.guarType'(val, oldVal) {
|
||||
if (val !== oldVal) {
|
||||
this.resetCollateralFields()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -237,7 +239,6 @@ export default {
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanTerm: undefined,
|
||||
repayMethod: undefined,
|
||||
isGreenLoan: false,
|
||||
isTradeBuildEnt: false,
|
||||
collType: undefined,
|
||||
@@ -255,6 +256,16 @@ export default {
|
||||
this.dialogVisible = false
|
||||
this.reset()
|
||||
},
|
||||
/** 清空抵质押字段 */
|
||||
resetCollateralFields() {
|
||||
this.form.collType = undefined
|
||||
this.form.collThirdParty = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate(['collType', 'collThirdParty'])
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
@@ -264,8 +275,13 @@ export default {
|
||||
const data = {
|
||||
...this.form,
|
||||
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
|
||||
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0',
|
||||
collThirdParty: this.form.collThirdParty ? '1' : '0'
|
||||
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
|
||||
}
|
||||
if (this.isCollateralGuarantee) {
|
||||
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
|
||||
} else {
|
||||
delete data.collType
|
||||
delete data.collThirdParty
|
||||
}
|
||||
|
||||
createCorporateWorkflow(data).then(response => {
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="还款方式">{{ detailData.repayMethod || '-' }}</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 label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">贷款特征</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="还款方式">{{ corpOutput.repayMethod || '-' }}</el-descriptions-item>
|
||||
<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>
|
||||
|
||||
@@ -82,19 +82,17 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 抵质押信息 -->
|
||||
<el-divider content-position="left">抵质押信息</el-divider>
|
||||
<el-row>
|
||||
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
|
||||
<el-row v-if="isCollateralGuarantee">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押类型" prop="collType">
|
||||
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
|
||||
<el-option label="一类" value="一类"/>
|
||||
<el-option label="二类" value="二类"/>
|
||||
<el-option label="三类" value="三类"/>
|
||||
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
|
||||
<el-form-item label="抵质押物是否第三方所有" prop="collThirdParty">
|
||||
<el-switch v-model="form.collThirdParty"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -194,6 +192,18 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
isCollateralGuarantee() {
|
||||
return this.form.guarType === '抵押' || this.form.guarType === '质押'
|
||||
},
|
||||
collateralTypeOptions() {
|
||||
if (this.form.guarType === '抵押') {
|
||||
return ['一类', '二类', '三类', '四类', '其他']
|
||||
}
|
||||
if (this.form.guarType === '质押') {
|
||||
return ['存单质押', '其他']
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -201,6 +211,11 @@ export default {
|
||||
if (val) {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
'form.guarType'(val, oldVal) {
|
||||
if (val !== oldVal) {
|
||||
this.resetCollateralFields()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -234,6 +249,16 @@ export default {
|
||||
this.dialogVisible = false
|
||||
this.reset()
|
||||
},
|
||||
/** 清空抵质押字段 */
|
||||
resetCollateralFields() {
|
||||
this.form.collType = undefined
|
||||
this.form.collThirdParty = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate(['collType', 'collThirdParty'])
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
@@ -243,8 +268,13 @@ export default {
|
||||
const data = {
|
||||
...this.form,
|
||||
bizProof: this.form.bizProof ? '1' : '0',
|
||||
loanLoop: this.form.loanLoop ? '1' : '0',
|
||||
collThirdParty: this.form.collThirdParty ? '1' : '0'
|
||||
loanLoop: this.form.loanLoop ? '1' : '0'
|
||||
}
|
||||
if (this.isCollateralGuarantee) {
|
||||
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
|
||||
} else {
|
||||
delete data.collType
|
||||
delete data.collThirdParty
|
||||
}
|
||||
|
||||
createPersonalWorkflow(data).then(response => {
|
||||
|
||||
@@ -33,11 +33,18 @@ assert(
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreateDialog.includes('label="一类"') &&
|
||||
personalCreateDialog.includes('label="二类"') &&
|
||||
personalCreateDialog.includes('label="三类"') &&
|
||||
personalCreateDialog.includes('v-if="isCollateralGuarantee"') &&
|
||||
personalCreateDialog.includes("this.form.guarType === '抵押' || this.form.guarType === '质押'") &&
|
||||
personalCreateDialog.includes('resetCollateralFields()'),
|
||||
'个人新增弹窗抵质押信息未按担保方式显示并清空'
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreateDialog.includes('collateralTypeOptions') &&
|
||||
personalCreateDialog.includes("return ['一类', '二类', '三类', '四类', '其他']") &&
|
||||
personalCreateDialog.includes("return ['存单质押', '其他']") &&
|
||||
!personalCreateDialog.includes('label="一线"'),
|
||||
'个人新增弹窗抵质押类型选项未按 Excel 对齐'
|
||||
'个人新增弹窗抵质押类型选项未按担保方式动态切换'
|
||||
)
|
||||
|
||||
assert(
|
||||
@@ -48,8 +55,10 @@ assert(
|
||||
assert(
|
||||
personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") &&
|
||||
personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") &&
|
||||
personalCreateDialog.includes("collThirdParty: this.form.collThirdParty ? '1' : '0'"),
|
||||
'个人新增弹窗开关字段未按 1/0 提交'
|
||||
personalCreateDialog.includes("data.collThirdParty = this.form.collThirdParty ? '1' : '0'") &&
|
||||
personalCreateDialog.includes('delete data.collType') &&
|
||||
personalCreateDialog.includes('delete data.collThirdParty'),
|
||||
'个人新增弹窗开关字段或非抵质押提交字段处理不正确'
|
||||
)
|
||||
|
||||
assert(
|
||||
|
||||
Reference in New Issue
Block a user