From 05c2a5aa15cf64068b421a29042a5dd3083ef696 Mon Sep 17 00:00:00 2001 From: Derran Date: Thu, 25 Apr 2024 17:15:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...PaymentChannelOrderApplicationService.java | 4 +- .../processor/IChannelPayService.java | 3 +- .../processor/wechat/WeChatPayProcessor.java | 80 +++++- .../processor/wechat/utils/WXPayV3Util.java | 229 ++++++++++++++++++ 4 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/utils/WXPayV3Util.java diff --git a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/PaymentChannelOrderApplicationService.java b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/PaymentChannelOrderApplicationService.java index db6c9ab..7478479 100644 --- a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/PaymentChannelOrderApplicationService.java +++ b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/PaymentChannelOrderApplicationService.java @@ -7,9 +7,7 @@ import com.qniao.dam.domain.aggregate.paymentchannelorder.entity.PaymentChannelO import com.qniao.dam.domain.aggregate.paymentorder.entity.PaymentOrder; import com.qniao.dam.domain.aggregate.paymentorder.repository.PaymentOrderRepository; import com.qniao.dam.query.useropenid.UserOpenIdQueryService; -import com.qniao.dau.domain.aggregate.useropenid.entity.UserOpenId; import org.springframework.stereotype.Service; -import org.springframework.util.Assert; import javax.annotation.Resource; import java.util.List; @@ -34,6 +32,6 @@ public class PaymentChannelOrderApplicationService { paymentChannelOrder.setComment("订单交易"); paymentChannelOrderAggregate.create(paymentChannelOrder); IChannelPayService channelPayService = IChannelPayService.getService(paymentChannelOrder.getPaymentMethod()); - return channelPayService.handle(paymentChannelOrder, fieldList, appFrom); + return channelPayService.prepay(paymentChannelOrder, fieldList, appFrom); } } diff --git a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/IChannelPayService.java b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/IChannelPayService.java index 0e740d5..6f71682 100644 --- a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/IChannelPayService.java +++ b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/IChannelPayService.java @@ -4,7 +4,6 @@ import com.qniao.dam.api.command.paymentchannelorder.user.request.ThirdPartyFiel import com.qniao.dam.application.service.paymentchannelorder.processor.wechat.WeChatPayProcessor; import com.qniao.dam.domain.aggregate.paymentchannelorder.entity.PaymentChannelOrder; import com.qniao.dam.domian.aggregate.paymentorder.constant.PaymentMethodEnum; -import com.qniao.dau.domain.aggregate.useropenid.entity.UserOpenId; import com.qniao.framework.exception.BizException; import com.qniao.framework.utils.SpringContextUtil; @@ -23,5 +22,5 @@ public abstract class IChannelPayService { } } - public abstract Map handle(PaymentChannelOrder paymentChannelOrder, List fieldList, Long appFrom); + public abstract Map prepay(PaymentChannelOrder paymentChannelOrder, List fieldList, Long appFrom); } diff --git a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/WeChatPayProcessor.java b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/WeChatPayProcessor.java index efe99c3..8c1b1f2 100644 --- a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/WeChatPayProcessor.java +++ b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/WeChatPayProcessor.java @@ -1,5 +1,9 @@ package com.qniao.dam.application.service.paymentchannelorder.processor.wechat; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult; import com.github.binarywang.wxpay.config.WxPayConfig; @@ -10,17 +14,17 @@ import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; import com.github.binarywang.wxpay.util.SignUtils; import com.qniao.dam.api.command.paymentchannelorder.user.request.ThirdPartyField; import com.qniao.dam.application.service.paymentchannelorder.processor.IChannelPayService; +import com.qniao.dam.application.service.paymentchannelorder.processor.wechat.utils.WXPayV3Util; import com.qniao.dam.application.service.paymentchannelorder.processor.wechat.vobj.ChannelPaymentField; import com.qniao.dam.application.service.paymentchannelorder.processor.wechat.vobj.WeChatPaymentBasic; import com.qniao.dam.domain.aggregate.paymentchannelorder.entity.PaymentChannelOrder; import com.qniao.dau.application.UecServerApplicationService; import com.qniao.dau.application.request.GetWechatSessionDto; import com.qniao.dau.application.response.WeChatAppletUserSessionVo; -import com.qniao.dau.domain.aggregate.useropenid.entity.UserOpenId; import com.qniao.framework.exception.BizException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; @@ -31,32 +35,79 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -@Component +@Service @Slf4j public class WeChatPayProcessor extends IChannelPayService { - @Value("${weixin.mchid}") - private String mchId; @Value("${weixin.mchkey}") private String mchKey; + @Value("${weixin.mchid}") + private String mchId; @Value("${weixin.notifyurl}") private String notifyUrl; @Resource private UecServerApplicationService uecServerApplicationService; + @Resource + private WXPayV3Util wxPayV3Util; @Override - public Map handle(PaymentChannelOrder paymentChannelOrder, + public Map prepay(PaymentChannelOrder paymentChannelOrder, List fieldList, Long appFrom) { // 获取账号 String jsCode = ThirdPartyField.findFiledValueByKey(ThirdPartyField.JS_CODE, fieldList); GetWechatSessionDto sessionDto = new GetWechatSessionDto(jsCode, appFrom); WeChatAppletUserSessionVo sessionVo = uecServerApplicationService.getWechatSession(sessionDto); - WxPayConfig wxPayConfig = getWxPayConfig(sessionVo.getPlatformAppId()); - WxPayUnifiedOrderRequest request = buildUnifiedOrderRequest(paymentChannelOrder, wxPayConfig, sessionVo.getOpenid()); - WeChatPaymentBasic weChatPaymentBasic = getWeChatPaymentBasic(request, wxPayConfig); - return transformToMap(weChatPaymentBasic); + + try { + Map reqParams = new HashMap<>(); + reqParams.put("mchid", mchId); + reqParams.put("out_trade_no", paymentChannelOrder.getId().toString()); + reqParams.put("appid", sessionVo.getPlatformAppId()); + reqParams.put("description", paymentChannelOrder.getComment()); + reqParams.put("notify_url", notifyUrl); + reqParams.put("amount", new HashMap() { + { + put("total", paymentChannelOrder.getAmount().multiply(BigDecimal.valueOf(100)).intValue()); + put("currency", "CNY"); + } + }); + reqParams.put("payer", new HashMap() { + { + put("openid", sessionVo.getOpenid()); + } + }); + JSONObject result = wxPayV3Util.doPostWeiXinV3(WXPayV3Util.api_v3_placeAnOrder_url, JSONUtil.toJsonStr(reqParams)); + if (ObjectUtil.isNull(result.get("prepay_id"))) { + throw new BizException("微信支付下单失败,请检查配置"); + } + + String prepayId = result.getString("prepay_id"); + // 获取app拉起支付签名 + String nonceStr = String.valueOf(System.currentTimeMillis() / 1000); + long timestamp = System.currentTimeMillis() / 1000; + String sign; + try { + sign = wxPayV3Util.sign(sessionVo.getPlatformAppId(), timestamp, nonceStr, prepayId); + } catch (Exception e) { + log.error(" sign fail AppV3PayStrategyImpl {}", e.getMessage(), e); + throw new BizException("生成签名错误:" + e.getMessage()); + } + log.info("sign____________________" + sign); + Map returnMap = new HashMap<>(); + returnMap.put("appId", sessionVo.getPlatformAppId()); + returnMap.put("nonceStr", nonceStr); + returnMap.put("timeStamp", String.valueOf(timestamp)); + returnMap.put("packages", "prepay_id=" + prepayId); + returnMap.put("signType", "RSA"); + returnMap.put("paySign", sign); + log.info("returnMap________" + returnMap); + return returnMap; + } catch (Exception e) { + log.error("pay fail AppV3PayStrategyImpl {}", e.getMessage(), e); + throw new BizException("发起微信预支付失败"); + } } public WxPayConfig getWxPayConfig(String appId) { @@ -77,7 +128,8 @@ public class WeChatPayProcessor extends IChannelPayService { * @param wxPayConfig * @return */ - public WxPayUnifiedOrderRequest buildUnifiedOrderRequest(PaymentChannelOrder paymentChannelOrder, WxPayConfig wxPayConfig, String openId) { + public WxPayUnifiedOrderRequest buildUnifiedOrderRequest(PaymentChannelOrder paymentChannelOrder, WxPayConfig + wxPayConfig, String openId) { // 微信统一下单请求对象 WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); request.setBody(paymentChannelOrder.getComment()); @@ -94,7 +146,8 @@ public class WeChatPayProcessor extends IChannelPayService { return request; } - private WeChatPaymentBasic getWeChatPaymentBasic(WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest, WxPayConfig wxPayConfig) { + private WeChatPaymentBasic getWeChatPaymentBasic(WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest, WxPayConfig + wxPayConfig) { WxPayService wxPayService = new WxPayServiceImpl(); wxPayService.setConfig(wxPayConfig); WxPayUnifiedOrderResult wxPayUnifiedOrderResult = null; @@ -149,7 +202,8 @@ public class WeChatPayProcessor extends IChannelPayService { return SignUtils.createSign(payInfo, wxPayConfig.getSignType(), wxPayConfig.getMchKey(), null); } - private String appSign(String prepayid, String partnerid, String timestamp, String noncestr, String appid, String packageValue, WxPayConfig wxPayConfig) { + private String appSign(String prepayid, String partnerid, String timestamp, String noncestr, String + appid, String packageValue, WxPayConfig wxPayConfig) { Map configMap = new HashMap<>(); configMap.put("prepayid", prepayid); configMap.put("partnerid", partnerid); diff --git a/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/utils/WXPayV3Util.java b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/utils/WXPayV3Util.java new file mode 100644 index 0000000..07a5a26 --- /dev/null +++ b/dating-agency-mall-server/src/main/java/com/qniao/dam/application/service/paymentchannelorder/processor/wechat/utils/WXPayV3Util.java @@ -0,0 +1,229 @@ +package com.qniao.dam.application.service.paymentchannelorder.processor.wechat.utils; + +import com.alibaba.fastjson.JSONObject; +import com.qniao.framework.exception.BizException; +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.cert.CertificatesManager; +import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +import java.io.*; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +@Slf4j +public class WXPayV3Util { + + public static final String api_v3_placeAnOrder_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; + + public static final String api_v3_refund_url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; + + private static CertificatesManager certificatesManager; + + private String APPLICATION_JSON = "application/json"; + // 你的商户私钥 + private String privateKey; + // 你的微信支付平台证书 + private String certificate; + + private CloseableHttpClient httpClient; + + private Verifier verifier; + + @Value("${weixin.mchid}") + private String mchId; + + @Value("${weixin.apiV3Key}") + private String v3Key; + + @Value("${weixin.mchSerialNo}") + private String mchSerialNo; + + @Value("${weixin.privateKeyPath}") + private String privateKeyPath; + + @Value("${weixin.platformCertPath}") + private String platformCertPath; + + public void setup() throws Exception { + // 获取商户 + try { + InputStream apiClientKey = Files.newInputStream(new File(privateKeyPath).toPath()); + File appKey = new File("/tmp/apiclient_key_temp.crt"); + FileUtils.copyInputStreamToFile(apiClientKey, appKey); + + InputStream wechatPayKey = Files.newInputStream(new File(platformCertPath).toPath()); + File appCert = new File("/tmp/wechatpay_cert_temp.crt"); + FileUtils.copyInputStreamToFile(wechatPayKey, appCert); + + this.privateKey = WXPayV3Util.getPrivateKeyStr(appKey.getAbsolutePath()); + this.certificate = WXPayV3Util.getPrivateKeyStr(appCert.getAbsolutePath()); + + PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); + + certificatesManager = CertificatesManager.getInstance(); + // 向证书管理器增加需要自动更新平台证书的商户信息 + certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId, + new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), + v3Key.getBytes(StandardCharsets.UTF_8)); + // 从证书管理器中获取verifier + verifier = certificatesManager.getVerifier(mchId); + + X509Certificate wechatPayCertificate = PemUtil.loadCertificate( + new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + + ArrayList listCertificates = new ArrayList<>(); + listCertificates.add(wechatPayCertificate); + + WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() + .withMerchant(mchId, mchSerialNo, merchantPrivateKey) + .withWechatPay(listCertificates); + httpClient = builder.build(); + } catch (IOException e) { + log.error("setup IOException " + e.getMessage(), e); + throw new BizException("IO异常:" + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + log.error("setup NoSuchAlgorithmException " + e.getMessage(), e); + throw new BizException("未找到配置文件:" + e.getMessage()); + } catch (InvalidKeySpecException e) { + log.error("InvalidKeySpecException " + e.getMessage(), e); + throw new BizException("key生成异常:" + e.getMessage()); + } + } + + + /** + * api_v3下单 + * + * @param body 请求体JSON字符串 + * @return + * @throws IOException + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public JSONObject doPostWeiXinV3(String url, String body) throws Exception { + if (httpClient == null) { + setup(); + } + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application/json;chartset=utf-8"); + httpPost.addHeader("Accept", "application/json"); + try { + if (body == null) { + throw new IllegalArgumentException("data参数不能为空"); + } + StringEntity stringEntity = new StringEntity(body, "utf-8"); + httpPost.setEntity(stringEntity); + // 直接执行execute方法,官方会自动处理签名和验签,并进行证书自动更新 + HttpResponse httpResponse = httpClient.execute(httpPost); + HttpEntity httpEntity = httpResponse.getEntity(); + + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String jsonResult = EntityUtils.toString(httpEntity); + return JSONObject.parseObject(jsonResult); + } else { + log.error("doPostWeiXinV3 status != 200, " + EntityUtils.toString(httpEntity)); + } + } catch (Exception e) { + log.error("doPostWeiXinV3 fail, " + e.getMessage(), e); + throw new BizException("微信支付异常:" + e.getMessage()); + } + return null; + } + + /** + * 获取私钥字符串。 + * + * @param filename 私钥文件路径 (required) + * @return 私钥对象 + */ + public static String getPrivateKeyStr(String filename) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + return new String(Files.readAllBytes(Paths.get(filename)), "UTF-8"); + } + + + /** + * V3 SHA256withRSA 签名. + * + * @param appId + * @param timestamp + * @param nonceStr + * @param prepayId + * @return + * @throws SignatureException + * @throws NoSuchAlgorithmException + * @throws IOException + * @throws InvalidKeySpecException + * @throws InvalidKeyException + * @throws URISyntaxException + */ + public String sign(String appId, long timestamp, String nonceStr, String prepayId) throws SignatureException, NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException, URISyntaxException { + String signatureStr = Stream.of(appId, String.valueOf(timestamp), nonceStr, prepayId) + .collect(Collectors.joining("\n", "", "\n")); + Signature sign = Signature.getInstance("SHA256withRSA"); + File file = copyTempFileByResourcePath(privateKeyPath); + sign.initSign(WXPayV3Util.getPrivateKey(file.getAbsolutePath())); + sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); + return Base64Utils.encodeToString(sign.sign()); + } + + /** + * 使用流获取resoucre下的文件(解决线上无法获取的问题) + * + * @param resourcePath 相对于resource目录的路径 + * @return + * @throws IOException + */ + public File copyTempFileByResourcePath(String resourcePath) throws IOException { + InputStream inputStream = Files.newInputStream(new File(resourcePath).toPath()); + File file = new File("/tmp/apiclient_key_temp.crt"); + FileUtils.copyInputStreamToFile(inputStream, file); + return file; + } + + /** + * 获取私钥。 + * + * @param filename 私钥文件路径 (required) + * @return 私钥对象 + */ + public static PrivateKey getPrivateKey(String filename) throws IOException { + String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8"); + try { + String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePrivate( + new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持RSA", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("无效的密钥格式"); + } + } +}