From 13449157f6f409b4cfb0b1a8fd94cbb7b795c4ee Mon Sep 17 00:00:00 2001
From: xuhy <3313886187@qq.com>
Date: 星期四, 06 十一月 2025 10:08:04 +0800
Subject: [PATCH] 微信小程序支付,小程序登录

---
 ruoyi-admin/src/main/resources/application-test.yml                                   |   25 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java               |   53 +
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java           |   41 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java                       |  108 ++
 ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java          |  122 ++
 ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java    |   80 +
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java              |  118 ++
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java      |  113 ++
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java              |  164 +++
 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java    |    2 
 ruoyi-admin/pom.xml                                                                   |    6 
 ruoyi-system/pom.xml                                                                  |   11 
 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java      |   28 
 ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java               |   53 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java            |  400 ++++++++
 ruoyi-applet/pom.xml                                                                  |    6 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java                |   31 
 ruoyi-applet/src/main/resources/application-test.yml                                  |   25 
 ruoyi-common/pom.xml                                                                  |    6 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java |  222 ++++
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java        |   20 
 /dev/null                                                                             |   77 -
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java       |  201 ++++
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java          |   55 +
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java                 |    9 
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java      |   52 +
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java     |   84 +
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java             |  441 +++++++++
 ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java                  |  235 +++++
 29 files changed, 2,671 insertions(+), 117 deletions(-)

diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 18b4c52..a53beab 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -155,6 +155,12 @@
             <version>2.0.10</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/ruoyi-admin/src/main/resources/application-test.yml b/ruoyi-admin/src/main/resources/application-test.yml
index d5ca74f..ffe4765 100644
--- a/ruoyi-admin/src/main/resources/application-test.yml
+++ b/ruoyi-admin/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
 wx:
   conf:
     appId:
-    secretId:
\ No newline at end of file
+    secretId:
+payment:
+  wx:
+    # 微信appid
+    appId:
+    # 微信商户号
+    mchId:
+    # 秘钥
+    secretId:
+    v3:
+      # 加签串
+      apiKey:
+      # 加签证书地址
+      privateKeyPath:
+      # 微信支付公钥id
+      publicKeyId:
+      # 微信支付公钥证书
+      publicKeyPath:
+      # 证书序列号
+      mchSerialNo:
+      # 支付成功回调地址
+      notifyPayUrl:
+      # 支付退款回调地址
+      notifyRefundUrl:
\ No newline at end of file
diff --git a/ruoyi-applet/pom.xml b/ruoyi-applet/pom.xml
index 335f596..2e585f6 100644
--- a/ruoyi-applet/pom.xml
+++ b/ruoyi-applet/pom.xml
@@ -170,6 +170,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
new file mode 100644
index 0000000..b18935b
--- /dev/null
+++ b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
@@ -0,0 +1,122 @@
+package com.ruoyi.web.controller.api;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.wxPay.enums.TradeStateEnum;
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.utils.WxV3Pay;
+import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * 微信相关接口
+ */
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/wx/")
+@Api(tags = {"微信支付相关接口"})
+public class WxPayController {
+    @Autowired
+    private WxV3Pay wxV3Pay;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private WeixinPayProperties weixinPayProperties;
+
+
+
+    /**
+     * 按实际修改
+     */
+    @ApiOperation("订单支付")
+    @PostMapping("orderPay")
+    public R<Map<String, Object>> orderPay(@RequestParam(value = "inspectionId") String inspectionId) {
+
+//        TSysOtherConfig sysOtherConfig = sysOtherConfigService.getById(1);
+//        if (Objects.isNull(sysOtherConfig.getAiPrice())) {
+//            return R.fail("系统未配置AI支付价格");
+//        }
+//
+//        String userId = tokenService.getLoginUserApplet().getUserId();
+//        TSysAppUser sysAppUser = sysAppUserService.getById(userId);
+//        // 价格
+//        Long totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).longValue();
+//        // 生成订单号
+//        String orderNo = CodeGenerateUtils.generateOrderSn();
+//        // 查询用户信息 用户openid
+//        String openId = sysAppUser.getOpenId();
+//        // 存储支付记录
+//        sysPayRecordService.saveData(orderNo, userId, sysOtherConfig.getAiPrice(), 1);
+//        // 调用支付方法
+//        Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"AI检测报告支付","");
+//        log.info("支付参数:{}", result);
+        return R.ok();
+    }
+
+    /**
+     * 支付回调
+     */
+    @PostMapping("pay/notify")
+    @ApiOperation("订单回调")
+    public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        try {
+            System.err.println("微信回调");
+            BufferedReader reader = request.getReader();
+            StringBuilder requestBody = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                requestBody.append(line);
+            }
+            System.err.println("全部请求体" + requestBody);
+            JSONObject jsonObject = JSONObject.parseObject(requestBody.toString());
+            JSONObject resource = jsonObject.getJSONObject("resource");
+
+            AesUtil aesUtil = new AesUtil(weixinPayProperties.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
+            String decryptedData = aesUtil.decryptToString(resource.getString("associated_data").getBytes(StandardCharsets.UTF_8), resource.getString("nonce").getBytes(StandardCharsets.UTF_8),
+                    resource.getString("ciphertext"));
+            System.err.println("微信解密的字符串信息" + decryptedData);
+            JSONObject jsonInfo = JSONObject.parse(decryptedData);
+            String out_trade_no = jsonInfo.getString("out_trade_no");
+            String transaction_id = jsonInfo.getString("transaction_id");
+            String trade_state = jsonInfo.getString("trade_state");
+            String success_time = jsonInfo.getString("success_time");
+            // 查询订单
+//            TSysPayRecord sysPayRecord = sysPayRecordService.getOne(Wrappers.lambdaQuery(TSysPayRecord.class)
+//                    .eq(TSysPayRecord::getOrderNo, out_trade_no).last("LIMIT 1"));
+            if (trade_state.equals(TradeStateEnum.SUCCESS.name())) {
+                log.info("回调成功");
+                // 订单号查询订单
+//                sysPayRecord.setPayState(2);
+//                sysPayRecord.setPayTime(WxTimeUtils.dateToLocalDateTime(WxTimeUtils.toRfc3339Date(success_time)));
+//                sysPayRecord.setTransactionId(transaction_id);
+//                sysPayRecordService.updateById(sysPayRecord);
+//
+//                // 处理检测报告为可见
+//                TSysInspection sysInspection = sysInspectionService.getById(sysPayRecord.getInspectionId());
+//                sysInspection.setIsPay(1);
+//                sysInspectionService.updateById(sysInspection);
+
+                PrintWriter out = response.getWriter();
+                out.print("SUCCESS");
+                out.flush();
+                out.close();
+            }
+        } catch (Exception e) {
+            log.error("支付回调异常:{}", e, e);
+            wxV3Pay.ack(false, e.getMessage());
+        }
+    }
+}
diff --git a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
index 78126f5..3936557 100644
--- a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
+++ b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -1,29 +1,48 @@
 package com.ruoyi.web.controller.system;
 
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.R;
 import com.ruoyi.common.core.domain.entity.SysMenu;
 import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.entity.TAppUserResp;
 import com.ruoyi.common.core.domain.model.LoginAppBody;
 import com.ruoyi.common.core.domain.model.LoginUserApplet;
 import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.redis.service.RedisService;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.framework.web.service.SysLoginService;
 import com.ruoyi.framework.web.service.SysPermissionService;
 import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.model.TAppUser;
 import com.ruoyi.system.service.ISysMenuService;
 import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.TAppUserService;
+import com.ruoyi.system.utils.wx.body.resp.Code2SessionRespBody;
+import com.ruoyi.system.utils.wx.body.resq.Code2SessionResqBody;
+import com.ruoyi.system.utils.wx.model.WeixinProperties;
+import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
+import com.ruoyi.system.utils.wx.pojo.AppletUserEncrypteData;
+import com.ruoyi.system.utils.wx.tools.WxAppletTools;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
 import com.ruoyi.web.controller.tool.MsgUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -31,6 +50,7 @@
  * 
  * @author ruoyi
  */
+@Slf4j
 @Api(tags = "登录")
 @RestController
 public class SysLoginController
@@ -51,7 +71,14 @@
     private ISysRoleService roleService;
     @Autowired
     private MsgUtils msgUtils;
-
+    @Autowired
+    private RestTemplate wxRestTemplate;
+    @Autowired
+    private WeixinProperties wxConfig;
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private TAppUserService appUserService;
     /**
      * 账号密码登录
      * 
@@ -59,7 +86,7 @@
      * @return 结果
      */
     @ApiOperation(value = "账号密码登录",notes = "账号密码登录")
-    @PostMapping("/api/loginApp")
+    @PostMapping("/loginApp")
     public AjaxResult login(@RequestBody String param)
     {
         LoginAppBody loginAppBody = JSON.parseObject(param, LoginAppBody.class);
@@ -68,10 +95,57 @@
         LoginUserApplet loginUser = loginService.loginCodeApplet(loginAppBody.getAccount(), loginAppBody.getPassword());
         ajax.put(Constants.TOKEN, tokenService.createTokenApplet(loginUser));
         ajax.put("data",loginUser);
-
         return ajax;
     }
 
