24 KiB
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
@JsonPropertyto 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-mapmock URL config.
- Adds
- Modify:
ruoyi-admin/src/main/resources/application-uat.yml- Adds
customer-mapmock URL config.
- Adds
- Modify:
ruoyi-admin/src/main/resources/application-pro.yml- Adds
customer-mapmock URL config for the production profile, currently pointing to local mock as requested.
- Adds
- 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_idforwarding, response parsing, and missing customer-id errors.
- Verifies personal/corporate URL routing,
- 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:
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:
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:
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:
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:
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:
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:
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:
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
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:
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:
@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:
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:
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:
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
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:
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:
@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:
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
- Step 4: Run controller tests
Run:
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-mapto every active profile
Place this block next to the existing model: block in each profile file:
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:
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:
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:
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:
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:
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.