踩坑开始
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)
}
});
评论区