ruoyi-admin/src/main/resources/application-test.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-common/src/main/java/com/ruoyi/common/config/WxConfig.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-common/src/main/java/com/ruoyi/common/utils/WxAppletTools.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/pom.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/src/main/java/com/ruoyi/system/task/utils/TaskUtil.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/JsapiPrepay.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WXPayUtility.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java | ●●●●● 补丁 | 查看 | 原始文档 | 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("获取支付数据错误!"); // } // } /**