Parcourir la source

feat:weixin-java from 4.7.2.B => 4.7.4.B
feat:微信支付 v3 从平台证书切换成微信支付公钥
feat:微信支付 v3 增加 header 解析

YunaiV il y a 6 mois
Parent
commit
a2bfa7a95a

+ 1 - 1
yudao-dependencies/pom.xml

@@ -75,7 +75,7 @@
         <justauth.version>1.16.7</justauth.version>
         <justauth-starter.version>1.4.0</justauth-starter.version>
         <jimureport.version>1.9.4</jimureport.version>
-        <weixin-java.version>4.7.2.B</weixin-java.version>
+        <weixin-java.version>4.7.4.B</weixin-java.version>
     </properties>
 
     <dependencyManagement>

+ 9 - 6
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java

@@ -66,7 +66,8 @@ public class PayNotifyController {
     @TenantIgnore
     public String notifyOrder(@PathVariable("channelId") Long channelId,
                               @RequestParam(required = false) Map<String, String> params,
-                              @RequestBody(required = false) String body) {
+                              @RequestBody(required = false) String body,
+                              @RequestHeader Map<String, String> headers) {
         log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
         // 1. 校验支付渠道是否存在
         PayClient payClient = channelService.getPayClient(channelId);
@@ -76,7 +77,7 @@ public class PayNotifyController {
         }
 
         // 2. 解析通知数据
-        PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
+        PayOrderRespDTO notify = payClient.parseOrderNotify(params, body, headers);
         orderService.notifyOrder(channelId, notify);
         return "success";
     }
@@ -87,7 +88,8 @@ public class PayNotifyController {
     @TenantIgnore
     public String notifyRefund(@PathVariable("channelId") Long channelId,
                                @RequestParam(required = false) Map<String, String> params,
-                               @RequestBody(required = false) String body) {
+                               @RequestBody(required = false) String body,
+                               @RequestHeader Map<String, String> headers) {
         log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
         // 1. 校验支付渠道是否存在
         PayClient payClient = channelService.getPayClient(channelId);
@@ -97,7 +99,7 @@ public class PayNotifyController {
         }
 
         // 2. 解析通知数据
-        PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
+        PayRefundRespDTO notify = payClient.parseRefundNotify(params, body, headers);
         refundService.notifyRefund(channelId, notify);
         return "success";
     }
@@ -108,7 +110,8 @@ public class PayNotifyController {
     @TenantIgnore
     public String notifyTransfer(@PathVariable("channelId") Long channelId,
                                  @RequestParam(required = false) Map<String, String> params,
-                                 @RequestBody(required = false) String body) {
+                                 @RequestBody(required = false) String body,
+                                 @RequestHeader Map<String, String> headers) {
         log.info("[notifyTransfer][channelId({}) 回调数据({}/{})]", channelId, params, body);
         // 1. 校验支付渠道是否存在
         PayClient payClient = channelService.getPayClient(channelId);
@@ -118,7 +121,7 @@ public class PayNotifyController {
         }
 
         // 2. 解析通知数据
-        PayTransferRespDTO notify = payClient.parseTransferNotify(params, body);
+        PayTransferRespDTO notify = payClient.parseTransferNotify(params, body, headers);
         payTransferService.notifyTransfer(channelId, notify);
         return "success";
     }

+ 3 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/WalletPayClient.java

@@ -89,7 +89,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
-    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
+    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("钱包支付无支付回调");
     }
 
@@ -144,7 +144,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
-    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
+    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("钱包支付无退款回调");
     }
 
@@ -178,7 +178,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
-    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("未实现");
     }
 

+ 6 - 3
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -39,9 +39,10 @@ public interface PayClient {
      *
      * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
      * @param body HTTP 回调接口的 request body
+     * @param headers HTTP 回调接口的 request headers
      * @return 支付订单信息
      */
-    PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
+    PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers);
 
     /**
      * 获得支付订单信息
@@ -66,9 +67,10 @@ public interface PayClient {
      *
      * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
      * @param body HTTP 回调接口的 request body
+     * @param headers HTTP 回调接口的 request headers
      * @return 支付订单信息
      */
