liujie
昨天 718ded0f5f8dd6f1da43b9de2ff20ddc12714007
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/haizhentong

# Conflicts:
# ruoyi-system/src/main/java/com/ruoyi/system/task/utils/TaskUtil.java
3个文件已删除
5个文件已修改
2个文件已添加
1036 ■■■■■ 已修改文件
ruoyi-admin/src/main/resources/application-test.yml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java 134 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/pom.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/task/utils/TaskUtil.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java 31 ●●●●● 补丁 | 查看 | 原始文档 | 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/WxV3Pay.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-test.yml
@@ -214,4 +214,27 @@
  priKeyStr: C:\Users\Admin\Desktop\test\OP00000003_private_key.pem
  lklNotifyCerStr: C:\Users\Admin\Desktop\test\lkl-apigw-v2.cer
  sm4Key: LHo55AjrT4aDhAIBZhb5KQ==
  serverUrl: https://test.wsmsd.cn/
  serverUrl: https://test.wsmsd.cn/
payment:
  wx:
    # 微信appid
    appId: wxa17e8d1331e50934
    # 微信商户号
    mchId: 1721757915
    # 秘钥
    secretId: 79c234527fd3b6553679d52be5e29b19
    v3:
      # 加签串
      apiKey: V7mKp9qL2Rs4jU6tX8wZ0bC3eF5hN1yD4gA
      # 加签证书地址
      privateKeyPath: D:/app/cert/weixin/apiclient_key.pem
      # 微信支付公钥id
      publicKeyId: PUB_KEY_ID_0117217579152025091800181718000000
      # 微信支付公钥证书
      publicKeyPath: D:/app/cert/weixin/apiclient_cert.pem
      # 证书序列号
      mchSerialNo: 39C7F6152E38A62B5786634D5C1F984FB5A38AD5
      # 支付成功回调地址
      notifyPayUrl: http://221.182.45.100/wx/pay/notify
      # 支付退款回调地址
      notifyRefundUrl: http://221.182.45.100/wx/refund/notify
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
@@ -1,8 +1,7 @@
package com.ruoyi.web.controller.api;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.CodeGenerateUtils;
import com.ruoyi.framework.web.service.TokenService;
@@ -14,12 +13,11 @@
import com.ruoyi.system.service.TSysInspectionService;
import com.ruoyi.system.service.TSysOtherConfigService;
import com.ruoyi.system.service.TSysPayRecordService;
import com.ruoyi.system.wxPay.enums.RefundEnum;
import com.ruoyi.system.wxPay.enums.TradeStateEnum;
import com.ruoyi.system.wxPay.model.WeixinPayProperties;
import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
import com.ruoyi.system.wxPay.utils.WxTimeUtils;
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;
@@ -27,8 +25,11 @@
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
@@ -73,7 +74,7 @@
        String userId = tokenService.getLoginUserApplet().getUserId();
        TSysAppUser sysAppUser = sysAppUserService.getById(userId);
        // 价格
        Integer totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).intValue();
        Long totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).longValue();
        // 生成订单号
        String orderNo = CodeGenerateUtils.generateOrderSn();
        // 查询用户信息 用户openid