+    @ApiOperation(value = "通过code获得openid,获取用户信息",tags = {"微信小程序登录"})
+    @PostMapping("/openIdByJsCode")
+    public R<Map<String, Object>> openIdByJsCode(@RequestBody AppletUserEncrypteData data) {
+        log.info("<<<<<<<<换取openid开始<<<<<<<<:{}", data.getCode());
+        WxAppletTools appletTools = new WxAppletTools(wxRestTemplate, wxConfig, redisService);
+        Code2SessionRespBody body = appletTools.getOpenIdByJscode2session(new Code2SessionResqBody().build(data.getCode()));
+        String openid = body.getOpenid();
+        String sessionKey = body.getSessionKey();
+        // 用户信息解密 数据验签
+//        if (StringUtils.isNotBlank(data.getSignature())) {
+//            WxUtils.verifySignature(data.getRawData(), sessionKey, data.getSignature());
+//        }
+        if(StringUtils.isEmpty(data.getEncryptedData()) || StringUtils.isEmpty(data.getIv())){
+            return R.fail("已拒绝授权");
+        }
+        AppletUserDecodeData appletUserDecodeData = WxUtils.encryptedData(data.getEncryptedData(), sessionKey,  data.getIv());
+        appletUserDecodeData.setOpenId(openid);
+        // 先使用openId和当前手机号进行查询
+        TAppUser sysAppUser = appUserService.getOne(Wrappers.lambdaQuery(TAppUser.class)
+                .and(e->e.eq(TAppUser::getOpenId, appletUserDecodeData.getOpenId()).or()
+                        .eq(TAppUser::getPhoneNumber, appletUserDecodeData.getPhoneNumber())));
+        if (sysAppUser==null){
+            sysAppUser = new TAppUser();
+            sysAppUser.setPhoneNumber(appletUserDecodeData.getPhoneNumber());
+            sysAppUser.setOpenId(appletUserDecodeData.getOpenId());
+            // 手机号中间四位替换为*
+            sysAppUser.setNickName(appletUserDecodeData.getPhoneNumber().replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
+            appUserService.save(sysAppUser);
+        }else {
+            // 判断账号是否被冻结
+            if (sysAppUser.getStatus() == 2){
+                return R.fail("账号已冻结,请联系管理员");
+            }
+            // 手机号相同但openId为空的情况
+            sysAppUser.setOpenId(appletUserDecodeData.getOpenId());
+            appUserService.updateById(sysAppUser);
+        }
+        LoginUserApplet loginUserApplet = new LoginUserApplet();
+        TAppUserResp sysUserResp = new TAppUserResp();
+        BeanUtils.copyProperties(sysAppUser, sysUserResp);
+        loginUserApplet.setUser(sysUserResp);
+        loginUserApplet.setUserId(sysAppUser.getId());
+        Map<String, Object> tokenInfos = new HashMap<>();
+        tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet));
+        tokenInfos.put("info",loginUserApplet);
+        return R.ok(tokenInfos);
+    }
+
 
     /**
      * 获取用户信息
diff --git a/ruoyi-applet/src/main/resources/application-test.yml b/ruoyi-applet/src/main/resources/application-test.yml
index d451e0f..4fcc7b6 100644
--- a/ruoyi-applet/src/main/resources/application-test.yml
+++ b/ruoyi-applet/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
 wx:
   conf:
     appId:
-    secretId:
\ No newline at end of file
+    secretId:
+payment:
+  wx:
+    # 微信appid
+    appId:
+    # 微信商户号
+    mchId:
+    # 秘钥
+    secretId:
+    v3:
+      # 加签串
+      apiKey:
+      # 加签证书地址
+      privateKeyPath:
+      # 微信支付公钥id
+      publicKeyId:
+      # 微信支付公钥证书
+      publicKeyPath:
+      # 证书序列号
+      mchSerialNo:
+      # 支付成功回调地址
+      notifyPayUrl:
+      # 支付退款回调地址
+      notifyRefundUrl:
\ No newline at end of file
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 4d6fbd9..047f099 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -219,6 +219,12 @@
 
         <!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
+
     </dependencies>
 
 </project>
\ No newline at end of file
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
deleted file mode 100644
index 770412a..0000000
--- a/ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.ruoyi.common.config;
-
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-
-/**
- * 项目中需继承此类
- *
- * @author lihen
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "wx.config")
-public class WxConfig {
-
-    /**
-     * 获取 App ID
-     *
-     * @return App ID
-     */
-    @JsonProperty("appId")
-    private String appId;
-
-    /**
-     * 获取 Secret
-     *
-     * @return Secret
-     */
-    @JsonProperty("secret")
-    private String secret;
-
-
-}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
index d1bd1e8..abf49de 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java
@@ -6,6 +6,8 @@
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.time.LocalDate;
+
 /**
  * <p>
  * 租户
@@ -28,30 +30,18 @@
     private String nickName;
 
     @ApiModelProperty(value = "手机号")
-    private String phone;
+    private String phoneNumber;
 
-    @ApiModelProperty(value = "登录账号")
-    private String account;
+    @ApiModelProperty(value = "头像")
+    private String avatar;
 
-    @ApiModelProperty(value = "密码")
-    private String password;
+    @ApiModelProperty(value = "性别 1=男 2=女")
+    private Integer gender;
 
-    @ApiModelProperty(value = "分队名称 注册提交时使用")
-    private String teamName;
-
-    @ApiModelProperty(value = "分队id")
-    private String teamId;
+    @ApiModelProperty(value = "生日")
+    private LocalDate birthDate;
 
     @ApiModelProperty(value = "状态 1=启用 2=禁用")
     private Integer status;
-
-    @ApiModelProperty(value = "实训次数")
-    private Integer practicalTrainCount;
-
-    @ApiModelProperty(value = "其他次数")
-    private Integer otherCount;
-
-    @ApiModelProperty(value = "审核状态 0=待审核 1=通过 2=拒绝")
-    private Integer state;
 
 }
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
index bf66bf3..f2d8b61 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java
@@ -127,7 +127,7 @@
     @Override
     public String getPassword()
     {
-        return user.getPassword();
+        return null;
     }
 
     @Override
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
deleted file mode 100644
index 7c6ee85..0000000
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.ruoyi.common.utils;
-
-import com.alibaba.fastjson2.JSONObject;
-import com.ruoyi.common.config.WxConfig;
-import com.ruoyi.common.core.redis.RedisCache;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-import org.springframework.web.client.RestTemplate;
-
-import java.text.MessageFormat;
-
-/**
- * @author liheng
- * @ClassName WxAppletTools
- * @Description
- * @date 2020-12-04 13:55
- */
-@Slf4j
-@Component
-public class WxAppletTools {
-    @Autowired
-    private RedisCache redisCache;
-    @Autowired
-    private RestTemplate restTemplate;
-    @Autowired
-    private WxConfig wxConfig;
-    private final static String ACCESSTOKEN_CACHE_KEY = "accessToken";
-    /**
-     * 请求参数
-     * 属性	类型	默认值	必填	说明
-     * appid	string		是	小程序 appId
-     * secret	string		是	小程序 appSecret
-     * js_code	string		是	登录时获取的 code
-     * grant_type	string		是	授权类型,此处只需填写 authorization_cod
-     * <p>
-     * 返回值:
-     * <p>
-     * 属性	类型	说明
-     * openid	string	用户唯一标识
-     * session_key	string	会话密钥
-     * unionid	string	用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
-     * errcode	number	错误码
-     * errmsg	string	错误信息
-     */
-    private static final String JSCODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
-
-
-    /**
-     * 请求参数
-     * 属性	类型	默认值	必填	说明
-     * grant_type	string		是	填写 client_credential
-     * appid	string		是	小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
-     * secret	string		是	小程序唯一凭证密钥,即 AppSecret,获取方式同 appid
-     * 返回值
-     * Object
-     * 返回的 JSON 数据包
-     * <p>
-     * 属性	类型	说明
-     * access_token	string	获取到的凭证
-     * expires_in	number	凭证有效时间,单位:秒。目前是7200秒之内的值。
-     * errcode	number	错误码
-     * errmsg	string	错误信息
-     */
-    private static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
-
-    /**
-     * @return
-     */
-    public String getAccessToken() {
-        String requestUrl = MessageFormat.format(ACCESS_TOKEN_URL, wxConfig.getAppId(), wxConfig.getSecret());
-        String respBody = restTemplate.getForEntity(requestUrl, String.class).getBody();
-        JSONObject jsonObject = JSONObject.parseObject(respBody);
-        return jsonObject.getString("access_token");
-    }
-}
diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml
index 5adce03..a19e805 100644
--- a/ruoyi-system/pom.xml
+++ b/ruoyi-system/pom.xml
@@ -88,6 +88,17 @@
             <groupId>com.fasterxml.jackson.datatype</groupId>
             <artifactId>jackson-datatype-jsr310</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-java-core</artifactId>
+            <version>0.2.12</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-apache-httpclient</artifactId>
+            <version>0.4.9</version>
+        </dependency>
     </dependencies>
 
 </project>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
index 6287358..3692b6c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
@@ -1,7 +1,7 @@
 package com.ruoyi.system.utils.wx.tools;
 
-import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
 import com.ruoyi.common.utils.sign.Base64;
+import com.ruoyi.system.utils.wx.pojo.AppletUserDecodeData;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.CharEncoding;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -11,14 +11,16 @@
 import javax.crypto.Cipher;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
 import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.security.AlgorithmParameters;
