侧边栏壁纸
  • 累计撰写 27 篇文章
  • 累计创建 9 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

微信支付JSAPI

仓鼠
2024-03-04 / 0 评论 / 0 点赞 / 38 阅读 / 18025 字 / 正在检测是否收录...

踩坑开始

java方面

maven导入依赖

        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.7</version>
        </dependency>

工具类


import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.internal.util.StringUtils;
import com.ruoyi.playment.constant.WxChartHeader;
import com.ruoyi.playment.enums.PaymentTradeState;
import com.ruoyi.playment.req.*;
import com.ruoyi.playment.res.*;
import com.ruoyi.playment.service.PaymentService;
import com.ruoyi.playment.util.ValidatorUtil;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.partnerpayments.app.model.Transaction;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;

/**
 * 微信支付工具类
 *
 * @author: yzy
 * @create: 2023/3/28
 * @Version 1.0
 **/
@Component
@Slf4j
public class WeChatJsApiPayCommon implements PaymentService {
    /**
     * 应用ID
     */
    @Value("${pay.wechatjsapi.appId:}")
    private String appId;
    /**
     * 商户号
     **/
    @Value("${pay.wechatjsapi.mchid:}")
    private String mchid;
    /**
     * 接口回调地址
     */
    @Value("${yuanbeibei.pay.wechatjsapi.notifyUrl:}")
    private String notifyUrl;
    /**
     * 接口退款回调地址
     */
    @Value("${pay.wechatjsapi.refundNotifyUrl:}")
    private String refundNotifyUrl;
    /**
     * 商户API私钥
     */
    @Value("${pay.wechatjsapi.privateKey:}")
    private String privateKey;
    /**
     * 商户证书序列号
     */
    @Value("${pay.wechatjsapi.merchantSerialNumber:}")
    private String merchantSerialNumber;
    /**
     * 商户APIV3密钥
     */
    @Value("${pay.wechatjsapi.apiV3key:}")
    private String apiV3key;
    /**
     * 获取构建的service
     * todo: 待优化 需要考虑高并发下是否存在争夺资源问题
     **/
    private static Config config;

    public static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX";
    @Override
    public PayRes pay(PayReq req) throws Exception {
        //校验数据
        String validates = ValidatorUtil.validates(req);
        if (!StringUtils.isEmpty(validates)) {
            throw new Exception(validates);
        }
        if (StringUtils.isEmpty(req.getOpenid())) {
            throw new Exception("用户标识不能为空");
        }
        //获取请求service
        JsapiService jsapiService = new JsapiService.Builder().config(getConfig()).build();
        //组装数据
        PrepayRequest prepayRequest = new PrepayRequest();
        prepayRequest.setAppid(appId);
        prepayRequest.setMchid(mchid);
        prepayRequest.setNotifyUrl(notifyUrl);
        prepayRequest.setDescription(req.getTitle());
        prepayRequest.setOutTradeNo(req.getOutTradeNo());
        //订单金额
        Amount amount = new Amount();
        amount.setTotal(req.getMoney().intValue());
        amount.setCurrency("CNY");
        prepayRequest.setAmount(amount);
        //支付者
        Payer payer = new Payer();
        payer.setOpenid(req.getOpenid());
        prepayRequest.setPayer(payer);
        //发送请求
        PrepayResponse prepay = jsapiService.prepay(prepayRequest);
        return PayRes.builder().message(prepay.getPrepayId()).build();
    }

    @Override
    public PayCancelRes cancelPay(PayCancelReq req) throws Exception {
        //校验数据
        String validates = ValidatorUtil.validates(req);
        if (!StringUtils.isEmpty(validates)) {
            throw new Exception(validates);
        }
        //获取请求service
        JsapiService jsapiService = new JsapiService.Builder().config(getConfig()).build();
        CloseOrderRequest closeOrderRequest = new CloseOrderRequest();
        closeOrderRequest.setMchid(mchid);
        closeOrderRequest.setOutTradeNo(req.getPayTradeNo());
        //发送请求
        jsapiService.closeOrder(closeOrderRequest);
        return new PayCancelRes();
    }

