diff --git a/doc/implementation-report-2026-04-29-business-type-history-rate-plans.md b/doc/implementation-report-2026-04-29-business-type-history-rate-plans.md new file mode 100644 index 0000000..e3454f3 --- /dev/null +++ b/doc/implementation-report-2026-04-29-business-type-history-rate-plans.md @@ -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` 记录代码改动和验证结果。 diff --git a/docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md b/docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md new file mode 100644 index 0000000..fa6b786 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md @@ -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 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 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 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 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 "记录业务种类历史利率后端验证" +``` diff --git a/docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md b/docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md new file mode 100644 index 0000000..f8fccaf --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md @@ -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 + +``` + +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 + + + + + + + + + + + + + + + + +``` + +Add selector under the form: + +```vue + +``` + +- [ ] **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 +{{ detailData.businessType || '-' }} +{{ detailData.loanRateHistory || '-' }} +``` + +- [ ] **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 "记录业务种类历史利率页面验证" +```