+import java.security.SecureRandom;
 import java.security.Security;
 import java.util.Arrays;
+import java.util.Random;
 
 /**
  * @Description 获取用户信息工具类
@@ -27,7 +29,12 @@
  */
 @Slf4j
 public class WxUtils {
+    /**
+     * 随机字符
+     */
+    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 
+    private static final Random RANDOM = new SecureRandom();
     /**
      * 微信小程序API 用户数据的解密
      *
@@ -95,20 +102,36 @@
      * @return
      */
     public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
-        log.info("微信异步回调地址:{}", request.getRequestURL());
-        StringBuffer buffer = new StringBuffer();
-        InputStream inputStream = request.getInputStream();
-        InputStreamReader reader = new InputStreamReader(inputStream);
-        BufferedReader bufferedReader = new BufferedReader(reader);
-        String body = null;
-        while ((body = bufferedReader.readLine()) != null) {
-            buffer.append(body);
+        BufferedReader reader = null;
+        StringBuffer sb = new StringBuffer();
+        try {
+            ServletInputStream stream = request.getInputStream();
+            // 获取响应
+            reader = new BufferedReader(new InputStreamReader(stream));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("读取微信支付接口数据流出现异常!");
+        } finally {
+            reader.close();
+            WxUtils.info(sb.toString());
         }
-        String data = buffer.toString();
-        reader.close();
-        inputStream.close();
-        log.info("微信异步回调数据:{}", data);
-        return data;
+        return sb.toString();
+    }
+
+    /**
+     * 获取随机字符串 Nonce Str
+     *
+     * @return String 随机字符串
+     */
+    public static String generateNonceStr() {
+        char[] nonceChars = new char[32];
+        for (int index = 0; index < nonceChars.length; ++index) {
+            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
+        }
+        return new String(nonceChars);
     }
 
     /**
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
new file mode 100644
index 0000000..925ab1e
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
@@ -0,0 +1,31 @@
+package com.ruoyi.system.wxPay.config;
+
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.utils.WxV3Pay;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 项目中需继承此类
+ *
+ * @author lihen
+ */
+@Configuration
+public class WxConfig {
+
+    private final WeixinPayProperties weixinPayProperties;
+
+    @Autowired
+    public WxConfig(WeixinPayProperties weixinPayProperties) {
+        this.weixinPayProperties = weixinPayProperties;
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(name = "wxV3Pay")
+    public WxV3Pay wxSpV3Pay() {
+        return new WxV3Pay(weixinPayProperties);
+    }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
new file mode 100644
index 0000000..61e3d2d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
@@ -0,0 +1,53 @@
+package com.ruoyi.system.wxPay.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+/**
+ * @author xiaochen
+ * @ClassName ProfitSharingEnum
+ * @Description
+ * @date 2021-11-21 11:15
+ */
+public enum RefundEnum {
+    /**
+     * 退款成功
+     */
+    SUCCESS("SUCCESS", "退款成功"),
+    /**
+     * 退款关闭
+     */
+    CLOSED("CLOSED", "退款关闭"),
+    /**
+     * 退款处理中
+     */
+    PROCESSING("PROCESSING", "退款处理中"),
+    /**
+     * 退款异常
+     */
+    ABNORMAL("ABNORMAL", "退款异常"),
+    ;
+
+    @Getter
+    private String code;
+    @Getter
+    private String desc;
+
+    RefundEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    /**
+     * 通过交易类型执行具体的交易方法
+     *
+     * @param code
+     * @return
+     */
+    public static RefundEnum fromValue(String code) {
+        return Stream.of(RefundEnum.values()).filter(fileType ->
+                fileType.getCode().toLowerCase().equals(code.toLowerCase())
+        ).findFirst().orElse(null);
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
new file mode 100644
index 0000000..c78c222
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
@@ -0,0 +1,41 @@
+package com.ruoyi.system.wxPay.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+/**
+ * @author xiaochen
+ * @ClassName AliTradeStateEnum
+ * @Description
+ * @date 2022-01-07 11:56
+ */
+public enum TradeStateEnum {
+    SUCCESS("支付成功"),
+    RETURN("已分账回退"),
+    FAIL("已失败"),
+    PROCESSING("处理中"),
+    FINISHED("分账完成"),
+    REFUND("转入退款"),
+    PAYERROR("支付失败(其他原因,如银行返回失败)"),
+    USERPAYING("用户支付中"),
+    CLOSED("已关闭"),
+    NOTPAY("未支付"),
+    UNKNOWN("未知"),
+    DONE("服务订单完成"),
+    // ...
+    ;
+    @Getter
+    private String desc;
+
+    TradeStateEnum(String desc) {
+        this.desc = desc;
+    }
+
+    public static TradeStateEnum tradeState(String code) {
+        return Stream.of(TradeStateEnum.values()).filter(fileType ->
+                fileType.name().equals(code)
+        ).findFirst().orElse(UNKNOWN);
+    }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
new file mode 100644
index 0000000..d79804a
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
@@ -0,0 +1,55 @@
+package com.ruoyi.system.wxPay.exception;
+
+/**
+ * @author lihen
+ */
+public class WxException extends RuntimeException {
+
+    private final static int OK = 0;
+    private final static int ValidateSignatureError = -40001;
+    private final static int ParseXmlError = -40002;
+    public final static int ComputeSignatureError = -40003;
+    private final static int IllegalAesKey = -40004;
+    private final static int ValidateAppidError = -40005;
+    private final static int EncryptAESError = -40006;
+    private final static int DecryptAESError = -40007;
+    private final static int IllegalBuffer = -40008;
+
+    private int code;
+
+    private static String getMessage(int code) {
+        switch (code) {
+            case ValidateSignatureError:
+                return "签名验证错误";
+            case ParseXmlError:
+                return "xml解析失败";
+            case ComputeSignatureError:
+                return "sha加密生成签名失败";
+            case IllegalAesKey:
+                return "SymmetricKey非法";
+            case ValidateAppidError:
+                return "appid校验失败";
+            case EncryptAESError:
+                return "aes加密失败";
+            case DecryptAESError:
+                return "aes解密失败";
+            case IllegalBuffer:
+                return "解密后得到的buffer非法";
+            default:
+                return null;
+        }
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public WxException(int code) {
+        super(getMessage(code));
+        this.code = code;
+    }
+
+    public WxException(String message) {
+        super(message);
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
new file mode 100644
index 0000000..18062cb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
@@ -0,0 +1,108 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.util.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author xiaochen
+ * @ClassName V3
+ * @Description
+ */
+@Slf4j
+@Data
+public class V3 {
+    /**
+     * 获取 API 密钥
+     *
+     * @return API密钥
+     */
+    private String apiKey;
+    /**
+     * 秘钥路径,apiclient_key.pem
+     */
+    private String privateKeyPath;
+    /**
+     * 微信支付公钥id
+     */
+    private String publicKeyId;
+    /**
+     * 微信支付公钥证书 apiclient_cert.pem
+     */
+    private String publicKeyPath;
+    /**
+     * 商户证书序列号
+     */
+    private String  mchSerialNo;
+
+    /**
+     * 支付回调地址
+     *
+     * @return
+     */
+    private String notifyPayUrl;
+
+    /**
+     * 退款回调地址
+     *
+     * @return
+     */
+    private String notifyRefundUrl;
+
+    /**
+     * 退款回调地址
+     */
+    private String notifyTravelRefundUrl;
+
+
+    public InputStream getPrivateKeyStream() {
+        // 需要证书释放
+        byte[] certData;
+//        InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
+        InputStream certStream = null;
+        try {
+            certStream = new FileInputStream(this.privateKeyPath);
+            certData = IOUtils.toByteArray(certStream);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new RuntimeException("私钥文件未找到");
+        }finally {
+            if(null != certStream){
+                try {
+                    certStream.close();
+                } catch (IOException e) {
+                    log.error("私钥流关闭异常");
+                }
+            }
+        }
+        return new ByteArrayInputStream(certData);
+    }
+
+    public InputStream getPublicKeyStream() {
+        // 需要证书释放
+        byte[] certData;
+//        InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
+        InputStream certStream = null;
+        try {
+            certStream = new FileInputStream(this.publicKeyPath);
+            certData = IOUtils.toByteArray(certStream);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new RuntimeException("公钥文件未找到");
+        }finally {
+            if(null != certStream){
+                try {
+                    certStream.close();
+                } catch (IOException e) {
+                    log.error("公钥流关闭异常");
+                }
+            }
+        }
+        return new ByteArrayInputStream(certData);
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
new file mode 100644
index 0000000..8463cfc
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
@@ -0,0 +1,113 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.ToString;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author xiaochen
+ * @ClassName WeixinProperties
+ * @Description
+ */
+@ToString
+@Component
+@ConfigurationProperties(prefix = "payment.wx")
+public class WeixinPayProperties {
+    /**
+     * 默认开启
+     */
+    private boolean enabled = true;
+    /**
+     * 获取 App ID
+     *
+     * @return App ID
+     */
+    private String appId;
+    /**
+     * 获取 Mch ID
+     *
+     * @return Mch ID
+     */
+    private String mchId;
+
+    /**
+     * 获取 secret ID
+     *
+     * @return secret ID
+     */
+    private String secretId;
+    /**
+     * 回调地址
+     */
+    private String callBackUrl;
+
+    public String getSecretId() {
+        return secretId;
+    }
+
+    public void setSecretId(String secretId) {
+        this.secretId = secretId;
+    }
+
+    /**
+     * v3
+     */
+    @NestedConfigurationProperty
+    private V3 v3;
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public V3 getV3() {
+        return v3;
+    }
+
+    public void setV3(V3 v3) {
+        this.v3 = v3;
+    }
+
+    /**
+     * HTTP(S) 连接超时时间,单位毫秒
+     *
+     */
+    public int getHttpConnectTimeoutMs() {
+        return 6 * 1000;
+    }
+
+    /**
+     * HTTP(S) 读数据超时时间,单位毫秒
+     */
+    public int getHttpReadTimeoutMs() {
+        return 8 * 1000;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getMchId() {
+        return mchId;
+    }
+
+    public void setMchId(String mchId) {
+        this.mchId = mchId;
+    }
+    
+    public String getCallBackUrl() {
+        return callBackUrl;
+    }
+    
+    public void setCallBackUrl(String callBackUrl) {
+        this.callBackUrl = callBackUrl;
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
new file mode 100644
index 0000000..cc4c82c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
@@ -0,0 +1,20 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentRefundModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxCloseOrderModel {
+    private String mchid;
+    
+    private String out_trade_no;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
new file mode 100644
index 0000000..af98ef9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
@@ -0,0 +1,201 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentInfoModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxPaymentInfoModel {
+    /**
+     * 合单商户appid
+     */
+    private String combine_appid;
+    /**
+     * 合单商户号
+     */
+    private String combine_mchid;
+    /**
+     * 合单商户订单号
+     */
+    private String combine_out_trade_no;
+    /**
+     * 合单--子单信息
+     */
+    private List<SubOrders> sub_orders;
+    /**
+     * 合单--支付者
+     */
+    private CombinePayerInfo combine_payer_info;
+
+    private String appid;
+    private String sp_appid;
+    private String mchid;
+    private String sp_mchid;
+    private String sub_appid;
+    private String sub_mchid;
+    private String description;
+    private String out_trade_no;
+    private String time_expire;
+    private String attach;
+    private String notify_url;
+    private String goods_tag;
+    private SettleInfo settle_info;
+    private Amount amount;
+    private Payer payer;
+    private Detail detail;
+    private SceneInfo scene_info;
+
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class SettleInfo {
+        private Boolean profit_sharing;
+        private Integer subsidy_amount;
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class Amount {
+        private Integer total;
+        /**
+         * 合单支付时需要
+         */
+        private Integer total_amount;
+        @Builder.Default
+        private String currency = "CNY";
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class Payer {
+        private String openid;
+        private String sp_openid;
+        private String sub_openid;
+    }
+
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class Detail {
+        private int cost_price;
+        private String invoice_id;
+        private List<GoodsDetail> goods_detail;
+
+        @Builder
+        @AllArgsConstructor
+        @NoArgsConstructor
+        @Getter
+        @Setter
+        @ToString
+        public static class GoodsDetail {
+            private String merchant_goods_id;
+            private String wechatpay_goods_id;
+            private String goods_name;
+            private int quantity;
+            private int unit_price;
+        }
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class SceneInfo {
+        private String payer_client_ip;
+        private String device_id;
+        private StoreInfo store_info;
+        private H5Info h5_info;
+
+        @Builder
+        @AllArgsConstructor
+        @NoArgsConstructor
+        @Getter
+        @Setter
+        @ToString
+        public static class StoreInfo {
+            private String id;
+            private String name;
+            private String area_code;
+            private String address;
+        }
+
+        @Builder
+        @AllArgsConstructor
+        @NoArgsConstructor
+        @Getter
+        @Setter
+        @ToString
+        public static class H5Info {
+            private String type;
+            private String app_name;
+            private String app_url;
+            private String bundle_id;
+            private String package_name;
+        }
+    }
+
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class SubOrders {
+
+        private String out_trade_no;
+
+
+        private Amount amount;
+
+        private String mchid;
+
+        private String sub_mchid;
+
+        private String attach;
+
+        private String description;
+        private String goods_tag;
+
+        private SettleInfo settle_info;
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class CombinePayerInfo {
+        private String openid;
+    }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
new file mode 100644
index 0000000..636acf5
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
@@ -0,0 +1,84 @@
+package com.ruoyi.system.wxPay.model;
+
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName WxPaymentRefundModel
+ * @Description
+ */
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+@ToString
+public class WxPaymentRefundModel {
+    /**
+     * 子商户,二级商户号
+     */
+    private String sub_mchid;
+    /**
+     * 电商平台APPID
+     */
+    private String sp_appid;
+    private String transaction_id;
+    private String out_trade_no;
+    /**
+     * 商户退款单号
+     */
+    private String out_refund_no;
+    /**
+     * 退款原因
+     */
+    private String reason;
+    private String notify_url;
+    /**
+     * 资金账户,否
+     */
+    private String funds_account;
+    /**
+     * 退款金额信息
+     */
+    private RefundAmount amount;
+    /**
+     * 退款商品
+     */
+    private List<RefundGoodsDetail> goods_detail;
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class RefundAmount {
+        /**
+         * 原订单金额
+         */
+        private int total;
+        @Builder.Default
+        private String currency = "CNY";
+        /**
+         * 退款金额
+         */
+        private int refund;
+    }
+
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    @ToString
+    public static class RefundGoodsDetail {
+        private String merchant_goods_id;
+        private String wechatpay_goods_id;
+        private String goods_name;
+        private int unit_price;
+        private int refund_amount;
+        private int refund_quantity;
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
new file mode 100644
index 0000000..ea6f700
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
@@ -0,0 +1,52 @@
+package com.ruoyi.system.wxPay.pojo;
+
+import lombok.Data;
+
+/**
+ * @author xiaochen
+ * @ClassName AppletUserDecodeData
+ * @Description
+ * 用户主体信息部分
+ * {
+ *     "openId": "OPENID",
+ *     "nickName": "NICKNAME",
+ *     "gender": GENDER,
+ *     "city": "CITY",
+ *     "province": "PROVINCE",
+ *     "country": "COUNTRY",
+ *     "avatarUrl": "AVATARURL",
+ *     "unionId": "UNIONID",
+ *     "watermark":
+ *     {
+ *         "appid":"APPID",
+ *         "timestamp":TIMESTAMP
+ *     }
+ * }
+ * 电话部分
+ * {
+ *     "phoneNumber": "13580006666",
+ *     "purePhoneNumber": "13580006666",
+ *     "countryCode": "86",
+ *     "watermark":
+ *     {
+ *         "appid":"APPID",
+ *         "timestamp": TIMESTAMP
+ *     }
+ * }
+ *
+ */
+@Data
+public class AppletUserDecodeData {
+    private String openId;
+    private String unionId;
+    private String nickName;
+    private int gender;
+    private String city;
+    private String province;
+    private String country;
+    private String avatarUrl;
+    private Watermark watermark;
+    private String phoneNumber;
+    private String purePhoneNumber;
+    private String countryCode;
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
new file mode 100644
index 0000000..2f92e2b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
@@ -0,0 +1,9 @@
+package com.ruoyi.system.wxPay.pojo;
+
+import lombok.Data;
+
+@Data
+public class Watermark {
+    private String appid;
+    private String timestamp;
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
new file mode 100644
index 0000000..b119786
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
@@ -0,0 +1,222 @@
+package com.ruoyi.system.wxPay.resp;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @author xiaochen
+ * @ClassName FacilV3PayNotifyRespBody
+ * @Description
+ */
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class NotifyV3PayDecodeRespBody implements Serializable {
+    // 合单--开始
+    private String combine_appid;
+    private String combine_mchid;
+    private String combine_out_trade_no;
+    private List<SubOrders> sub_orders;
+    // 合单--结束
+    /**
+     * 服务商应用ID
+     */
+    private String sp_appid;
+    /**
+     * 服务商户号
+     */
+    private String sp_mchid;
+    /**
+     * 商户号
+     */
+    private String mchid;
+    /**
+     * 子商户应用ID
+     */
+    private String sub_appid;
+    /**
+     * 子商户号
+     */
+    private String sub_mchid;
+    /**
+     * 商户订单号
+     */
+    private String out_trade_no;
+    /**
+     * 交易状态描述
+     */
+    private String trade_state_desc;
+    /**
+     * 交易类型,枚举值:
+     * JSAPI:公众号支付
+     * NATIVE:扫码支付
+     * APP:APP支付
+     * MICROPAY:付款码支付
+     * MWEB:H5支付
+     * FACEPAY:刷脸支付
+     */
+    private String trade_type;
+    /**
+     * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
+     */
+    private String attach;
+    /**
+     * 微信支付订单号
+     */
+    private String transaction_id;
+    /**
+     * 交易状态,枚举值:
+     * SUCCESS:支付成功
+     * REFUND:转入退款
+     * NOTPAY:未支付
+     * CLOSED:已关闭
+     * REVOKED:已撤销(付款码支付)
+     * USERPAYING:用户支付中(付款码支付)
+     * PAYERROR:支付失败(其他原因,如银行返回失败)
+     */
+    private String trade_state;
+    /**
+     * 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》
+     * https://pay.weixin.qq.com/wiki/doc/apiv3_partner/terms_definition/chapter1_1_3.shtml#part-6
+     */
+    private String bank_type;
+    /**
+     * 支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,
+     * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,
+     * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
+     * 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
+     * 示例值:2018-06-08T10:34:56+08:00
+     */
+    private String success_time;
+    /**
+     * 支付者信息
+     */
+    private Payer payer;
+
+    /**
+     * 支付者
+     */
+    private Payer combine_payer_info;
+
+    /**
+     * 订单金额信息
+     */
+    private Amount amount;
+    /**
+     * 场景信息
+     */
+    private SceneInfo scene_info;
+    /**
+     * 优惠功能,享受优惠时返回该字段
+     */
+    private List<PromotionDetail> promotion_detail;
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class Amount implements Serializable{
+        /**
+         * 用户支付金额
+         */
+        private int payer_total;
+        /**
+         * 总金额
+         */
+        private int total;
+        /**
+         * 标价金额
+         */
+        private int total_amount;
+        /**
+         * 现金支付金额
+         */
+        private int payer_amount;
+        private String currency;
+        private String payer_currency;
+    }
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class GoodsDetail implements Serializable{
+        private String goods_id;
+        private int quantity;
+        private int unit_price;
+        private int discount_amount;
+        private String goods_remark;
+
+    }
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class Payer implements Serializable{
+        private String openid;
+        private String sp_openid;
+        private String sub_openid;
+    }
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class PromotionDetail implements Serializable{
+        private String coupon_id;
+        private String name;
+        private String scope;
+        private String type;
+        private int amount;
+        private String stock_id;
+        private int wechatpay_contribute;
+
+        private int merchant_contribute;
+        private int other_contribute;
+        private String currency;
+
+        private List<GoodsDetail> goods_detail;
+
+    }
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class SceneInfo implements Serializable{
+        /**
+         * 商户端设备号
+         */
+        private String device_id;
+    }
+
+
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Data
+    public static class SubOrders implements Serializable{
+        private String mchid;
+        private String trade_type;
+        private String trade_state;
+        private String trade_state_desc;
+        private String bank_type;
+        private String attach;
+        private String success_time;
+        private String transaction_id;
+        private String out_trade_no;
+        private String sub_mchid;
+        private Amount amount;
+        /**
+         * 优惠功能,享受优惠时返回该字段
+         */
+        private List<PromotionDetail> promotion_detail;
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java
new file mode 100644
index 0000000..ded7b52
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java
@@ -0,0 +1,118 @@
+package com.ruoyi.system.wxPay.utils;
+
+
+import com.google.gson.annotations.SerializedName;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+/**
+ * JSAPI下单
+ */
+public class JsapiPrepay {
+  private static String HOST = "https://api.mch.weixin.qq.com";
+  private static String METHOD = "POST";
+  private static String PATH = "/v3/pay/transactions/jsapi";
+  private final String mchId;
+  private final String certificateSerialNo;
+  private final PrivateKey privateKey;
+  private final String wechatPayPublicKeyId;
+  private final PublicKey wechatPayPublicKey;
+
+  public JsapiPrepay(String mchId, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
+    this.mchId = mchId;
+    this.certificateSerialNo = certificateSerialNo;
+    this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
+    this.wechatPayPublicKeyId = wechatPayPublicKeyId;
+    this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+  }
+
+  public DirectAPIv3JsapiPrepayResponse run(DirectAPIv3JsapiPrepayRequest request) {
+    String uri = PATH;
+    String reqBody = WXPayUtility.toJson(request);
+
+    Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
+    reqBuilder.addHeader("Accept", "application/json");
+    reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+    reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchId, certificateSerialNo,privateKey, METHOD, uri, reqBody));
+    reqBuilder.addHeader("Content-Type", "application/json");
+    RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+    reqBuilder.method(METHOD, requestBody);
+    Request httpRequest = reqBuilder.build();
+
+    // 发送HTTP请求
+    OkHttpClient client = new OkHttpClient.Builder().build();
+    try (Response httpResponse = client.newCall(httpRequest).execute()) {
+      String respBody = WXPayUtility.extractBody(httpResponse);
+      if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+        // 2XX 成功,验证应答签名
+        WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
+            httpResponse.headers(), respBody);
+
+        // 从HTTP应答报文构建返回数据
+        return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
+      } else {
+        throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+    }
+  }
+  public static class DirectAPIv3JsapiPrepayRequest {
+    @SerializedName("appid")
+    public String appid;
+  
+    @SerializedName("mchid")
+    public String mchid;
+  
+    @SerializedName("description")
+    public String description;
+  
+    @SerializedName("out_trade_no")
+    public String outTradeNo;
+  
+    @SerializedName("time_expire")
+    public String timeExpire;
+  
+    @SerializedName("attach")
+    public String attach;
+  
+    @SerializedName("notify_url")
+    public String notifyUrl;
+  
+    @SerializedName("goods_tag")
+    public String goodsTag;
+  
+    @SerializedName("support_fapiao")
+    public Boolean supportFapiao;
+  
+    @SerializedName("amount")
+    public CommonAmountInfo amount;
+
+    @SerializedName("payer")
+    public JsapiReqPayerInfo payer;
+
+  }
+  
+  public static class DirectAPIv3JsapiPrepayResponse {
+    @SerializedName("prepay_id")
+    public String prepayId;
+  }
+  
+  public static class CommonAmountInfo {
+    @SerializedName("total")
+    public Long total;
+
+    @SerializedName("currency")
+    public String currency;
+  }
+
+  public static class JsapiReqPayerInfo {
+    @SerializedName("openid")
+    public String openid;
+  }
+
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java
new file mode 100644
index 0000000..834edae
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java
@@ -0,0 +1,441 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.wechat.pay.java.core.util.GsonUtil;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+
+public class WXPayUtility {
+  private static final Gson gson = new GsonBuilder()
+      .disableHtmlEscaping()
+      .addSerializationExclusionStrategy(new ExclusionStrategy() {
+        @Override
+        public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+          final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+          return expose != null && !expose.serialize();
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> aClass) {
+          return false;
+        }
+      })
+      .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+        @Override
+        public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+          final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+          return expose != null && !expose.deserialize();
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> aClass) {
+          return false;
+        }
+      })
+      .create();
+  private static final char[] SYMBOLS =
+      "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+  private static final SecureRandom random = new SecureRandom();
+
+  /**
+   * 将 Object 转换为 JSON 字符串
+   */
+  public static String toJson(Object object) {
+    return gson.toJson(object);
+  }
+
+  /**
+   * 将 JSON 字符串解析为特定类型的实例
+   */
+  public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+    return gson.fromJson(json, classOfT);
+  }
+
+  /**
+   * 从公私钥文件路径中读取文件内容
+   *
+   * @param keyPath 文件路径
+   * @return 文件内容
+   */
+  private static String readKeyStringFromPath(String keyPath) {
+    try {
+      return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  /**
+   * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+   *
+   * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+   * @return PrivateKey 对象
+   */
+  public static PrivateKey loadPrivateKeyFromString(String keyString) {
+    try {
+      keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+          .replace("-----END PRIVATE KEY-----", "")
+          .replaceAll("\\s+", "");
+      return KeyFactory.getInstance("RSA").generatePrivate(
+          new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException(e);
+    } catch (InvalidKeySpecException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  /**
+   * 从 PKCS#8 格式的私钥文件中加载私钥
+   *
+   * @param keyPath 私钥文件路径
+   * @return PrivateKey 对象
+   */
+  public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+    return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+  }
+
+  /**
+   * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+   *
+   * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+   * @return PublicKey 对象
+   */
+  public static PublicKey loadPublicKeyFromString(String keyString) {
+    try {
+      keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+          .replace("-----END PUBLIC KEY-----", "")
+          .replaceAll("\\s+", "");
+      return KeyFactory.getInstance("RSA").generatePublic(
+          new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException(e);
+    } catch (InvalidKeySpecException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  /**
+   * 从 PKCS#8 格式的公钥文件中加载公钥
+   *
+   * @param keyPath 公钥文件路径
+   * @return PublicKey 对象
+   */
+  public static PublicKey loadPublicKeyFromPath(String keyPath) {
+    return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+  }
+
+  /**
+   * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+   */
+  public static String createNonce(int length) {
+    char[] buf = new char[length];
+    for (int i = 0; i < length; ++i) {
+      buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+    }
+    return new String(buf);
+  }
+
+  /**
+   * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+   *
+   * @param publicKey 加密用公钥对象
+   * @param plaintext 待加密明文
+   * @return 加密后密文
+   */
+  public static String encrypt(PublicKey publicKey, String plaintext) {
+    final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+
+    try {
+      Cipher cipher = Cipher.getInstance(transformation);
+      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+      return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+      throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+    } catch (BadPaddingException | IllegalBlockSizeException e) {
+      throw new IllegalArgumentException("Plaintext is too long", e);
+    }
+  }
+
+  /**
+   * 使用私钥按照指定算法进行签名
+   *
+   * @param message 待签名串
+   * @param algorithm 签名算法,如 SHA256withRSA
+   * @param privateKey 签名用私钥对象
+   * @return 签名结果
+   */
+  public static String sign(String message, String algorithm, PrivateKey privateKey) {
+    byte[] sign;
+    try {
+      Signature signature = Signature.getInstance(algorithm);
+      signature.initSign(privateKey);
+      signature.update(message.getBytes(StandardCharsets.UTF_8));
+      sign = signature.sign();
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+    } catch (SignatureException e) {
+      throw new RuntimeException("An error occurred during the sign process.", e);
+    }
+    return Base64.getEncoder().encodeToString(sign);
+  }
+
+  /**
+   * 使用公钥按照特定算法验证签名
+   *
+   * @param message 待签名串
+   * @param signature 待验证的签名内容
+   * @param algorithm 签名算法,如:SHA256withRSA
+   * @param publicKey 验签用公钥对象
+   * @return 签名验证是否通过
+   */
+  public static boolean verify(String message, String signature, String algorithm,
+                               PublicKey publicKey) {
+    try {
+      Signature sign = Signature.getInstance(algorithm);
+      sign.initVerify(publicKey);
+      sign.update(message.getBytes(StandardCharsets.UTF_8));
+      return sign.verify(Base64.getDecoder().decode(signature));
+    } catch (SignatureException e) {
+      return false;
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+    }
+  }
+
+  /**
+   * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+   *
+   * @param mchid 商户号
+   * @param certificateSerialNo 商户API证书序列号
+   * @param privateKey 商户API证书私钥
+   * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+   * @param uri 请求接口的URL
+   * @param body 请求接口的Body
+   * @return 构造好的微信支付APIv3 Authorization 头
+   */
+  public static String buildAuthorization(String mchid, String certificateSerialNo,
+                                          PrivateKey privateKey,
+                                          String method, String uri, String body) {
+    String nonce = createNonce(32);
+    long timestamp = Instant.now().getEpochSecond();
+
+    String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+        body == null ? "" : body);
+
+    String signature = sign(message, "SHA256withRSA", privateKey);
+
+    return String.format(
+        "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+            "timestamp=\"%d\",serial_no=\"%s\"",
+        mchid, nonce, signature, timestamp, certificateSerialNo);
+  }
+
+  /**
+   * 对参数进行 URL 编码
+   *
+   * @param content 参数内容
+   * @return 编码后的内容
+   */
+  public static String urlEncode(String content) {
+    try {
+      return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * 对参数Map进行 URL 编码,生成 QueryString
+   *
+   * @param params Query参数Map
+   * @return QueryString
+   */
+  public static String urlEncode(Map<String, Object> params) {
+    if (params == null || params.isEmpty()) {
+      return "";
+    }
+
+    int index = 0;
+    StringBuilder result = new StringBuilder();
+    for (Map.Entry<String, Object> entry : params.entrySet()) {
+      result.append(entry.getKey())
+          .append("=")
+          .append(urlEncode(entry.getValue().toString()));
+      index++;
+      if (index < params.size()) {
+        result.append("&");
+      }
+    }
+    return result.toString();
+  }
+
+  /**
+   * 从应答中提取 Body
+   *
+   * @param response HTTP 请求应答对象
+   * @return 应答中的Body内容,Body为空时返回空字符串
+   */
+  public static String extractBody(Response response) {
+    if (response.body() == null) {
+      return "";
+    }
+
+    try {
+      BufferedSource source = response.body().source();
+      return source.readUtf8();
+    } catch (IOException e) {
+      throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
+    }
+  }
+
+  /**
+   * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+   *
+   * @param wechatpayPublicKeyId 微信支付公钥ID
+   * @param wechatpayPublicKey 微信支付公钥对象
+   * @param headers 微信支付应答 Header 列表
+   * @param body 微信支付应答 Body
+   */
+  public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+                                      Headers headers,
+                                      String body) {
+    String timestamp = headers.get("Wechatpay-Timestamp");
+    try {
+      Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+      // 拒绝过期请求
+      if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+        throw new IllegalArgumentException(
+            String.format("Validate http response,timestamp[%s] of httpResponse is expires, "
+                    + "request-id[%s]",
+                timestamp, headers.get("Request-ID")));
+      }
+    } catch (DateTimeException | NumberFormatException e) {
+      throw new IllegalArgumentException(
+          String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " +
+                  "request-id[%s]", timestamp,
+              headers.get("Request-ID")));
+    }
+    String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+        body == null ? "" : body);
+    String serialNumber = headers.get("Wechatpay-Serial");
+    if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+      throw new IllegalArgumentException(
+          String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId,
+              serialNumber));
+    }
+    String signature = headers.get("Wechatpay-Signature");
+
+    boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+    if (!success) {
+      throw new IllegalArgumentException(
+          String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+                  + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+              headers.get("Request-ID"), headers, body));
+    }
+  }
+
+  /**
+   * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+   */
+  public static class ApiException extends RuntimeException {
+    private static final long serialVersionUID = 2261086748874802175L;
+
+    private final int statusCode;
+    private final String body;
+    private final Headers headers;
+    private final String errorCode;
+    private final String errorMessage;
+
+    public ApiException(int statusCode, String body, Headers headers) {
+      super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers));
+      this.statusCode = statusCode;
+      this.body = body;
+      this.headers = headers;
+
+      if (body != null && !body.isEmpty()) {
+        JsonElement code;
+        JsonElement message;
+
+        try {
+          JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class);
+          code = jsonObject.get("code");
+          message = jsonObject.get("message");
+        } catch (JsonSyntaxException ignored) {
+          code = null;
+          message = null;
+        }
+        this.errorCode = code == null ? null : code.getAsString();
+        this.errorMessage = message == null ? null : message.getAsString();
+      } else {
+        this.errorCode = null;
+        this.errorMessage = null;
+      }
+    }
+
+    /**
+     * 获取 HTTP 应答状态码
+     */
+    public int getStatusCode() {
+      return statusCode;
+    }
+
+    /**
+     * 获取 HTTP 应答包体内容
+     */
+    public String getBody() {
+      return body;
+    }
+
+    /**
+     * 获取 HTTP 应答 Header
+     */
+    public Headers getHeaders() {
+      return headers;
+    }
+
+    /**
+     * 获取 错误码 (错误应答中的 code 字段)
+     */
+    public String getErrorCode() {
+      return errorCode;
+    }
+
+    /**
+     * 获取 错误消息 (错误应答中的 message 字段)
+     */
+    public String getErrorMessage() {
+      return errorMessage;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
new file mode 100644
index 0000000..a888f1b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
@@ -0,0 +1,400 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.system.utils.wx.tools.WebUtils;
+import com.ruoyi.system.utils.wx.tools.WxJsonUtils;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
+import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
+import com.ruoyi.system.wxPay.model.WxPaymentInfoModel;
+import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
+import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
+import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
+import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
+
+/**
+ * @author xiaochen
+ * @ClassName WxWifiV3Pay
+ * @Description
+ * @date 2021-11-13 21:10
+ */
+@Slf4j
+public abstract class WxAbstractPay {
+    /**
+     * 请求成功相应码
+     */
+    private static final int STATUS_CODE = 200;
+    /**
+     * 请求成功相应码
+     */
+    private static final int OTHER_STATUS_CODE = 204;
+    /**
+     * 请求根地址
+     */
+    private static final String HOST = "https://api.mch.weixin.qq.com";
+
+    private static RuntimeException parameterError(String message, Object... args) {
+        message = String.format(message, args);
+        return new IllegalArgumentException("parameter error: " + message);
+    }
+
+    /**
+     * 封装基础数据
+     *
+     * @param requestBody
+     * @param notifyUrl
+     * @return
+     */
+    protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) {
+        // 封装基础数据
+        requestBody.setNotify_url(notifyUrl + requestBody.getNotify_url());
+        String reqBody = WxJsonUtils.toJsonString(requestBody);
+        return reqBody;
+    }
+
+    /**
+     * 微信调起支付参数
+     * 返回参数如有不理解 请访问微信官方文档
+     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
+     *
+     * @param prepayId 微信下单返回的prepay_id
+     * @param appId    应用ID(appid)
+     * @return 当前调起支付所需的参数
+     * @throws Exception
+     */
+    protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) {
+        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
+        String nonceStr = WxUtils.generateNonceStr();
+        String packageStr = "prepay_id=" + prepayId;
+        //加载签名
+        String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
+        String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign();
+        Map<String, Object> map = new HashMap<>(6);
+        map.put("appId", appId);
+        map.put("timeStamp", timeStamp);
+        map.put("nonceStr", nonceStr);
+        map.put("package", packageStr);
+        map.put("signType", "RSA");
+        map.put("paySign", packageSign);
+        return map;
+    }
+
+    /**
+     * 构建方法请求
+     *
+     * @param uri
+     * @param socketTimeout
+     * @param connectTimeout
+     * @return
+     */
+    protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) {
+        //请求URL
+        HttpGet httpGet = new HttpGet(HOST + uri);
+        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
+                .setConnectTimeout(connectTimeout).build();
+        httpGet.setConfig(requestConfig);
+        httpGet.setHeader("Content-type", "application/json");
+        httpGet.setHeader("Accept", "application/json");
+        return httpGet;
+    }
+
+    /**
+     * 构建方法请求
+     *
+     * @param uri
+     * @param socketTimeout
+     * @param connectTimeout
+     * @param reqdata
+     * @return
+     */
+    protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) {
+        //请求URL
+        HttpPost httpPost = new HttpPost(HOST + uri);
+        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
+                .setConnectTimeout(connectTimeout).build();
+        httpPost.setConfig(requestConfig);
+        StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
+        entity.setContentType("application/json");
+        httpPost.setEntity(entity);
+        httpPost.setHeader("Accept", "application/json");
+        return httpPost;
+    }
+
+    public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception;
+
+    /**
+     * 接收回调
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception {
+        String body = WxUtils.streamBodyByReceive(request);
+        String requestId = request.getHeader(REQUEST_ID);
+        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
+        String value;
+        // 验签必须参数检验
+        for (String headerName : headers) {
+            value = request.getHeader(headerName);
+            if (value == null || "".equals(value)) {
+                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
+            }
+        }
+        String serial = request.getHeader(WECHAT_PAY_SERIAL);
+        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
+        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
+        String nonce = request.getHeader(WECHAT_PAY_NONCE);
+        // 构建request,传入必要参数
+        NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial)
+                .withNonce(nonce)
+                .withTimestamp(timestamp)
+                .withSignature(signature)
+                .withBody(body)
+                .build();
+        NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8));
+        // 验签和解析请求体
+        Notification notification = handler.parse(notificationRequest);
+        assert notification != null;
+        T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef);
+        return respBody;
+    }
+
+    /**
+     * 订单查询
+     *
+     * @param httpClient
+     * @param socketTimeout
+     * @param connectTimeout
+     * @param url
+     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
+     * @author xiaochen
+     * @date 2021-12-20 17:12
+     */
+    protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) {
+        //请求URL
+        HttpGet httpGet = requestGet(
+                url
+                , socketTimeout
+                , connectTimeout);
+        CloseableHttpResponse response = null;
+        try {
+            response = httpClient.execute(httpGet);
+            int statusCode = response.getStatusLine().getStatusCode();
+            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+            if (WxUtils.getLogger().isDebugEnabled()) {
+                WxUtils.debug("请求成功:{}", respBodyStr);
+            }
+            if(404 == statusCode){
+               return null;
+            }
+            // 成功相应
+            if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode ) {
+                NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(respBodyStr, NotifyV3PayDecodeRespBody.class);
+                return body;
+            } else {
+                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
+                throw new RuntimeException(respBodyStr);
+            }
+        } catch (ConnectTimeoutException e) {
+            e.printStackTrace();
+            throw new RuntimeException("接口超时");
+        } catch (SocketTimeoutException e) {
+            e.printStackTrace();
+            throw new RuntimeException("读取接口数据超时");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
+        } finally {
+            // 关闭响应
+            try {
+                if (response != null) {
+                    //关闭结果集
+                    response.getEntity().getContent().close();
+                    response.close();
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("关闭流异常");
+            }
+        }
+    }
+
+    /**
+     * 子级实现
+     *
+     * @param out_trade_no
+     * @return
+     */
+    public abstract NotifyV3PayDecodeRespBody query(String out_trade_no);
+
+
+    /**
+     * 订单退款
+     *
+     * @param refundModel
+     * @return
+     */
+    public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
+    
+    
+    
+    public abstract String close(String out_trade_no);
+
+    /**
+     * 订单退款
+     *
+     * @param httpClient
+     * @param uri
+     * @param httpReadTimeoutMs
+     * @param httpConnectTimeoutMs
+     * @param refundModel
+     * @return
+     */
+    public Map<String, Object> refund(CloseableHttpClient httpClient,
+                                      String uri,
+                                      int httpReadTimeoutMs,
+                                      int httpConnectTimeoutMs,
+                                      WxPaymentRefundModel refundModel) {
+        String reqBody = WxJsonUtils.toJsonString(refundModel);
+        //请求URL
+        HttpEntityEnclosingRequestBase httpPost = requestPost(
+                uri
+                , httpReadTimeoutMs
+                , httpConnectTimeoutMs, reqBody);
+        String repBody = result(httpClient, httpPost);
+        Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class);
+        return body;
+    }
+
+    /**
+     * 请求结果
+     *
+     * @param request
+     * @return
+     */
+    protected String result(CloseableHttpClient httpClient, HttpRequestBase request) {
+        CloseableHttpResponse response = null;
+        try {
+            response = httpClient.execute(request);
+            int statusCode = response.getStatusLine().getStatusCode();
+            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+            if (WxUtils.getLogger().isDebugEnabled()) {
+                WxUtils.debug("请求成功:{}", respBodyStr);
+            }
+            // 成功相应
+            if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) {
+                return respBodyStr;
+            } else {
+                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
+                throw new RuntimeException(respBodyStr);
+            }
+        } catch (ConnectTimeoutException e) {
+            e.printStackTrace();
+            throw new RuntimeException("接口超时");
+        } catch (SocketTimeoutException e) {
+            e.printStackTrace();
+            throw new RuntimeException("读取接口数据超时");
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
+        } finally {
+            // 关闭响应
+            try {
+                if (response != null) {
+                    //关闭结果集
+                    response.getEntity().getContent().close();
+                    response.close();
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("关闭流异常");
+            }
+        }
+    }
+
+    /**
+     * 微信结果确认应答
+     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
+     *
+     * @param
+     * @throws IOException
+     */
+    public void ack() throws IOException {
+        ack(true, null);
+    }
+
+    /**
+     * 微信结果确认应答
+     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
+     *
+     * @param
+     * @throws IOException
+     */
+    public void ack(boolean ackSucc, String erroMsg) throws IOException {
+        HttpServletResponse response = WebUtils.response();
+        PrintWriter writer = response.getWriter();
+        if (ackSucc) {
+            log.info("响应微信回调成功!");
+            response.setStatus(200);
+            writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
+
+        } else {
+            log.info("响应微信回调失败:{}!", erroMsg);
+            response.setStatus(500);
+            writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}");
+        }
+        // 关闭流
+        if (Objects.nonNull(writer)) {
+            writer.close();
+        }
+    }
+    
+    
+    /**
+     * 关闭订单
+     * @param httpClient
+     * @param uri
+     * @param httpReadTimeoutMs
+     * @param httpConnectTimeoutMs
+     * @param closeModel
+     * @return
+     */
+    public String close(CloseableHttpClient httpClient,
+                                      String uri,
+                                      int httpReadTimeoutMs,
+                                      int httpConnectTimeoutMs,
+                                     WxCloseOrderModel closeModel) {
+        String reqBody = WxJsonUtils.toJsonString(closeModel);
+        //请求URL
+        HttpEntityEnclosingRequestBase httpPost = requestPost(
+                uri
+                , httpReadTimeoutMs
+                , httpConnectTimeoutMs, reqBody);
+        String repBody = result(httpClient, httpPost);
+        return repBody;
+    }
+
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
new file mode 100644
index 0000000..160e952
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
@@ -0,0 +1,164 @@
+package com.ruoyi.system.wxPay.utils;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.springframework.util.StringUtils;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * @author xiaochen
+ * @ClassName WxTimeUtils
+ * @Description
+ * @date 2021-12-16 16:07
+ */
+public class WxTimeUtils {
+    /**
+     * 系统默认时区
+     */
+    private static final ZoneId ZONE = ZoneId.systemDefault();
+    /**
+     * yyyy-MM-dd'T'HH:mm:ssxxx 比如:2020-05-23T17:06:30+08:00 0时区时末尾 为+00:00
+     */
+    public static final DateTimeFormatter YYYY_MM_DD_T_HH_MM_SS_XXX_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx");
+    /**
+     * yyyy-MM-dd HH:mm:ss 比如:2020-05-23 17:06:30
+     */
+    public static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE);
+
+    /**
+     * 时间转 TimeZone
+     *
+     * @param date
+     * @return
+     * @throws Exception
+     */
+    public static String dateToTimeZone(Date date) throws Exception {
+        String time;
+        if (date == null) {
+            throw new Exception("date is not null");
+        }
+        ZonedDateTime zonedDateTime = toZonedDateTime(date);
+        time = format(zonedDateTime, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+        return time;
+    }
+
+    /**
+     * Date转ZonedDateTime,时区为系统默认时区
+     *
+     * @param date Date
+     * @return ZonedDateTime
+     */
+    public static ZonedDateTime toZonedDateTime(Date date) {
+        Objects.requireNonNull(date, "date");
+        return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault());
+    }
+
+    /**
+     * 根据 formatter格式化 zonedDateTime
+     *
+     * @param zonedDateTime ZonedDateTime
+     * @param formatter     DateTimeFormatter
+     * @return String
+     */
+    public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter) {
+        Objects.requireNonNull(zonedDateTime, "zonedDateTime");
+        Objects.requireNonNull(formatter, "formatter");
+        return zonedDateTime.format(formatter);
+    }
+
+
+    /**
+     * TimeZone 时间转标准时间
+     *
+     * @param date
+     * @return
+     * @throws Exception
+     */
+    public static String toTimeZoneStr(String date) {
+        String time;
+        if (!StringUtils.hasLength(date)) {
+            throw new RuntimeException("str is not null");
+        }
+        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+        if (zonedDateTime == null) {
+            throw new RuntimeException("str to zonedDateTime fail");
+        }
+        time = zonedDateTime.format(YYYY_MM_DD_HH_MM_SS_FMT);
+        return time;
+    }
+
+    /**
+     * 转date
+     *
+     * @param date
+     * @return
+     * @throws Exception
+     */
+    public static Date toDate(String date) {
+        String time;
+        if (!StringUtils.hasLength(date)) {
+            throw new RuntimeException("str is not null");
+        }
+        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
+        if (zonedDateTime == null) {
+            throw new RuntimeException("str to zonedDateTime fail");
+        }
+        return Date.from(zonedDateTime.toInstant());
+    }
+
+    /**
+     * str --> Date
+     *
+     * @param date
+     * @return java.util.Date
+     * @author xiaochen
+     * @date 2022-01-20 18:20
+     */
+    public static Date toRfc3339Date(String date) {
+        DateTime dt2 = new DateTime(date);
+        return dt2.toDate();
+    }
+
+    /**
+     * 将 Date 转为 LocalDateTime
+     *
+     * @param date
+     * @return java.time.LocalDateTime;
+     */
+    public static LocalDateTime dateToLocalDateTime(Date date) {
+        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+    }
+
+
+    /**
+     * str --> Date
+     *
+     * @param date
+     * @return java.util.Date
+     * @author xiaochen
+     * @date 2022-01-20 18:20
+     */
+    public static String toRfc3339Str(Date date) {
+        DateTime dt1 = new DateTime(new Date(), DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai")));
+        return dt1.toString();
+    }
+
+    /**
+     * 根据 formatter解析为 ZonedDateTime
+     *
+     * @param text      待解析字符串
+     * @param formatter DateTimeFormatter
+     * @return ZonedDateTime
+     */
+    public static ZonedDateTime parseToZonedDateTime(String text, DateTimeFormatter formatter) {
+        return ZonedDateTime.parse(text, formatter);
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
new file mode 100644
index 0000000..2ba770d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
@@ -0,0 +1,235 @@
+package com.ruoyi.system.wxPay.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.ruoyi.system.utils.wx.tools.WxUtils;
+import com.ruoyi.system.wxPay.model.WeixinPayProperties;
+import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
+import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
+import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
+import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
+import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
+import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
+import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
+import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.PrivateKey;
+import java.util.Map;
+
+/**
+ * @author xiaochen
+ * @ClassName WxWifiV3Pay
+ * @Description
+ * @date 2021-11-13 21:10
+ */
+@Slf4j
+public class WxV3Pay extends WxAbstractPay {
+    @Getter
+    private WeixinPayProperties config;
+    @Getter
+    private Verifier verifier;
+    private WechatPayHttpClientBuilder builder;
+    @Getter
+    private CloseableHttpClient httpClient;
+    private PrivateKeySigner privateKeySigner;
+    private WechatPay2Validator validator;
+    @Getter
+    private PrivateKey privateKey;
+
+    /**
+     * 初始化
+     *
+     * @param config
+     */
+    public WxV3Pay(WeixinPayProperties config) {
+        // 检查v3支付配置信息
+        if (WxUtils.getLogger().isDebugEnabled()) {
+            WxUtils.debug("开始检查v3支付配置信息....");
+        }
+        try {
+            this.config = config;
+            checkWxConfig();
+            this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream());
+            this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey);
+            this.validator = new WechatPay2Validator(verifier);
+            this.builder = WechatPayHttpClientBuilder.create()
+                    .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey)
+                    .withValidator(this.validator);
+            this.httpClient = this.builder.build();
+        } catch (Exception e) {
+            // 打印异常信息
+            e.printStackTrace();
+            WxUtils.warn("检查v3支付配置信息出现错误,直连商户商户号:{},{}", config.getMchId(), e.getMessage());
+            return;
+        }
+        if (WxUtils.getLogger().isDebugEnabled()) {
+            WxUtils.debug("检查v3支付配置信息完成,未出现异常....");
+        }
+    }
+
+    /**
+     * 检查支付配置信息
+     *
+     * @throws Exception
+     */
+    private void checkWxConfig() throws Exception {
+        if (this.config == null) {
+            throw new Exception("config is null");
+        }
+        if (config.getMchId() == null || config.getMchId().trim().length() == 0) {
+            throw new Exception("MchID in config is empty");
+        }
+        if (config.getV3().getMchSerialNo() == null) {
+            throw new Exception("mchSerialNo in config is empty");
+        }
+        if (config.getV3().getPrivateKeyStream() == null) {
+            throw new Exception("cert stream in config is empty");
+        }
+        if (this.config.getHttpConnectTimeoutMs() < 10) {
+            throw new Exception("http connect timeout is too small");
+        }
+        if (this.config.getHttpReadTimeoutMs() < 10) {
+            throw new Exception("http read timeout is too small");
+        }
+    }
+
+    /**
+     * jsApi下单
+     *
+     * @param tradeNo     订单号
+     * @param amount      金额 分
+     * @param openid      openid
+     * @param description 订单描述
+     * @return java.util.Map<java.lang.String, java.lang.Object>
+     * @author xiaochen
+     * @date 2022-03-22 12:47
+     */
+    public Map<String, Object> jsApi(String tradeNo, Long amount, String openid, String description,String attach) {
+        JsapiPrepay client = new JsapiPrepay(
+                this.config.getMchId(),                    // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
+                this.config.getV3().getMchSerialNo(),         // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
+                this.config.getV3().getPrivateKeyPath(),     // 商户API证书私钥文件路径,本地文件路径
+                this.config.getV3().getPublicKeyId(),      // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
+                this.config.getV3().getPublicKeyPath()           // 微信支付公钥文件路径,本地文件路径
+        );
+        JsapiPrepay.DirectAPIv3JsapiPrepayRequest request = new JsapiPrepay.DirectAPIv3JsapiPrepayRequest();
+        request.appid = this.config.getAppId();
+        request.mchid = this.config.getMchId();
+        request.description = description;
+        request.outTradeNo = tradeNo;
+//        request.timeExpire = "2018-06-08T10:34:56+08:00";
+        request.attach = attach;
+        request.notifyUrl = this.config.getCallBackUrl();
+        request.goodsTag = "WXG";
+        request.supportFapiao = false;
+        request.amount = new JsapiPrepay.CommonAmountInfo();
+        request.amount.total = amount;
+        request.amount.currency = "CNY";
+        request.payer = new JsapiPrepay.JsapiReqPayerInfo();
+        request.payer.openid = openid;
+        try {
+            JsapiPrepay.DirectAPIv3JsapiPrepayResponse response = client.run(request);
+            return wxTuneUp(this.privateKeySigner, this.config.getAppId(), response.prepayId);
+        } catch (WXPayUtility.ApiException e) {
+            throw new RuntimeException("获取支付数据错误!");
+        }
+    }
+//    /**
+//     * jsApi下单
+//     *
+//     * @param tradeNo     订单号
+//     * @param amount      金额 分
+//     * @param openid      openid
+//     * @param description 订单描述
+//     * @return java.util.Map<java.lang.String, java.lang.Object>
+//     * @author xiaochen
+//     * @date 2022-03-22 12:47
+//     */
+//    public Map<String, Object> jsApi(String tradeNo, Integer amount, String openid, String description) {
+//        WxPaymentInfoModel requestBody = WxPaymentInfoModel.builder()
+//                .mchid(this.config.getMchId())
+//                .appid(this.config.getAppId())
+//                .description(description)
+//                .out_trade_no(tradeNo)
+////                .attach("")
+//                .amount(WxPaymentInfoModel.Amount.builder().total(amount).build())
+//                .payer(WxPaymentInfoModel.Payer.builder().openid(openid).build())
+//                .build();
+//        // 封装基础数据
+//        String reqBody = buildBaseParam(requestBody
+//                , this.config.getV3().getNotifyPayUrl());
+//        //请求URL
+//        HttpEntityEnclosingRequestBase httpPost = requestPost(
+//                "/v3/pay/transactions/jsapi"
+//                , this.config.getHttpReadTimeoutMs()
+//                , this.config.getHttpConnectTimeoutMs()
+//                , reqBody);
+//        String repBody = result(httpClient, httpPost);
+//        ObjectMapper om = new ObjectMapper();
+//        try {
+//            JsonNode rootNode = om.readTree(repBody);
+//            String prepayId = rootNode.path("prepay_id").asText();
+//            return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId);
+//        } catch (JsonProcessingException e) {
+//            throw new RuntimeException("获取支付数据错误!");
+//        }
+//    }
+
+
+    /**
+     * 接收回调
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    @Override
+    public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception {
+        return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef);
+    }
+
+    /**
+     * 订单查询
+     *
+     * @param out_trade_no
+     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
+     * @author xiaochen
+     * @date 2021-12-20 16:47
+     */
+    @Override
+    public NotifyV3PayDecodeRespBody query(String out_trade_no) {
+        String url =
+                String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", this.getConfig().getMchId());
+        return query(this.httpClient, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), url);
+    }
+
+    /**
+     * 退款
+     *
+     * @param refundModel
+     * @return java.util.Map<java.lang.String, java.lang.Object>
+     * @author xiaochen
+     */
+    @Override
+    public Map<String, Object> refund(WxPaymentRefundModel refundModel) {
+        refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl() + refundModel.getNotify_url());
+        return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
+    }
+    
+    
+    /**
+     * 关闭订单
+     * @param out_trade_no
+     * @return
+     */
+    @Override
+    public String close(String out_trade_no) {
+        String uri = String.format("/v3/pay/transactions/out-trade-no/%s/close", out_trade_no);
+        WxCloseOrderModel wxCloseOrderModel = new WxCloseOrderModel();
+        wxCloseOrderModel.setMchid(this.config.getMchId());
+        return close(this.httpClient, uri, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), wxCloseOrderModel);
+    }
+}

--
Gitblit v1.7.1