Ver Fonte

微信支付修改

zhangchen há 1 dia atrás
pai
commit
e994375afa

+ 141 - 76
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantWechatPaymentStrategyImpl.java

@@ -11,22 +11,25 @@ import org.springframework.stereotype.Component;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.MerchantPaymentOrder;
 import shop.alien.entity.store.RefundRecord;
-import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.UserReservation;
 import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.store.service.MerchantPaymentOrderService;
 import shop.alien.store.service.RefundRecordAsyncService;
 import shop.alien.store.service.RefundRecordService;
-import shop.alien.store.service.StorePaymentConfigService;
 import shop.alien.store.service.ReservationOrderPaymentTimeoutService;
 import shop.alien.store.service.UserReservationOrderService;
 import shop.alien.store.service.UserReservationService;
 import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
+import shop.alien.store.strategy.payment.impl.WeChatPartnerPaymentStrategyImpl;
 import shop.alien.store.strategy.payment.impl.WeChatPaymentStrategyImpl;
 import shop.alien.store.util.WXPayUtility;
 import shop.alien.util.common.UniqueRandomNumGenerator;
 import shop.alien.util.common.constant.PaymentEnum;
+import shop.alien.util.system.OSUtil;
 
+import javax.annotation.PostConstruct;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
@@ -37,7 +40,9 @@ import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
- * 商户微信支付策略(预订订单,使用 StorePaymentConfig 按门店配置,业务逻辑与支付宝一致)
+ * 商户微信支付策略(预订订单):业务与支付宝侧一致;支付接口走微信服务商模式(特约商户),
+ * 与 {@link WeChatPartnerPaymentStrategyImpl} 一致使用 sp_mchid + sub_mchid、服务商证书;
+ * 不查 {@code StorePaymentConfig};预下单请求体不设 sub_appid(与 Partner 的 APP 预下单一致)。
  *
  * @author system
  */
@@ -59,20 +64,47 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     /** 退款记录状态:退款失败 */
     private static final String REFUND_STATUS_FAIL = "FAIL";
 
-    @Value("${payment.wechatPay.host:https://api.mch.weixin.qq.com}")
+    @Value("${payment.wechatPartnerPay.host:https://api.mch.weixin.qq.com}")
     private String wechatPayApiHost;
-    @Value("${payment.wechatPay.prePayPath:/v3/pay/transactions/app}")
+    @Value("${payment.wechatPartnerPay.prePayPath:/v3/pay/partner/transactions/app}")
     private String prePayPath;
-    @Value("${payment.wechatPay.searchOrderByOutTradeNoPath:/v3/pay/transactions/out-trade-no/{out_trade_no}}")
+    @Value("${payment.wechatPartnerPay.searchOrderByOutTradeNoPath:/v3/pay/partner/transactions/out-trade-no/{out_trade_no}}")
     private String searchOrderByOutTradeNoPath;
-    @Value("${payment.wechatPay.refundPath:/v3/refund/domestic/refunds}")
+    @Value("${payment.wechatPartnerPay.refundPath:/v3/refund/domestic/refunds}")
     private String refundPath;
+
+    @Value("${payment.wechatPartnerPay.business.spAppId}")
+    private String spAppId;
+    @Value("${payment.wechatPartnerPay.business.spMchId}")
+    private String spMchId;
+
+    @Value("${payment.wechatPartnerPay.business.win.privateKeyPath}")
+    private String privateWinKeyPath;
+    @Value("${payment.wechatPartnerPay.business.linux.privateKeyPath}")
+    private String privateLinuxKeyPath;
+    @Value("${payment.wechatPartnerPay.business.win.wechatPayPublicKeyFilePath}")
+    private String wechatWinPayPublicKeyFilePath;
+    @Value("${payment.wechatPartnerPay.business.linux.wechatPayPublicKeyFilePath}")
+    private String wechatLinuxPayPublicKeyFilePath;
+    @Value("${payment.wechatPartnerPay.business.merchantSerialNumber}")
+    private String merchantSerialNumber;
+    @Value("${payment.wechatPartnerPay.business.wechatPayPublicKeyId}")
+    private String wechatPayPublicKeyId;
+
+    /** 服务商下单回调 URL:优先 partner 配置(与 {@link WeChatPartnerPaymentStrategyImpl} 一致),否则回退直连配置 */
+    @Value("${payment.wechatPartnerPay.business.prePayNotifyUrl:}")
+    private String partnerPrePayNotifyUrl;
     @Value("${payment.wechatPay.business.prePayNotifyUrl:}")