    @Override
    public PayRefundRes refund(PayRefundReq req) throws Exception {
        //校验数据
        String validates = ValidatorUtil.validates(req);
        if (!StringUtils.isEmpty(validates)) {
            throw new Exception(validates);
        }
        //获取请求service
        RefundService refundService = new RefundService.Builder().config(getConfig()).build();
        //组装数据
        CreateRequest createRequest = new CreateRequest();
        createRequest.setOutTradeNo(req.getPayTradeNo());
        createRequest.setOutRefundNo(req.getRefundRefundNo());
        createRequest.setNotifyUrl(refundNotifyUrl);
        //退款金额
        AmountReq amount = new AmountReq();
        //单位为分
        amount.setRefund(req.getMoney().longValue());
        amount.setTotal(req.getTotal().longValue());
        amount.setCurrency("CNY");
        createRequest.setAmount(amount);
        Refund refund = refundService.create(createRequest);
        return PayRefundRes.builder()
                .original(refund.toString())
                .build();
    }

    @Override
    public PayCallbackRes payCallback(PayCallbackReq params) throws Exception {
        JSONObject headers = params.getHeaders();
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(headers.getString(WxChartHeader.SERIAL))
                .nonce(headers.getString(WxChartHeader.NONCE))
                .signature(headers.getString(WxChartHeader.SIGNATURE))
                .timestamp(headers.getString(WxChartHeader.TIMESTAMP))
                .body(params.getOriginal())
                .build();
        NotificationParser parser = new NotificationParser((RSAAutoCertificateConfig) getConfig());
        Transaction transaction = parser.parse(requestParam, Transaction.class);
        return PayCallbackRes.builder()
                .tradeNo(transaction.getOutTradeNo())
                //三方生成的回执id
                .outTradeNo(transaction.getTransactionId())
                .tradeState(transaction.getTradeState().name())
                .successTime(DateUtil.parse(transaction.getSuccessTime(),PATTERN))
                .tradeType(transaction.getTradeType().name())
                .money(new BigDecimal(transaction.getAmount().getTotal()))
                .userMoney(new BigDecimal(transaction.getAmount().getPayerTotal()))
                .openid(transaction.getPayer().getSpOpenid())
                .original(JSON.toJSONString(transaction))
                .build();
    }

    @Override
    public PayRefundCallbackRes payRefundCallback(PayCallbackReq params) throws Exception {
        JSONObject headers = params.getHeaders();
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(headers.getString(WxChartHeader.SERIAL))
                .nonce(headers.getString(WxChartHeader.NONCE))
                .signature(headers.getString(WxChartHeader.SIGNATURE))
                .timestamp(headers.getString(WxChartHeader.TIMESTAMP))
                .body(params.getOriginal())
                .build();
        NotificationParser parser = new NotificationParser((RSAAutoCertificateConfig) getConfig());
        RefundNotification refund = parser.parse(requestParam, RefundNotification.class);

        return PayRefundCallbackRes.builder()
                .tradeNo(refund.getOutTradeNo())
                .refundRefundNo(refund.getOutRefundNo())
                .outRefundTradeNo(refund.getRefundId())
                .refundMoney(new BigDecimal(refund.getAmount().getPayerRefund()))
                .money(new BigDecimal(refund.getAmount().getRefund()))
                .tradeState(refundTradeState(refund.getRefundStatus()))
                .successTime(DateUtil.parse(refund.getSuccessTime(),PATTERN))
                .userInfo(refund.getUserReceivedAccount())
                 //三方生成的回执id
                .original(JSON.toJSONString(refund))
                .build();
    }

