From 6ef3cfcaea168ec900fca02f2044bf6803848442 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Mon, 11 May 2026 18:02:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=B8=8A=E8=99=9E=E5=88=A9?= =?UTF-8?q?=E7=8E=87=E5=AE=9A=E4=BB=B7=E5=AD=97=E6=AE=B5=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-05-11-shangyu-pricing-field-adjustment.md | 76 +++++++++ .../dto/CorporateLoanPricingCreateDTO.java | 11 +- .../domain/dto/ModelInvokeDTO.java | 5 + .../dto/PersonalLoanPricingCreateDTO.java | 17 +-- .../domain/entity/LoanPricingWorkflow.java | 5 +- .../impl/LoanPricingWorkflowServiceImpl.java | 56 ++++++- .../util/LoanPricingConverter.java | 4 +- ...PricingModelServicePersonalParamsTest.java | 19 +-- .../service/LoanPricingModelServiceTest.java | 4 + .../LoanPricingWorkflowServiceImplTest.java | 144 ++++++++++++++++++ .../components/CorporateCreateDialog.vue | 54 ++++++- .../components/PersonalCreateDialog.vue | 78 ++++++---- .../personal-create-input-params.test.js | 23 +-- sql/add_coupon_rate_20260511.sql | 3 + sql/loan_pricing_prod_init_20260331.sql | 1 + sql/loan_pricing_schema_20260328.sql | 1 + sql/loan_pricing_workflow.sql | 1 + 17 files changed, 430 insertions(+), 72 deletions(-) create mode 100644 doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md create mode 100644 sql/add_coupon_rate_20260511.sql diff --git a/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md b/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md new file mode 100644 index 0000000..8c88101 --- /dev/null +++ b/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md @@ -0,0 +1,76 @@ +# 上虞利率定价字段调整实施记录 + +## 基本信息 + +- 日期:2026-05-11 +- 范围:上虞利率定价个人/企业新增链路、服务端校验、模型入参、表结构脚本 +- 目标:按已确认需求调整业务种类、抵质押类型、存单票面利率字段,以及对私新增入口字段剔除 + +## 修改内容 + +### 后端 + +- 个人新增 DTO: + - 业务种类调整为 `新增/存量新增/存量转贷`。 + - 移除 `loanPurpose`、`bizProof` 新增入口字段。 + - 新增 `couponRate`。 +- 企业新增 DTO: + - 业务种类调整为 `新增/存量新增/存量转贷`。 + - 企业抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`。 + - 企业质押类型调整为 `存单质押/股权质押/其他质押`。 + - 新增 `couponRate`。 +- 流程实体和模型入参: + - `LoanPricingWorkflow` 新增 `couponRate`。 + - `ModelInvokeDTO` 新增 `couponRate`,未增加 `businessType` 模型入参。 +- 转换器: + - 个人/企业新增 DTO 均映射 `couponRate`。 + - 个人新增 DTO 不再映射 `loanPurpose`、`bizProof`。 +- 服务校验: + - 业务种类仅允许 `新增/存量新增/存量转贷`。 + - 仅 `存量转贷` 要求历史贷款合同。 + - 抵押/质押时要求选择抵质押类型。 + - 对私/对公按客户类型和担保方式校验各自抵质押类型。 + - `质押 + 存单质押` 时要求填写 `couponRate`。 +- SQL: + - 新增 `sql/add_coupon_rate_20260511.sql`。 + - 同步更新 `loan_pricing_workflow` 建表脚本中的 `coupon_rate` 字段。 + +### 前端 + +- 个人新增弹窗: + - 业务种类调整为 `新增/存量新增/存量转贷`。 + - 移除 `贷款用途` 和 `是否有经营佐证`。 + - 抵押类型调整为 `一线/一类/二类/三类`。 + - 质押类型调整为 `存单质押/其他质押`。 + - `质押 + 存单质押` 时显示并必填 `存单票面利率`。 +- 企业新增弹窗: + - 业务种类调整为 `新增/存量新增/存量转贷`。 + - 抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`。 + - 质押类型调整为 `存单质押/股权质押/其他质押`。 + - `质押 + 存单质押` 时显示并必填 `存单票面利率`。 +- 共同逻辑: + - 仅 `存量转贷` 触发历史贷款合同查询。 + - 非存单质押提交时清理 `couponRate`。 + +## 验证结果 + +- 后端单元测试: + - `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test` + - 结果:通过,23 个测试全部成功。 +- 前端静态断言: + - `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate'` + - 结果:通过。 +- 前端生产构建: + - `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'` + - 结果:构建通过,仅存在既有包体积 warning。 +- 真实页面验证: + - 使用 Playwright 打开 `http://localhost:1024/index`。 + - 使用 `/login/test` 获取登录 token 后访问真实流程列表页面。 + - 个人新增弹窗验证:已移除 `贷款用途/是否有经营佐证`;业务种类仅 `存量转贷` 触发历史利率逻辑;个人抵押/质押选项正确;`存单质押` 下 `couponRate` 显示并进入必填校验。 + - 企业新增弹窗验证:抵押/质押选项正确;`存单质押` 下 `couponRate` 显示并进入必填校验;业务种类仅 `存量转贷` 触发历史利率逻辑。 + - 验证后已关闭 Playwright 浏览器会话;本次未新启动前后端进程。 + +## 注意事项 + +- 控制台中的 `sockjs-node` 报错来自本地 dev-server HMR 连接内网地址失败,不影响本次页面功能验证。 +- 表单校验 warning 来自验证时故意触发必填校验。 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 2d5d63b..1465827 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,9 @@ public class CorporateLoanPricingCreateDTO implements Serializable { @NotBlank(message = "申请金额不能为空") private String applyAmt; - @Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"}) + @Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"}) @NotBlank(message = "业务种类不能为空") - @Pattern(regexp = "^(新客|存量新增|存量转贷)$", message = "业务种类必须是:新客、存量新增、存量转贷之一") + @Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一") private String businessType; @Schema(description = "历史贷款利率", example = "3.65") @@ -62,10 +62,13 @@ public class CorporateLoanPricingCreateDTO implements Serializable { @Schema(description = "贸易和建筑业企业标识", example = "0") private String isTradeBuildEnt; - @Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "其他", "存单质押"}) - @Pattern(regexp = "^(一类|二类|三类|四类|其他|存单质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、其他、存单质押之一") + @Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"}) + @Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押、存单质押、股权质押、其他质押之一") private String collType; @Schema(description = "抵质押物是否三方所有", example = "0") private String collThirdParty; + + @Schema(description = "存单票面利率", example = "2.15") + private String couponRate; } diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java index 4492ac7..7a65b75 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java @@ -158,6 +158,11 @@ public class ModelInvokeDTO { */ private String loanRateHistory; + /** + * 存单票面利率 + */ + private String couponRate; + // /** // * 贷款利率(必填) // */ diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java index 76b6e0d..c8ca9c3 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java @@ -41,14 +41,9 @@ public class PersonalLoanPricingCreateDTO implements Serializable { @NotBlank(message = "申请金额不能为空") private String applyAmt; - @Schema(description = "贷款用途", requiredMode = Schema.RequiredMode.REQUIRED, example = "business", allowableValues = {"consumer", "business"}) - @NotBlank(message = "贷款用途不能为空") - @Pattern(regexp = "^(consumer|business)$", message = "贷款用途必须是:consumer、business之一") - private String loanPurpose; - - @Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"}) + @Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"}) @NotBlank(message = "业务种类不能为空") - @Pattern(regexp = "^(新客|存量新增|存量转贷)$", message = "业务种类必须是:新客、存量新增、存量转贷之一") + @Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一") private String businessType; @Schema(description = "历史贷款利率", example = "3.65") @@ -58,15 +53,15 @@ public class PersonalLoanPricingCreateDTO implements Serializable { @NotBlank(message = "借款期限不能为空") private String loanTerm; - @Schema(description = "是否有经营佐证", example = "1") - private String bizProof; - @Schema(description = "循环功能", example = "0") private String loanLoop; - @Schema(description = "抵质押类型", example = "一类") + @Schema(description = "抵质押类型", example = "一类", allowableValues = {"一线", "一类", "二类", "三类", "存单质押", "其他质押"}) private String collType; @Schema(description = "抵质押物是否三方所有", example = "0") private String collThirdParty; + + @Schema(description = "存单票面利率", example = "2.15") + private String couponRate; } diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java index da13512..6644311 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java @@ -103,12 +103,15 @@ public class LoanPricingWorkflow implements Serializable /** 贷款用途: consumer-消费/business-经营 */ private String loanPurpose; - /** 业务种类: 新客/存量新增/存量转贷 */ + /** 业务种类: 新增/存量新增/存量转贷 */ private String businessType; /** 历史贷款利率 */ private String loanRateHistory; + /** 存单票面利率 */ + private String couponRate; + /** 是否有经营佐证: true/false */ private String bizProof; diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java index 046c90f..c2cfc65 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java @@ -67,6 +67,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow) { validateBusinessTypeAndHistoryRate(loanPricingWorkflow); + validateCollateralTypeAndCouponRate(loanPricingWorkflow); // 自动生成业务方流水号(时间戳) SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); @@ -95,10 +96,10 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi if (!StringUtils.hasText(workflow.getBusinessType())) { throw new ServiceException("业务种类不能为空"); } - if (!"新客".equals(workflow.getBusinessType()) + if (!"新增".equals(workflow.getBusinessType()) && !"存量新增".equals(workflow.getBusinessType()) && !"存量转贷".equals(workflow.getBusinessType())) { - throw new ServiceException("业务种类必须是:新客、存量新增、存量转贷之一"); + throw new ServiceException("业务种类必须是:新增、存量新增、存量转贷之一"); } if ("存量转贷".equals(workflow.getBusinessType()) && !StringUtils.hasText(workflow.getLoanRateHistory())) { @@ -106,6 +107,57 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi } } + private void validateCollateralTypeAndCouponRate(LoanPricingWorkflow workflow) { + if (!"抵押".equals(workflow.getGuarType()) && !"质押".equals(workflow.getGuarType())) { + return; + } + if (!StringUtils.hasText(workflow.getCollType())) { + throw new ServiceException("请选择抵质押类型"); + } + if ("个人".equals(workflow.getCustType())) { + validatePersonalCollateralType(workflow); + } + if ("企业".equals(workflow.getCustType())) { + validateCorporateCollateralType(workflow); + } + if ("质押".equals(workflow.getGuarType()) + && "存单质押".equals(workflow.getCollType()) + && !StringUtils.hasText(workflow.getCouponRate())) { + throw new ServiceException("存单票面利率不能为空"); + } + } + + private void validatePersonalCollateralType(LoanPricingWorkflow workflow) { + if ("抵押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "一线", "一类", "二类", "三类")) { + throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一"); + } + if ("质押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) { + throw new ServiceException("个人质押抵质押类型必须是:存单质押、其他质押之一"); + } + } + + private void validateCorporateCollateralType(LoanPricingWorkflow workflow) { + if ("抵押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) { + throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一"); + } + if ("质押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "存单质押", "股权质押", "其他质押")) { + throw new ServiceException("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一"); + } + } + + private boolean isOneOf(String value, String... allowedValues) { + for (String allowedValue : allowedValues) { + if (allowedValue.equals(value)) { + return true; + } + } + return false; + } + /** * 发起个人客户利率定价流程 * diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java index 9ac0fa1..44ada46 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java @@ -28,14 +28,13 @@ public class LoanPricingConverter { entity.setIdNum(dto.getIdNum()); entity.setGuarType(dto.getGuarType()); entity.setApplyAmt(dto.getApplyAmt()); - entity.setLoanPurpose(dto.getLoanPurpose()); entity.setBusinessType(dto.getBusinessType()); entity.setLoanRateHistory(dto.getLoanRateHistory()); + entity.setCouponRate(dto.getCouponRate()); entity.setLoanTerm(dto.getLoanTerm()); entity.setCollType(dto.getCollType()); entity.setCollThirdParty(dto.getCollThirdParty()); // 映射个人特有字段 - entity.setBizProof(dto.getBizProof()); entity.setLoanLoop(dto.getLoanLoop()); return entity; } @@ -58,6 +57,7 @@ public class LoanPricingConverter { entity.setApplyAmt(dto.getApplyAmt()); entity.setBusinessType(dto.getBusinessType()); entity.setLoanRateHistory(dto.getLoanRateHistory()); + entity.setCouponRate(dto.getCouponRate()); entity.setRepayMethod(dto.getRepayMethod()); entity.setCollType(dto.getCollType()); entity.setCollThirdParty(dto.getCollThirdParty()); 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 202aa67..31ac3dd 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 @@ -18,6 +18,7 @@ import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -46,24 +47,28 @@ class LoanPricingModelServicePersonalParamsTest { private LoanPricingModelService loanPricingModelService; @Test - void shouldContainLoanPurposeAndLoanTermInPersonalCreateDto() throws NoSuchFieldException { - assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose")); + void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException { + assertThrows(NoSuchFieldException.class, + () -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose")); + assertThrows(NoSuchFieldException.class, + () -> PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof")); assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm")); } @Test - void shouldMapLoanPurposeAndLoanTermFromPersonalDto() { + void shouldNotMapLoanPurposeAndBizProofFromPersonalDto() { PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); dto.setCustIsn("CUST001"); dto.setCustName("张三"); dto.setGuarType("信用"); dto.setApplyAmt("100000"); - dto.setLoanPurpose("business"); + dto.setBusinessType("新增"); dto.setLoanTerm("3"); LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto); - assertEquals("business", workflow.getLoanPurpose()); + assertNull(workflow.getLoanPurpose()); + assertNull(workflow.getBizProof()); assertEquals("3", workflow.getLoanTerm()); } @@ -87,9 +92,7 @@ class LoanPricingModelServicePersonalParamsTest { 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("一类"); @@ -115,9 +118,7 @@ class LoanPricingModelServicePersonalParamsTest { && 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()))); 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 7aa060d..5a2dfa4 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 @@ -29,6 +29,7 @@ class LoanPricingModelServiceTest assertEquals("张三", context.modelService.personalRequest.getCustName()); assertEquals("110101199001011234", context.modelService.personalRequest.getIdNum()); + assertEquals("2.15", context.modelService.personalRequest.getCouponRate()); assertEquals(1, context.modelService.personalCalls.get()); assertEquals(0, context.modelService.corporateCalls.get()); assertEquals(1, context.retailInsertCount.get()); @@ -59,6 +60,7 @@ class LoanPricingModelServiceTest workflow.setIsGreenLoan("true"); workflow.setIsTradeBuildEnt("false"); workflow.setCollThirdParty("true"); + workflow.setCouponRate("2.35"); TestContext context = createContext(workflow); context.service.invokeModelAsync(3L); @@ -68,6 +70,7 @@ class LoanPricingModelServiceTest assertEquals("1", request.getIsGreenLoan()); assertEquals("0", request.getIsTradeBuildEnt()); assertEquals("1", request.getCollThirdParty()); + assertEquals("2.35", request.getCouponRate()); assertEquals(0, context.modelService.personalCalls.get()); assertEquals(1, context.modelService.corporateCalls.get()); assertEquals(0, context.retailInsertCount.get()); @@ -81,6 +84,7 @@ class LoanPricingModelServiceTest workflow.setCustType("个人"); workflow.setCustName("cipher-name"); workflow.setIdNum("cipher-id"); + workflow.setCouponRate("2.15"); return workflow; } diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java index dac8aa5..640563e 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java @@ -1,6 +1,7 @@ package com.ruoyi.loanpricing.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -12,6 +13,9 @@ 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.common.exception.ServiceException; +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; @@ -65,6 +69,7 @@ class LoanPricingWorkflowServiceImplTest workflow.setCustName("张三"); workflow.setIdNum("110101199001011234"); workflow.setCustIsn("CUST001"); + workflow.setBusinessType("新增"); when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name"); when(sensitiveFieldCryptoService.encrypt("110101199001011234")).thenReturn("cipher-id"); @@ -133,6 +138,134 @@ class LoanPricingWorkflowServiceImplTest assertTrue(!sqlSegment.contains("cust_name"), sqlSegment); } + @Test + void shouldRejectOldBusinessType() + { + LoanPricingWorkflow workflow = validWorkflow(); + workflow.setBusinessType("新客"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("业务种类必须是:新增、存量新增、存量转贷之一", exception.getMessage()); + } + + @Test + void shouldRequireHistoryRateForStockTransfer() + { + LoanPricingWorkflow workflow = validWorkflow(); + workflow.setBusinessType("存量转贷"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("请选择历史贷款合同", exception.getMessage()); + } + + @Test + void shouldRequireCouponRateForCertificatePledge() + { + LoanPricingWorkflow workflow = validWorkflow(); + workflow.setCustType("企业"); + workflow.setGuarType("质押"); + workflow.setCollType("存单质押"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("存单票面利率不能为空", exception.getMessage()); + } + + @Test + void shouldAllowPersonalMortgageOneLineAndRejectOldOther() + { + LoanPricingWorkflow allowedWorkflow = validWorkflow(); + allowedWorkflow.setCustType("个人"); + allowedWorkflow.setGuarType("抵押"); + allowedWorkflow.setCollType("一线"); + + loanPricingWorkflowService.createLoanPricing(allowedWorkflow); + + LoanPricingWorkflow rejectedWorkflow = validWorkflow(); + rejectedWorkflow.setCustType("个人"); + rejectedWorkflow.setGuarType("抵押"); + rejectedWorkflow.setCollType("其他"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(rejectedWorkflow)); + + assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", exception.getMessage()); + } + + @Test + void shouldAllowCorporateMortgagePollutionRightAndPledgeEquity() + { + LoanPricingWorkflow mortgageWorkflow = validWorkflow(); + mortgageWorkflow.setCustType("企业"); + mortgageWorkflow.setGuarType("抵押"); + mortgageWorkflow.setCollType("排污权抵押"); + + loanPricingWorkflowService.createLoanPricing(mortgageWorkflow); + + LoanPricingWorkflow pledgeWorkflow = validWorkflow(); + pledgeWorkflow.setCustType("企业"); + pledgeWorkflow.setGuarType("质押"); + pledgeWorkflow.setCollType("股权质押"); + + loanPricingWorkflowService.createLoanPricing(pledgeWorkflow); + + LoanPricingWorkflow rejectedWorkflow = validWorkflow(); + rejectedWorkflow.setCustType("企业"); + rejectedWorkflow.setGuarType("质押"); + rejectedWorkflow.setCollType("其他"); + + ServiceException exception = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(rejectedWorkflow)); + + assertEquals("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一", exception.getMessage()); + } + + @Test + void shouldCreatePersonalWorkflowWithCouponRateAndWithoutRemovedFields() + { + PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); + dto.setCustIsn("CUST001"); + dto.setGuarType("质押"); + dto.setApplyAmt("100000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("3"); + dto.setCollType("存单质押"); + dto.setCouponRate("2.15"); + + loanPricingWorkflowService.createPersonalLoanPricing(dto); + + verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) -> + Objects.equals("个人", entity.getCustType()) + && Objects.equals("2.15", entity.getCouponRate()) + && Objects.isNull(entity.getLoanPurpose()) + && Objects.isNull(entity.getBizProof()))); + } + + @Test + void shouldCreateCorporateWorkflowWithCouponRateAndBusinessType() + { + CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO(); + dto.setCustIsn("CORP001"); + dto.setGuarType("质押"); + dto.setApplyAmt("1000000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("3"); + dto.setCollType("存单质押"); + dto.setCouponRate("2.35"); + + loanPricingWorkflowService.createCorporateLoanPricing(dto); + + verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) -> + Objects.equals("企业", entity.getCustType()) + && Objects.equals("新增", entity.getBusinessType()) + && Objects.equals("2.35", entity.getCouponRate()))); + } + @Test void shouldUseRetailModelOutputFinalCalculateRateForWorkflowDetail() { @@ -255,4 +388,15 @@ class LoanPricingWorkflowServiceImplTest assertEquals("测试****公司", result.getModelCorpOutputFields().getCustName()); assertEquals("91*************00X", result.getModelCorpOutputFields().getIdNum()); } + + private LoanPricingWorkflow validWorkflow() + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setCustIsn("CUST001"); + workflow.setCustType("个人"); + workflow.setGuarType("信用"); + workflow.setApplyAmt("100000"); + workflow.setBusinessType("新增"); + return workflow; + } } diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue index 8b5b74a..02f04d3 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue @@ -64,7 +64,7 @@ - + @@ -107,6 +107,11 @@ + + + + + { + if (this.isCertificatePledge && !value) { + callback(new Error('存单票面利率不能为空')) + return + } + callback() + } + // 贷款期限验证 const validateLoanTerm = (rule, value, callback) => { if (!value && value !== 0) { @@ -196,7 +209,8 @@ export default { isGreenLoan: false, isTradeBuildEnt: false, collType: undefined, - collThirdParty: false + collThirdParty: false, + couponRate: undefined }, rules: { custIsn: [ @@ -230,6 +244,9 @@ export default { ], collType: [ {required: true, message: "请选择抵质押类型", trigger: "change"} + ], + couponRate: [ + {validator: validateCouponRate, trigger: "blur"} ] } } @@ -249,12 +266,15 @@ export default { isStockTransfer() { return this.form.businessType === '存量转贷' }, + isCertificatePledge() { + return this.form.guarType === '质押' && this.form.collType === '存单质押' + }, collateralTypeOptions() { if (this.form.guarType === '抵押') { - return ['一类', '二类', '三类', '四类', '其他'] + return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押'] } if (this.form.guarType === '质押') { - return ['存单质押', '其他'] + return ['存单质押', '股权质押', '其他质押'] } return [] } @@ -269,6 +289,9 @@ export default { if (val !== oldVal) { this.resetCollateralFields() } + }, + 'form.collType'() { + this.resetCouponRateIfNotCertificatePledge() } }, methods: { @@ -289,7 +312,8 @@ export default { isGreenLoan: false, isTradeBuildEnt: false, collType: undefined, - collThirdParty: false + collThirdParty: false, + couponRate: undefined } this.submitting = false this.showHistorySelector = false @@ -315,12 +339,23 @@ export default { resetCollateralFields() { this.form.collType = undefined this.form.collThirdParty = false + this.resetCouponRateIfNotCertificatePledge() this.$nextTick(() => { if (this.$refs.form) { - this.$refs.form.clearValidate(['collType', 'collThirdParty']) + this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate']) } }) }, + resetCouponRateIfNotCertificatePledge() { + if (!this.isCertificatePledge) { + this.form.couponRate = undefined + this.$nextTick(() => { + if (this.$refs.form) { + this.$refs.form.clearValidate(['couponRate']) + } + }) + } + }, handleBusinessTypeChange(value) { this.clearHistoryContract() if (value === '存量转贷') { @@ -370,6 +405,10 @@ export default { this.$modal.msgWarning("请选择历史贷款合同") return } + if (this.isCertificatePledge && !this.form.couponRate) { + this.$modal.msgWarning("存单票面利率不能为空") + return + } this.submitting = true // 转换开关值为字符串 const data = { @@ -386,6 +425,9 @@ export default { if (!this.isStockTransfer) { delete data.loanRateHistory } + if (!this.isCertificatePledge) { + delete data.couponRate + } createCorporateWorkflow(data).then(response => { this.$modal.msgSuccess("新增成功") diff --git a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue index 261e782..15309e2 100644 --- a/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue +++ b/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue @@ -52,14 +52,6 @@ - - - - - - - - @@ -72,7 +64,7 @@ - + @@ -85,11 +77,6 @@ - - - - - @@ -112,6 +99,11 @@ + + + + + { + if (this.isCertificatePledge && !value) { + callback(new Error('存单票面利率不能为空')) + return + } + callback() + } + return { loanTermOptions: [ '1', '2', '3', '4', '5', '6' @@ -181,14 +181,13 @@ export default { idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined, guarType: undefined, applyAmt: undefined, - loanPurpose: undefined, loanTerm: undefined, businessType: undefined, loanRateHistory: undefined, - bizProof: false, loanLoop: false, collType: undefined, - collThirdParty: false + collThirdParty: false, + couponRate: undefined }, rules: { custIsn: [ @@ -211,9 +210,6 @@ export default { applyAmt: [ {required: true, validator: validateApplyAmt, trigger: "blur"} ], - loanPurpose: [ - {required: true, message: "请选择贷款用途", trigger: "change"} - ], loanTerm: [ {required: true, message: "请选择借款期限", trigger: "change"} ], @@ -222,6 +218,12 @@ export default { ], loanRateHistory: [ {required: true, message: "请选择历史贷款合同", trigger: "change"} + ], + collType: [ + {required: true, message: "请选择抵质押类型", trigger: "change"} + ], + couponRate: [ + {validator: validateCouponRate, trigger: "blur"} ] } } @@ -241,12 +243,15 @@ export default { isStockTransfer() { return this.form.businessType === '存量转贷' }, + isCertificatePledge() { + return this.form.guarType === '质押' && this.form.collType === '存单质押' + }, collateralTypeOptions() { if (this.form.guarType === '抵押') { - return ['一类', '二类', '三类', '四类', '其他'] + return ['一线', '一类', '二类', '三类'] } if (this.form.guarType === '质押') { - return ['存单质押', '其他'] + return ['存单质押', '其他质押'] } return [] } @@ -261,6 +266,9 @@ export default { if (val !== oldVal) { this.resetCollateralFields() } + }, + 'form.collType'() { + this.resetCouponRateIfNotCertificatePledge() } }, methods: { @@ -275,14 +283,13 @@ export default { idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined, guarType: undefined, applyAmt: undefined, - loanPurpose: undefined, loanTerm: undefined, businessType: undefined, loanRateHistory: undefined, - bizProof: false, loanLoop: false, collType: undefined, - collThirdParty: false + collThirdParty: false, + couponRate: undefined } this.submitting = false this.showHistorySelector = false @@ -308,12 +315,23 @@ export default { resetCollateralFields() { this.form.collType = undefined this.form.collThirdParty = false + this.resetCouponRateIfNotCertificatePledge() this.$nextTick(() => { if (this.$refs.form) { - this.$refs.form.clearValidate(['collType', 'collThirdParty']) + this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate']) } }) }, + resetCouponRateIfNotCertificatePledge() { + if (!this.isCertificatePledge) { + this.form.couponRate = undefined + this.$nextTick(() => { + if (this.$refs.form) { + this.$refs.form.clearValidate(['couponRate']) + } + }) + } + }, handleBusinessTypeChange(value) { this.clearHistoryContract() if (value === '存量转贷') { @@ -363,11 +381,14 @@ export default { this.$modal.msgWarning("请选择历史贷款合同") return } + if (this.isCertificatePledge && !this.form.couponRate) { + this.$modal.msgWarning("存单票面利率不能为空") + return + } this.submitting = true // 转换开关值为字符串 const data = { ...this.form, - bizProof: this.form.bizProof ? '1' : '0', loanLoop: this.form.loanLoop ? '1' : '0' } if (this.isCollateralGuarantee) { @@ -379,6 +400,9 @@ export default { if (!this.isStockTransfer) { delete data.loanRateHistory } + if (!this.isCertificatePledge) { + delete data.couponRate + } createPersonalWorkflow(data).then(response => { this.$modal.msgSuccess("新增成功") diff --git a/ruoyi-ui/tests/personal-create-input-params.test.js b/ruoyi-ui/tests/personal-create-input-params.test.js index 4ee6149..332bc52 100644 --- a/ruoyi-ui/tests/personal-create-input-params.test.js +++ b/ruoyi-ui/tests/personal-create-input-params.test.js @@ -10,8 +10,10 @@ const personalCreateDialog = read('src/views/loanPricing/workflow/components/Per const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue') assert( - personalCreateDialog.includes('label="贷款用途"') && personalCreateDialog.includes('prop="loanPurpose"'), - '个人新增弹窗缺少贷款用途字段' + !personalCreateDialog.includes('label="贷款用途"') && + !personalCreateDialog.includes('prop="loanPurpose"') && + !personalCreateDialog.includes('loanPurpose:'), + '个人新增弹窗不应继续保留贷款用途字段' ) assert( @@ -20,8 +22,8 @@ assert( ) assert( - personalCreateDialog.includes("value=\"consumer\"") && personalCreateDialog.includes("value=\"business\""), - '个人新增弹窗缺少贷款用途选项' + !personalCreateDialog.includes("value=\"consumer\"") && !personalCreateDialog.includes("value=\"business\""), + '个人新增弹窗不应继续保留贷款用途选项' ) assert( @@ -41,19 +43,20 @@ assert( assert( personalCreateDialog.includes('collateralTypeOptions') && - personalCreateDialog.includes("return ['一类', '二类', '三类', '四类', '其他']") && - personalCreateDialog.includes("return ['存单质押', '其他']") && - !personalCreateDialog.includes('label="一线"'), + personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") && + personalCreateDialog.includes("return ['存单质押', '其他质押']"), '个人新增弹窗抵质押类型选项未按担保方式动态切换' ) assert( - !personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'), - '个人新增弹窗仍将抵质押类型设为必填' + personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'), + '个人新增弹窗抵质押类型应为必填' ) assert( - personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") && + !personalCreateDialog.includes('label="是否有经营佐证"') && + !personalCreateDialog.includes('prop="bizProof"') && + !personalCreateDialog.includes('bizProof:') && personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") && personalCreateDialog.includes("data.collThirdParty = this.form.collThirdParty ? '1' : '0'") && personalCreateDialog.includes('delete data.collType') && diff --git a/sql/add_coupon_rate_20260511.sql b/sql/add_coupon_rate_20260511.sql new file mode 100644 index 0000000..ca1e181 --- /dev/null +++ b/sql/add_coupon_rate_20260511.sql @@ -0,0 +1,3 @@ +-- 上虞利率定价存单票面利率字段 +ALTER TABLE `loan_pricing_workflow` + ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`; diff --git a/sql/loan_pricing_prod_init_20260331.sql b/sql/loan_pricing_prod_init_20260331.sql index dbea035..6367ce1 100644 --- a/sql/loan_pricing_prod_init_20260331.sql +++ b/sql/loan_pricing_prod_init_20260331.sql @@ -765,6 +765,7 @@ CREATE TABLE `loan_pricing_workflow` ( `loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营', `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类', `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率', + `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率', `biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false', `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false(贷款合同是否开通循环功能)', `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类', diff --git a/sql/loan_pricing_schema_20260328.sql b/sql/loan_pricing_schema_20260328.sql index 5ba3769..2c9d5d1 100644 --- a/sql/loan_pricing_schema_20260328.sql +++ b/sql/loan_pricing_schema_20260328.sql @@ -347,6 +347,7 @@ CREATE TABLE `loan_pricing_workflow` ( `loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营', `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类', `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率', + `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率', `biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false', `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false(贷款合同是否开通循环功能)', `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类', diff --git a/sql/loan_pricing_workflow.sql b/sql/loan_pricing_workflow.sql index 7f3fc18..4bea856 100644 --- a/sql/loan_pricing_workflow.sql +++ b/sql/loan_pricing_workflow.sql @@ -28,6 +28,7 @@ CREATE TABLE `loan_pricing_workflow` ( `loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营', `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类', `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率', + `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率', `biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false', `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false(贷款合同是否开通循环功能)', `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',