From a50b25e4ec8d93e56b197e68cbba3163349f17bd Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Mon, 11 May 2026 17:34:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=8A=E8=99=9E=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E8=B0=83=E6=95=B4=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-shangyu-pricing-field-adjustment-plans.md | 19 + ...u-pricing-field-adjustment-backend-plan.md | 647 ++++++++++++++++++ ...-pricing-field-adjustment-frontend-plan.md | 485 +++++++++++++ 3 files changed, 1151 insertions(+) create mode 100644 doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment-plans.md create mode 100644 docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md create mode 100644 docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md diff --git a/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment-plans.md b/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment-plans.md new file mode 100644 index 0000000..05f996b --- /dev/null +++ b/doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment-plans.md @@ -0,0 +1,19 @@ +# 2026-05-11 上虞利率定价字段口径调整实施计划记录 + +## 修改内容 + +- 新增后端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md`。 +- 新增前端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md`。 +- 后端计划覆盖创建 DTO、流程实体、模型入参、转换器、服务层校验、SQL schema 和后端测试。 +- 前端计划覆盖个人/企业新增弹窗、业务种类选项、抵质押类型选项、`couponRate` 条件必填、静态断言、构建和 Playwright 真实页面验证。 + +## 范围说明 + +- 计划依据:`docs/superpowers/specs/2026-05-11-shangyu-pricing-field-adjustment-design.md`。 +- 对公 `businessType` 上传模型这一条已按用户确认从本次实施范围排除;计划只覆盖 `couponRate` 的模型入参新增。 +- 本次仅产出实施计划,未进入业务代码实现。 + +## 待验证 + +- 计划需通过计划审查后再进入实施。 +- 后续实现完成后需要补充 `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`,记录真实代码改动、测试命令和页面验证结果。 diff --git a/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md b/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md new file mode 100644 index 0000000..60ccc8b --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md @@ -0,0 +1,647 @@ +# Shangyu Pricing Field Adjustment Backend Implementation Plan + +> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Update the backend create-flow contract so `businessType`, `couponRate`, personal removed fields, and customer-type-specific collateral options match the approved Shangyu pricing spec. + +**Architecture:** Keep the existing workflow creation API and entity flow. Add `couponRate` to the DTO/entity/model path, tighten service-layer validation in `LoanPricingWorkflowServiceImpl`, remove personal create dependencies on `loanPurpose` and `bizProof`, and update SQL schema files in place. + +**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, Jakarta Validation, JUnit 5, Mockito, MySQL SQL files. + +--- + +## File Structure + +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java` + - Change `businessType` values to `新增/存量新增/存量转贷`. + - Remove personal create DTO dependency on `loanPurpose` and `bizProof`. + - Add `couponRate`. +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java` + - Change `businessType` values to `新增/存量新增/存量转贷`. + - Expand `collType` values for corporate mortgage and pledge paths. + - Add `couponRate`. +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java` + - Update `businessType` comment and add persisted `couponRate`. +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java` + - Add `couponRate` only. Do not add `businessType`; the user excluded that model-input branch from this scope. +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java` + - Map `couponRate`. + - Stop mapping removed personal fields from the personal create DTO. +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java` + - Update business type validation. + - Add collateral-option validation by `custType + guarType`. + - Add required `couponRate` validation for `质押 + 存单质押`. +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java` + - Add validation coverage and update existing create tests to set valid `businessType`. +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java` + - Remove old personal `loanPurpose` DTO expectations so Maven test compilation remains valid after DTO removal. + - Keep `loanTerm/loanLoop` model assertions. +- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java` + - Confirm personal removed fields no longer drive conversion and `couponRate` maps. +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java` + - Confirm `couponRate` is copied into `ModelInvokeDTO` via the model invocation path. +- Create: `sql/add_coupon_rate_20260511.sql` + - Migration for existing databases. +- Modify: `sql/loan_pricing_workflow.sql` + - Add `coupon_rate`. +- Modify: `sql/loan_pricing_schema_20260328.sql` + - Add `coupon_rate` to `loan_pricing_workflow`. +- Modify: `sql/loan_pricing_prod_init_20260331.sql` + - Add `coupon_rate` to production init schema. + +## Task 1: Add Failing Backend Contract Tests + +**Files:** +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java` +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java` +- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java` +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java` + +- [ ] **Step 1: Update existing create tests with valid business type** + +In `LoanPricingWorkflowServiceImplTest`, every test that calls `createLoanPricing` must set: + +```java +workflow.setBusinessType("新增"); +``` + +For tests using mortgage or pledge, also set an allowed `collType` for the matching `custType`. + +- [ ] **Step 2: Update old personal parameter tests** + +In `LoanPricingModelServicePersonalParamsTest`, replace the old DTO field test: + +Add the static import if it is not present: + +```java +import static org.junit.jupiter.api.Assertions.assertNull; +``` + +```java +@Test +void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException { + assertThrows(NoSuchFieldException.class, + () -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose")); + assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm")); +} +``` + +Replace the old converter test that called `dto.setLoanPurpose(...)`: + +```java +@Test +void shouldMapLoanTermWithoutLoanPurposeFromPersonalDto() { + PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); + dto.setCustIsn("CUST001"); + dto.setCustName("张三"); + dto.setGuarType("信用"); + dto.setApplyAmt("100000"); + dto.setBusinessType("新增"); + dto.setLoanTerm("3"); + + LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto); + + assertNull(workflow.getLoanPurpose()); + assertNull(workflow.getBizProof()); + assertEquals("3", workflow.getLoanTerm()); +} +``` + +In `shouldInvokePersonalModelWithExpectedParams`, remove: + +```java +workflow.setLoanPurpose("business"); +workflow.setBizProof("true"); +``` + +and remove these argument matcher clauses: + +```java +&& Objects.equals("business", dto.getLoanPurpose()) +&& Objects.equals("1", dto.getBizProof()) +``` + +Keep the existing `loanTerm`, `loanLoop`, `collThirdParty`, and `collType` assertions. + +- [ ] **Step 3: Add failing business type and collateral validation tests** + +Add tests like: + +```java +@Test +void shouldRejectOldBusinessType() { + LoanPricingWorkflow workflow = validPersonalWorkflow(); + workflow.setBusinessType("新客"); + + ServiceException ex = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("业务种类必须是:新增、存量新增、存量转贷之一", ex.getMessage()); +} + +@Test +void shouldRequireHistoryRateForStockTransfer() { + LoanPricingWorkflow workflow = validCorporateWorkflow(); + workflow.setBusinessType("存量转贷"); + workflow.setLoanRateHistory(null); + + ServiceException ex = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("请选择历史贷款合同", ex.getMessage()); +} + +@Test +void shouldRequireCouponRateForCertificatePledge() { + LoanPricingWorkflow workflow = validPersonalWorkflow(); + workflow.setGuarType("质押"); + workflow.setCollType("存单质押"); + workflow.setCouponRate(null); + + ServiceException ex = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("存单票面利率不能为空", ex.getMessage()); +} +``` + +Add helper methods: + +```java +private LoanPricingWorkflow validPersonalWorkflow() { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setCustType("个人"); + workflow.setCustIsn("P001"); + workflow.setCustName("张三"); + workflow.setIdNum("330102199001011234"); + workflow.setGuarType("信用"); + workflow.setApplyAmt("100000"); + workflow.setBusinessType("新增"); + return workflow; +} + +private LoanPricingWorkflow validCorporateWorkflow() { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setCustType("企业"); + workflow.setCustIsn("C001"); + workflow.setCustName("测试企业"); + workflow.setIdNum("91330100MA0000000X"); + workflow.setGuarType("信用"); + workflow.setApplyAmt("1000000"); + workflow.setBusinessType("新增"); + return workflow; +} +``` + +- [ ] **Step 4: Add failing allowed-option tests** + +Add focused tests: + +```java +@Test +void shouldAllowPersonalMortgageLineType() { + LoanPricingWorkflow workflow = validPersonalWorkflow(); + workflow.setGuarType("抵押"); + workflow.setCollType("一线"); + + when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + loanPricingWorkflowService.createLoanPricing(workflow); + + verify(loanPricingWorkflowMapper).insert(any()); +} + +@Test +void shouldRejectCorporateMortgageTypeForPersonalMortgage() { + LoanPricingWorkflow workflow = validPersonalWorkflow(); + workflow.setGuarType("抵押"); + workflow.setCollType("排污权抵押"); + + ServiceException ex = assertThrows(ServiceException.class, + () -> loanPricingWorkflowService.createLoanPricing(workflow)); + + assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", ex.getMessage()); +} + +@Test +void shouldAllowCorporatePledgeEquityType() { + LoanPricingWorkflow workflow = validCorporateWorkflow(); + workflow.setGuarType("质押"); + workflow.setCollType("股权质押"); + + when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + loanPricingWorkflowService.createLoanPricing(workflow); + + verify(loanPricingWorkflowMapper).insert(any()); +} +``` + +- [ ] **Step 5: Add failing converter test** + +Create `LoanPricingConverterTest`: + +```java +package com.ruoyi.loanpricing.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO; +import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO; +import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow; +import org.junit.jupiter.api.Test; + +class LoanPricingConverterTest { + @Test + void shouldMapCouponRateForPersonalWorkflow() { + PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO(); + dto.setCustIsn("P001"); + dto.setGuarType("质押"); + dto.setApplyAmt("100000"); + dto.setBusinessType("新增"); + dto.setCouponRate("2.15"); + + LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto); + + assertEquals("2.15", workflow.getCouponRate()); + assertNull(workflow.getLoanPurpose()); + assertNull(workflow.getBizProof()); + } + + @Test + void shouldMapCouponRateForCorporateWorkflow() { + CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO(); + dto.setCustIsn("C001"); + dto.setGuarType("质押"); + dto.setApplyAmt("1000000"); + dto.setBusinessType("新增"); + dto.setCouponRate("2.35"); + + LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto); + + assertEquals("2.35", workflow.getCouponRate()); + } +} +``` + +- [ ] **Step 6: Add failing model DTO test** + +In `LoanPricingModelServiceTest`, add a test or extend the existing argument-captor test to assert: + +```java +assertEquals("2.15", capturedModelInvokeDto.getCouponRate()); +``` + +The workflow used by that test must set: + +```java +loanPricingWorkflow.setCouponRate("2.15"); +loanPricingWorkflow.setBusinessType("新增"); +``` + +- [ ] **Step 7: Run backend tests and confirm failure** + +Run: + +```bash +mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +Expected: FAIL because `couponRate` and new validation are not implemented yet. + +## Task 2: Implement DTO, Entity, Converter, and Model DTO Fields + +**Files:** +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java` +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java` +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java` +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java` +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java` +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java` + +- [ ] **Step 1: Update personal create DTO** + +In `PersonalLoanPricingCreateDTO`: + +- Remove the `loanPurpose` field and its validation annotations. +- Remove the `bizProof` field. +- Change `businessType` annotations to: + +```java +@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"}) +@NotBlank(message = "业务种类不能为空") +@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一") +private String businessType; +``` + +- Add: + +```java +@Schema(description = "存单票面利率", example = "2.15") +private String couponRate; +``` + +- [ ] **Step 2: Update corporate create DTO** + +In `CorporateLoanPricingCreateDTO`: + +```java +@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"}) +@NotBlank(message = "业务种类不能为空") +@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一") +private String businessType; +``` + +Change `collType` annotation to include the combined corporate option set: + +```java +@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"}) +@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型不符合当前客户类型和担保方式") +private String collType; +``` + +Add: + +```java +@Schema(description = "存单票面利率", example = "2.35") +private String couponRate; +``` + +- [ ] **Step 3: Update workflow entity** + +In `LoanPricingWorkflow`: + +```java +/** 业务种类: 新增/存量新增/存量转贷 */ +private String businessType; + +/** 存单票面利率 */ +private String couponRate; +``` + +Keep `loanPurpose` and `bizProof` on the entity for historical rows and existing schema compatibility; they are no longer populated by personal create DTO conversion. + +- [ ] **Step 4: Update model DTO** + +In `ModelInvokeDTO`, add: + +```java +/** + * 存单票面利率 + */ +private String couponRate; +``` + +Do not add `businessType` in this task. + +- [ ] **Step 5: Update converter** + +In personal conversion, remove: + +```java +entity.setLoanPurpose(dto.getLoanPurpose()); +entity.setBizProof(dto.getBizProof()); +``` + +Add for both personal and corporate conversion: + +```java +entity.setCouponRate(dto.getCouponRate()); +``` + +- [ ] **Step 6: Run targeted converter and model tests** + +Run: + +```bash +mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +Expected: personal parameter, converter, and model field tests PASS; service validation tests may still fail until Task 3. + +- [ ] **Step 7: Commit field and converter changes** + +```bash +git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java \ + ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java \ + ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java \ + ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java \ + ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java \ + ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \ + ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java \ + ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java +git commit -m "调整上虞利率定价后端字段" +``` + +## Task 3: Implement Service-Layer Validation + +**Files:** +- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java` +- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java` + +- [ ] **Step 1: Replace business type validation** + +Update `validateBusinessTypeAndHistoryRate`: + +```java +private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) { + if (!StringUtils.hasText(workflow.getBusinessType())) { + throw new ServiceException("业务种类不能为空"); + } + if (!isOneOf(workflow.getBusinessType(), "新增", "存量新增", "存量转贷")) { + throw new ServiceException("业务种类必须是:新增、存量新增、存量转贷之一"); + } + if ("存量转贷".equals(workflow.getBusinessType()) + && !StringUtils.hasText(workflow.getLoanRateHistory())) { + throw new ServiceException("请选择历史贷款合同"); + } +} +``` + +- [ ] **Step 2: Add collateral and coupon validators** + +Add calls before insert: + +```java +validateBusinessTypeAndHistoryRate(loanPricingWorkflow); +validateCollateralType(loanPricingWorkflow); +validateCouponRate(loanPricingWorkflow); +``` + +Add helper methods: + +```java +private void validateCouponRate(LoanPricingWorkflow workflow) { + if ("质押".equals(workflow.getGuarType()) + && "存单质押".equals(workflow.getCollType()) + && !StringUtils.hasText(workflow.getCouponRate())) { + throw new ServiceException("存单票面利率不能为空"); + } +} + +private void validateCollateralType(LoanPricingWorkflow workflow) { + if (!"抵押".equals(workflow.getGuarType()) && !"质押".equals(workflow.getGuarType())) { + return; + } + if (!StringUtils.hasText(workflow.getCollType())) { + throw new ServiceException("请选择抵质押类型"); + } + if ("个人".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "一线", "一类", "二类", "三类")) { + throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一"); + } + if ("个人".equals(workflow.getCustType()) && "质押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) { + throw new ServiceException("个人质押抵质押类型必须是:存单质押、其他质押之一"); + } + if ("企业".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType()) + && !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) { + throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一"); + } + if ("企业".equals(workflow.getCustType()) && "质押".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; +} +``` + +- [ ] **Step 3: Run service tests** + +Run: + +```bash +mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +Expected: PASS. + +- [ ] **Step 4: Run backend targeted suite** + +Run: + +```bash +mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +Expected: PASS. + +- [ ] **Step 5: Commit validation changes** + +```bash +git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \ + ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java +git commit -m "增加上虞利率定价创建校验" +``` + +## Task 4: Update SQL Schema Files + +**Files:** +- Create: `sql/add_coupon_rate_20260511.sql` +- Modify: `sql/loan_pricing_workflow.sql` +- Modify: `sql/loan_pricing_schema_20260328.sql` +- Modify: `sql/loan_pricing_prod_init_20260331.sql` + +- [ ] **Step 1: Create migration SQL** + +Create `sql/add_coupon_rate_20260511.sql`: + +```sql +-- 上虞利率定价存单票面利率字段 +ALTER TABLE `loan_pricing_workflow` + ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`; +``` + +- [ ] **Step 2: Update standalone workflow schema** + +In `sql/loan_pricing_workflow.sql`, add after `loan_rate_history`: + +```sql + `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率', +``` + +- [ ] **Step 3: Update bundled schema files** + +Apply the same column to the `loan_pricing_workflow` table definitions in: + +- `sql/loan_pricing_schema_20260328.sql` +- `sql/loan_pricing_prod_init_20260331.sql` + +- [ ] **Step 4: Verify SQL references** + +Run: + +```bash +rg -n "coupon_rate|存单票面利率" sql +``` + +Expected: `coupon_rate` appears in the migration and all three schema/init files. + +- [ ] **Step 5: Commit SQL changes** + +```bash +git add sql/add_coupon_rate_20260511.sql \ + sql/loan_pricing_workflow.sql \ + sql/loan_pricing_schema_20260328.sql \ + sql/loan_pricing_prod_init_20260331.sql +git commit -m "新增存单票面利率数据库字段" +``` + +## Task 5: Backend Verification and Record + +**Files:** +- Create or Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md` + +- [ ] **Step 1: Run backend verification** + +Run: + +```bash +mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +Expected: PASS. + +- [ ] **Step 2: Check staged-independent status** + +Run: + +```bash +git status --short +``` + +Expected: only intended backend and SQL files are modified or staged for this implementation slice. Existing unrelated files such as `AGENTS.md`, `.DS_Store`, and operation-manual docs must stay out of these commits. + +- [ ] **Step 3: Update implementation record** + +Add backend notes to `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`: + +```markdown +## 后端实现 + +- 调整个人/企业创建 DTO 的业务种类口径为 `新增/存量新增/存量转贷`。 +- 新增 `couponRate` 存单票面利率字段,并贯通流程保存和模型入参。 +- 取消对私创建链路对 `loanPurpose`、`bizProof` 的依赖。 +- 增加服务层校验,覆盖业务种类、历史贷款利率、抵质押类型和存单票面利率。 +- 新增 `coupon_rate` 数据库字段及 schema/init SQL。 + +## 后端验证 + +- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test` +``` + +- [ ] **Step 4: Commit backend record** + +```bash +git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md +git commit -m "记录上虞字段调整后端实现" +``` diff --git a/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md b/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md new file mode 100644 index 0000000..53c7f9f --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md @@ -0,0 +1,485 @@ +# Shangyu Pricing Field Adjustment Frontend Implementation Plan + +> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Update the personal and corporate workflow create dialogs so the displayed fields, dynamic options, required `couponRate`, and submitted payload match the approved Shangyu pricing spec. + +**Architecture:** Keep the existing Vue 2 create-dialog components and static Node assertion tests. Update each dialog in place, using computed properties for customer-type-specific collateral options and the `质押 + 存单质押` coupon-rate condition; verify with static tests, production build, and a real Playwright browser check. + +**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, npm scripts, nvm-controlled frontend runtime, Playwright real browser verification. + +--- + +## File Structure + +- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue` + - Change `businessType` options. + - Remove `loanPurpose` and `bizProof` UI, form fields, rules, reset values, and submit payload handling. + - Change personal `collateralTypeOptions`. + - Add `couponRate` conditional field and submit cleanup. +- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue` + - Change `businessType` options. + - Change corporate `collateralTypeOptions`. + - Add `couponRate` conditional field and submit cleanup. +- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js` + - Update business-type assertions from `新客` to `新增`. + - Add `couponRate` assertions. +- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js` + - Assert personal removed fields are absent. + - Assert personal collateral options match the new spec. +- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js` + - Assert corporate collateral options match the new spec. +- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md` + - Add frontend implementation and Playwright verification notes after execution. + +## Task 1: Update Frontend Static Assertions First + +**Files:** +- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js` +- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js` +- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js` + +- [ ] **Step 1: Update business-type assertions** + +In `business-type-history-rate.test.js`, replace old assertions for `新客` with: + +```js +;[ + ['个人新增弹窗', personalCreate], + ['企业新增弹窗', corporateCreate] +].forEach(([name, source]) => { + assert(source.includes('label="业务种类"'), `${name} 缺少业务种类`) + assert(source.includes('value="新增"'), `${name} 缺少新增选项`) + assert(source.includes('value="存量新增"'), `${name} 缺少存量新增选项`) + assert(source.includes('value="存量转贷"'), `${name} 缺少存量转贷选项`) + assert(!source.includes('value="新客"'), `${name} 仍保留旧业务种类 新客`) + assert(source.includes("if (value === '存量转贷')"), `${name} 未保持仅存量转贷查询历史合同`) +}) +``` + +- [ ] **Step 2: Add coupon-rate assertions** + +In the same test file, add: + +```js +;[ + ['个人新增弹窗', personalCreate], + ['企业新增弹窗', corporateCreate] +].forEach(([name, source]) => { + assert(source.includes('label="存单票面利率"'), `${name} 缺少存单票面利率字段`) + assert(source.includes('prop="couponRate"'), `${name} 缺少 couponRate prop`) + assert(source.includes('isCertificatePledge'), `${name} 缺少存单质押条件计算`) + assert(source.includes("this.form.guarType === '质押'") && source.includes("this.form.collType === '存单质押'"), `${name} couponRate 条件不正确`) + assert(source.includes('delete data.couponRate'), `${name} 未在非适用条件删除 couponRate`) + assert(source.includes('存单票面利率不能为空'), `${name} 缺少 couponRate 必填提示`) +}) +``` + +This shared `business-type-history-rate.test.js` is expected to remain failing until both personal and corporate dialogs are updated. Do not use it as a passing checkpoint after only the personal dialog is changed. + +- [ ] **Step 3: Update personal-field assertions** + +In `personal-create-input-params.test.js`, replace old required assertions for `loanPurpose` and `bizProof` with absence checks: + +```js +assert( + !personalCreateDialog.includes('label="贷款用途"') && + !personalCreateDialog.includes('prop="loanPurpose"') && + !personalCreateDialog.includes('loanPurpose:'), + '个人新增弹窗不应继续保留贷款用途字段' +) + +assert( + !personalCreateDialog.includes('label="是否有经营佐证"') && + !personalCreateDialog.includes('prop="bizProof"') && + !personalCreateDialog.includes('bizProof:'), + '个人新增弹窗不应继续保留是否有经营佐证字段' +) +``` + +Update personal collateral assertion: + +```js +assert( + personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") && + personalCreateDialog.includes("return ['存单质押', '其他质押']"), + '个人新增弹窗抵质押类型选项未按新口径动态切换' +) +``` + +- [ ] **Step 4: Update corporate collateral assertions** + +In `corporate-create-input-params.test.js`, replace old collateral assertion with: + +```js +assert( + corporateCreateDialog.includes("return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']") && + corporateCreateDialog.includes("return ['存单质押', '股权质押', '其他质押']"), + '企业新增弹窗抵质押类型选项未按新口径动态切换' +) +``` + +- [ ] **Step 5: Run frontend static tests and confirm failure** + +Run: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params' +``` + +Expected: FAIL because the UI is not updated yet. + +## Task 2: Update Personal Create Dialog + +**Files:** +- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue` + +- [ ] **Step 1: Change business type options** + +Replace the first option: + +```vue + + + +``` + +- [ ] **Step 2: Remove personal fields from template** + +Delete the `贷款用途` form item and its surrounding column. Delete the `是否有经营佐证` form item. Keep `借款期限(年)` and `循环功能`. + +If a row becomes single-column, leave it as a normal `el-row` with one `el-col :span="12"`; do not restructure the whole dialog. + +- [ ] **Step 3: Remove personal fields from data, rules, and submit payload** + +Remove these from `form` initial state and `reset()`: + +```js +loanPurpose: undefined, +bizProof: false, +``` + +Remove `loanPurpose` from `rules`. + +Remove this submit conversion: + +```js +bizProof: this.form.bizProof ? '1' : '0', +``` + +Keep: + +```js +loanLoop: this.form.loanLoop ? '1' : '0' +``` + +- [ ] **Step 4: Update personal collateral options** + +Change `collateralTypeOptions()` to: + +```js +collateralTypeOptions() { + if (this.form.guarType === '抵押') { + return ['一线', '一类', '二类', '三类'] + } + if (this.form.guarType === '质押') { + return ['存单质押', '其他质押'] + } + return [] +} +``` + +- [ ] **Step 5: Add coupon-rate field** + +Add under the collateral row: + +```vue + + + + + +``` + +If the row already has two columns, put this field in the same `抵质押信息` area and keep the existing `900px` dialog. + +- [ ] **Step 6: Add computed condition and validator** + +Add: + +```js +isCertificatePledge() { + return this.form.guarType === '质押' && this.form.collType === '存单质押' +} +``` + +Add a validator in `data()`: + +```js +const validateCouponRate = (rule, value, callback) => { + if (this.isCertificatePledge && !value) { + callback(new Error('存单票面利率不能为空')) + return + } + callback() +} +``` + +Add rule: + +```js +couponRate: [ + {validator: validateCouponRate, trigger: "blur"} +] +``` + +- [ ] **Step 7: Add coupon-rate state cleanup** + +Add `couponRate: undefined` to `form` initial state and `reset()`. + +Add watcher: + +```js +'form.collType'() { + this.resetCouponRateIfNotCertificatePledge() +} +``` + +Add method: + +```js +resetCouponRateIfNotCertificatePledge() { + if (!this.isCertificatePledge) { + this.form.couponRate = undefined + this.$nextTick(() => { + if (this.$refs.form) { + this.$refs.form.clearValidate(['couponRate']) + } + }) + } +} +``` + +Call it at the end of `resetCollateralFields()`. + +- [ ] **Step 8: Add submit cleanup and front-end guard** + +In `submitForm`, before `this.submitting = true`, add: + +```js +if (this.isCertificatePledge && !this.form.couponRate) { + this.$modal.msgWarning("存单票面利率不能为空") + return +} +``` + +After collateral handling: + +```js +if (!this.isCertificatePledge) { + delete data.couponRate +} +``` + +- [ ] **Step 9: Run personal static tests** + +Run: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params' +``` + +Expected: PASS. The shared `test:business-type-history-rate` is intentionally not run here because it also asserts corporate changes that are implemented in Task 3. + +- [ ] **Step 10: Commit personal frontend changes** + +```bash +git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \ + ruoyi-ui/tests/personal-create-input-params.test.js +git commit -m "调整上虞对私新增字段口径" +``` + +## Task 3: Update Corporate Create Dialog + +**Files:** +- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue` +- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js` +- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js` + +- [ ] **Step 1: Change business type options** + +Use: + +```vue + + + +``` + +- [ ] **Step 2: Update corporate collateral options** + +Change `collateralTypeOptions()` to: + +```js +collateralTypeOptions() { + if (this.form.guarType === '抵押') { + return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押'] + } + if (this.form.guarType === '质押') { + return ['存单质押', '股权质押', '其他质押'] + } + return [] +} +``` + +- [ ] **Step 3: Add coupon-rate field and state** + +Mirror the personal dialog implementation: + +```vue + + + + + +``` + +Add: + +```js +couponRate: undefined +``` + +to initial `form` and `reset()`. + +- [ ] **Step 4: Add computed condition, validator, watcher, and submit cleanup** + +Use the same names as personal dialog: + +```js +isCertificatePledge() { + return this.form.guarType === '质押' && this.form.collType === '存单质押' +} +``` + +Use the same `validateCouponRate`, `couponRate` rule, `form.collType` watcher, `resetCouponRateIfNotCertificatePledge`, front-end guard, and: + +```js +if (!this.isCertificatePledge) { + delete data.couponRate +} +``` + +- [ ] **Step 5: Run corporate static tests** + +Run: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate' +``` + +Expected: PASS. + +- [ ] **Step 6: Commit corporate frontend changes** + +```bash +git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \ + ruoyi-ui/tests/corporate-create-input-params.test.js \ + ruoyi-ui/tests/business-type-history-rate.test.js +git commit -m "调整上虞对公新增字段口径" +``` + +## Task 4: Frontend Build and Real Page Verification + +**Files:** +- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md` + +- [ ] **Step 1: Run all related static tests** + +Run: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params' +``` + +Expected: PASS. + +- [ ] **Step 2: Run production build** + +Run: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod' +``` + +Expected: PASS and `ruoyi-ui/dist` generated. + +- [ ] **Step 3: Start the local app stack** + +Use the repository's existing startup scripts if available. If a frontend dev server is needed, run it with Node controlled by `nvm`: + +```bash +zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev' +``` + +Record the PID and URL. If the backend also needs restart, use the existing repository backend restart script rather than inventing a new start path. + +- [ ] **Step 4: Use Playwright on the real page** + +Open the actual local workflow page, not a prototype. Verify: + +- Personal create dialog: + - `业务种类` shows `新增/存量新增/存量转贷`. + - `新客` is absent. + - `贷款用途` is absent. + - `是否有经营佐证` is absent. + - `抵押` shows `一线/一类/二类/三类`. + - `质押 + 存单质押` shows `存单票面利率`. + - Leaving `存单票面利率` empty blocks submit. +- Corporate create dialog: + - `业务种类` shows `新增/存量新增/存量转贷`. + - `抵押` shows `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`. + - `质押` shows `存单质押/股权质押/其他质押`. + - `质押 + 存单质押` shows `存单票面利率`. + - Leaving `存单票面利率` empty blocks submit. +- History-rate behavior: + - `存量转贷` triggers historical-contract query. + - `新增` and `存量新增` do not trigger historical-contract query. + +- [ ] **Step 5: Stop test processes** + +Stop any frontend/backend processes started in this task. Verify with: + +```bash +ps -ef | rg 'vue-cli-service|ruoyi-admin|RuoYiApplication' +``` + +Expected: no leftover processes from this test run. + +- [ ] **Step 6: Update implementation record** + +Append: + +```markdown +## 前端实现 + +- 调整个人/企业新增弹窗业务种类选项为 `新增/存量新增/存量转贷`。 +- 个人新增弹窗剔除 `loanPurpose`、`bizProof`。 +- 按客户类型和担保方式调整抵质押类型选项。 +- 新增 `质押 + 存单质押` 下的 `couponRate` 存单票面利率字段、必填校验和提交清理。 + +## 前端验证 + +- `npm --prefix ruoyi-ui run test:business-type-history-rate` +- `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 build:prod` +- Playwright 真实页面验证:通过 +``` + +- [ ] **Step 7: Commit frontend verification record** + +```bash +git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md +git commit -m "记录上虞字段调整前端验证" +```