    /**
     * 枚举转码
     * 退款状态,枚举值:
     * SUCCESS:退款成功
     * CLOSED:退款关闭
     * ABNORMAL:退款异常,退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,可前往【商户平台—>交易中心】,手动处理此笔退款
     * @param refundStatus
     * @return
     */
    private String refundTradeState(Status refundStatus) {
        switch (refundStatus){
            case SUCCESS:
                return PaymentTradeState.SUCCESS.getIntValue();
            case CLOSED:
                return PaymentTradeState.CLOSED.getIntValue();
            default:
                return PaymentTradeState.ABNORMAL.getIntValue();
        }
    }


    /**
     * 微信JSAPI退款
     *
     * @param req 请求参数
     * @return
     */
    public Refund refund(WeChatJsApiRefundReq req) throws Exception {
        //校验数据
        String validates = ValidatorUtil.validates(req);
        if (!StringUtils.isEmpty(req.getOutTradeNo())) {
            throw new Exception(validates);
        }
        if (StringUtils.isEmpty(req.getTransactionId()) && StringUtils.isEmpty(req.getOutTradeNo())) {
            throw new Exception("支付订单号和商户订单号不能都为空");
        }
        //获取请求service
        RefundService refundService = new RefundService.Builder().config(getConfig()).build();
        //组装数据
        CreateRequest createRequest = new CreateRequest();
        BeanUtils.copyProperties(req, createRequest);
        //退款金额
        AmountReq amount = new AmountReq();
        amount.setRefund(req.getRefund());
        amount.setTotal(req.getTotal());
        amount.setCurrency(StringUtils.isEmpty(req.getCurrency()) ? "CNY" : req.getCurrency());
        createRequest.setAmount(amount);
        Refund refund = refundService.create(createRequest);
        return refund;
    }

    /**
     * 使用自动更新平台证书的RSA配置
     * 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
     *
     * @return
     */
    private Config getConfig() throws Exception {
        if (config == null) {
            synchronized (Config.class) {
                if (StringUtils.isEmpty(mchid)) {
                    throw new Exception("商户号不能为空");
                }
                if (StringUtils.isEmpty(privateKey)) {
                    throw new Exception("商户API私钥不能为空");
                }
                if (StringUtils.isEmpty(merchantSerialNumber)) {
                    throw new Exception("商户证书序列号不能为空");
                }
                if (StringUtils.isEmpty(apiV3key)) {
                    throw new Exception("商户APIV3密钥不能为空");
                }
                config = new RSAAutoCertificateConfig.Builder()
                        .merchantId(mchid)
                        .privateKey(privateKey)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3key)
                        .build();
            }
        }
        return config;
    }

    /**
     * 获取支付必要参数
     * @param prepayId
     * @return
     */
    public PayWeChatToolParameter getParameter(String prepayId) {
        StringBuffer paySignMsg = new StringBuffer();
        paySignMsg.append(appId);
        paySignMsg.append("\n");
        long timeStamp = DateUtil.currentSeconds();
        paySignMsg.append(timeStamp);
        paySignMsg.append("\n");
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        if(nonceStr.length() >= 32){
            nonceStr = nonceStr.substring(0,31);
        }
        paySignMsg.append(nonceStr);
        paySignMsg.append("\n");
        String packageMsg = "prepay_id="+prepayId;
        paySignMsg.append(packageMsg);
        String paySign = null;
        try {
            PrivateKey privateKey = KeyFactory.getInstance("RSA")
                    .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(this.privateKey)));
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(privateKey);
            signature.update(paySignMsg.toString().getBytes("UTF-8"));
            byte[] signedData = signature.sign();
            paySign = Base64.getEncoder().encodeToString(signedData);
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return PayWeChatToolParameter.builder()
                .appId(appId)
                .timeStamp(String.valueOf(timeStamp))
                .nonceStr(nonceStr)
                .packageMsg(packageMsg)
                .signType("RSA")
                .paySign(paySign)
                .build();

    }
}

