新增业务种类历史利率实施计划

This commit is contained in:
wkc
2026-04-29 15:03:51 +08:00
parent 0f2a22f2c6
commit ca19ba754d
3 changed files with 1492 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
# 2026-04-29 业务种类与历史贷款利率实施计划记录
## 修改内容
- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md`
- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md`
- 后端计划覆盖字段、转换器、服务层校验、历史合同代理接口、mock、SQL 和后端测试。
- 前端计划覆盖历史合同查询 API、历史合同单选弹窗、个人/企业新增弹窗、详情展示、静态测试和 browser-use 真实页面验证。
- 根据计划审查意见,补充固定客户号 mock 场景 `HISTORY_EMPTY` / `HISTORY_EMPTY_RATE`,确保 browser-use 真实页面测试能稳定覆盖空列表和空历史利率。
- 根据计划审查意见,修正前端客户号选择测试命令,避免调用不存在的 npm script。
## 验证说明
- 本次仅产出实施计划,未进入代码实现。
- 真实页面测试已按用户要求明确使用 `browser-use:browser`,并禁止打开 prototype 页面。
- 首轮计划审查发现 4 个执行风险,已按意见补充和修订。
- 第二轮计划审查结论为 Approved。
- 后续实施完成后需要补充 `doc/implementation-report-2026-04-29-business-type-history-rate.md` 记录代码改动和验证结果。

View File