-    private String prePayNotifyUrl;
+    private String directPrePayNotifyUrl;
+    @Value("${payment.wechatPartnerPay.business.refundNotifyUrl:}")
+    private String partnerRefundNotifyUrl;
     @Value("${payment.wechatPay.business.refundNotifyUrl:}")
-    private String refundNotifyUrl;
+    private String directRefundNotifyUrl;
+
+    private PrivateKey partnerPrivateKey;
+    private PublicKey partnerWechatPayPublicKey;
 
-    private final StorePaymentConfigService storePaymentConfigService;
+    private final StoreInfoMapper storeInfoMapper;
     private final UserReservationOrderService userReservationOrderService;
     private final MerchantPaymentOrderService merchantPaymentOrderService;
     private final RefundRecordAsyncService refundRecordAsyncService;
@@ -81,6 +113,22 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
     private final UserReservationService userReservationService;
     private final StringRedisTemplate stringRedisTemplate;
 
+    @PostConstruct
+    public void loadPartnerCertificates() {
+        String privateKeyPath;
+        String wechatPayPublicKeyFilePath;
+        if ("windows".equals(OSUtil.getOsName())) {
+            privateKeyPath = privateWinKeyPath;
+            wechatPayPublicKeyFilePath = wechatWinPayPublicKeyFilePath;
+        } else {
+            privateKeyPath = privateLinuxKeyPath;
+            wechatPayPublicKeyFilePath = wechatLinuxPayPublicKeyFilePath;
+        }
+        log.info("[MerchantWechat] 加载服务商商户证书私钥与平台公钥,os={}, keyPath={}", OSUtil.getOsName(), privateKeyPath);
+        this.partnerPrivateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
+        this.partnerWechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+    }
+
     @Override
     public R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId, String smid) {
         if (storeId == null || orderId == null) {
@@ -92,16 +140,15 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         if (StringUtils.isBlank(subject)) {
             return R.fail("订单描述不能为空");
         }
-        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
-        if (config == null) {
-            return R.fail("该门店未配置支付参数");
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (StringUtils.isBlank(subMchid)) {
+            return R.fail("门店未配置微信特约商户号 wechat_sub_mchid");
         }
-        if (StringUtils.isBlank(config.getWechatAppId()) || StringUtils.isBlank(config.getWechatMchId())
-                || StringUtils.isBlank(config.getMerchantSerialNumber()) || StringUtils.isBlank(config.getApiV3Key())
-                || StringUtils.isBlank(config.getWechatPayPublicKeyId())
-                || config.getWechatPrivateKeyFile() == null || config.getWechatPrivateKeyFile().length == 0
-                || config.getWechatPayPublicKeyFile() == null || config.getWechatPayPublicKeyFile().length == 0) {
-            return R.fail("门店微信支付配置不完整");
+        if (StringUtils.isBlank(spMchId) || StringUtils.isBlank(spAppId)) {
+            return R.fail("服务商微信支付未配置(payment.wechatPartnerPay.business.spMchId / spAppId)");
+        }
+        if (partnerPrivateKey == null || partnerWechatPayPublicKey == null) {
+            return R.fail("服务商微信证书未加载,请检查 payment.wechatPartnerPay 私钥与平台公钥路径");
         }
         UserReservationOrder order = userReservationOrderService.getById(orderId);
         if (order == null) {
@@ -162,28 +209,29 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         merchantPaymentOrderService.save(paymentOrder);
 
         try {
-            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
-
-            WeChatPaymentStrategyImpl.CommonPrepayRequest request = new WeChatPaymentStrategyImpl.CommonPrepayRequest();
-            request.appid = config.getWechatAppId();
-            request.mchid = config.getWechatMchId();
+            WeChatPartnerPaymentStrategyImpl.PartnerAppPrepayRequest request = new WeChatPartnerPaymentStrategyImpl.PartnerAppPrepayRequest();
+            request.spAppid = spAppId;
+            request.spMchid = spMchId;
+            request.subMchid = subMchid;
             request.description = subject;
             request.outTradeNo = outTradeNo;
-            request.notifyUrl = StringUtils.isNotBlank(prePayNotifyUrl) ? prePayNotifyUrl : "";
+            request.notifyUrl = resolveNotifyUrl(partnerPrePayNotifyUrl, directPrePayNotifyUrl);
             request.amount = new WeChatPaymentStrategyImpl.CommonAmountInfo();
             request.amount.total = amount.multiply(new BigDecimal(100)).longValue();
             request.amount.currency = "CNY";
 
-            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = prePayOrderRun(config, privateKey, request);
+            WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse response = partnerPrePayOrderRun(request);
             if (response == null || StringUtils.isBlank(response.prepayId)) {
                 return R.fail("微信预支付失败");
             }
 
             long timestamp = System.currentTimeMillis() / 1000;
             String nonce = WXPayUtility.createNonce(32);
-            String message = String.format("%s\n%s\n%s\n%s\n", config.getWechatAppId(), timestamp, nonce, response.prepayId);
+            // APP 调起支付验签串(与微信文档一致):appId\ntimeStamp\nnonceStr\nprepayId\n
+            // 须与传给 OpenSDK 的 appId、timeStamp、nonceStr、prepayId 完全一致;此处与直连 {@link WeChatPaymentStrategyImpl} 一致
+            String message = String.format("%s\n%s\n%s\n%s\n", spAppId, timestamp, nonce, response.prepayId);
             Signature sign = Signature.getInstance("SHA256withRSA");
-            sign.initSign(privateKey);
+            sign.initSign(partnerPrivateKey);
             sign.update(message.getBytes(StandardCharsets.UTF_8));
             String signStr = Base64.getEncoder().encodeToString(sign.sign());
 
@@ -193,8 +241,11 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             data.put("orderId", order.getId());
             data.put("paymentNo", paymentOrder.getPaymentNo());
             data.put("prepayId", response.prepayId);
-            data.put("appId", config.getWechatAppId());
-            data.put("mchId", config.getWechatMchId());
+            data.put("appId", spAppId);
+            data.put("spAppId", spAppId);
+            data.put("spMchId", spMchId);
+            data.put("subMchId", subMchid);
+            data.put("mchId", subMchid);
             data.put("sign", signStr);
             data.put("timestamp", String.valueOf(timestamp));
             data.put("nonce", nonce);
@@ -220,10 +271,14 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=参数为空", storeId, outTradeNo);
             return R.fail("门店ID和商户订单号不能为空");
         }
-        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
-        if (config == null) {
-            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=未配置支付参数", storeId, outTradeNo);
-            return R.fail("该门店未配置支付参数");
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (StringUtils.isBlank(subMchid)) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=未配置特约商户号", storeId, outTradeNo);
+            return R.fail("门店未配置微信特约商户号 wechat_sub_mchid");
+        }
+        if (partnerPrivateKey == null || partnerWechatPayPublicKey == null) {
+            log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=服务商证书未加载", storeId, outTradeNo);
+            return R.fail("服务商微信证书未加载");
         }
         MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
         if (paymentOrder == null) {
@@ -236,13 +291,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             return R.fail("预订订单不存在");
         }
         try {
-            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
-            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
-
-            WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest req = new WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest();
-            req.transactionId = outTradeNo;
-            req.mchid = config.getWechatMchId();
-            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = searchOrderRun(config, privateKey, wechatPayPublicKey, req);
+            WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse response = partnerSearchOrderRun(outTradeNo, subMchid);
             if (response == null) {
                 log.info("queryPayStatus 结束 storeId={} outTradeNo={} result=fail reason=微信返回为空", storeId, outTradeNo);
                 return R.fail("查询失败");
@@ -306,9 +355,12 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         if (StringUtils.isBlank(refundAmount) || new BigDecimal(refundAmount).compareTo(BigDecimal.ZERO) <= 0) {
             return R.fail("退款金额必须大于0");
         }
-        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
-        if (config == null) {
-            return R.fail("该门店未配置支付参数");
+        String subMchid = resolveSubMchidFromStore(storeId);
+        if (StringUtils.isBlank(subMchid)) {
+            return R.fail("门店未配置微信特约商户号 wechat_sub_mchid");
+        }
+        if (partnerPrivateKey == null || partnerWechatPayPublicKey == null) {
+            return R.fail("服务商微信证书未加载");
         }
         MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
         if (paymentOrder == null) {
@@ -322,14 +374,12 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             return R.fail("订单未支付或已退款,无法退款");
         }
         try {
-            PrivateKey privateKey = WXPayUtility.loadPrivateKeyFromString(bytesToUtf8String(config.getWechatPrivateKeyFile()));
-            PublicKey wechatPayPublicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
-
-            WeChatPaymentStrategyImpl.CreateRequest request = new WeChatPaymentStrategyImpl.CreateRequest();
+            WeChatPartnerPaymentStrategyImpl.PartnerRefundCreateRequest request = new WeChatPartnerPaymentStrategyImpl.PartnerRefundCreateRequest();
+            request.subMchid = subMchid;
             request.outTradeNo = outTradeNo;
             request.outRefundNo = UniqueRandomNumGenerator.generateUniqueCode(19);
             request.reason = StringUtils.isNotBlank(refundReason) ? refundReason : "用户申请退款";
-            request.notifyUrl = StringUtils.isNotBlank(refundNotifyUrl) ? refundNotifyUrl : "";
+            request.notifyUrl = resolveNotifyUrl(partnerRefundNotifyUrl, directRefundNotifyUrl);
             request.amount = new WeChatPaymentStrategyImpl.AmountReq();
             request.amount.refund = new BigDecimal(refundAmount).multiply(new BigDecimal(100)).longValue();
             // 微信 V3 amount.total 单位为分,depositAmount 为元,需 *100 转分
@@ -341,7 +391,7 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
             int maxAttempts = 3;
             for (int attempt = 1; attempt <= maxAttempts; attempt++) {
                 try {
-                    response = refundRun(config, privateKey, wechatPayPublicKey, request);
+                    response = partnerRefundRun(request);
                     break;
                 } catch (WXPayUtility.ApiException e) {
                     lastApiException = e;
@@ -472,15 +522,14 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         return PaymentEnum.WECHAT_PAY.getType();
     }
 
-    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse prePayOrderRun(StorePaymentConfig config,
-                                                                                  PrivateKey privateKey,
-                                                                                  WeChatPaymentStrategyImpl.CommonPrepayRequest request) throws IOException {
+    private WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse partnerPrePayOrderRun(
+            WeChatPartnerPaymentStrategyImpl.PartnerAppPrepayRequest request) throws IOException {
         String uri = prePayPath;
         String reqBody = WXPayUtility.toJson(request);
         Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
         reqBuilder.addHeader("Accept", "application/json");
-        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
-        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, partnerPrivateKey, POSTMETHOD, uri, reqBody));
         reqBuilder.addHeader("Content-Type", "application/json");
         MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
         RequestBody body = RequestBody.create(reqBody, jsonMediaType);
@@ -489,29 +538,26 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
             String respBody = WXPayUtility.extractBody(httpResponse);
             if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
-                PublicKey publicKey = WXPayUtility.loadPublicKeyFromString(bytesToUtf8String(config.getWechatPayPublicKeyFile()));
-                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), publicKey, httpResponse.headers(), respBody);
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, partnerWechatPayPublicKey, httpResponse.headers(), respBody);
                 return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3AppPrepayResponse.class);
             }
             throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
         }
     }
 
-    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse searchOrderRun(StorePaymentConfig config,
-                                                                              PrivateKey privateKey,
-                                                                              PublicKey wechatPayPublicKey,
-                                                                              WeChatPaymentStrategyImpl.QueryByWxTradeNoRequest request) throws IOException {
-        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(request.transactionId));
+    private WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse partnerSearchOrderRun(String outTradeNo, String subMchid) throws IOException {
+        String uri = searchOrderByOutTradeNoPath.replace("{out_trade_no}", WXPayUtility.urlEncode(outTradeNo));
         Map<String, Object> args = new HashMap<>();
-        args.put("mchid", config.getWechatMchId());
+        args.put("sp_mchid", spMchId);
+        args.put("sub_mchid", subMchid);
         String queryString = WXPayUtility.urlEncode(args);
         if (!queryString.isEmpty()) {
             uri = uri + "?" + queryString;
         }
         Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
         reqBuilder.addHeader("Accept", "application/json");
-        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
-        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, GETMETHOD, uri, null));
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, partnerPrivateKey, GETMETHOD, uri, null));
         reqBuilder.method(GETMETHOD, null);
         OkHttpClient client = new OkHttpClient.Builder()
                 .connectTimeout(10, TimeUnit.SECONDS)
