24 KiB
Shangyu Pricing Field Adjustment Backend Implementation Plan
For agentic workers: Follow this repository's
AGENTS.md: do not invokeusing-superpowersor 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
businessTypevalues to新增/存量新增/存量转贷. - Remove personal create DTO dependency on
loanPurposeandbizProof. - Add
couponRate.
- Change
- Modify:
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java- Change
businessTypevalues to新增/存量新增/存量转贷. - Expand
collTypevalues for corporate mortgage and pledge paths. - Add
couponRate.
- Change
- Modify:
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java- Update
businessTypecomment and add persistedcouponRate.
- Update
- Modify:
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java- Add
couponRateonly. Do not addbusinessType; the user excluded that model-input branch from this scope.
- Add
- 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.
- Map
- 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
couponRatevalidation 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.
- Add validation coverage and update existing create tests to set valid
- Modify Test:
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java- Remove old personal
loanPurposeDTO expectations so Maven test compilation remains valid after DTO removal. - Keep
loanTerm/loanLoopmodel assertions.
- Remove old personal
- Create Test:
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java- Confirm personal removed fields no longer drive conversion and
couponRatemaps.
- Confirm personal removed fields no longer drive conversion and
- Modify Test:
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java- Confirm
couponRateis copied intoModelInvokeDTOvia the model invocation path.
- Confirm
- Create:
sql/add_coupon_rate_20260511.sql- Migration for existing databases.
- Modify:
sql/loan_pricing_workflow.sql- Add
coupon_rate.
- Add
- Modify:
sql/loan_pricing_schema_20260328.sql- Add
coupon_ratetoloan_pricing_workflow.
- Add
- Modify:
sql/loan_pricing_prod_init_20260331.sql- Add
coupon_rateto production init schema.
- Add
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:
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:
import static org.junit.jupiter.api.Assertions.assertNull;
@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(...):
@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:
workflow.setLoanPurpose("business");
workflow.setBizProof("true");
and remove these argument matcher clauses:
&& 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:
@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:
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:
@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:
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:
assertEquals("2.15", capturedModelInvokeDto.getCouponRate());
The workflow used by that test must set:
loanPricingWorkflow.setCouponRate("2.15");
loanPricingWorkflow.setBusinessType("新增");
- Step 7: Run backend tests and confirm failure
Run:
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
loanPurposefield and its validation annotations. - Remove the
bizProoffield. - Change
businessTypeannotations to:
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
private String businessType;
- Add:
@Schema(description = "存单票面利率", example = "2.15")
private String couponRate;
- Step 2: Update corporate create DTO
In CorporateLoanPricingCreateDTO:
@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:
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型不符合当前客户类型和担保方式")
private String collType;
Add:
@Schema(description = "存单票面利率", example = "2.35")
private String couponRate;
- Step 3: Update workflow entity
In LoanPricingWorkflow:
/** 业务种类: 新增/存量新增/存量转贷 */
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:
/**
* 存单票面利率
*/
private String couponRate;
Do not add businessType in this task.
- Step 5: Update converter
In personal conversion, remove:
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBizProof(dto.getBizProof());
Add for both personal and corporate conversion:
entity.setCouponRate(dto.getCouponRate());
- Step 6: Run targeted converter and model tests
Run:
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
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:
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:
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
validateCollateralType(loanPricingWorkflow);
validateCouponRate(loanPricingWorkflow);
Add helper methods:
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:
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: PASS.
- Step 4: Run backend targeted suite
Run:
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: PASS.
- Step 5: Commit validation changes
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:
-- 上虞利率定价存单票面利率字段
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:
`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:
rg -n "coupon_rate|存单票面利率" sql
Expected: coupon_rate appears in the migration and all three schema/init files.
- Step 5: Commit SQL changes
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:
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: PASS.
- Step 2: Check staged-independent status
Run:
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:
## 后端实现
- 调整个人/企业创建 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
git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md
git commit -m "记录上虞字段调整后端实现"