liujie
2025-11-06 33761d82e3f5569ecb6fc1aeaf77843a85032d87
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/mobile_community
2个文件已删除
18个文件已添加
10个文件已修改
2825 ■■■■■ 已修改文件
ruoyi-admin/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-test.yml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/TAppUserResp.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUserApplet.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/pom.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java 441 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java 400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
ruoyi-admin/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
wx:
  conf:
    appId:
    secretId:
    secretId:
payment:
  wx:
    # 微信appid
    appId:
    # 微信商户号
    mchId:
    # 秘钥
    secretId:
    v3:
      # 加签串
      apiKey:
      # 加签证书地址
      privateKeyPath:
      # 微信支付公钥id
      publicKeyId:
      # 微信支付公钥证书
      publicKeyPath:
      # 证书序列号
      mchSerialNo:
      # 支付成功回调地址
      notifyPayUrl:
      # 支付退款回调地址
      notifyRefundUrl:
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>
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
New file
@@ -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());
        }
    }
}
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);
    }
    /**
     * 获取用户信息
ruoyi-applet/src/main/resources/application-test.yml
@@ -194,4 +194,27 @@
wx:
  conf:
    appId:
    secretId:
    secretId:
payment:
  wx:
    # 微信appid
    appId:
    # 微信商户号
    mchId:
    # 秘钥
    secretId:
    v3:
      # 加签串
      apiKey:
      # 加签证书地址
      privateKeyPath:
      # 微信支付公钥id
      publicKeyId:
      # 微信支付公钥证书
      publicKeyPath:
      # 证书序列号
      mchSerialNo:
      # 支付成功回调地址
      notifyPayUrl:
      # 支付退款回调地址
      notifyRefundUrl:
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>
ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
File was deleted
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;
}
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
ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
File was deleted
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>
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);
    }
    /**
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
New file
@@ -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;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
New file
@@ -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;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
New file
@@ -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;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.system.wxPay.pojo;
import lombok.Data;
@Data
public class Watermark {
    private String appid;
    private String timestamp;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
New file
@@ -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;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java
New file
@@ -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;
  }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java
New file
@@ -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;
    }
  }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
New file
@@ -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;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
New file
@@ -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);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
New file
@@ -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);
    }
}