@@ -520,23 +566,20 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
             String respBody = WXPayUtility.extractBody(httpResponse);
             if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
-                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, partnerWechatPayPublicKey, httpResponse.headers(), respBody);
                 return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.DirectAPIv3QueryResponse.class);
             }
             throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
         }
     }
 
-    private WeChatPaymentStrategyImpl.Refund refundRun(StorePaymentConfig config,
-                                                       PrivateKey privateKey,
-                                                       PublicKey wechatPayPublicKey,
-                                                       WeChatPaymentStrategyImpl.CreateRequest request) throws IOException {
+    private WeChatPaymentStrategyImpl.Refund partnerRefundRun(WeChatPartnerPaymentStrategyImpl.PartnerRefundCreateRequest request) throws IOException {
         String uri = refundPath;
         String reqBody = WXPayUtility.toJson(request);
         Request.Builder reqBuilder = new Request.Builder().url(wechatPayApiHost + uri);
         reqBuilder.addHeader("Accept", "application/json");
-        reqBuilder.addHeader("Wechatpay-Serial", config.getWechatPayPublicKeyId());
-        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(config.getWechatMchId(), config.getMerchantSerialNumber(), privateKey, POSTMETHOD, uri, reqBody));
+        reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+        reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(spMchId, merchantSerialNumber, partnerPrivateKey, POSTMETHOD, uri, reqBody));
         reqBuilder.addHeader("Content-Type", "application/json");
         MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");
         RequestBody body = RequestBody.create(reqBody, jsonMediaType);