@@ -81,62 +82,41 @@
        // 存储支付记录
        sysPayRecordService.saveData(orderNo, userId, sysOtherConfig.getAiPrice(), 1);
        // 调用支付方法
        Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"AI检测报告支付");
        Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId,"AI检测报告支付","");
        log.info("支付参数:{}", result);
        return R.ok(result);
    }
    /**
     * 微信v3支付-订单退款
     *
     * @return
     */
    @ApiOperation("订单退款")
    @PostMapping(value = "refund-order")
    public AjaxResult<String> refundOrder() {
         Map<String, Object> result = wxV3Pay.refund(new WxPaymentRefundModel());
         log.info("退款结果:{}", result);
        // 微信支付退款单号
        String refund_id = result.get("refund_id").toString();
        // 商户退款单号
        String out_refund_no = result.get("out_refund_no").toString();
        // 微信支付订单号
        String transaction_id = result.get("transaction_id").toString();
        // 商户订单号 tradeNo
        String out_trade_no = result.get("out_trade_no").toString();
        // 退款成功时间
        String success_time = Objects.nonNull(result.get("success_time")) ? result.get("success_time").toString() : null;
        // 退款状态 RefundEnum
        String status = result.get("status").toString();
        // TODO 退款业务处理
        return AjaxResult.success();
    }
    /**
     * 支付回调
     */
    @PostMapping("pay/notify")
    @ApiOperation("订单回调")
    public void payNotify(HttpServletRequest request) throws Exception {
    public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {});
            log.info("支付回调:{}", params);
            // 商户订单号
            String tradeNo = params.get("out_trade_no").toString();
            // 交易状态
            String trade_state = params.get("trade_state").toString();
            // 交易状态描述
            String trade_state_desc = params.get("trade_state_desc").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 支付完成时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            // 附加数据
            Integer attach = Integer.parseInt(params.get("attach").toString());
            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, tradeNo).last("LIMIT 1"));
            // 处理订单
                    .eq(TSysPayRecord::getOrderNo, out_trade_no).last("LIMIT 1"));
            if (trade_state.equals(TradeStateEnum.SUCCESS.name())) {
                log.info("回调成功");
                // 订单号查询订单
@@ -150,60 +130,14 @@
                sysInspection.setIsPay(1);
                sysInspectionService.updateById(sysInspection);
                wxV3Pay.ack();
                PrintWriter out = response.getWriter();
                out.print("SUCCESS");
                out.flush();
                out.close();
            }
            wxV3Pay.ack();
        } catch (Exception e) {
            log.error("支付回调异常:{}", e, e);
            wxV3Pay.ack(false, e.getMessage());
        }
    }
    /**
     * 支付回调成功后
     */
    @PostMapping("pay/ack")
    public void ack(){
        try {
            wxV3Pay.ack();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 退款回调
     */
    @PostMapping("refund/notify")
    public void refundNotify(HttpServletRequest request) throws IOException {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
            });
            // 商户订单号
            String out_trade_no = params.get("out_trade_no").toString();
            // 商户退款单号
            String out_refund_no = params.get("out_refund_no").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 微信支付退款单号
            String refund_id = params.get("refund_id").toString();
            // 退款状态
            String tradeState = params.get("refund_status").toString();
            // 退款成功时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            if (tradeState.equals(RefundEnum.SUCCESS.name())) {
                wxV3Pay.ack();
            } else {
                wxV3Pay.ack(false, "不是成功的退款状态");
            }
        } catch (Exception e) {
            e.printStackTrace();
            wxV3Pay.ack(false, e.getMessage());
        }
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java
File was deleted
ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java
File was deleted
ruoyi-system/pom.xml
@@ -96,8 +96,14 @@
        </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.3</version>
            <version>0.4.9</version>
        </dependency>
    </dependencies>
ruoyi-system/src/main/java/com/ruoyi/system/task/utils/TaskUtil.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
@@ -28,6 +28,14 @@
     */
    private String privateKeyPath;
    /**
     * 微信支付公钥id
     */
    private String publicKeyId;
    /**
     * 微信支付公钥证书 apiclient_cert.pem
     */
    private String publicKeyPath;
    /**
     * 商户证书序列号
     */
    private String  mchSerialNo;
@@ -74,4 +82,27 @@
        }
        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/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/WxV3Pay.java
@@ -1,29 +1,21 @@
package com.ruoyi.system.wxPay.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.WxPaymentInfoModel;
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.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Map;
@@ -62,13 +54,6 @@
            checkWxConfig();
            this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream());
            this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey);
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(config.getMchId(), new WechatPay2Credentials(config.getMchId(),
                    this.privateKeySigner), config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            this.verifier = certificatesManager.getVerifier(config.getMchId());
            this.validator = new WechatPay2Validator(verifier);
            this.builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey)
@@ -122,37 +107,76 @@
     * @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())
                // 分不分账
//                .settle_info(WxPaymentInfoModel.SettleInfo.builder().profit_sharing(true).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();
    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 {
            JsonNode rootNode = om.readTree(repBody);
            String prepayId = rootNode.path("prepay_id").asText();
            return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId);
        } catch (JsonProcessingException e) {
            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("获取支付数据错误!");
//        }
//    }
    /**