新增客户号映射选择实施计划
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
# Customer Map Selection Backend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
|
||||
|
||||
**Goal:** Add backend customer-id-to-customer-internal-code query APIs for personal and corporate workflow creation, backed by local mock interfaces and profile configuration.
|
||||
|
||||
**Architecture:** Keep workflow creation APIs unchanged. Add a small customer-map service that reads separate personal/corporate URLs from configuration, calls them with GET `cust_id`, and returns mapping records with underscore JSON field names through the existing RuoYi `AjaxResult` response convention. Add mock endpoints under the existing mock controller so all active profiles can point to local mock URLs first.
|
||||
|
||||
**Tech Stack:** Spring Boot, RuoYi `AjaxResult`, Spring `RestTemplate`, Jackson `@JsonProperty`, JUnit 5, Mockito, Maven.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
|
||||
- Holds one customer mapping record.
|
||||
- Uses Java camelCase fields internally and `@JsonProperty` to serialize/deserialize underscore field names.
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
|
||||
- Owns config URL selection, GET forwarding, parameter validation, and response parsing.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Adds authenticated business endpoints used by the frontend.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Adds anonymous local mock endpoints.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Adds `customer-map` mock URL config.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Adds `customer-map` mock URL config.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Adds `customer-map` mock URL config for the production profile, currently pointing to local mock as requested.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
|
||||
- Verifies JSON field names stay underscored.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
|
||||
- Verifies personal/corporate URL routing, `cust_id` forwarding, response parsing, and missing customer-id errors.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
- Verifies mock endpoints return one or more underscore-field records.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
|
||||
- Verifies business controller methods delegate to the service and preserve the result records.
|
||||
|
||||
## Task 1: Add Customer Map Record VO
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing serialization test**
|
||||
|
||||
Create `CustomerMapRecordVOTest`:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CustomerMapRecordVOTest
|
||||
{
|
||||
@Test
|
||||
void shouldSerializeCustomerMapFieldsWithUnderscoreNames() throws Exception
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId("101330419198206033217");
|
||||
record.setCustIsn("81033011438");
|
||||
record.setCustName("张三");
|
||||
record.setFaithDay("20");
|
||||
record.setBalanceAvg("300000");
|
||||
record.setLoanCountHis("2");
|
||||
record.setLastLoanDate("2025-12-01");
|
||||
|
||||
String json = new ObjectMapper().writeValueAsString(record);
|
||||
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"cust_isn\""));
|
||||
assertTrue(json.contains("\"cust_name\""));
|
||||
assertTrue(json.contains("\"faith_day\""));
|
||||
assertTrue(json.contains("\"balance_avg\""));
|
||||
assertTrue(json.contains("\"loan_count_his\""));
|
||||
assertTrue(json.contains("\"last_loan_date\""));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `CustomerMapRecordVO` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the VO**
|
||||
|
||||
Create `CustomerMapRecordVO.java`:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CustomerMapRecordVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty("cust_id")
|
||||
private String custId;
|
||||
|
||||
@JsonProperty("cust_isn")
|
||||
private String custIsn;
|
||||
|
||||
@JsonProperty("cust_name")
|
||||
private String custName;
|
||||
|
||||
@JsonProperty("faith_day")
|
||||
private String faithDay;
|
||||
|
||||
@JsonProperty("balance_avg")
|
||||
private String balanceAvg;
|
||||
|
||||
@JsonProperty("loan_count_his")
|
||||
private String loanCountHis;
|
||||
|
||||
@JsonProperty("last_loan_date")
|
||||
private String lastLoanDate;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 2: Add Customer Map Service
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Write service tests first**
|
||||
|
||||
Create tests that use `MockRestServiceServer` and the package-private test constructor:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
class LoanPricingCustomerMapServiceTest
|
||||
{
|
||||
@Test
|
||||
void shouldQueryPersonalCustomerMapWithCustIdParam()
|
||||
{
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
restTemplate,
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
server.expect(requestTo("http://mock/personal?cust_id=P001"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"操作成功\",\"data\":[{\"cust_id\":\"P001\",\"cust_isn\":\"81033011438\",\"cust_name\":\"张三\"}]}",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
List<CustomerMapRecordVO> result = service.queryPersonal("P001");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("81033011438", result.get(0).getCustIsn());
|
||||
assertEquals("张三", result.get(0).getCustName());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryCorporateCustomerMapWithCustIdParam()
|
||||
{
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
restTemplate,
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
server.expect(requestTo("http://mock/corporate?cust_id=C001"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_id\":\"C001\",\"cust_isn\":\"82002469287\",\"cust_name\":\"测试企业\"}]}",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
List<CustomerMapRecordVO> result = service.queryCorporate("C001");
|
||||
|
||||
assertEquals("82002469287", result.get(0).getCustIsn());
|
||||
assertEquals("测试企业", result.get(0).getCustName());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBlankCustId()
|
||||
{
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
new RestTemplate(),
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> service.queryPersonal(" "));
|
||||
|
||||
assertEquals("客户号不能为空", ex.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `LoanPricingCustomerMapService` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the service**
|
||||
|
||||
Create the service with a default Spring constructor and a package-private test constructor:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@Service
|
||||
public class LoanPricingCustomerMapService
|
||||
{
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${customer-map.personal-url}")
|
||||
private String personalUrl;
|
||||
|
||||
@Value("${customer-map.corporate-url}")
|
||||
private String corporateUrl;
|
||||
|
||||
public LoanPricingCustomerMapService()
|
||||
{
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
LoanPricingCustomerMapService(RestTemplate restTemplate)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
LoanPricingCustomerMapService(RestTemplate restTemplate, String personalUrl, String corporateUrl)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
this.personalUrl = personalUrl;
|
||||
this.corporateUrl = corporateUrl;
|
||||
}
|
||||
|
||||
public List<CustomerMapRecordVO> queryPersonal(String custId)
|
||||
{
|
||||
return query(personalUrl, custId);
|
||||
}
|
||||
|
||||
public List<CustomerMapRecordVO> queryCorporate(String custId)
|
||||
{
|
||||
return query(corporateUrl, custId);
|
||||
}
|
||||
|
||||
private List<CustomerMapRecordVO> query(String url, String custId)
|
||||
{
|
||||
if (!StringUtils.hasText(custId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
URI uri = UriComponentsBuilder.fromHttpUrl(url)
|
||||
.queryParam("cust_id", custId)
|
||||
.build()
|
||||
.toUri();
|
||||
CustomerMapResponse response = restTemplate.getForObject(uri, CustomerMapResponse.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 CustomerMapResponse
|
||||
{
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private List<CustomerMapRecordVO> data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run service tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 3: Add Mock Endpoints
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
|
||||
- [ ] **Step 1: Write controller tests for mock endpoints**
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LoanRatePricingMockControllerCustomerMapTest
|
||||
{
|
||||
@Test
|
||||
void shouldReturnPersonalCustomerMapRecords() throws Exception
|
||||
{
|
||||
LoanRatePricingMockController controller = new LoanRatePricingMockController();
|
||||
|
||||
AjaxResult result = controller.queryPersonalCustomerMap("P001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertFalse(rows.isEmpty());
|
||||
String json = new ObjectMapper().writeValueAsString(rows.get(0));
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"cust_isn\""));
|
||||
assertTrue(json.contains("\"cust_name\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCorporateCustomerMapRecords() throws Exception
|
||||
{
|
||||
LoanRatePricingMockController controller = new LoanRatePricingMockController();
|
||||
|
||||
AjaxResult result = controller.queryCorporateCustomerMap("C001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertFalse(rows.isEmpty());
|
||||
String json = new ObjectMapper().writeValueAsString(rows.get(0));
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"loan_count_his\""));
|
||||
assertTrue(json.contains("\"last_loan_date\""));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because the mock methods do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement mock methods**
|
||||
|
||||
Modify `LoanRatePricingMockController`:
|
||||
|
||||
```java
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟个人客户号映射查询")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "个人客户"));
|
||||
}
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟企业客户号映射查询")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "企业客户"));
|
||||
}
|
||||
```
|
||||
|
||||
Add a private helper in the same controller:
|
||||
|
||||
```java
|
||||
private List<CustomerMapRecordVO> randomCustomerMapRecords(String custId, String namePrefix)
|
||||
{
|
||||
if (!StringUtils.hasText(custId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
int count = ThreadLocalRandom.current().nextInt(1, 4);
|
||||
List<CustomerMapRecordVO> records = new ArrayList<>();
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(custId);
|
||||
record.setCustIsn(String.valueOf(81000000000L + ThreadLocalRandom.current().nextInt(1000000)));
|
||||
record.setCustName(namePrefix + i);
|
||||
record.setFaithDay(String.valueOf(ThreadLocalRandom.current().nextInt(1, 365)));
|
||||
record.setBalanceAvg(String.valueOf(ThreadLocalRandom.current().nextInt(10000, 900000)));
|
||||
record.setLoanCountHis(String.valueOf(ThreadLocalRandom.current().nextInt(0, 10)));
|
||||
record.setLastLoanDate(LocalDate.now().minusDays(ThreadLocalRandom.current().nextInt(1, 800)).toString());
|
||||
records.add(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
```
|
||||
|
||||
Required imports:
|
||||
|
||||
```java
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run mock controller tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 4: Add Business Endpoints
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
|
||||
|
||||
- [ ] **Step 1: Write controller delegation tests**
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class LoanPricingWorkflowControllerCustomerMapTest
|
||||
{
|
||||
@Test
|
||||
void shouldReturnPersonalCustomerMapFromService() throws Exception
|
||||
{
|
||||
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
|
||||
CustomerMapRecordVO row = new CustomerMapRecordVO();
|
||||
row.setCustIsn("81033011438");
|
||||
when(service.queryPersonal("P001")).thenReturn(List.of(row));
|
||||
|
||||
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
|
||||
setField(controller, "customerMapService", service);
|
||||
|
||||
AjaxResult result = controller.queryPersonalCustomerMap("P001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertEquals(1, rows.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCorporateCustomerMapFromService() throws Exception
|
||||
{
|
||||
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
|
||||
CustomerMapRecordVO row = new CustomerMapRecordVO();
|
||||
row.setCustIsn("82002469287");
|
||||
when(service.queryCorporate("C001")).thenReturn(List.of(row));
|
||||
|
||||
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
|
||||
setField(controller, "customerMapService", service);
|
||||
|
||||
AjaxResult result = controller.queryCorporateCustomerMap("C001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertEquals(1, rows.size());
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because controller methods and field do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement controller methods**
|
||||
|
||||
In `LoanPricingWorkflowController`, add:
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private LoanPricingCustomerMapService customerMapService;
|
||||
|
||||
@Operation(summary = "查询个人客户号映射")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryPersonal(custId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询企业客户号映射")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryCorporate(custId));
|
||||
}
|
||||
```
|
||||
|
||||
Required import:
|
||||
|
||||
```java
|
||||
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run controller tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 5: Add Profile Configuration
|
||||
|
||||
**Files:**
|
||||
- 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`
|
||||
|
||||
- [ ] **Step 1: Add `customer-map` to every active profile**
|
||||
|
||||
Place this block next to the existing `model:` block in each profile file:
|
||||
|
||||
```yaml
|
||||
customer-map:
|
||||
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
|
||||
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate
|
||||
```
|
||||
|
||||
Keep the existing `model:` URLs unchanged.
|
||||
|
||||
- [ ] **Step 2: Verify config keys exist**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "customer-map:|personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal|corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate" ruoyi-admin/src/main/resources/application-dev.yml ruoyi-admin/src/main/resources/application-uat.yml ruoyi-admin/src/main/resources/application-pro.yml
|
||||
```
|
||||
|
||||
Expected: each profile contains one `customer-map` block with both mock URLs.
|
||||
|
||||
## Task 6: Backend Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only, no new files.
|
||||
|
||||
- [ ] **Step 1: Run focused backend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run affected existing model/workflow tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Manual API verification after backend restart**
|
||||
|
||||
Restart backend so config and routes are active, then call:
|
||||
|
||||
```bash
|
||||
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/personal?cust_id=P001'
|
||||
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/corporate?cust_id=C001'
|
||||
TOKEN=$(curl -sS -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' 'http://localhost:63310/login/test' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/personal?custId=P001'
|
||||
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/corporate?custId=C001'
|
||||
```
|
||||
|
||||
Expected: each successful response uses the RuoYi response wrapper and `data` contains one or more records with underscore fields. If the local admin password differs, replace `admin/admin123` with a valid local test account before calling the authenticated workflow endpoints.
|
||||
|
||||
- [ ] **Step 4: Commit backend work**
|
||||
|
||||
Use a Chinese commit message and do not include unrelated dirty files:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.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-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.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
|
||||
git commit -m "新增客户号映射后端接口"
|
||||
```
|
||||
|
||||
Do not commit temporary curl output, screenshots, or generated test data.
|
||||
@@ -0,0 +1,662 @@
|
||||
# Customer Map Selection Frontend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
|
||||
|
||||
**Goal:** Add the frontend customer-id query and customer-internal-code selection step before opening personal or corporate workflow creation dialogs.
|
||||
|
||||
**Architecture:** Keep the existing list page and personal/corporate creation dialogs. Insert a shared customer-map selector dialog between customer-type selection and workflow creation, then pass the selected underscore-field record into the existing create dialogs. Existing workflow create APIs stay unchanged and still receive `custIsn` and `custName` in camelCase.
|
||||
|
||||
**Tech Stack:** Vue 2, Element UI, existing RuoYi request wrapper, Node static assertion tests, nvm-managed frontend runtime, Playwright/browser verification after implementation.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Adds personal/corporate customer-map query functions.
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
|
||||
- Owns customer-id input, query action, result table, loading state, and row selection.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Opens customer-map selector after customer type selection and then opens the correct create dialog after row selection.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Accepts selected customer-map record, fills `custIsn` and `custName`, and makes both fields read-only.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Same selected-record behavior for corporate creation.
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
- Adds a focused test script for the new customer-map selection checks.
|
||||
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
- Static test coverage for API paths, selector fields, parent wiring, selected underscore fields, and read-only create dialog inputs.
|
||||
|
||||
## Task 1: Add Frontend API Methods
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
|
||||
- [ ] **Step 1: Write failing API assertions**
|
||||
|
||||
Create the first version of `ruoyi-ui/tests/customer-map-selection.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 queryPersonalCustomerMap') &&
|
||||
workflowApi.includes("url: '/loanPricing/workflow/customer-map/personal'") &&
|
||||
workflowApi.includes('params: { custId: custId }'),
|
||||
'缺少个人客户号映射查询 API'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowApi.includes('export function queryCorporateCustomerMap') &&
|
||||
workflowApi.includes("url: '/loanPricing/workflow/customer-map/corporate'") &&
|
||||
workflowApi.includes('params: { custId: custId }'),
|
||||
'缺少企业客户号映射查询 API'
|
||||
)
|
||||
|
||||
console.log('customer map selection assertions passed')
|
||||
```
|
||||
|
||||
Add a script in `ruoyi-ui/package.json`:
|
||||
|
||||
```json
|
||||
"test:customer-map-selection": "node tests/customer-map-selection.test.js"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run with the project Node version:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because the API methods do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the API methods**
|
||||
|
||||
Append to `workflow.js`:
|
||||
|
||||
```js
|
||||
// 查询个人客户号映射
|
||||
export function queryPersonalCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/personal',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询企业客户号映射
|
||||
export function queryCorporateCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/corporate',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the API test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS for API assertions.
|
||||
|
||||
## Task 2: Create Customer Map Selector Dialog
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend the static test for selector requirements**
|
||||
|
||||
Add these assertions to `customer-map-selection.test.js`:
|
||||
|
||||
```js
|
||||
const selector = read('src/views/loanPricing/workflow/components/CustomerMapSelector.vue')
|
||||
|
||||
assert(
|
||||
selector.includes('title="客户号查询"') &&
|
||||
selector.includes('v-model="queryForm.custId"') &&
|
||||
selector.includes('handleQuery'),
|
||||
'客户号查询弹窗缺少客户号输入或查询动作'
|
||||
)
|
||||
|
||||
assert(
|
||||
selector.includes("queryPersonalCustomerMap") &&
|
||||
selector.includes("queryCorporateCustomerMap") &&
|
||||
selector.includes("this.customerType === 'personal'"),
|
||||
'客户号查询弹窗未按客户类型调用个人/企业接口'
|
||||
)
|
||||
|
||||
;['cust_id', 'cust_isn', 'cust_name', 'faith_day', 'balance_avg', 'loan_count_his', 'last_loan_date'].forEach((field) => {
|
||||
assert(selector.includes(`prop="${field}"`) || selector.includes(`row.${field}`), `查询结果表格缺少字段 ${field}`)
|
||||
})
|
||||
|
||||
assert(
|
||||
selector.includes("this.$emit('select', row)") &&
|
||||
selector.includes('未查询到客户信息') &&
|
||||
selector.includes('请输入客户号'),
|
||||
'客户号查询弹窗缺少选择事件或关键提示'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because `CustomerMapSelector.vue` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `CustomerMapSelector.vue`**
|
||||
|
||||
Create the component:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog title="客户号查询" :visible.sync="dialogVisible" width="900px" append-to-body
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<el-form :model="queryForm" inline size="small">
|
||||
<el-form-item label="客户号">
|
||||
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="customerList">
|
||||
<el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="用信天数" prop="faith_day" align="center"/>
|
||||
<el-table-column label="存款年日均" prop="balance_avg" align="center"/>
|
||||
<el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
|
||||
<el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
|
||||
<el-table-column label="操作" align="center" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"
|
||||
|
||||
export default {
|
||||
name: "CustomerMapSelector",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerType: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
queryForm: {
|
||||
custId: undefined
|
||||
},
|
||||
customerList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleQuery() {
|
||||
if (!this.queryForm.custId) {
|
||||
this.$modal.msgWarning("请输入客户号")
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
const request = this.customerType === 'personal'
|
||||
? queryPersonalCustomerMap
|
||||
: queryCorporateCustomerMap
|
||||
request(this.queryForm.custId).then(response => {
|
||||
this.customerList = response.data || []
|
||||
if (this.customerList.length === 0) {
|
||||
this.$modal.msgWarning("未查询到客户信息")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSelect(row) {
|
||||
this.$emit('select', row)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleClose() {
|
||||
this.queryForm.custId = undefined
|
||||
this.customerList = []
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the selector test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through selector assertions.
|
||||
|
||||
## Task 3: Wire Selector Into Workflow List Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend the test for parent wiring**
|
||||
|
||||
Add assertions:
|
||||
|
||||
```js
|
||||
const workflowIndex = read('src/views/loanPricing/workflow/index.vue')
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('CustomerMapSelector') &&
|
||||
workflowIndex.includes('<customer-map-selector') &&
|
||||
workflowIndex.includes(':customer-type="selectedCustomerType"') &&
|
||||
workflowIndex.includes('@select="handleCustomerMapSelect"'),
|
||||
'流程列表页未接入客户号查询选择弹窗'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('selectedCustomerType') &&
|
||||
workflowIndex.includes('selectedCustomerMap') &&
|
||||
workflowIndex.includes('showCustomerMapSelector'),
|
||||
'流程列表页缺少客户类型、客户映射选择状态'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes("this.selectedCustomerType = type") &&
|
||||
workflowIndex.includes('this.showCustomerMapSelector = true') &&
|
||||
!workflowIndex.includes("if (type === 'personal') {\\n this.showPersonalDialog = true"),
|
||||
'选择客户类型后应先打开客户号查询弹窗,而不是直接打开新增弹窗'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('handleCustomerMapSelect') &&
|
||||
workflowIndex.includes('this.selectedCustomerMap = row') &&
|
||||
workflowIndex.includes('this.showPersonalDialog = true') &&
|
||||
workflowIndex.includes('this.showCorporateDialog = true'),
|
||||
'选择客户内码后未打开对应新增弹窗'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because the list page does not yet use the selector.
|
||||
|
||||
- [ ] **Step 3: Update `index.vue` template and script**
|
||||
|
||||
Add component usage after the customer type selector:
|
||||
|
||||
```vue
|
||||
<customer-map-selector
|
||||
:visible.sync="showCustomerMapSelector"
|
||||
:customer-type="selectedCustomerType"
|
||||
@select="handleCustomerMapSelect"
|
||||
/>
|
||||
```
|
||||
|
||||
Pass the selected record into both create dialogs:
|
||||
|
||||
```vue
|
||||
<personal-create-dialog
|
||||
:visible.sync="showPersonalDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
|
||||
<corporate-create-dialog
|
||||
:visible.sync="showCorporateDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
```
|
||||
|
||||
Import and register the selector:
|
||||
|
||||
```js
|
||||
import CustomerMapSelector from "./components/CustomerMapSelector"
|
||||
```
|
||||
|
||||
Add state:
|
||||
|
||||
```js
|
||||
showCustomerMapSelector: false,
|
||||
selectedCustomerType: undefined,
|
||||
selectedCustomerMap: null,
|
||||
```
|
||||
|
||||
Change `handleSelectType`:
|
||||
|
||||
```js
|
||||
handleSelectType(type) {
|
||||
this.selectedCustomerType = type
|
||||
this.selectedCustomerMap = null
|
||||
this.showCustomerMapSelector = true
|
||||
}
|
||||
```
|
||||
|
||||
Add selected-row handler:
|
||||
|
||||
```js
|
||||
handleCustomerMapSelect(row) {
|
||||
this.selectedCustomerMap = row
|
||||
if (this.selectedCustomerType === 'personal') {
|
||||
this.showPersonalDialog = true
|
||||
} else if (this.selectedCustomerType === 'corporate') {
|
||||
this.showCorporateDialog = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clear selected state when the create flow completes:
|
||||
|
||||
```js
|
||||
handleCreateSuccess() {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
this.getList()
|
||||
}
|
||||
```
|
||||
|
||||
Add watchers for dialog close so cancellation also clears the selected record:
|
||||
|
||||
```js
|
||||
watch: {
|
||||
showPersonalDialog(val) {
|
||||
if (!val && !this.showCorporateDialog) {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
}
|
||||
},
|
||||
showCorporateDialog(val) {
|
||||
if (!val && !this.showPersonalDialog) {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the parent wiring test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through parent wiring assertions.
|
||||
|
||||
## Task 4: Make Create Dialog Customer Fields Read-Only and Auto-Filled
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend test for create dialog behavior**
|
||||
|
||||
Add assertions:
|
||||
|
||||
```js
|
||||
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
|
||||
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')
|
||||
|
||||
;[
|
||||
['个人新增弹窗', personalCreateDialog],
|
||||
['企业新增弹窗', corporateCreateDialog]
|
||||
].forEach(([name, source]) => {
|
||||
assert(source.includes('customerMap'), `${name} 缺少 customerMap 入参`)
|
||||
assert(source.includes(':readonly="true"') || source.includes('readonly'), `${name} 客户内码和客户名称应只读`)
|
||||
assert(source.includes('this.customerMap.cust_isn'), `${name} 未从 cust_isn 自动带入客户内码`)
|
||||
assert(source.includes('this.customerMap.cust_name'), `${name} 未从 cust_name 自动带入客户名称`)
|
||||
assert(source.includes('clearValidate'), `${name} 应清空校验而不是用 resetFields 覆盖已选客户`)
|
||||
assert(!source.includes('this.resetForm("form")'), `${name} 不应在 reset() 中调用 resetForm("form") 覆盖只读客户字段`)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because dialogs do not yet consume `customerMap`.
|
||||
|
||||
- [ ] **Step 3: Update personal create dialog**
|
||||
|
||||
Add prop:
|
||||
|
||||
```js
|
||||
customerMap: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
```
|
||||
|
||||
Make customer fields read-only:
|
||||
|
||||
```vue
|
||||
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
|
||||
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
|
||||
```
|
||||
|
||||
In `reset()`, initialize from selected row and do not call `this.resetForm("form")`. The existing RuoYi helper delegates to Element UI `resetFields()`, which can restore stale initial values and overwrite the selected read-only customer fields. After assigning the new form object, only clear validation:
|
||||
|
||||
```js
|
||||
reset() {
|
||||
this.form = {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanPurpose: undefined,
|
||||
loanTerm: undefined,
|
||||
bizProof: false,
|
||||
loanLoop: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
}
|
||||
this.submitting = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Keep existing required validation for `custIsn` and `custName`.
|
||||
|
||||
- [ ] **Step 4: Update corporate create dialog**
|
||||
|
||||
Apply the same prop, read-only inputs, and `reset()` initialization to `CorporateCreateDialog.vue`. Do not call `this.resetForm("form")` in the corporate dialog reset path; assign the new form object with `custIsn` and `custName` from `customerMap`, then use `this.$refs.form.clearValidate()` inside `$nextTick()`.
|
||||
|
||||
- [ ] **Step 5: Run the create dialog test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through create dialog assertions.
|
||||
|
||||
## Task 5: Frontend Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only, no new source files.
|
||||
|
||||
- [ ] **Step 1: Run focused customer-map test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run affected existing frontend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Build frontend**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Browser verification after backend and frontend are running**
|
||||
|
||||
Use the real application page, not a prototype page:
|
||||
|
||||
1. Open the workflow list page.
|
||||
2. Click “新增”.
|
||||
3. Select “个人客户”.
|
||||
4. Confirm the customer-id query dialog opens.
|
||||
5. Query any customer number.
|
||||
6. Select one result row.
|
||||
7. Confirm the personal create dialog opens and `客户内码` / `客户名称` are auto-filled and read-only.
|
||||
8. Fill the remaining required fields and submit.
|
||||
9. Repeat the same flow for “企业客户”.
|
||||
10. Close and reopen the create flow at least once with a second selected row, and confirm the new row's `cust_isn` / `cust_name` replace the previous values in the create dialog.
|
||||
11. Confirm both created records appear in the list or detail page.
|
||||
|
||||
Expected: both personal and corporate flows pass through customer-map selection before creation.
|
||||
|
||||
- [ ] **Step 5: Cleanup test processes**
|
||||
|
||||
Stop any backend or frontend processes started for verification before ending the task.
|
||||
|
||||
- [ ] **Step 6: Commit frontend work**
|
||||
|
||||
Use a Chinese commit message and avoid unrelated dirty files:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ruoyi-ui/src/api/loanPricing/workflow.js \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/index.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
|
||||
ruoyi-ui/package.json \
|
||||
ruoyi-ui/tests/customer-map-selection.test.js
|
||||
git commit -m "新增客户号查询选择前端流程"
|
||||
```
|
||||
|
||||
Do not commit screenshots, browser traces, temporary spreadsheets, or generated test data.
|
||||
|
||||
## Task 6: Final End-to-End Verification and Implementation Record
|
||||
|
||||
**Files:**
|
||||
- Create: `doc/implementation-report-2026-04-29-customer-map-selection.md`
|
||||
|
||||
- [ ] **Step 1: Run backend and frontend verification together**
|
||||
|
||||
After backend and frontend implementation are both complete:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Complete browser verification**
|
||||
|
||||
Run the browser flow from Task 5 for both personal and corporate customers. Confirm the backend API response wrapper is still the existing RuoYi wrapper while records inside `data` keep underscore fields.
|
||||
|
||||
- [ ] **Step 3: Write implementation report**
|
||||
|
||||
Create `doc/implementation-report-2026-04-29-customer-map-selection.md` with:
|
||||
|
||||
```markdown
|
||||
# 2026-04-29 客户号查询选择客户内码实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端新增个人/企业客户号映射业务接口。
|
||||
- 后端新增个人/企业客户号映射 mock 接口。
|
||||
- 配置文件新增 `customer-map` 个人/企业地址并指向本项目 mock。
|
||||
- 前端新增客户号查询选择弹窗。
|
||||
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
|
||||
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 后端测试:填写实际执行命令和通过/失败结果。
|
||||
- 前端测试:填写实际执行命令和通过/失败结果。
|
||||
- 真实页面验证:填写个人、企业两条浏览器验证流程和结果。
|
||||
- 进程清理:填写本次启动的前后端进程是否已关闭。
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit implementation report**
|
||||
|
||||
```bash
|
||||
git add doc/implementation-report-2026-04-29-customer-map-selection.md
|
||||
git commit -m "补充客户号映射选择实施记录"
|
||||
```
|
||||
Reference in New Issue
Block a user