Browse Source

完成对微信公众号支付的封装

YunaiV 3 years ago
parent
commit
1ed6656bbb
16 changed files with 437 additions and 81 deletions
  1. 34 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
  2. 30 8
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  3. 33 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java
  4. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  5. 0 17
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCodeMapping.java
  6. 2 5
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java
  7. 10 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java
  8. 3 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  9. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
  10. 8 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java
  11. 14 7
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
  12. 6 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
  13. 50 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java
  14. 88 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java
  15. 148 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java
  16. 9 30
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java

+ 34 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import lombok.SneakyThrows;
+
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(String data) {
+        // 创建文件,通过 UUID 保证唯一
+        File file = File.createTempFile(IdUtil.simpleUUID(), null);
+        // 标记 JVM 退出时,自动删除
+        file.deleteOnExit();
+        // 写入内容
+        FileUtil.writeUtf8String(data, file);
+        return file;
+    }
+
+}

+ 30 - 8
yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -11,7 +11,10 @@
 
     <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
     <name>${artifactId}</name>
-    <description>支付拓展,基于 IJPay 简单封装,支持微信、支付宝等常见支付渠道</description>
+    <description>支付拓展,接入国内多个支付渠道
+        1. 支付宝,基于官方 SDK 接入
+        2. 微信支付,基于 weixin-java-pay 接入
+    </description>
 
     <dependencies>
         <dependency>
@@ -34,17 +37,36 @@
             <artifactId>slf4j-api</artifactId>
         </dependency>
 
-        <!-- 三方云服务相关 -->
         <dependency>
-            <groupId>com.github.javen205</groupId>
-            <artifactId>IJPay-AliPay</artifactId>
-            <version>2.7.8</version>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.github.javen205</groupId>
-            <artifactId>IJPay-WxPay</artifactId>
-            <version>2.7.8</version>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
         </dependency>
 
+        <!-- 三方云服务相关 -->
+<!--        <dependency>-->
+<!--            <groupId>com.github.javen205</groupId>-->
+<!--            <artifactId>IJPay-AliPay</artifactId>-->
+<!--            <version>2.7.8</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.github.javen205</groupId>-->
+<!--            <artifactId>IJPay-WxPay</artifactId>-->
+<!--            <version>2.7.8</version>-->
+<!--        </dependency>-->
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+            <version>4.17.9.ALL</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>weixin-java-pay</artifactId>
+            <version>4.1.9.B</version>
+        </dependency>
+        <!-- TODO 芋艿:清理 -->
     </dependencies>
 </project>

+ 33 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 将 API 的错误码,转换为通用的错误码
+ *
+ * @see PayCommonResult
+ * @see PayFrameworkErrorCodeConstants
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public abstract class AbstractPayCodeMapping {
+
+    public final ErrorCode apply(String apiCode, String apiMsg) {
+        if (apiCode == null) {
+            log.error("[apply][API 错误码为空,请排查]");
+            return PayFrameworkErrorCodeConstants.EXCEPTION;
+        }
+        ErrorCode errorCode = this.apply0(apiCode, apiMsg);
+        if (errorCode == null) {
+            log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
+            return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
+        }
+        return errorCode;
+    }
+
+    protected abstract ErrorCode apply0(String apiCode, String apiMsg);
+
+}

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

@@ -18,6 +18,6 @@ public interface PayClient {
     Long getId();
 
     // TODO 缺少注释
-    CommonResult<Object> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
+    CommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
 
 }

+ 0 - 17
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCodeMapping.java

@@ -1,17 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
-
-import java.util.function.Function;
-
-/**
- * 将 API 的错误码,转换为通用的错误码
- *
- * @see PayCommonResult
- * @see PayFrameworkErrorCodeConstants
- *
- * @author 芋道源码
- */
-public interface PayCodeMapping extends Function<String, ErrorCode> {
-}

+ 2 - 5
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java