-    PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
+    PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers);
 
     /**
      * 获得退款订单信息
@@ -103,8 +105,9 @@ public interface PayClient {
      *
      * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
      * @param body HTTP 回调接口的 request body
+     * @param headers HTTP 回调接口的 request headers
      * @return 转账信息
      */
-    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body);
+    PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers);
 
 }

+ 2 - 1
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
-
 import jakarta.validation.Validator;
 
 /**
@@ -14,6 +14,7 @@ import jakarta.validation.Validator;
 // @JsonTypeInfo 注解的作用,Jackson 多态
 // 1. 序列化到时数据库时,增加 @class 属性。
 // 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
+@JsonIgnoreProperties(ignoreUnknown = true) // 目的:忽略未知的属性,避免反序列化失败
 public interface PayClientConfig {
 
     /**

+ 15 - 15
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -101,19 +101,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
             throws Throwable;
 
     @Override
-    public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
+    public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
         try {
-            return doParseOrderNotify(params, body);
+            return doParseOrderNotify(params, body, headers);
         } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
-            log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
-                    getId(), params, body, ex);
+            log.error("[parseOrderNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
+                    getId(), params, body, headers, ex);
             throw buildPayException(ex);
         }
     }
 
-    protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
+    protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers)
             throws Throwable;
 
     @Override
@@ -155,19 +155,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
 
     @Override
-    public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
+    public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
         try {
-            return doParseRefundNotify(params, body);
+            return doParseRefundNotify(params, body, headers);
         } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
-            log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
-                    getId(), params, body, ex);
+            log.error("[parseRefundNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
+                    getId(), params, body, headers, ex);
             throw buildPayException(ex);
         }
     }
 
-    protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
+    protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers)
             throws Throwable;
 
     @Override
@@ -220,19 +220,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     }
 
     @Override
-    public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body) {
+    public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
         try {
-            return doParseTransferNotify(params, body);
+            return doParseTransferNotify(params, body, headers);
         } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
-            log.error("[doParseTransferNotify][客户端({}) params({}) body({}) 解析失败]",
-                    getId(), params, body, ex);
+            log.error("[doParseTransferNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
+                    getId(), params, body, headers, ex);
             throw buildPayException(ex);
         }
     }
 
-    protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body)
+    protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers)
             throws Throwable;
 
     @Override

+ 3 - 3
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java

@@ -79,7 +79,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     }
 
     @Override
-    public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
+    public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
         // 1. 校验回调数据
         Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
         AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
@@ -175,7 +175,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     }
 
     @Override
-    public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
+    public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
         // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
         // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
         // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
@@ -327,7 +327,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
 
     // TODO @chihuo:这里是不是也要实现,支付宝的。
     @Override
-    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("未实现");
     }
 

+ 3 - 3
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java

@@ -58,17 +58,17 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
     }
 
     @Override
-    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws Throwable {
+    protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("未实现");
     }
 
     @Override
-    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
+    protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("模拟支付无退款回调");
     }
 
     @Override
-    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) {
+    protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
         throw new UnsupportedOperationException("模拟支付无支付回调");
     }
 

+ 32 - 17
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -18,10 +18,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPart
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
-import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
-import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
+import com.github.binarywang.wxpay.bean.notify.*;
 import com.github.binarywang.wxpay.bean.request.*;
 import com.github.binarywang.wxpay.bean.result.*;
 import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
@@ -67,13 +64,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected void doInit(String tradeType) {
         // 创建 config 配置
         WxPayConfig payConfig = new WxPayConfig();
-        BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent");
+        BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent");
         payConfig.setTradeType(tradeType);
         // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
         if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
             payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
         } else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
             payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
+            payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());
         }
 
         // 创建 client 客户端
@@ -157,12 +155,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     }
 
     @Override
-    public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
+    public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
         switch (config.getApiVersion()) {
             case API_VERSION_V2:
                 return doParseOrderNotifyV2(body);
             case API_VERSION_V3:
-                return doParseOrderNotifyV3(body);
+                return doParseOrderNotifyV3(body, headers);
             default:
                 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
         }
@@ -179,9 +177,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
                 response.getOutTradeNo(), body);
     }
 
-    private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
+    private PayOrderRespDTO doParseOrderNotifyV3(String body, Map<String, String> headers) throws WxPayException {
         // 1. 解析回调
-        WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
+        SignatureHeader signatureHeader = getRequestHeader(headers);
+        WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader);
         WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
         // 2. 构建结果
         Integer status = parseStatus(result.getTradeState());
@@ -321,12 +320,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     }
 
     @Override
-    public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
+    public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
         switch (config.getApiVersion()) {
             case API_VERSION_V2:
                 return doParseRefundNotifyV2(body);
             case API_VERSION_V3:
-                return parseRefundNotifyV3(body);
+                return parseRefundNotifyV3(body, headers);
             default:
                 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
         }
@@ -344,9 +343,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
     }
 
-    private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
+    private PayRefundRespDTO parseRefundNotifyV3(String body, Map<String, String> headers) throws WxPayException {
         // 1. 解析回调
-        WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
+        SignatureHeader signatureHeader = getRequestHeader(headers);
+        WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
         WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
         // 2. 构建结果
         if (Objects.equals("SUCCESS", result.getRefundStatus())) {
@@ -357,10 +357,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     }
 
     @Override
-    public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body) throws WxPayException {
+    public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
         switch (config.getApiVersion()) {
             case API_VERSION_V3:
-                return parseTransferNotifyV3(body);
+                return parseTransferNotifyV3(body, headers);
             case API_VERSION_V2:
                 throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
             default:
@@ -368,10 +368,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         }
     }
 
-    private PayTransferRespDTO parseTransferNotifyV3(String body) throws WxPayException {
+    private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
         // 1. 解析回调
+        SignatureHeader signatureHeader = getRequestHeader(headers);
         // TODO @luchi:这个可以复用 wxjava 里的类么?
-        WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, null, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
+        WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, signatureHeader, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
         WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
         // 2. 构建结果
         if (Objects.equals("FINISHED", result.getBatchStatus())) {
@@ -513,6 +514,20 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
 
     // ========== 各种工具方法 ==========
 
+    /**
+     * 组装请求头重的签名信息
+     *
+     * @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221">官方示例</a>
+     */
+    private SignatureHeader getRequestHeader(Map<String, String> headers) {
+        return SignatureHeader.builder()
+                .signature(headers.get("wechatpay-signature"))
+                .nonce(headers.get("wechatpay-nonce"))
+                .serial(headers.get("wechatpay-serial"))
+                .timeStamp(headers.get("wechatpay-timestamp"))
+                .build();
+    }
+
     static String formatDateV2(LocalDateTime time) {
         return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
     }

+ 8 - 3
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java

@@ -73,13 +73,18 @@ public class WxPayClientConfig implements PayClientConfig {
     @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
     private String apiV3Key;
     /**
-     * 证书序列号
+     * 证书序列号(merchantSerialNumber)
      */
     @NotBlank(message = "证书序列号不能为空", groups = V3.class)
     private String certSerialNo;
 
-    @Deprecated // TODO 芋艿:V2.3.0 进行移除
-    private String privateCertContent;
+    /**
+     * pub_key.pem 证书文件的对应字符串
+     */
+    @NotBlank(message = "pub_key.pem 不能为空", groups = V3.class)
+    private String publicKeyContent;
+    @NotBlank(message = "publicKeyId 不能为空", groups = V3.class)
+    private String publicKeyId;
 
     /**
      * 分组校验 v2版本