services = new ConcurrentHashMap<>();
+ private final WxPayMultiProperties wxPayMultiProperties;
+
+ public WxPayMultiServicesImpl(WxPayMultiProperties wxPayMultiProperties) {
+ this.wxPayMultiProperties = wxPayMultiProperties;
+ }
+
+ @Override
+ public WxPayService getWxPayService(String configKey) {
+ if (StringUtils.isBlank(configKey)) {
+ log.warn("配置标识为空,无法获取WxPayService");
+ return null;
+ }
+
+ // 使用 computeIfAbsent 实现线程安全的懒加载,避免使用 synchronized(this) 带来的性能问题
+ return services.computeIfAbsent(configKey, key -> {
+ WxPaySingleProperties properties = wxPayMultiProperties.getConfigs().get(key);
+ if (properties == null) {
+ log.warn("未找到配置标识为[{}]的微信支付配置", key);
+ return null;
+ }
+ return this.buildWxPayService(properties);
+ });
+ }
+
+ @Override
+ public void removeWxPayService(String configKey) {
+ services.remove(configKey);
+ }
+
+ /**
+ * 根据配置构建 WxPayService.
+ *
+ * @param properties 单个配置属性
+ * @return WxPayService
+ */
+ private WxPayService buildWxPayService(WxPaySingleProperties properties) {
+ WxPayServiceImpl wxPayService = new WxPayServiceImpl();
+ WxPayConfig payConfig = new WxPayConfig();
+
+ payConfig.setAppId(StringUtils.trimToNull(properties.getAppId()));
+ payConfig.setMchId(StringUtils.trimToNull(properties.getMchId()));
+ payConfig.setMchKey(StringUtils.trimToNull(properties.getMchKey()));
+ payConfig.setSubAppId(StringUtils.trimToNull(properties.getSubAppId()));
+ payConfig.setSubMchId(StringUtils.trimToNull(properties.getSubMchId()));
+ payConfig.setKeyPath(StringUtils.trimToNull(properties.getKeyPath()));
+ payConfig.setUseSandboxEnv(properties.isUseSandboxEnv());
+ payConfig.setNotifyUrl(StringUtils.trimToNull(properties.getNotifyUrl()));
+ payConfig.setRefundNotifyUrl(StringUtils.trimToNull(properties.getRefundNotifyUrl()));
+
+ // 以下是apiv3以及支付分相关
+ payConfig.setServiceId(StringUtils.trimToNull(properties.getServiceId()));
+ payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(properties.getPayScoreNotifyUrl()));
+ payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(properties.getPayScorePermissionNotifyUrl()));
+ payConfig.setPrivateKeyPath(StringUtils.trimToNull(properties.getPrivateKeyPath()));
+ payConfig.setPrivateCertPath(StringUtils.trimToNull(properties.getPrivateCertPath()));
+ payConfig.setCertSerialNo(StringUtils.trimToNull(properties.getCertSerialNo()));
+ payConfig.setApiV3Key(StringUtils.trimToNull(properties.getApiv3Key()));
+ payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId()));
+ payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath()));
+ payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
+ payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial());
+ payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel());
+
+ wxPayService.setConfig(payConfig);
+ return wxPayService;
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..d257d3727
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
new file mode 100644
index 000000000..25a091da0
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java
@@ -0,0 +1,104 @@
+package com.binarywang.spring.starter.wxjava.pay;
+
+import com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration;
+import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties;
+import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties;
+import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
+import com.github.binarywang.wxpay.service.WxPayService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 微信支付多公众号关联配置测试.
+ *
+ * @author Binary Wang
+ */
+@SpringBootTest(classes = {WxPayMultiAutoConfiguration.class, WxPayMultiServicesTest.TestApplication.class})
+@TestPropertySource(properties = {
+ "wx.pay.configs.app1.app-id=wx1111111111111111",
+ "wx.pay.configs.app1.mch-id=1111111111",
+ "wx.pay.configs.app1.mch-key=11111111111111111111111111111111",
+ "wx.pay.configs.app1.notify-url=https://example.com/pay/notify",
+ "wx.pay.configs.app2.app-id=wx2222222222222222",
+ "wx.pay.configs.app2.mch-id=2222222222",
+ "wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222",
+ "wx.pay.configs.app2.cert-serial-no=2222222222222222",
+ "wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem",
+ "wx.pay.configs.app2.private-cert-path=classpath:cert/apiclient_cert.pem"
+})
+public class WxPayMultiServicesTest {
+
+ @Autowired
+ private WxPayMultiServices wxPayMultiServices;
+
+ @Autowired
+ private WxPayMultiProperties wxPayMultiProperties;
+
+ @Test
+ public void testConfiguration() {
+ assertNotNull(wxPayMultiServices, "WxPayMultiServices should be autowired");
+ assertNotNull(wxPayMultiProperties, "WxPayMultiProperties should be autowired");
+
+ // 验证配置正确加载
+ assertEquals(2, wxPayMultiProperties.getConfigs().size(), "Should have 2 configurations");
+
+ WxPaySingleProperties app1Config = wxPayMultiProperties.getConfigs().get("app1");
+ assertNotNull(app1Config, "app1 configuration should exist");
+ assertEquals("wx1111111111111111", app1Config.getAppId());
+ assertEquals("1111111111", app1Config.getMchId());
+ assertEquals("11111111111111111111111111111111", app1Config.getMchKey());
+
+ WxPaySingleProperties app2Config = wxPayMultiProperties.getConfigs().get("app2");
+ assertNotNull(app2Config, "app2 configuration should exist");
+ assertEquals("wx2222222222222222", app2Config.getAppId());
+ assertEquals("2222222222", app2Config.getMchId());
+ assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
+ }
+
+ @Test
+ public void testGetWxPayService() {
+ WxPayService app1Service = wxPayMultiServices.getWxPayService("app1");
+ assertNotNull(app1Service, "Should get WxPayService for app1");
+ assertEquals("wx1111111111111111", app1Service.getConfig().getAppId());
+ assertEquals("1111111111", app1Service.getConfig().getMchId());
+
+ WxPayService app2Service = wxPayMultiServices.getWxPayService("app2");
+ assertNotNull(app2Service, "Should get WxPayService for app2");
+ assertEquals("wx2222222222222222", app2Service.getConfig().getAppId());
+ assertEquals("2222222222", app2Service.getConfig().getMchId());
+
+ // 测试相同key返回相同实例
+ WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1");
+ assertSame(app1Service, app1ServiceAgain, "Should return the same instance for the same key");
+ }
+
+ @Test
+ public void testGetWxPayServiceWithInvalidKey() {
+ WxPayService service = wxPayMultiServices.getWxPayService("nonexistent");
+ assertNull(service, "Should return null for non-existent key");
+ }
+
+ @Test
+ public void testRemoveWxPayService() {
+ // 首先获取一个服务实例
+ WxPayService app1Service = wxPayMultiServices.getWxPayService("app1");
+ assertNotNull(app1Service, "Should get WxPayService for app1");
+
+ // 移除服务
+ wxPayMultiServices.removeWxPayService("app1");
+
+ // 再次获取时应该创建新实例
+ WxPayService app1ServiceNew = wxPayMultiServices.getWxPayService("app1");
+ assertNotNull(app1ServiceNew, "Should get new WxPayService for app1");
+ assertNotSame(app1Service, app1ServiceNew, "Should return a new instance after removal");
+ }
+
+ @SpringBootApplication
+ static class TestApplication {
+ }
+}
diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java
new file mode 100644
index 000000000..48ae32d5b
--- /dev/null
+++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java
@@ -0,0 +1,249 @@
+package com.binarywang.spring.starter.wxjava.pay.example;
+
+import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.service.WxPayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 微信支付多公众号关联使用示例.
+ *
+ * 本示例展示了如何使用 wx-java-pay-multi-spring-boot-starter 来管理多个公众号的支付配置。
+ *
+ *
+ * @author Binary Wang
+ */
+@Slf4j
+@Service
+public class WxPayMultiExample {
+
+ @Autowired
+ private WxPayMultiServices wxPayMultiServices;
+
+ /**
+ * 示例1:根据appId创建支付订单.
+ *
+ * 适用场景:系统需要支持多个公众号,根据用户所在的公众号动态选择支付配置
+ *
+ *
+ * @param appId 公众号appId
+ * @param openId 用户的openId
+ * @param totalFee 支付金额(分)
+ * @param body 商品描述
+ * @return JSAPI支付参数
+ */
+ public WxPayUnifiedOrderV3Result.JsapiResult createJsapiOrder(String appId, String openId,
+ Integer totalFee, String body) {
+ try {
+ // 根据appId获取对应的WxPayService
+ WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
+
+ if (wxPayService == null) {
+ log.error("未找到appId对应的微信支付配置: {}", appId);
+ throw new IllegalArgumentException("未找到appId对应的微信支付配置");
+ }
+
+ // 构建支付请求
+ WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+ request.setOutTradeNo(generateOutTradeNo());
+ request.setDescription(body);
+ request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee));
+ request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
+ request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl());
+
+ // 调用微信支付API创建订单
+ WxPayUnifiedOrderV3Result.JsapiResult result =
+ wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
+
+ log.info("创建JSAPI支付订单成功,appId: {}, outTradeNo: {}", appId, request.getOutTradeNo());
+ return result;
+
+ } catch (Exception e) {
+ log.error("创建JSAPI支付订单失败,appId: {}", appId, e);
+ throw new RuntimeException("创建支付订单失败", e);
+ }
+ }
+
+ /**
+ * 示例2:服务商模式 - 为不同子商户创建订单.
+ *
+ * 适用场景:服务商为多个子商户提供支付服务
+ *
+ *
+ * @param configKey 配置标识(在配置文件中定义)
+ * @param subOpenId 子商户用户的openId
+ * @param totalFee 支付金额(分)
+ * @param body 商品描述
+ * @return JSAPI支付参数
+ */
+ public WxPayUnifiedOrderV3Result.JsapiResult createPartnerOrder(String configKey, String subOpenId,
+ Integer totalFee, String body) {
+ try {
+ // 根据配置标识获取WxPayService
+ WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
+
+ if (wxPayService == null) {
+ log.error("未找到配置: {}", configKey);
+ throw new IllegalArgumentException("未找到配置");
+ }
+
+ // 获取子商户信息
+ String subAppId = wxPayService.getConfig().getSubAppId();
+ String subMchId = wxPayService.getConfig().getSubMchId();
+ log.info("使用服务商模式,子商户appId: {}, 子商户号: {}", subAppId, subMchId);
+
+ // 构建支付请求
+ WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+ request.setOutTradeNo(generateOutTradeNo());
+ request.setDescription(body);
+ request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee));
+ request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(subOpenId));
+ request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl());
+
+ // 调用微信支付API创建订单
+ WxPayUnifiedOrderV3Result.JsapiResult result =
+ wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
+
+ log.info("创建服务商支付订单成功,配置: {}, outTradeNo: {}", configKey, request.getOutTradeNo());
+ return result;
+
+ } catch (Exception e) {
+ log.error("创建服务商支付订单失败,配置: {}", configKey, e);
+ throw new RuntimeException("创建支付订单失败", e);
+ }
+ }
+
+ /**
+ * 示例3:查询订单状态.
+ *
+ * 适用场景:查询不同公众号的订单支付状态
+ *
+ *
+ * @param appId 公众号appId
+ * @param outTradeNo 商户订单号
+ * @return 订单状态
+ */
+ public String queryOrderStatus(String appId, String outTradeNo) {
+ try {
+ WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
+
+ if (wxPayService == null) {
+ log.error("未找到appId对应的微信支付配置: {}", appId);
+ throw new IllegalArgumentException("未找到appId对应的微信支付配置");
+ }
+
+ // 查询订单
+ WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo);
+ String tradeState = result.getTradeState();
+
+ log.info("查询订单状态成功,appId: {}, outTradeNo: {}, 状态: {}", appId, outTradeNo, tradeState);
+ return tradeState;
+
+ } catch (Exception e) {
+ log.error("查询订单状态失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e);
+ throw new RuntimeException("查询订单失败", e);
+ }
+ }
+
+ /**
+ * 示例4:申请退款.
+ *
+ * 适用场景:为不同公众号的订单申请退款
+ *
+ *
+ * @param appId 公众号appId
+ * @param outTradeNo 商户订单号
+ * @param refundFee 退款金额(分)
+ * @param totalFee 订单总金额(分)
+ * @param reason 退款原因
+ * @return 退款单号
+ */
+ public String refund(String appId, String outTradeNo, Integer refundFee,
+ Integer totalFee, String reason) {
+ try {
+ WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId);
+
+ if (wxPayService == null) {
+ log.error("未找到appId对应的微信支付配置: {}", appId);
+ throw new IllegalArgumentException("未找到appId对应的微信支付配置");
+ }
+
+ // 构建退款请求
+ com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request request =
+ new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request();
+ request.setOutTradeNo(outTradeNo);
+ request.setOutRefundNo(generateRefundNo());
+ request.setReason(reason);
+ request.setNotifyUrl(wxPayService.getConfig().getRefundNotifyUrl());
+
+ com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount amount =
+ new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount();
+ amount.setRefund(refundFee);
+ amount.setTotal(totalFee);
+ amount.setCurrency("CNY");
+ request.setAmount(amount);
+
+ // 调用微信支付API申请退款
+ WxPayRefundV3Result result = wxPayService.refundV3(request);
+
+ log.info("申请退款成功,appId: {}, outTradeNo: {}, outRefundNo: {}",
+ appId, outTradeNo, request.getOutRefundNo());
+ return request.getOutRefundNo();
+
+ } catch (Exception e) {
+ log.error("申请退款失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e);
+ throw new RuntimeException("申请退款失败", e);
+ }
+ }
+
+ /**
+ * 示例5:动态管理配置.
+ *
+ * 适用场景:需要在运行时更新配置(如证书更新后需要重新加载)
+ *
+ *
+ * @param configKey 配置标识
+ */
+ public void reloadConfig(String configKey) {
+ try {
+ // 移除缓存的WxPayService实例
+ wxPayMultiServices.removeWxPayService(configKey);
+ log.info("移除配置成功,下次获取时将重新创建: {}", configKey);
+
+ // 下次调用 getWxPayService 时会重新创建实例
+ WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey);
+ if (wxPayService != null) {
+ log.info("重新加载配置成功: {}", configKey);
+ }
+
+ } catch (Exception e) {
+ log.error("重新加载配置失败: {}", configKey, e);
+ throw new RuntimeException("重新加载配置失败", e);
+ }
+ }
+
+ /**
+ * 生成商户订单号.
+ *
+ * @return 商户订单号
+ */
+ private String generateOutTradeNo() {
+ return "ORDER_" + System.currentTimeMillis();
+ }
+
+ /**
+ * 生成商户退款单号.
+ *
+ * @return 商户退款单号
+ */
+ private String generateRefundNo() {
+ return "REFUND_" + System.currentTimeMillis();
+ }
+}