diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 201926e..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index babb65a..0b3dcfa 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,7 @@ ruoyi-ui/tests .playwright-cli tongweb_63310.properties -audit.log \ No newline at end of file +audit.log + +.DS_Store +*/.DS_Store \ No newline at end of file diff --git a/doc/implementation-report-2026-04-27-corporate-repay-collateral-linkage.md b/doc/implementation-report-2026-04-27-corporate-repay-collateral-linkage.md new file mode 100644 index 0000000..7f4088c --- /dev/null +++ b/doc/implementation-report-2026-04-27-corporate-repay-collateral-linkage.md @@ -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` 仍返回参数错误。 +- 真实前端页面验证通过: + - 对公新增弹窗不显示 `还款方式`。 + - 对公、对私新增弹窗在 `信用/保证` 下隐藏抵质押字段。 + - 对公、对私新增弹窗在 `抵押/质押` 下显示抵质押字段,且选项分别符合规则。 + - 对公详情页与模型输出区域不再显示 `还款方式`。 + +## 说明 + +- 本次不删除数据库字段和实体字段,仅停止创建入口要求和页面展示,保留历史数据结构。 diff --git a/doc/implementation-report-2026-04-27-model-interface-split.md b/doc/implementation-report-2026-04-27-model-interface-split.md new file mode 100644 index 0000000..0ff9ade --- /dev/null +++ b/doc/implementation-report-2026-04-27-model-interface-split.md @@ -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`。 +- 本次不改前端页面和现有业务接口路径。 diff --git a/doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx b/doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx deleted file mode 100644 index 3c3a794..0000000 Binary files a/doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx and /dev/null differ diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index ccf100f..b77858f 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -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: diff --git a/ruoyi-admin/src/main/resources/application-pro.yml b/ruoyi-admin/src/main/resources/application-pro.yml index 3e27510..6872549 100644 --- a/ruoyi-admin/src/main/resources/application-pro.yml +++ b/ruoyi-admin/src/main/resources/application-pro.yml @@ -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" diff --git a/ruoyi-admin/src/main/resources/application-uat.yml b/ruoyi-admin/src/main/resources/application-uat.yml index 452ef82..b06d5ec 100644 --- a/ruoyi-admin/src/main/resources/application-uat.yml +++ b/ruoyi-admin/src/main/resources/application-uat.yml @@ -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: diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java index 55e7707..09bf49b 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java @@ -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){ diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java index 8164ec1..f9c5666 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java @@ -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") diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java index e987db3..6a588b6 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java @@ -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(); diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java index 73fd9cf..65f371c 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java @@ -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 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("调用模型失败"); diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java index f777773..202aa67 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java @@ -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()) diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java index a76133b..7aa060d 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java @@ -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 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 insertCountingMapper(Class 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 proxy(Class 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 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; + } } } diff --git a/ruoyi-ui/dist.zip b/ruoyi-ui/dist.zip deleted file mode 100644 index 20132d9..0000000 Binary files a/ruoyi-ui/dist.zip and /dev/null differ diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue index 4a07d18..537d768 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue @@ -59,14 +59,6 @@ - - - - - - - - @@ -85,20 +77,17 @@ - 抵质押信息 - + 抵质押信息 + - - - - + - + @@ -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 => { diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue index 68b9ffb..fe97011 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue @@ -80,7 +80,6 @@ {{ detailData.guarType }} {{ detailData.applyAmt }} 元 - {{ detailData.repayMethod || '-' }} {{ formatBoolean(detailData.isGreenLoan) }} {{ formatBoolean(detailData.isTradeBuildEnt) }} {{ detailData.collType || '-' }} diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue index 25edb02..df73600 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue @@ -182,7 +182,6 @@

贷款特征

- {{ corpOutput.repayMethod || '-' }} {{ corpOutput.loanTerm || '-' }} {{ corpOutput.bpLoanTerm || '-' }} {{ corpOutput.applyAmt || '-' }} diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue index 910cb95..2ce148a 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue @@ -82,19 +82,17 @@ - 抵质押信息 - + 抵质押信息 + - - - + - + @@ -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 => { diff --git a/ruoyi-ui/tests/personal-create-input-params.test.js b/ruoyi-ui/tests/personal-create-input-params.test.js index 88ac252..4ee6149 100644 --- a/ruoyi-ui/tests/personal-create-input-params.test.js +++ b/ruoyi-ui/tests/personal-create-input-params.test.js @@ -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(