@@ -545,15 +588,37 @@ public class MerchantWechatPaymentStrategyImpl implements MerchantPaymentStrateg
         try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
             String respBody = WXPayUtility.extractBody(httpResponse);
             if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
-                WXPayUtility.validateResponse(config.getWechatPayPublicKeyId(), wechatPayPublicKey, httpResponse.headers(), respBody);
+                WXPayUtility.validateResponse(wechatPayPublicKeyId, partnerWechatPayPublicKey, httpResponse.headers(), respBody);
                 return WXPayUtility.fromJson(respBody, WeChatPaymentStrategyImpl.Refund.class);
             }
             throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
         }
     }
 
-    /** 仅在使用处将证书 byte[] 转为 String(UTF-8),不参与存储,保证存取一致 */
-    private static String bytesToUtf8String(byte[] bytes) {
-        return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
+    private static String resolveNotifyUrl(String primary, String fallback) {
+        if (StringUtils.isNotBlank(primary)) {
+            return primary.trim();
+        }
+        if (StringUtils.isNotBlank(fallback)) {
+            return fallback.trim();
+        }
+        return "";
+    }
+
+    private String resolveSubMchidFromStore(Integer storeId) {
+        if (storeId == null) {
+            log.warn("[MerchantWechat] storeId 为空,无法解析 wechat_sub_mchid");
+            return null;
+        }
+        StoreInfo info = storeInfoMapper.selectById(storeId);
+        if (info == null) {
+            log.warn("[MerchantWechat] 门店不存在 storeId={}", storeId);
+            return null;
+        }
+        if (StringUtils.isBlank(info.getWechatSubMchid())) {
+            log.warn("[MerchantWechat] 门店未配置 wechat_sub_mchid storeId={}", storeId);
+            return null;
+        }
+        return info.getWechatSubMchid().trim();
     }
 }