Split model URLs for personal and corporate clients

This commit is contained in:
wkc
2026-04-27 15:22:08 +08:00
parent 5a80653917
commit 571f7bc075
20 changed files with 393 additions and 158 deletions

View File

@@ -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){

View File

@@ -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")

View File

@@ -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();

View File

@@ -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("调用模型失败");

View File

@@ -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())

View File

@@ -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;
}
}
}