# 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 "记录上虞字段调整后端实现"
```
