From 3d1383ffd11f1b692af8f0b8e1c66db2a0a62969 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:51:38 +0000 Subject: [PATCH 1/2] Initial plan From 5740ba5b394e6594f5630e7cc28fc1ce75b46fe0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:02:50 +0000 Subject: [PATCH 2/2] Add IExchangeResultDataConvertor interface for customizing appended exchange data format Co-authored-by: juwencheng <2663764+juwencheng@users.noreply.github.com> --- .../AutoExchangeAutoConfiguration.java | 12 +- .../DefaultExchangeResultDataConvertor.java | 15 +++ .../IExchangeResultDataConvertor.java | 18 +++ .../strategy/AutoApplyExchangeStrategy.java | 7 +- .../testapp/CustomConvertorTest.java | 104 ++++++++++++++++++ 5 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/DefaultExchangeResultDataConvertor.java create mode 100644 auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/IExchangeResultDataConvertor.java create mode 100644 auto-exchange-spring-boot-test-app/src/test/java/io/github/juwencheng/autoexchange/testapp/CustomConvertorTest.java diff --git a/auto-exchange-spring-boot-autoconfigure/src/main/java/io/github/juwencheng/autoexchange/autoconfigure/AutoExchangeAutoConfiguration.java b/auto-exchange-spring-boot-autoconfigure/src/main/java/io/github/juwencheng/autoexchange/autoconfigure/AutoExchangeAutoConfiguration.java index 3d9999c..809695a 100644 --- a/auto-exchange-spring-boot-autoconfigure/src/main/java/io/github/juwencheng/autoexchange/autoconfigure/AutoExchangeAutoConfiguration.java +++ b/auto-exchange-spring-boot-autoconfigure/src/main/java/io/github/juwencheng/autoexchange/autoconfigure/AutoExchangeAutoConfiguration.java @@ -1,6 +1,8 @@ package io.github.juwencheng.autoexchange.autoconfigure; import io.github.juwencheng.autoexchange.autoconfigure.validation.RateRefreshConfigurationValidator; +import io.github.juwencheng.autoexchange.core.convertor.DefaultExchangeResultDataConvertor; +import io.github.juwencheng.autoexchange.core.convertor.IExchangeResultDataConvertor; import io.github.juwencheng.autoexchange.core.interceptor.AutoExchangeInterceptor; import org.springframework.boot.autoconfigure.condition.*; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; @@ -33,8 +35,14 @@ public class AutoExchangeAutoConfiguration { // ------------- 注册应用汇率的策略方法类 ------ @Bean @ConditionalOnMissingBean - public IApplyExchangeStrategy autoExchangeStrategy(AutoExchangeProperties properties, ExchangeManager exchangeManager) { - return new AutoApplyExchangeStrategy(properties, exchangeManager); + public IExchangeResultDataConvertor exchangeResultDataConvertor() { + return new DefaultExchangeResultDataConvertor(); + } + + @Bean + @ConditionalOnMissingBean + public IApplyExchangeStrategy autoExchangeStrategy(AutoExchangeProperties properties, ExchangeManager exchangeManager, IExchangeResultDataConvertor convertor) { + return new AutoApplyExchangeStrategy(properties, exchangeManager, convertor); } @Bean diff --git a/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/DefaultExchangeResultDataConvertor.java b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/DefaultExchangeResultDataConvertor.java new file mode 100644 index 0000000..ed97d61 --- /dev/null +++ b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/DefaultExchangeResultDataConvertor.java @@ -0,0 +1,15 @@ +package io.github.juwencheng.autoexchange.core.convertor; + +import io.github.juwencheng.autoexchange.core.dto.ExchangeResultDto; + +import java.util.Map; + +/** + * 默认的汇率转换结果数据转换器,返回包含 base、trans、rate、price 字段的 Map + */ +public class DefaultExchangeResultDataConvertor implements IExchangeResultDataConvertor { + @Override + public Map convert(ExchangeResultDto exchangeResultDto) { + return exchangeResultDto.toMap(); + } +} diff --git a/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/IExchangeResultDataConvertor.java b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/IExchangeResultDataConvertor.java new file mode 100644 index 0000000..96cf038 --- /dev/null +++ b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/convertor/IExchangeResultDataConvertor.java @@ -0,0 +1,18 @@ +package io.github.juwencheng.autoexchange.core.convertor; + +import io.github.juwencheng.autoexchange.core.dto.ExchangeResultDto; + +import java.util.Map; + +/** + * 汇率转换结果数据转换器接口,允许用户自定义追加的汇率信息格式 + */ +public interface IExchangeResultDataConvertor { + /** + * 将汇率转换结果转换为追加到响应中的数据格式 + * + * @param exchangeResultDto 汇率转换结果 + * @return 追加到响应中的数据 + */ + Map convert(ExchangeResultDto exchangeResultDto); +} diff --git a/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/strategy/AutoApplyExchangeStrategy.java b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/strategy/AutoApplyExchangeStrategy.java index 20d916b..0de9236 100644 --- a/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/strategy/AutoApplyExchangeStrategy.java +++ b/auto-exchange-spring-boot-core/src/main/java/io/github/juwencheng/autoexchange/core/strategy/AutoApplyExchangeStrategy.java @@ -8,6 +8,7 @@ import io.github.juwencheng.autoexchange.core.annotation.AutoExchangeField; import io.github.juwencheng.autoexchange.core.context.AutoExchangeContext; import io.github.juwencheng.autoexchange.core.context.AutoExchangeContextHolder; +import io.github.juwencheng.autoexchange.core.convertor.IExchangeResultDataConvertor; import io.github.juwencheng.autoexchange.core.dto.ExchangeInfoRateDto; import io.github.juwencheng.autoexchange.core.dto.ExchangeResultDto; import io.github.juwencheng.autoexchange.core.manager.ExchangeManager; @@ -25,10 +26,12 @@ public class AutoApplyExchangeStrategy extends AbstractApplyExchangeStrategy implements IApplyExchangeStrategy { private static final Logger log = LoggerFactory.getLogger(AutoApplyExchangeStrategy.class); private final ExchangeManager exchangeManager; + private final IExchangeResultDataConvertor convertor; - public AutoApplyExchangeStrategy(AutoExchangeProperties properties, ExchangeManager exchangeManager) { + public AutoApplyExchangeStrategy(AutoExchangeProperties properties, ExchangeManager exchangeManager, IExchangeResultDataConvertor convertor) { super(properties); this.exchangeManager = exchangeManager; + this.convertor = convertor; } @Override @@ -75,7 +78,7 @@ private void traverseObjectGraph(Object object, Map visitedMap) if (newFieldName == null || newFieldName.trim().isEmpty()) { newFieldName = exchangeableField.getName() + "AutoExchange"; } - context.addAppendedData(object, newFieldName, exchangeResult.toMap()); + context.addAppendedData(object, newFieldName, convertor.convert(exchangeResult)); } catch (IllegalAccessException e) { log.error("对@AutoExchangeField注解标注的属性进行自动换汇失败:" + object.getClass() + "." + exchangeableField.getName(), e); throw new ExchangeProcessingException("对@AutoExchangeField注解标注的属性进行自动换汇失败:" + object.getClass() + "." + exchangeableField.getName(), e); diff --git a/auto-exchange-spring-boot-test-app/src/test/java/io/github/juwencheng/autoexchange/testapp/CustomConvertorTest.java b/auto-exchange-spring-boot-test-app/src/test/java/io/github/juwencheng/autoexchange/testapp/CustomConvertorTest.java new file mode 100644 index 0000000..06eca5d --- /dev/null +++ b/auto-exchange-spring-boot-test-app/src/test/java/io/github/juwencheng/autoexchange/testapp/CustomConvertorTest.java @@ -0,0 +1,104 @@ +package io.github.juwencheng.autoexchange.testapp; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Primary; +import org.springframework.http.MediaType; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import io.github.juwencheng.autoexchange.core.convertor.IExchangeResultDataConvertor; +import io.github.juwencheng.autoexchange.core.dto.ExchangeResultDto; +import io.github.juwencheng.autoexchange.core.dto.ExchangeInfoRateDto; +import io.github.juwencheng.autoexchange.provider.IExchangeDataProvider; +import io.github.juwencheng.autoexchange.testapp.controller.TestController; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(TestController.class) +@TestPropertySource(properties = { + "auto.exchange.refresh-on-launch=true", + "auto.exchange.default-base-currency=CNY", + "auto.exchange.rate-refresh.enabled=false" +}) +@EnableScheduling +@EnableAspectJAutoProxy +public class CustomConvertorTest { + + @Autowired + private MockMvc mockMvc; + + @TestConfiguration + static class MockConfig { + @Bean + @Primary + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(1); + scheduler.setThreadNamePrefix("test-scheduler-"); + scheduler.initialize(); + return scheduler; + } + + @Bean + @Primary + public IExchangeDataProvider mockExchangeRateService() { + IExchangeDataProvider mock = Mockito.mock(IExchangeDataProvider.class); + when(mock.fetchData()) + .thenReturn(List.of( + new ExchangeInfoRateDto("CNY", "CNY", BigDecimal.valueOf(1)), + new ExchangeInfoRateDto("USD", "CNY", BigDecimal.valueOf(8)))); + return mock; + } + + /** + * 自定义汇率转换结果数据转换器,追加额外的 convertedBy 字段 + */ + @Bean + @Primary + public IExchangeResultDataConvertor customConvertor() { + return exchangeResultDto -> { + Map result = new HashMap<>(); + result.put("convertedPrice", exchangeResultDto.getPrice()); + result.put("currency", exchangeResultDto.getTransCurrency()); + result.put("convertedBy", "custom"); + return result; + }; + } + } + + @Test + @DisplayName("测试自定义IExchangeResultDataConvertor可以替换默认的格式") + void testCustomConvertor() throws Exception { + mockMvc.perform(get("/test/simple") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.name").value("Test Product")) + .andExpect(jsonPath("$.priceUsd").value(100.00)) + // 验证自定义格式的字段存在 + .andExpect(jsonPath("$.priceInCny.convertedPrice").value(100.00)) + .andExpect(jsonPath("$.priceInCny.currency").value("CNY")) + .andExpect(jsonPath("$.priceInCny.convertedBy").value("custom")) + // 验证默认格式的字段不存在 + .andExpect(jsonPath("$.priceInCny.base").doesNotExist()) + .andExpect(jsonPath("$.priceInCny.trans").doesNotExist()) + .andDo(print()); + } +}