Files
loan-pricing/docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md

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 @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:

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-map to 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.