diff --git a/pom.xml b/pom.xml index 4dc1cdb..ca57eb3 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,24 @@ wechatpay-apache-httpclient 0.4.8 + + + com.squareup.okhttp3 + okhttp + 3.5.0 + + + com.github.wechatpay-apiv3 + wechatpay-java + 0.2.14 + + + + com.github.binarywang + weixin-java-pay + 4.1.0 + + commons-lang commons-lang diff --git a/src/main/java/com/bigdata/wxappserver/config/MyWxPayConfig.java b/src/main/java/com/bigdata/wxappserver/config/MyWxPayConfig.java new file mode 100644 index 0000000..e7ee012 --- /dev/null +++ b/src/main/java/com/bigdata/wxappserver/config/MyWxPayConfig.java @@ -0,0 +1,45 @@ +package com.bigdata.wxappserver.config; + +import com.bigdata.wxappserver.properties.WeChatProperties; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Created with IntelliJ IDEA. + * + * @Author: Cool + * @Date: 2024/09/03/22:19 + * @Description: + */ + +@Configuration +@ConditionalOnClass(WxPayService.class) +public class MyWxPayConfig { + + @Autowired + private WeChatProperties properties; + + @Bean + @ConditionalOnMissingBean + public WxPayService wxService() { + WxPayConfig payConfig = new WxPayConfig(); + payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId())); + payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId())); + payConfig.setMchKey(StringUtils.trimToNull(this.properties.getSecret())); + payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyFilePath())); + + // 可以指定是否使用沙箱环境 + payConfig.setUseSandboxEnv(false); + + WxPayService wxPayService = new WxPayServiceImpl(); + wxPayService.setConfig(payConfig); + return wxPayService; + } +} diff --git a/src/main/java/com/bigdata/wxappserver/controller/TestController.java b/src/main/java/com/bigdata/wxappserver/controller/TestController.java new file mode 100644 index 0000000..21372a9 --- /dev/null +++ b/src/main/java/com/bigdata/wxappserver/controller/TestController.java @@ -0,0 +1,52 @@ +package com.bigdata.wxappserver.controller; + +import com.bigdata.wxappserver.result.Result; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult; +import com.github.binarywang.wxpay.service.WxPayService; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Created with IntelliJ IDEA. + * + * @Author: Cool + * @Date: 2024/09/03/22:25 + * @Description: + */ +@RestController +@Slf4j +public class TestController { + @Autowired + private WxPayService wxService; + + + @PostMapping("/unifiedOrder") + public Result unifiedOrder(String outTradeNo) throws Exception + { + WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = new WxPayUnifiedOrderRequest(); + wxPayUnifiedOrderRequest.setBody("测试"); + wxPayUnifiedOrderRequest.setOutTradeNo(outTradeNo); + wxPayUnifiedOrderRequest.setTotalFee(50); + wxPayUnifiedOrderRequest.setSpbillCreateIp("127.0.0.1"); + wxPayUnifiedOrderRequest.setNotifyUrl("payCallBack"); + wxPayUnifiedOrderRequest.setTradeType("JSAPI"); + wxPayUnifiedOrderRequest.setOpenid("oU5Ta5f9Vx6f-***********"); + wxPayUnifiedOrderRequest.setSignType("MD5"); + + return Result.success(wxService.unifiedOrder(wxPayUnifiedOrderRequest)); + } + + @PostMapping("/queryOrder") + public Result queryOrder(String outTradeNo) throws Exception + { + WxPayOrderQueryResult wxPayOrderQueryResult = wxService.queryOrder(null, outTradeNo); + log.info(wxPayOrderQueryResult.toString()); + return Result.success(wxPayOrderQueryResult); + } + + +} diff --git a/src/main/java/com/bigdata/wxappserver/properties/WeChatProperties.java b/src/main/java/com/bigdata/wxappserver/properties/WeChatProperties.java index 482ee59..9256bf6 100644 --- a/src/main/java/com/bigdata/wxappserver/properties/WeChatProperties.java +++ b/src/main/java/com/bigdata/wxappserver/properties/WeChatProperties.java @@ -9,9 +9,9 @@ import org.springframework.stereotype.Component; @Data public class WeChatProperties { - private String appid; //小程序的appid + private String appId; //小程序的appid private String secret; //小程序的秘钥 - private String mchid; //商户号 + private String mchId; //商户号 private String mchSerialNo; //商户API证书的证书序列号 private String privateKeyFilePath; //商户私钥文件 private String apiV3Key; //证书解密的密钥 diff --git a/src/main/java/com/bigdata/wxappserver/utils/SignV3Util.java b/src/main/java/com/bigdata/wxappserver/utils/SignV3Util.java new file mode 100644 index 0000000..c3d921a --- /dev/null +++ b/src/main/java/com/bigdata/wxappserver/utils/SignV3Util.java @@ -0,0 +1,138 @@ +package com.bigdata.wxappserver.utils; + +import okhttp3.HttpUrl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import java.io.UnsupportedEncodingException; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.HashMap; +import java.util.Random; + +/** + * Created with IntelliJ IDEA. + * + * @Author: Cool + * @Date: 2024/09/03/20:34 + * @Description: + */ +@Component +public class SignV3Util{ + //V3主商户ID + private static String merchantId; + //微信商户平台APIv3证书序列号 + private static String certificateSerialNo; + //私钥(不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。) + private static String privateKey; + + //配置文件配置好主商户号 + @Value("${dx.wechat.mchid}") + public void setMerchantId(String merchantId) { + SignV3Util.merchantId = merchantId; + } + + //配置文件配置好序列号 + @Value("${dx.wechat.mchSerialNo}") + public void setCertificateSerialNo(String certificateSerialNo) { + SignV3Util.certificateSerialNo = certificateSerialNo; + } + + //配置文件配置好私钥 + @Value("${dx.wechat.apiV3Key}") + public void setPrivateKey(String privateKey) { + SignV3Util.privateKey = privateKey; + } + + /** + * 使用方法 + * + * @param method 请求方法 + * @param url 请求url + * @param body 请求内容 + * @return + */ + public static HashMap getSignMap(String method, String url, String body) throws InvalidKeySpecException, NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, SignatureException { + String authorization = getSign(method, url, body); + + HashMap headsMap = new HashMap<>(); + headsMap.put("Authorization", authorization); + headsMap.put("Content-Type", "application/json"); + headsMap.put("Accept", "application/json"); + + return headsMap; + } + + public static String getSign(String method, String url, String body) throws NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException, UnsupportedEncodingException { + return "WECHATPAY2-SHA256-RSA2048 " + getToken(method, HttpUrl.parse(url), body); + } + + public static String getToken(String method, HttpUrl url, String body) throws UnsupportedEncodingException, SignatureException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + String nonceStr = nonceString(); + long timestamp = System.currentTimeMillis() / 1000; + String message = buildMessage(method, url, timestamp, nonceStr, body); + String signature = sign(message.getBytes("utf-8")); + return "mchid=\"" + merchantId + "\"," + + "nonce_str=\"" + nonceStr + "\"," + + "timestamp=\"" + timestamp + "\"," + + "serial_no=\"" + certificateSerialNo + "\"," + + "signature=\"" + signature + "\""; + } + + public static String sign(byte[] message) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initSign(getPKCS8PrivateKey(privateKey)); + sign.update(message); + + return Base64.getEncoder().encodeToString(sign.sign()); + } + + public static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { + String canonicalUrl = url.encodedPath(); + if (url.encodedQuery() != null) { + canonicalUrl += "?" + url.encodedQuery(); + } + + return method + "\n" + + canonicalUrl + "\n" + + timestamp + "\n" + + nonceStr + "\n" + + body + "\n"; + } + + + private static PrivateKey getPKCS8PrivateKey(String strPk) throws NoSuchAlgorithmException, InvalidKeySpecException { + String realPK = strPk.replaceAll("-----END PRIVATE KEY-----", "") + .replaceAll("-----BEGIN PRIVATE KEY-----", "") + .replaceAll("\n", ""); + + byte[] b1 = Base64.getDecoder().decode(realPK); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(b1); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + + return kf.generatePrivate(spec); + } + + public static String nonceString() { + + String currTime = String.format("%d", (long) System.currentTimeMillis() / 1000); + + String strTime = currTime.substring(8, currTime.length()); + + Random random = new Random(); + int num = (int) (random.nextDouble() * (1000000 - 100000) + 100000); + String code = String.format("%06d", num); + + String nonce_str = currTime.substring(2) + code; + return nonce_str; + + } +} + + diff --git a/src/main/java/com/bigdata/wxappserver/utils/WeChatPayUtil.java b/src/main/java/com/bigdata/wxappserver/utils/WeChatPayUtil.java index 9e90ce5..c1d4991 100644 --- a/src/main/java/com/bigdata/wxappserver/utils/WeChatPayUtil.java +++ b/src/main/java/com/bigdata/wxappserver/utils/WeChatPayUtil.java @@ -24,10 +24,7 @@ import java.math.BigDecimal; import java.security.PrivateKey; import java.security.Signature; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; +import java.util.*; /** * 微信支付工具类 @@ -36,7 +33,7 @@ import java.util.List; public class WeChatPayUtil { //微信支付下单接口地址 - public static final String JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; + public static final String PAY_API = "https://api.mch.weixin.qq.com/pay/unifiedorder"; //申请退款接口地址 public static final String REFUNDS = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; @@ -60,7 +57,7 @@ public class WeChatPayUtil { List wechatPayCertificates = Arrays.asList(x509Certificate); WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() - .withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey) + .withMerchant(weChatProperties.getMchId(), weChatProperties.getMchSerialNo(), merchantPrivateKey) .withWechatPay(wechatPayCertificates); // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签 @@ -133,10 +130,11 @@ public class WeChatPayUtil { */ private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception { JSONObject jsonObject = new JSONObject(); - jsonObject.put("appid", weChatProperties.getAppid()); - jsonObject.put("mchid", weChatProperties.getMchid()); + jsonObject.put("appid", weChatProperties.getAppId()); + jsonObject.put("mchid", weChatProperties.getMchId()); jsonObject.put("description", description); jsonObject.put("out_trade_no", orderNum); + jsonObject.put("nonce_str", UUID.randomUUID().toString()); jsonObject.put("notify_url", weChatProperties.getNotifyUrl()); JSONObject amount = new JSONObject(); @@ -151,7 +149,7 @@ public class WeChatPayUtil { jsonObject.put("payer", payer); String body = jsonObject.toJSONString(); - return post(JSAPI, body); + return post(PAY_API, body); } /** @@ -175,7 +173,7 @@ public class WeChatPayUtil { String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = RandomStringUtils.randomNumeric(32); ArrayList list = new ArrayList<>(); - list.add(weChatProperties.getAppid()); + list.add(weChatProperties.getAppId()); list.add(timeStamp); list.add(nonceStr); list.add("prepay_id=" + prepayId); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 594b27d..ab07ec3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,13 +16,13 @@ mybatis-plus: map-underscore-to-camel-case: true dx: wechat: - appid: wx865aefa5a7115ae0 - secret: 3f9849429894435abc935eea88178dfd - mchid: 1684540409 + appId: wx865aefa5a7115ae0 + secret: df0817d59696a6160de2770222d8ec53 + mchId: 1684540409 mchSerialNo: 250414966CEADA52CF0A989445FB3190C5A82F40 - privateKeyFilePath: template/bddf2dc508484b6bb086fe748e813260.pem + privateKeyFilePath: classpath:template/bddf2dc508484b6bb086fe748e813260.pem apiV3Key: d5a58d44588b42cbbe01daa5cfa4e792 #未填写及以下 - weChatPayCertFilePath: template/wechatpay_429733475DFDCEBE2A6135485F984EE337297F4F.pem + weChatPayCertFilePath: classpath:template/wechatpay_429733475DFDCEBE2A6135485F984EE337297F4F.pem notifyUrl: https://www.weixin.qq.com/wxpay/pay.php refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php diff --git a/src/test/java/com/bigdata/wxappserver/WxappServerApplicationTests.java b/src/test/java/com/bigdata/wxappserver/WxappServerApplicationTests.java index 37c2f13..65ad281 100644 --- a/src/test/java/com/bigdata/wxappserver/WxappServerApplicationTests.java +++ b/src/test/java/com/bigdata/wxappserver/WxappServerApplicationTests.java @@ -1,7 +1,22 @@ package com.bigdata.wxappserver; +import com.alibaba.fastjson.JSON; +import com.bigdata.wxappserver.utils.SignV3Util; +import org.apache.http.HttpEntity; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import java.util.HashMap; +import java.util.Map; @SpringBootTest class WxappServerApplicationTests { @@ -10,4 +25,35 @@ class WxappServerApplicationTests { void contextLoads() { } + @Test + public void test() throws Exception { + CloseableHttpClient httpClient = HttpClients.createDefault(); + //处理请求参数 + String param = JSON.toJSONString("请求参数"); + //获取签名请求头 + HashMap heads = SignV3Util.getSignMap("POST", "https://api.mch.weixin.qq.com/pay/unifiedorder", param); + //请求微信接口 + HttpPost post = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder"); + for (Map.Entry entry : heads.entrySet()) { + post.addHeader(entry.getKey(), entry.getValue()); + } + // 设置请求参数 + StringEntity entity = new StringEntity(JSON.toJSONString("请求参数"), "UTF-8"); + post.setEntity(entity); + + // 发送POST请求 + CloseableHttpResponse response = httpClient.execute(post); + + try { + // 获取响应实体 + HttpEntity responseEntity = response.getEntity(); + + if (responseEntity != null) { + // 打印响应内容 + System.out.println("Response content: " + EntityUtils.toString(responseEntity, "UTF-8")); + } + } finally { + response.close(); + } + } }