前端

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>支付测试页面</title>
		<meta name="viewport" content="width=device-width, initial-scale=0.8, minimum-scale=0.8, maximum-scale=0.8, user-scalable=yes">
		
	</head>
	<body style="text-align: center;">
		<h1>支付测试</h1>
		<div id="canshu" style="width: 100%; word-break:break-all;">第一次请求内容</div>
		<h2 id="canshu1" style="margin-top: 30px;">状态</h2>
		<div id="canshu2" style="width: 100%; word-break:break-all;">发送的内容</div>
		<h2 id="canshu1" style="margin-top: 30px;">反馈消息</h2>
		<div id="canshu3" style="width: 100%; word-break:break-all;margin-top: 30px;">调用支付返回内容</div>
	</body>
	<script src="js/jquery.js"></script>
	<script type="text/javascript">
		var token = "c7d280a17c5b4178981393f65a81e407";
		
		/*  修改内容  */
		var prepay_id = "wx21201855730335ac86f8c43d1889123400";
		function onBridgeReady() {
			$.ajax({
				url: '/mall/payment/api/getParameter',
				type: 'get',
				dateType: 'json',
				beforeSend: function(xhr) {
					xhr.setRequestHeader('Authorization', token);
				},
				headers: {
					'Content-Type': 'application/json;charset=utf8',
					'Authorization': token
				},
				data: {"prepayId":prepay_id},
				success: function(msg) {
					console.log("sucess",msg,msg.data);
					var wx = msg.data;
					$("#canshu1").html("请求完毕");
					$("#canshu").html(JSON.stringify(wx));
					var wxdatas = {
						"appId": wx.appId, //公众号ID,由商户传入
						"timeStamp": wx.timeStamp, //时间戳,自1970年以来的秒数     
						"nonceStr": wx.nonceStr, //随机串     
						"package": wx.packageMsg,
						"signType": wx.signType, //微信签名方式: 
						"paySign": wx.paySign
					};
					
					$("#canshu2").html(JSON.stringify(wxdatas));
					WeixinJSBridge.invoke('getBrandWCPayRequest', wxdatas,
						function(res) {
							console.log(res)
							$("#canshu3").html(JSON.stringify(res));
							if (res.err_msg == "get_brand_wcpay_request:ok") {
								// 使用以上方式判断前端返回,微信团队郑重提示:
								//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
							}
						});
					
				},
				error: function(data) {
					console.log("error",data);
				}
			});

			
		}
		if (typeof WeixinJSBridge == "undefined") {
			if (document.addEventListener) {
				document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
			} else if (document.attachEvent) {
				document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
				document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
			}
		} else {
			onBridgeReady();
		}
	</script>
</html>
   wx.chooseWXPay({
      timestamp: 1680600220, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
      nonceStr: '0ef3d2b7200249e09bcc88897eb2ffc', // 支付签名随机串,不长于 32 位
      package: 'prepay_id=wx21201855730335ac86f8c43d1889123400', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
      signType: 'RSA', // 微信支付V3的传入RSA,微信支付V2的传入格式与V2统一下单的签名格式保持一致
      paySign: 'YOI8U/m6IsP8E0xa1doIacrCMTgjB2HF3ba7f6IJsTTR17lx/Ltvgn2ZHYyEL77/+5XvT+XAjgkRC9mYeE0WsT5WnwfTbS/PM8Iq/+H/QT+xdj1I//i0Z8BcmnLan+EckHY/2VRYQ9lqrzM/fihJgsCEE9wjlu/ss7g4ZG426/eP251OjmEKtiztnNhDsPJCwUpuKQtQTu5d1jUxLSZSrHFSKIq5Z9ntUVv9+b6Xezt4RuwHYerVUtdzhSS0Wu8/CNWyL4pIm5VRR/y44DMqIPcfdxkgUMOyKMQiC2qFgltCjOYSP30mN5hmILU48ofeG7Z7Nigy/Ze3IpUpB10ZfQ==', // 支付签名
      success: function (res) {
        // 支付成功后的回调函数
        console.log(res)
      },
      fail:function(res){
        console.log(res)
      },
      complete:function(res){
        console.log(res)
      }
    });
0

评论区