From a90ab42be6b8679e70797b0b8584003c15dca411 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 29 Apr 2026 11:33:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7=E5=8F=B7?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E9=80=89=E6=8B=A9=E5=AE=9E=E6=96=BD=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2026-04-29-customer-map-selection-plans.md | 14 + ...-29-customer-map-selection-backend-plan.md | 678 ++++++++++++++++++ ...29-customer-map-selection-frontend-plan.md | 662 +++++++++++++++++ 3 files changed, 1354 insertions(+) create mode 100644 doc/implementation-report-2026-04-29-customer-map-selection-plans.md create mode 100644 docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md create mode 100644 docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md diff --git a/doc/implementation-report-2026-04-29-customer-map-selection-plans.md b/doc/implementation-report-2026-04-29-customer-map-selection-plans.md new file mode 100644 index 0000000..055bad9 --- /dev/null +++ b/doc/implementation-report-2026-04-29-customer-map-selection-plans.md @@ -0,0 +1,14 @@ +# 2026-04-29 客户号查询选择客户内码实施计划记录 + +## 修改内容 + +- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md`。 +- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md`。 +- 后端计划覆盖客户号映射 VO、服务、业务接口、mock 接口、profile 配置和接口验证。 +- 前端计划覆盖客户号查询 API、查询选择弹窗、列表页流程串联、个人/企业新增弹窗只读带入和真实页面验证。 + +## 验证说明 + +- 本次仅完成实施计划文档,未进入代码实现。 +- 计划已按已确认设计拆分为前端和后端两份执行文档。 +- 实施计划已通过审查子代理审查,结论为 Approved,无阻塞问题。 diff --git a/docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md b/docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md new file mode 100644 index 0000000..107fa26 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md @@ -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 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 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 queryPersonal(String custId) + { + return query(personalUrl, custId); + } + + public List queryCorporate(String custId) + { + return query(corporateUrl, custId); + } + + private List 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 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 randomCustomerMapRecords(String custId, String namePrefix) +{ + if (!StringUtils.hasText(custId)) + { + throw new ServiceException("客户号不能为空"); + } + int count = ThreadLocalRandom.current().nextInt(1, 4); + List 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. diff --git a/docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md b/docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md new file mode 100644 index 0000000..7ce31a9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md @@ -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 + + + +``` + +- [ ] **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('/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 + +``` + +Pass the selected record into both create dialogs: + +```vue + + + +``` + +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 + + +``` + +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 "补充客户号映射选择实施记录" +```