@@ -35,16 +35,13 @@ public class PayCommonResult<T> extends CommonResult<T> {
     private PayCommonResult() {
     }
 
-    public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, PayCodeMapping codeMapping) {
+    public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
         Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
         PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
         result.setData(data);
         // 翻译错误码
         if (codeMapping != null) {
-            ErrorCode errorCode = codeMapping.apply(apiCode);
-            if (errorCode == null) {
-                errorCode = PayFrameworkErrorCodeConstants.EXCEPTION;
-            }
+            ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
             result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
         }
         return result;

+ 10 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.dto;
 
 import lombok.Data;
 import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.URL;
 
 import javax.validation.constraints.DecimalMin;
 import javax.validation.constraints.NotEmpty;
@@ -39,6 +40,12 @@ public class PayOrderUnifiedReqDTO {
     @NotEmpty(message = "商品描述信息不能为空")
     @Length(max = 128, message = "商品描述信息长度不能超过128")
     private String body;
+    /**
+     * 支付结果的回调地址
+     */
+    @NotEmpty(message = "支付结果的回调地址不能为空")
+    @URL(message = "支付结果的回调地址必须是 URL 格式")
+    private String notifyUrl;
 
     // ========== 订单相关字段 ==========
 
@@ -55,4 +62,7 @@ public class PayOrderUnifiedReqDTO {
     @NotNull(message = "支付过期时间不能为空")
     private Date expireTime;
 
+    // ========== 拓展参数 ==========
+    // TODO 芋艿:待完善
+
 }

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

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl;
 
 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
-import cn.iocoder.yudao.framework.pay.core.client.PayCodeMapping;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
 import lombok.extern.slf4j.Slf4j;
 
 /**
@@ -24,7 +24,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
     /**
      * 错误码枚举类
      */
-    protected PayCodeMapping codeMapping;
+    protected AbstractPayCodeMapping codeMapping;
     /**
      * 支付配置
      */
@@ -34,7 +34,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         return amount / 100.0;
     }
 
-    public AbstractPayClient(Long channelId, String channelCode, Config config, PayCodeMapping codeMapping) {
+    public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
         this.channelId = channelId;
         this.channelCode = channelCode;
         this.codeMapping = codeMapping;

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayConfig.java → yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java

@@ -11,7 +11,7 @@ import lombok.Data;
  * @author 芋道源码
  */
 @Data
-public class AlipayPayConfig implements PayClientConfig {
+public class AlipayPayClientConfig implements PayClientConfig {
 
     /**
      * 网关地址 - 线上

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

@@ -1,12 +1,17 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.pay.core.client.PayCodeMapping;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
 
-public class AlipayPayCodeMapping implements PayCodeMapping {
+/**
+ * 支付宝的 PayCodeMapping 实现类
+ *
+ * @author 芋道源码
+ */
+public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
 
     @Override
-    public ErrorCode apply(String s) {
+    protected ErrorCode apply0(String apiCode, String apiMsg) {
         return null;
     }
 

+ 14 - 7
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java

@@ -12,6 +12,9 @@ import com.alipay.api.domain.AlipayTradePrecreateModel;
 import com.alipay.api.request.AlipayTradePrecreateRequest;
 import com.alipay.api.response.AlipayTradePrecreateResponse;
 import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * 支付宝【扫码支付】的 PayClient 实现类
@@ -19,11 +22,12 @@ import lombok.SneakyThrows;
  *
  * @author 芋道源码
  */
-public class AlipayQrPayClient extends AbstractPayClient<AlipayPayConfig> {
+@Slf4j
+public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig> {
 
     private DefaultAlipayClient client;
 
-    public AlipayQrPayClient(Long channelId, String channelCode, AlipayPayConfig config) {
+    public AlipayQrPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
         super(channelId, channelCode, config, new AlipayPayCodeMapping());
     }
 
@@ -32,17 +36,18 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayConfig> {
     protected void doInit() {
         AlipayConfig alipayConfig = new AlipayConfig();
         BeanUtil.copyProperties(config, alipayConfig, false);
+        // 真实客户端
         this.client = new DefaultAlipayClient(alipayConfig);
     }
 
     @Override
-    public CommonResult<Object> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+    public CommonResult<AlipayTradePrecreateResponse> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
         // 构建 AlipayTradePrecreateModel 请求
         AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
         model.setOutTradeNo(reqDTO.getMerchantOrderId());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
+        model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
         // TODO 芋艿:clientIp + expireTime
         // 构建 AlipayTradePrecreateRequest
         AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
@@ -53,6 +58,7 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayConfig> {
         try {
             response = client.execute(request);
         } catch (AlipayApiException e) {
+            log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e);
             return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
         }
 
@@ -64,10 +70,10 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayConfig> {
     }
 
     public static void main(String[] args) {
-        AlipayPayConfig config = new AlipayPayConfig();
+        AlipayPayClientConfig config = new AlipayPayClientConfig();
         config.setAppId("2021000118634035");
-        config.setServerUrl(AlipayPayConfig.SERVER_URL_SANDBOX);
-        config.setSignType(AlipayPayConfig.SIGN_TYPE_DEFAULT);
+        config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
+        config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
         config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
         config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
         AlipayQrPayClient client = new AlipayQrPayClient(1L, "biu", config);
@@ -79,4 +85,5 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayConfig> {
         reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
         client.unifiedOrder(reqDTO);
     }
+
 }

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

@@ -19,11 +19,11 @@ import lombok.SneakyThrows;
  *
  * @author 芋道源码
  */
-public class AlipayWapPayClient extends AbstractPayClient<AlipayPayConfig> {
+public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
 
     private DefaultAlipayClient client;
 
-    public AlipayWapPayClient(Long channelId, String channelCode, AlipayPayConfig config) {
+    public AlipayWapPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
         super(channelId, channelCode, config, new AlipayPayCodeMapping());
     }
 
@@ -36,7 +36,7 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayConfig> {
     }
 
     @Override
-    public CommonResult<Object> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+    public CommonResult<AlipayTradeWapPayResponse> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
         // 构建 AlipayTradeWapPayModel 请求
         AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
         model.setOutTradeNo(reqDTO.getMerchantOrderId());
@@ -65,10 +65,10 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayConfig> {
     }
 
     public static void main(String[] args) {
-        AlipayPayConfig config = new AlipayPayConfig();
+        AlipayPayClientConfig config = new AlipayPayClientConfig();
         config.setAppId("2021000118634035");
-        config.setServerUrl(AlipayPayConfig.SERVER_URL_SANDBOX);
-        config.setSignType(AlipayPayConfig.SIGN_TYPE_DEFAULT);
+        config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX);
+        config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
         config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
         config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
         AlipayWapPayClient client = new AlipayWapPayClient(1L, "biubiubiu", config);

+ 50 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
+
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*;
+
+/**
+ * 微信支付 PayCodeMapping 实现类
+ *
+ * @author 芋道源码
+ */
+public class WXCodeMapping extends AbstractPayCodeMapping {
+
+    /**
+     * 错误码 - 成功
+     * 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下
+     */
+    public static final String CODE_SUCCESS = "SUCCESS";
+    /**
+     * 错误提示 - 成功
+     */
+    public static final String MESSAGE_SUCCESS = "成功";
+
+    @Override
+    protected ErrorCode apply0(String apiCode, String apiMsg) {
+        if (Objects.equals(apiCode, CODE_SUCCESS)) {
+            return GlobalErrorCodeConstants.SUCCESS;
+        }
+        if (Objects.equals(apiCode, "FAIL")) {
+            if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) {
+                return PAY_CONFIG_APP_ID_ERROR;
+            }
+            if (Objects.equals(apiMsg, "签名错误,请检查后再试")
+                || Objects.equals(apiMsg, "签名错误")) {
+                return PAY_CONFIG_SIGN_ERROR;
+            }
+        }
+        if (Objects.equals(apiCode, "PARAM_ERROR")) {
+            if (Objects.equals(apiMsg, "无效的openid")) {
+                return PAY_OPENID_ERROR;
+            }
+        }
+        return null;
+    }
+
+}

+ 88 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java

@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.hutool.core.io.IoUtil;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
+import lombok.Data;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+// TODO 芋艿:参数校验
+/**
+ * 微信支付的 PayClientConfig 实现类
+ * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
+ *
+ * @author 芋道源码
+ */
+@Data
+public class WXPayClientConfig implements PayClientConfig {
+
+    // TODO 芋艿:V2 or V3 客户端
+    /**
+     * API 版本 - V2
+     *
+     * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
+     */
+    public static final String API_VERSION_V2 = "v2";
+    /**
+     * API 版本 - V3
+     *
+     * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
+     */
+    public static final String API_VERSION_V3 = "v3";
+
+    /**
+     * 公众号或者小程序的 appid
+     */
+    private String appId;
+    /**
+     * 商户号
+     */
+    private String mchId;
+    /**
+     * API 版本
+     */
+    private String apiVersion;
+
+    // ========== V2 版本的参数 ==========
+
+    /**
+     * 商户密钥
+     */
+    private String mchKey;
+//    /**
+//     * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
+//     * 对应的字符串
+//     *
+//     * 注意,可通过 {@link #main(String[])} 读取
+//     */
+//    private String keyContent;
+
+    // ========== V3 版本的参数 ==========
+    /**
+     * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
+     * 对应的字符串
+     *
+     * 注意,可通过 {@link #main(String[])} 读取
+     */
+    private String privateKeyContent;
+    /**
+     * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
+     * 对应的字符串
+     *
+     * 注意,可通过 {@link #main(String[])} 读取
+     */
+    private String privateCertContent;
+    /**
+     * apiV3 秘钥值
+     */
+    private String apiV3Key;
+
+    public static void main(String[] args) throws FileNotFoundException {
+        String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
+//        String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
+//        String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
+        System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
+    }
+
+}

+ 148 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java

@@ -0,0 +1,148 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
+import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import static cn.hutool.core.util.ObjectUtil.defaultIfNull;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
+
+/**
+ * 微信支付(公众号)的 PayClient 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
+
+    private WxPayService client;
+
+    public WXPubPayClient(Long channelId, String channelCode, WXPayClientConfig config) {
+        super(channelId, channelCode, config, new WXCodeMapping());
+    }
+
+    @Override
+    protected void doInit() {
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtil.copyProperties(config, payConfig, "keyContent");
+        payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
+//        if (StrUtil.isNotEmpty(config.getKeyContent())) {
+//            payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
+//        }
+        if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
+            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
+            payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
+        }
+        if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
+            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
+            payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
+        }
+        // 真实客户端
+        this.client = new WxPayServiceImpl();
+        client.setConfig(payConfig);
+    }
+
+    @Override
+    public CommonResult<WxPayMpOrderResult> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+        WxPayMpOrderResult response;
+        try {
+            switch (config.getApiVersion()) {
+                case WXPayClientConfig.API_VERSION_V2:
+                    response = this.unifiedOrderV2(reqDTO);
+                    break;
+                case WXPayClientConfig.API_VERSION_V3:
+                    WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
+                    // 将 V3 的结果,统一转换成 V2。返回的字段是一致的
+                    response = new WxPayMpOrderResult();
+                    BeanUtil.copyProperties(responseV3, response, true);
+                    break;
+                default:
+                    throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+            }
+        } catch (WxPayException e) {
+            log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
+            return PayCommonResult.build(defaultIfNull(e.getErrCode(), e.getReturnCode()),
+                    defaultIfNull(e.getErrCodeDes(), e.getReturnMsg()),null, codeMapping);
+        }
+        return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
+    }
+
+    private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
+                .outTradeNo(reqDTO.getMerchantOrderId())
+                // TODO 芋艿:貌似没 title?
+                .body(reqDTO.getBody())
+                .totalFee(reqDTO.getAmount()) // 单位分
+                .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"))
+                .spbillCreateIp(reqDTO.getClientIp())
+                .openid("ockUAwIZ-0OeMZl9ogcZ4ILrGba0") // TODO 芋艿:先随便写死
+                .notifyUrl(reqDTO.getNotifyUrl())
+                .build();
+        // 执行请求
+        return client.createOrder(request);
+    }
+
+    private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+        request.setOutTradeNo(reqDTO.getMerchantOrderId());
+        // TODO 芋艿:貌似没 title?
+        request.setDescription(reqDTO.getBody());
+        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
+        request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"));
+        request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid("ockUAwIZ-0OeMZl9ogcZ4ILrGba0")); // TODO 芋艿:先随便写死
+        request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getClientIp()));
+        request.setNotifyUrl(reqDTO.getNotifyUrl());
+        // 执行请求
+        return client.createOrderV3(TradeTypeEnum.JSAPI, request);
+    }
+
+    public static void main(String[] args) throws FileNotFoundException {
+        WXPayClientConfig config = new WXPayClientConfig();
+        config.setAppId("wx041349c6f39b268b");
+        config.setMchId("1545083881");
+        config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
+        config.setApiVersion(WXPayClientConfig.API_VERSION_V3);
+//        config.setKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12")));
+        config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
+        config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
+        config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
+
+        WXPubPayClient client = new WXPubPayClient(1L, "biu", config);
+        client.init();
+
+        PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
+        reqDTO.setAmount(123);
+        reqDTO.setSubject("IPhone 13");
+        reqDTO.setBody("biubiubiu");
+        reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
+        reqDTO.setClientIp("127.0.0.1");
+        reqDTO.setNotifyUrl("http://127.0.0.1:8080");
+        CommonResult<WxPayMpOrderResult> result = client.unifiedOrder(reqDTO);
+        System.out.println(result);
+    }
+
+}

+ 9 - 30
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java

@@ -11,36 +11,15 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
  */
 public interface PayFrameworkErrorCodeConstants {
 
-    ErrorCode PAY_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
-
-//    // ========== 权限 / 限流等相关 2001000100 ==========
-//
-//    ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
-//    // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信
-//    ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
-//
-//    // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。
-//    ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
-//    // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
-//    ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
-//
-//    ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
-//
-//    // ========== 模板相关 2001000200 ==========
-//    ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
-//    ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
-//
-//    // ========== 签名相关 2001000300 ==========
-//    ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
-//
-//    // ========== 账户相关 2001000400 ==========
-//    ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
-//    ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
-//
-//    // ========== 其它相关 2001000900 开头 ==========
-//    ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
-//    ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
-//    ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
+    ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析");
+
+    // ========== 配置相关相关 2002000100 ==========
+    ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确");
+    ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey
+
+
+    // ========== 其它相关 2002000900 开头 ==========
+    ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过
 
     ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");