@@ -0,0 +1,742 @@
# Business Type History Rate Backend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add backend support for business type persistence, historical loan-rate lookup, and historical loan-rate model input for both personal and corporate workflow creation.
**Architecture:** Keep the existing workflow creation API shape and extend the request DTOs, entity, converter, service validation, SQL schema, and model DTO. Add one backend proxy service/controller endpoint for historical contracts, following the existing customer-map proxy pattern, plus a mock endpoint for local development and browser-use testing.
**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, JUnit 5, Mockito, Spring `MockRestServiceServer`, MySQL schema SQL.
---
## File Structure
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Add `businessType` and `loanRateHistory`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
- Add the same fields.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Add persisted fields `businessType` and `loanRateHistory`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Add `loanRateHistory` only.
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
- Represent external historical contract rows with underscore JSON names.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Map `businessType` and `loanRateHistory` for personal and corporate create DTOs.
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- Proxy external historical contract lookup.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Add `GET /loanPricing/workflow/history-contract`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Add service-layer cross-field validation before insert/model invocation.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Add mock historical contract endpoint with fixed normal, empty, and empty-rate scenarios.
- Add fixed customer-map mock customer IDs that return `cust_isn=EMPTY_HISTORY` and `cust_isn=EMPTY_RATE` for browser-use testing.
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Add local mock `loan-rate-history.url`.
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Add local mock `loan-rate-history.url`.
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- Add real external `loan-rate-history.url` without `cust_isn=`.
- Create: `sql/add_business_type_history_rate_20260429.sql`
- Migration for existing databases.
- Modify: `sql/loan_pricing_workflow.sql`
- Update standalone workflow schema.
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Update bundled schema.
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- Update production init schema.
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- 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`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
## Task 1: Add Backend Field Contracts
**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`
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- [ ] **Step 1: Write failing field tests**
Add to `LoanPricingModelServicePersonalParamsTest`:
```java
@Test
void shouldContainBusinessTypeAndLoanRateHistoryInCreateDtosAndWorkflow() throws NoSuchFieldException {
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("businessType"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("businessType"));
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("businessType"));
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("loanRateHistory"));
}
@Test
void shouldContainLoanRateHistoryButNotBusinessTypeInModelInvokeDto() throws NoSuchFieldException {
assertNotNull(ModelInvokeDTO.class.getDeclaredField("loanRateHistory"));
assertThrows(NoSuchFieldException.class, () -> ModelInvokeDTO.class.getDeclaredField("businessType"));
}
```
Create `HistoryLoanContractVOTest`:
```java
package com.ruoyi.loanpricing.domain.vo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
class HistoryLoanContractVOTest {
@Test
void shouldDeserializeUnderscoreFields() throws Exception {
String json = "{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"guar_type_history\":\"信用\",\"product_code_history\":\"P001\",\"loan_rate_history\":\"3.65\",\"loan_amount_history\":\"100000\",\"loan_sign_date_history\":\"2025-01-01\"}";
HistoryLoanContractVO vo = new ObjectMapper().readValue(json, HistoryLoanContractVO.class);
assertNotNull(vo.getCustIsn());
assertNotNull(vo.getLoanContractHistory());
assertNotNull(vo.getLoanRateHistory());
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
```
Expected: FAIL because the fields and VO do not exist.
- [ ] **Step 3: Add DTO/entity/model fields**
Add to both create DTOs:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新客|存量新增|存量转贷)$", message = "业务种类必须是:新客、存量新增、存量转贷之一")
private String businessType;
@Schema(description = "历史贷款利率", example = "3.65")
private String loanRateHistory;
```
Add to `LoanPricingWorkflow`:
```java
/** 业务种类: 新客/存量新增/存量转贷 */
private String businessType;
/** 历史贷款利率 */
private String loanRateHistory;
```
Add only this field to `ModelInvokeDTO`:
```java
/**
* 历史贷款利率
*/
private String loanRateHistory;
```
- [ ] **Step 4: Create `HistoryLoanContractVO`**
```java
package com.ruoyi.loanpricing.domain.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import lombok.Data;
@Data
public class HistoryLoanContractVO implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("cust_isn")
private String custIsn;
@JsonProperty("loan_contract_history")
private String loanContractHistory;
@JsonProperty("guar_type_history")
private String guarTypeHistory;
@JsonProperty("product_code_history")
private String productCodeHistory;
@JsonProperty("loan_rate_history")
private String loanRateHistory;
@JsonProperty("loan_amount_history")
private String loanAmountHistory;
@JsonProperty("loan_sign_date_history")
private String loanSignDateHistory;
}
```
- [ ] **Step 5: Run field tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
```
Expected: PASS.
- [ ] **Step 6: Commit**
```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/domain/vo/HistoryLoanContractVO.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java
git commit -m "新增业务种类与历史利率后端字段"
```
## Task 2: Map and Validate Workflow Creation
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- 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/LoanPricingModelServicePersonalParamsTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: Write failing converter tests**
Add:
```java
@Test
void shouldMapBusinessTypeAndLoanRateHistoryFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setBusinessType("存量转贷");
dto.setLoanRateHistory("3.65");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("存量转贷", workflow.getBusinessType());
assertEquals("3.65", workflow.getLoanRateHistory());
}
```
Add a similar corporate DTO test in `LoanPricingModelServiceTest` or a new converter-focused test.
- [ ] **Step 2: Write failing service validation tests**
Add to `LoanPricingWorkflowServiceImplTest`:
```java
@Test
void shouldRejectMissingBusinessTypeBeforeInsert() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType(null);
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类不能为空", ex.getMessage());
verify(loanPricingWorkflowMapper, never()).insert(any());
}
@Test
void shouldRejectInvalidBusinessTypeBeforeInsert() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("其他");
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类必须是:新客、存量新增、存量转贷之一", ex.getMessage());
}
@Test
void shouldRejectStockTransferWithoutLoanRateHistory() {
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory(" ");
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("请选择历史贷款合同", ex.getMessage());
}
```
Add helper:
```java
private LoanPricingWorkflow validWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustIsn("81033011438");
workflow.setCustType("个人");
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新客");
return workflow;
}
```
- [ ] **Step 3: Run tests to verify they fail**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
```
Expected: FAIL because mapping and validation do not exist.
- [ ] **Step 4: Implement converter mapping**
Add to both `toEntity(...)` methods:
```java
entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory());
```
- [ ] **Step 5: Implement service validation**
In `LoanPricingWorkflowServiceImpl`, before defaults/encryption/insert in `createLoanPricing(...)`, call:
```java
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
```
Add:
```java
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
if (!StringUtils.hasText(workflow.getBusinessType())) {
throw new ServiceException("业务种类不能为空");
}
if (!"新客".equals(workflow.getBusinessType())
&& !"存量新增".equals(workflow.getBusinessType())
&& !"存量转贷".equals(workflow.getBusinessType())) {
throw new ServiceException("业务种类必须是:新客、存量新增、存量转贷之一");
}
if ("存量转贷".equals(workflow.getBusinessType())
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
throw new ServiceException("请选择历史贷款合同");
}
}
```
Import `com.ruoyi.common.exception.ServiceException`.
- [ ] **Step 6: Run validation tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
```
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "校验并映射业务种类历史利率"
```
## Task 3: Add Historical Contract Proxy and Mock
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
- [ ] **Step 1: Write failing service tests**
Create tests following `LoanPricingCustomerMapServiceTest`:
```java
@Test
void shouldQueryHistoryContractsWithCustIsnParam() {
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
LoanRateHistoryService service = new LoanRateHistoryService(restTemplate, "http://mock/history?appCode=abc");
server.expect(requestTo("http://mock/history?appCode=abc&cust_isn=81033011438"))
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"loan_rate_history\":\"3.65\"}]}", MediaType.APPLICATION_JSON));
List<HistoryLoanContractVO> result = service.query(" 81033011438 ");
assertEquals(1, result.size());
assertEquals("3.65", result.get(0).getLoanRateHistory());
server.verify();
}
@Test
void shouldRejectBlankCustIsn() {
LoanRateHistoryService service = new LoanRateHistoryService(new RestTemplate(), "http://mock/history");
ServiceException ex = assertThrows(ServiceException.class, () -> service.query(" "));
assertEquals("客户内码不能为空", ex.getMessage());
}
```
- [ ] **Step 2: Write failing controller/mock tests**
Mock endpoint tests must cover:
- Normal `cust_isn=81033011438` returns at least one row with `loan_rate_history`.
- Fixed empty scenario, for example `cust_isn=EMPTY_HISTORY`, returns `data: []`.
- Fixed empty-rate scenario, for example `cust_isn=EMPTY_RATE`, returns a row whose `loan_rate_history` is empty.
Customer-map mock tests must also cover browser-use fixed customer numbers:
- `cust_id=HISTORY_EMPTY` returns exactly one customer-map row with `cust_isn=EMPTY_HISTORY`.
- `cust_id=HISTORY_EMPTY_RATE` returns exactly one customer-map row with `cust_isn=EMPTY_RATE`.
Controller test must verify `/loanPricing/workflow/history-contract?custIsn=81033011438` delegates to `LoanRateHistoryService.query("81033011438")`.
- [ ] **Step 3: Run tests to verify they fail**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest test
```
Expected: FAIL because service/controller/mock do not exist.
- [ ] **Step 4: Implement `LoanRateHistoryService`**
```java
@Service
public class LoanRateHistoryService {
private final RestTemplate restTemplate;
@Value("${loan-rate-history.url}")
private String historyUrl;
public LoanRateHistoryService() {
this(new RestTemplate());
}
LoanRateHistoryService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
LoanRateHistoryService(RestTemplate restTemplate, String historyUrl) {
this.restTemplate = restTemplate;
this.historyUrl = historyUrl;
}
public List<HistoryLoanContractVO> query(String custIsn) {
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
if (!StringUtils.hasText(normalizedCustIsn)) {
throw new ServiceException("客户内码不能为空");
}
URI uri = UriComponentsBuilder.fromHttpUrl(historyUrl)
.queryParam("cust_isn", normalizedCustIsn)
.build()
.encode()
.toUri();
HistoryLoanContractResponse response = restTemplate.getForObject(uri, HistoryLoanContractResponse.class);
if (response == null) {
throw new ServiceException("历史贷款合同接口无返回");
}
if (response.getCode() != null && response.getCode() != 200) {
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "历史贷款合同查询失败");
}
return response.getData() == null ? Collections.emptyList() : response.getData();
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class HistoryLoanContractResponse {
private Integer code;
private String msg;
private List<HistoryLoanContractVO> data;
}
}
```
- [ ] **Step 5: Add controller endpoint**
Inject `LoanRateHistoryService` into `LoanPricingWorkflowController` and add:
```java
@Operation(summary = "查询历史贷款合同")
@GetMapping("/history-contract")
public AjaxResult queryHistoryContract(@RequestParam("custIsn") String custIsn) {
return success(loanRateHistoryService.query(custIsn));
}
```
- [ ] **Step 6: Add mock endpoint**
Add to `LoanRatePricingMockController`:
```java
@Anonymous
@Operation(summary = "模拟历史贷款合同查询")
@GetMapping("/history-contract")
public AjaxResult queryHistoryContract(@RequestParam("cust_isn") String custIsn) {
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
if (!StringUtils.hasText(normalizedCustIsn)) {
throw new ServiceException("客户内码不能为空");
}
if ("EMPTY_HISTORY".equals(normalizedCustIsn)) {
return success(Collections.emptyList());
}
List<HistoryLoanContractVO> records = new ArrayList<>();
HistoryLoanContractVO record = new HistoryLoanContractVO();
record.setCustIsn(normalizedCustIsn);
record.setLoanContractHistory("HT" + normalizedCustIsn);
record.setGuarTypeHistory("信用");
record.setProductCodeHistory("P001");
record.setLoanRateHistory("EMPTY_RATE".equals(normalizedCustIsn) ? "" : "3.65");
record.setLoanAmountHistory("100000");
record.setLoanSignDateHistory(LocalDate.now().minusMonths(6).toString());
records.add(record);
return success(records);
}
```
- [ ] **Step 7: Add fixed customer-map mock scenarios**
Before the random-record loop in `randomCustomerMapRecords`, add:
```java
if ("HISTORY_EMPTY".equals(normalizedCustId)) {
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId(normalizedCustId);
record.setCustIsn("EMPTY_HISTORY");
record.setCustName(namePrefix + "空历史合同");
record.setFaithDay("0");
record.setBalanceAvg("0");
record.setLoanCountHis("0");
record.setLastLoanDate("");
return Collections.singletonList(record);
}
if ("HISTORY_EMPTY_RATE".equals(normalizedCustId)) {
CustomerMapRecordVO record = new CustomerMapRecordVO();
record.setCustId(normalizedCustId);
record.setCustIsn("EMPTY_RATE");
record.setCustName(namePrefix + "空历史利率");
record.setFaithDay("30");
record.setBalanceAvg("10000");
record.setLoanCountHis("1");
record.setLastLoanDate(LocalDate.now().minusMonths(3).toString());
return Collections.singletonList(record);
}
```
Import `java.util.Collections` if needed.
- [ ] **Step 8: Add profile config**
Use mock URLs in dev/uat:
```yaml
loan-rate-history:
url: http://localhost:63310/rate/pricing/mock/history-contract
```
Use real URL in pro without `cust_isn=`:
```yaml
loan-rate-history:
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
```
- [ ] **Step 9: Run proxy/mock tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerCustomerMapTest test
```
Expected: PASS.
- [ ] **Step 10: Commit**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java \
ruoyi-admin/src/main/resources/application-dev.yml \
ruoyi-admin/src/main/resources/application-uat.yml \
ruoyi-admin/src/main/resources/application-pro.yml \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java
git commit -m "新增历史贷款合同查询接口"
```
## Task 4: Persist SQL Schema Changes
**Files:**
- Create: `sql/add_business_type_history_rate_20260429.sql`
- Modify: `sql/loan_pricing_workflow.sql`
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: Write migration SQL**
Create:
```sql
-- 为利率定价流程添加业务种类和历史贷款利率字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
```
- [ ] **Step 2: Update schema SQL files**
In each `loan_pricing_workflow` create-table definition, add:
```sql
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
```
Place them after `loan_purpose`.
- [ ] **Step 3: Verify SQL text**
Run:
```bash
rg -n "business_type|loan_rate_history" sql/add_business_type_history_rate_20260429.sql sql/loan_pricing_workflow.sql sql/loan_pricing_schema_20260328.sql sql/loan_pricing_prod_init_20260331.sql
```
Expected: each file contains both fields in the workflow table context.
- [ ] **Step 4: Commit**
```bash
git add sql/add_business_type_history_rate_20260429.sql \
sql/loan_pricing_workflow.sql \
sql/loan_pricing_schema_20260328.sql \
sql/loan_pricing_prod_init_20260331.sql
git commit -m "新增业务种类历史利率数据库字段"
```
## Task 5: Verify Model Input Chain
**Files:**
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- [ ] **Step 1: Add personal model assertion**
In `shouldInvokePersonalModelWithExpectedParams`, set:
```java
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory("3.65");
```
Add to the `argThat`:
```java
&& Objects.equals("3.65", dto.getLoanRateHistory())
```
- [ ] **Step 2: Add corporate model assertion**
Add or update a corporate model invocation test in `LoanPricingModelServiceTest`:
```java
workflow.setCustType("企业");
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory("3.75");
```
Verify:
```java
verify(modelService).invokeCorporateModel(argThat((ModelInvokeDTO dto) ->
Objects.equals("3.75", dto.getLoanRateHistory())));
```
- [ ] **Step 3: Run model tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
```
Expected: PASS and no `businessType` field exists in `ModelInvokeDTO`.
- [ ] **Step 4: Commit**
```bash
git add ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java
git commit -m "验证历史利率模型入参链路"
```
## Task 6: Backend Final Verification
**Files:**
- No new files unless a test failure requires a focused fix.
- [ ] **Step 1: Run focused backend test suite**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=HistoryLoanContractVOTest,LoanRateHistoryServiceTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
```
Expected: PASS.
- [ ] **Step 2: Run existing related customer-map tests**
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanPricingWorkflowControllerCustomerMapTest,LoanRatePricingMockControllerCustomerMapTest,CustomerMapRecordVOTest test
```
Expected: PASS, confirming the new proxy pattern did not break customer-map behavior.
- [ ] **Step 3: Record backend implementation notes**
Append backend verification notes to a new or existing implementation report, for example:
```text
doc/implementation-report-2026-04-29-business-type-history-rate.md
```
Include commands run and whether browser-use verification remains for the frontend plan.
- [ ] **Step 4: Commit**
```bash
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
git commit -m "记录业务种类历史利率后端验证"
```

View File

@@ -0,0 +1,732 @@
# Business Type History Rate Frontend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add business type selection and historical loan-contract single-selection to the personal and corporate workflow creation UI, then verify the real page with browser-use.
**Architecture:** Reuse the current customer-map driven creation flow. Add one shared `HistoryContractSelector` component, one API function, fields and validation in both create dialogs, and detail display in both detail components. Final browser verification must use `browser-use:browser` with the in-app browser, not Playwright CLI or prototype pages.
**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, nvm Node 14.21.3, browser-use `iab` runtime for real page testing.
---
## File Structure
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Add `queryHistoryContracts(custIsn)`.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- Shared modal for historical contract query results and single selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Add business type, history-rate display, query trigger, selection handling, and submit validation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Same behavior for corporate creation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Show business type and historical loan rate.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Show business type and historical loan rate.
- Modify: `ruoyi-ui/package.json`
- Add `test:business-type-history-rate`.
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Static coverage for API, selector, create dialogs, validation, clearing, and detail display.
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
- Add frontend and browser-use verification notes after execution.
## Task 1: Add Frontend API and Static Test Skeleton
**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Modify: `ruoyi-ui/package.json`
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Write failing API assertions**
Create `ruoyi-ui/tests/business-type-history-rate.test.js`:
```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const workflowApi = read('src/api/loanPricing/workflow.js')
assert(
workflowApi.includes('export function queryHistoryContracts') &&
workflowApi.includes("url: '/loanPricing/workflow/history-contract'") &&
workflowApi.includes('params: { custIsn: custIsn }'),
'缺少历史贷款合同查询 API'
)
console.log('business type history rate assertions passed')
```
Add package script:
```json
"test:business-type-history-rate": "node tests/business-type-history-rate.test.js"
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL because the API function does not exist.
- [ ] **Step 3: Implement API function**
Append to `workflow.js`:
```js
// 查询历史贷款合同
export function queryHistoryContracts(custIsn) {
return request({
url: '/loanPricing/workflow/history-contract',
method: 'get',
params: { custIsn: custIsn }
})
}
```
- [ ] **Step 4: Run API test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/api/loanPricing/workflow.js ruoyi-ui/package.json ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同前端接口"
```
## Task 2: Create Historical Contract Selector
**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing selector assertions**
Add:
```js
const historySelector = read('src/views/loanPricing/workflow/components/HistoryContractSelector.vue')
assert(
historySelector.includes('title="历史贷款合同选择"') &&
historySelector.includes('width="80%"') &&
historySelector.includes(':data="contracts"'),
'历史贷款合同选择弹窗缺少标题、宽度或表格数据'
)
;[
'cust_isn',
'loan_contract_history',
'guar_type_history',
'product_code_history',
'loan_rate_history',
'loan_amount_history',
'loan_sign_date_history'
].forEach((field) => {
assert(historySelector.includes(`prop="${field}"`) || historySelector.includes(`row.${field}`), `历史合同弹窗缺少字段 ${field}`)
})
assert(
historySelector.includes('type="radio"') &&
historySelector.includes('selectedContract') &&
historySelector.includes("this.$emit('select', this.selectedContract)") &&
historySelector.includes('请选择历史贷款合同'),
'历史合同弹窗缺少单选、确定选择或未选提示'
)
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL because the component does not exist.
- [ ] **Step 3: Implement `HistoryContractSelector.vue`**
Use a small presentational component. It receives already-loaded `contracts` and does not call the API itself.
```vue
<template>
<el-dialog title="历史贷款合同选择" :visible.sync="dialogVisible" width="80%" append-to-body
:close-on-click-modal="false" @close="handleClose">
<el-table :data="contracts" v-loading="loading" @row-click="handleRowClick">
<el-table-column label="选择" align="center" width="70">
<template slot-scope="scope">
<el-radio v-model="selectedContract" :label="scope.row">&nbsp;</el-radio>
</template>
</el-table-column>
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="历史贷款合同号" prop="loan_contract_history" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="历史贷款担保方式" prop="guar_type_history" align="center"/>
<el-table-column label="历史贷款产品代码" prop="product_code_history" align="center"/>
<el-table-column label="历史贷款利率" prop="loan_rate_history" align="center"/>
<el-table-column label="历史贷款金额" prop="loan_amount_history" align="center"/>
<el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmSelect"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</div>
</el-dialog>
</template>
```
Script:
```js
export default {
name: "HistoryContractSelector",
props: {
visible: { type: Boolean, default: false },
contracts: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
},
data() {
return { selectedContract: null }
},
computed: {
dialogVisible: {
get() { return this.visible },
set(val) { this.$emit('update:visible', val) }
}
},
methods: {
handleRowClick(row) {
this.selectedContract = row
},
confirmSelect() {
if (!this.selectedContract) {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
if (!this.selectedContract.loan_rate_history) {
this.$modal.msgWarning("历史贷款利率不能为空")
return
}
this.$emit('select', this.selectedContract)
this.dialogVisible = false
},
handleClose() {
this.selectedContract = null
}
}
}
```
- [ ] **Step 4: Run selector test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS for selector assertions.
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同选择弹窗"
```
## Task 3: Add Personal Create Dialog Behavior
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing personal dialog assertions**
Add:
```js
const personalCreate = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
assert(
personalCreate.includes('label="业务种类"') &&
personalCreate.includes('v-model="form.businessType"') &&
personalCreate.includes('新客') &&
personalCreate.includes('存量新增') &&
personalCreate.includes('存量转贷'),
'个人新增弹窗缺少业务种类选择'
)
assert(
personalCreate.includes('label="历史贷款利率"') &&
personalCreate.includes('v-model="form.loanRateHistory"') &&
personalCreate.includes(':readonly="true"'),
'个人新增弹窗缺少只读历史贷款利率'
)
assert(
personalCreate.includes('queryHistoryContracts') &&
personalCreate.includes('HistoryContractSelector') &&
personalCreate.includes('handleBusinessTypeChange') &&
personalCreate.includes('handleHistoryContractSelect'),
'个人新增弹窗缺少历史合同查询和选择逻辑'
)
assert(
personalCreate.includes('请选择历史贷款合同') &&
personalCreate.includes('未查询到历史贷款合同') &&
personalCreate.includes('delete data.loanRateHistory'),
'个人新增弹窗缺少存量转贷拦截、空列表提示或非存量转贷清理'
)
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Implement personal form fields**
In the loan information area, add:
```vue
<el-row>
<el-col :span="12">
<el-form-item label="业务种类" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择业务种类" style="width: 100%" @change="handleBusinessTypeChange">
<el-option label="新客" value="新客"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="isStockTransfer">
<el-form-item label="历史贷款利率" prop="loanRateHistory">
<el-input v-model="form.loanRateHistory" placeholder="请选择历史贷款合同" :readonly="true"/>
</el-form-item>
</el-col>
</el-row>
```
Add selector under the form:
```vue
<history-contract-selector
:visible.sync="showHistorySelector"
:contracts="historyContracts"
:loading="historyLoading"
@select="handleHistoryContractSelect"
/>
```
- [ ] **Step 4: Implement personal script**
Import:
```js
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
import HistoryContractSelector from "./HistoryContractSelector"
```
Register component, then add state:
```js
businessTypeOptions: ['新客', '存量新增', '存量转贷'],
showHistorySelector: false,
historyLoading: false,
historyContracts: [],
selectedHistoryContract: null,
```
Add form fields in initial data and `reset()`:
```js
businessType: undefined,
loanRateHistory: undefined,
```
Add rules:
```js
businessType: [
{required: true, message: "请选择业务种类", trigger: "change"}
],
loanRateHistory: [
{required: true, message: "请选择历史贷款合同", trigger: "change"}
]
```
Add computed:
```js
isStockTransfer() {
return this.form.businessType === '存量转贷'
}
```
Add methods:
```js
handleBusinessTypeChange(value) {
this.clearHistoryContract()
if (value === '存量转贷') {
this.queryHistoryContracts()
}
},
queryHistoryContracts() {
if (!this.form.custIsn) {
this.$modal.msgWarning("客户内码不能为空")
return
}
this.historyLoading = true
queryHistoryContracts(this.form.custIsn).then(response => {
this.historyContracts = response.data || []
if (this.historyContracts.length === 0) {
this.$modal.msgWarning("未查询到历史贷款合同")
return
}
this.showHistorySelector = true
}).finally(() => {
this.historyLoading = false
})
},
handleHistoryContractSelect(row) {
if (!row.loan_rate_history) {
this.$modal.msgWarning("历史贷款利率不能为空")
return
}
this.selectedHistoryContract = row
this.form.loanRateHistory = row.loan_rate_history
},
clearHistoryContract() {
this.selectedHistoryContract = null
this.form.loanRateHistory = undefined
this.historyContracts = []
}
```
In `submitForm`, before setting `submitting`:
```js
if (this.isStockTransfer && !this.form.loanRateHistory) {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
```
When building `data`, remove historical rate for non-stock-transfer:
```js
if (!this.isStockTransfer) {
delete data.loanRateHistory
}
```
- [ ] **Step 5: Run personal static test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS for personal assertions.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "个人新增支持业务种类和历史利率"
```
## Task 4: Add Corporate Create Dialog Behavior
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Extend failing corporate assertions**
Mirror the personal assertions for `CorporateCreateDialog.vue`, replacing assertion messages with “企业新增弹窗...”.
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Implement corporate fields and selector**
Apply the same form, selector, state, rules, computed, and methods from Task 3, but keep existing corporate-specific fields and submission conversion intact.
- [ ] **Step 4: Keep collateral and green/trade behavior untouched**
When editing submit payload, keep:
```js
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
```
Do not reintroduce `repayMethod` UI.
- [ ] **Step 5: Run corporate static test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "企业新增支持业务种类和历史利率"
```
## Task 5: Add Detail Display
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Add failing detail assertions**
```js
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')
;[
['个人详情', personalDetail],
['企业详情', corporateDetail]
].forEach(([name, source]) => {
assert(source.includes('label="业务种类"') && source.includes('detailData.businessType'), `${name} 缺少业务种类展示`)
assert(source.includes('label="历史贷款利率"') && source.includes('detailData.loanRateHistory'), `${name} 缺少历史贷款利率展示`)
})
```
- [ ] **Step 2: Run test to verify it fails**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: FAIL.
- [ ] **Step 3: Add personal detail fields**
In personal “业务信息” descriptions, add:
```vue
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
```
- [ ] **Step 4: Add corporate detail fields**
Add the same two fields to corporate “业务信息” descriptions.
- [ ] **Step 5: Run detail test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue \
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "详情展示业务种类和历史利率"
```
## Task 6: Frontend Static Verification
**Files:**
- No new files unless tests expose a focused defect.
- [ ] **Step 1: Run new focused test**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 2: Run related existing frontend tests**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: PASS.
- [ ] **Step 3: Build frontend**
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: build succeeds.
- [ ] **Step 4: Commit only if fixes were required**
```bash
git status --short
```
Expected: no unexpected frontend changes beyond this plan.
## Task 7: Real Page Verification With browser-use
**Files:**
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
- [ ] **Step 1: Start backend with latest code**
Start or restart the backend on port `63310` using the repo's existing scripts or Maven command. Confirm the backend has loaded the latest backend code before browser testing.
Example:
```bash
bin/restart_java_backend_test.sh
```
Expected: backend listens on `63310`.
- [ ] **Step 2: Start frontend with Node 14**
Use nvm-managed Node:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'
```
Expected: frontend dev server is available at `http://localhost:8080`.
- [ ] **Step 3: Initialize browser-use**
Use the Node REPL `js` tool and the in-app browser backend. First browser cell:
```js
if (!globalThis.agent) {
const { setupAtlasRuntime } = await import("/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/scripts/browser-client.mjs");
const backend = "iab";
await setupAtlasRuntime({ globals: globalThis, backend });
}
await agent.browser.nameSession("🔎 利率定价历史利率测试");
if (typeof tab === "undefined") {
globalThis.tab = await agent.browser.tabs.new();
}
await tab.goto("http://localhost:8080");
await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 });
console.log(await tab.title());
```
- [ ] **Step 4: Log in on the real page**
Use the real login page, not a prototype. If the default test login path is available, use the project-supported login route. Otherwise log in through the visible login form with the configured test account.
After login, verify that the actual workflow list is visible.
- [ ] **Step 5: Verify personal new customer path**
In the browser:
1. Open 利率定价流程 list.
2. Click 新增.
3. Select 个人客户.
4. Query a customer number and choose a customer internal code.
5. In the personal create dialog, choose 业务种类 = 新客.
6. Confirm no historical contract selector appears.
7. Fill required fields and submit.
8. Open detail and verify 业务种类 displays 新客 and 历史贷款利率 is empty/`-`.
- [ ] **Step 6: Verify personal existing-new path**
Repeat personal creation with 业务种类 = 存量新增. Confirm no historical contract selector appears, submit succeeds, and detail displays 存量新增.
- [ ] **Step 7: Verify personal stock-transfer path**
Repeat personal creation with 业务种类 = 存量转贷. Confirm:
1. Historical contract selector opens.
2. All 7 columns are visible.
3. A single radio selection is possible.
4. Selecting a row fills 历史贷款利率.
5. Submit succeeds.
6. Detail displays 业务种类 = 存量转贷 and the chosen 历史贷款利率.
- [ ] **Step 8: Verify corporate three paths**
Repeat Steps 5-7 for 企业客户: 新客、存量新增、存量转贷.
- [ ] **Step 9: Verify blocked stock-transfer submission**
Use a stock-transfer flow and attempt to submit without selecting a historical contract. Confirm the page blocks submission and shows “请选择历史贷款合同”.
- [ ] **Step 10: Verify empty history scenario**
Use the fixed customer-map mock customer number `HISTORY_EMPTY`. In the customer number selector, query `HISTORY_EMPTY`, select the returned row with `cust_isn=EMPTY_HISTORY`, then choose 业务种类 = 存量转贷. Confirm “未查询到历史贷款合同” appears and submission remains blocked.
- [ ] **Step 11: Verify empty historical-rate scenario**
Use the fixed customer-map mock customer number `HISTORY_EMPTY_RATE`. In the customer number selector, query `HISTORY_EMPTY_RATE`, select the returned row with `cust_isn=EMPTY_RATE`, then choose 业务种类 = 存量转贷. Confirm the history selector opens with a row whose `loan_rate_history` is empty; selecting it and clicking 确定 must show “历史贷款利率不能为空”, must not fill the create form's historical loan-rate field, and submission must remain blocked.
- [ ] **Step 12: Capture evidence**
Capture screenshots or DOM snapshots for:
- Personal stock-transfer selector with all 7 columns.
- Corporate stock-transfer selector with all 7 columns.
- Detail page showing 业务种类 and 历史贷款利率.
- Blocked submit warning.
- Empty-history warning from `HISTORY_EMPTY`.
- Empty-rate warning from `HISTORY_EMPTY_RATE`.
Use browser-use screenshots through `await display(await tab.playwright.screenshot({ fullPage: false }))` when visual confirmation matters.
- [ ] **Step 13: Stop test processes**
Stop only the backend/frontend processes started for this test. Do not kill unrelated user processes.
- [ ] **Step 14: Record verification**
Update:
```text
doc/implementation-report-2026-04-29-business-type-history-rate.md
```
Include:
- Static test commands and results.
- Backend/frontend server commands.
- browser-use URL and scenarios covered.
- Confirmation that test processes were stopped.
- [ ] **Step 15: Commit verification notes**
```bash
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
git commit -m "记录业务种类历史利率页面验证"
```