背景:pc网站点击支付按钮,进行扫码支付。一共调研了三种支付方式
- 微信支付,只可以扫描微信二维码,二维码动态生成,没有测试环境,注册申请手续比较繁琐,可以自己先打开注册页面看看,都需要什么证件。
- 支付宝支付,只可扫描支付宝二维码,有沙盒环境。
- 银联支付,可生成一个公共码,微信、支付宝都可以扫码。但是费用高,除了收取正常每笔手续费外,还需支付对接接口费用,故放弃。
- 问题:支付成功后,前端没办法知道回调成功的通知,需要用户手动关闭二维码,这样操作不友好。微信官方没有提供,只能前端通过调用接口-根据订单号查询该订单是否支付成功,前端接收通知进行判断,各位如果有好的方法可以给我留言,让我优化一下。
微信支付native工具类
package com.hbisdt.dqbasic.modular.nativePay.utils;
import cn.hutool.system.SystemUtil;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.*;
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.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Native 支付工具类 获取支付地址、解析支付数据
*
* @Author: likun
* @Date: 2024年3月19日 下午15:30:11
*/
@Slf4j
public class PayUtils {
//商户号
public static String mchid = "xxx";
//商户API私钥路径
public static String privateKeyPath;
//商户证书序列号
public static String merchantSerialNumber = "xxx";
//商户APIV3密钥
public static String apiV3key = "xxx";
public static String appId = "xxx";
//退款后的回调地址(不用可以不写)
public static final String refundNotifyUrl = "xxx";
// 使用自动更新平台证书的RSA配置
public static Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(mchid) // 商户号
.privateKeyFromPath(SystemUtil.getOsInfo().isWindows()?"D:\\native\\apiclient_key.pem":"/home/native/apiclient_key.pem") // API证书地址(此处的路径自己调试一下,能找到就行)
.merchantSerialNumber(merchantSerialNumber) // API证书序列号
.apiV3Key(apiV3key) // API密匙
.build();
// 初始化 解析器 NotificationParser
public static NotificationParser parser = new NotificationParser((NotificationConfig) config);
// 构建支付、查询的service
public static NativePayService nativePayService = new NativePayService.Builder().config(config).build();
// 构建退款的service
public static RefundService refundService = new RefundService.Builder().config(config).build();
/**
* 支付解析,解析成订单数据
*/
public static Transaction parserTransaction(HttpServletRequest request) {
RequestParam requestParam = parseRequest(request);
// 解析为 Transaction 对象(解密数据) 解密完成后的数据
Transaction transaction = parser.parse(requestParam, Transaction.class);
return transaction;
}
/**
* 退款解析,解析成退款单
*/
public static RefundNotification parserRefundNotification(HttpServletRequest request) throws Exception {
RequestParam requestParam = parseRequest(request);
RefundNotification refundNotification = parser.parse(requestParam, RefundNotification.class);
return refundNotification;
}
public static RequestParam parseRequest(HttpServletRequest request) {
try {
// 获取请求体原内容(此时获取的数据是加密的)
BufferedReader reader = request.getReader();
StringBuilder requestBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
// 获取请求携带的数据,构造参数
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial")) // 微信支付平台证书的序列号
.nonce(request.getHeader("Wechatpay-Nonce")) // 签名中的随机数
.signature(request.getHeader("Wechatpay-Signature")) // 应答的微信支付签名
.timestamp(request.getHeader("Wechatpay-Timestamp")) // 签名中的时间戳
.body(requestBody.toString()) // 请求体内容(原始内容,不要解析)
.build();
//解密完成后的数据
return requestParam;
} catch (Exception e) {
e.printStackTrace();
log.error("解析失败:{}", e.getMessage());
return null;
}
}
/**
* 获取支付地址 设置回调路径
*
* @param total 【总金额】 订单总金额,单位为分
* @param desc 商品描述
* @param notifyUrl 回调地址(必须是http) 如:"http://47.92.***.2**:80*7/pay/notifyNative"
* @return
* @Param orderNo 自己后端的唯一订单号
*/
public static String nativePayAddr(Integer total, String desc, String notifyUrl, String orderNo, String attach) {
try {
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(total); // 【总金额】 订单总金额,单位为分。
request.setAmount(amount);
request.setAppid(appId); // 应用ID
request.setMchid(mchid); // 商户号
request.setDescription(desc); // 商品描述
request.setNotifyUrl(notifyUrl); // 支付成功的回调地址 "http://47.92.132.209:8027/pay/notifyNative"
request.setAttach(attach);//自定义数据说明
request.setOutTradeNo(orderNo); // 自己后端的唯一订单号,此处使用时间模拟
// 调用下单方法,得到应答,发送请求
PrepayResponse response = nativePayService.prepay(request);
// 使用微信扫描 code_url 对应的二维码,即可体验Native支付
log.info("支付地址:{}", response.getCodeUrl());
return response.getCodeUrl();
} catch (Exception e) {
e.printStackTrace();
log.info("获取支付的url失败:" + e.getMessage());
return "获取支付的地址失败";
}
}
/**
* 手动根据微信订单号查询订单
*
* @throws Exception
*/
public static Transaction queryOrderById(String transactionId) {
try {
QueryOrderByIdRequest queryRequest = new QueryOrderByIdRequest();
queryRequest.setMchid(mchid);
queryRequest.setTransactionId(transactionId);
Transaction result = nativePayService.queryOrderById(queryRequest);
log.info("订单状态: {}", result.getTradeState());
return result;
} catch (ServiceException e) {
e.printStackTrace();
System.out.printf("code=[%s], message=[%s]\n", e.getErrorCode(), e.getErrorMessage());
System.out.printf("reponse body=[%s]\n", e.getResponseBody());
return null;
}
}
/**
* 手动根据 商 户 订单号查询订单
*
* @throws Exception
*/
public static Transaction queryOutTradeNoByOrderNO(String outTradeNo) {
try {
QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
queryRequest.setMchid(mchid);
queryRequest.setOutTradeNo(outTradeNo);
Transaction transaction = nativePayService.queryOrderByOutTradeNo(queryRequest);
log.info("订单状态: {}", transaction);
return transaction;
} catch (ServiceException e) {
e.printStackTrace();
log.info("code=[%s], message=[%s]\n", e.getErrorCode(), e.getErrorMessage());
log.info("reponse body=[%s]\n", e.getResponseBody());
return null;
}
}
/**
* 根据商户订单号进行退款申请
*
* @param refundNo
* @param refuntMoney
* @param orderTotal
* @throws Exception
*/
public static Refund refundByNo(String refundNo, Long refuntMoney, Long orderTotal) {
try {
CreateRequest request = new CreateRequest();
AmountReq amountReq = new AmountReq();
//退款金额
amountReq.setRefund(refuntMoney);
//原订单金额
amountReq.setTotal(orderTotal);
//货币类型(默认人民币)
amountReq.setCurrency("CNY");
request.setAmount(amountReq);
//商户单号 微信支付订单号 二选一
//request.setTransactionId("");
request.setOutTradeNo(refundNo);
//商户退款单号
request.setOutRefundNo(refundNo);
request.setNotifyUrl(refundNotifyUrl);
Refund refundOrder = refundService.create(request);
log.info("退款订单: {}", refundOrder.toString());
log.info("订单状态:{}", refundOrder.getStatus(), refundOrder.toString());
return refundOrder;
} catch (ServiceException e) {
e.printStackTrace();
// API返回失败, 例如ORDER_NOT_EXISTS
log.info("code=[%s], message=[%s]\n", e.getErrorCode(), e.getErrorMessage());
log.info("reponse body=[%s]\n", e.getResponseBody());
return null;
}
}
/**
* 查询退款
*
* @throws Exception
*/
public static Refund queryByOutRefundNo(String outTradeNo) {
try {
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
request.setOutRefundNo(outTradeNo);
Refund refund = refundService.queryByOutRefundNo(request);
log.info("订单状态: {}", refund);
return refund;
} catch (ServiceException e) {
e.printStackTrace();
// API返回失败, 例如ORDER_NOT_EXISTS
log.info("code=[%s], message=[%s]\n", e.getErrorCode(), e.getErrorMessage());
log.info("reponse body=[%s]\n", e.getResponseBody());
return null;
}
}
/**
*日期格式转化
* @author li-kun
* @date 2024/3/22 19:39
* @param oldDateStr
* @return java.lang.String
*/
public static String dealDateFormat(String oldDateStr){
try{
if(!oldDateStr.contains(".")){
oldDateStr = oldDateStr.replace("+",".00+");
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
SimpleDateFormat df1 = new SimpleDateFormat ("EEE MMM dd HH:mm:ss zzzz yyyy", Locale.UK);
Date date = df.parse(oldDateStr);
Date date1 = df1.parse(date.toString());
DateFormat df2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return df2.format(date1);
}catch(Exception e){
return e.getMessage();
}
}
}
调取预支付接口
public Map<String,String> nativePayAddr(JSONObject data) {
//生成二维码
Integer total = tenderBiddingAnnouncement.getTenderFee().multiply(new BigDecimal("100")).intValue();//【总金额】 订单总金额,单位为分。
JSONObject attach = new JSONObject();//自定义数据说明(可传空)
String notifyUrl = “公网回调地址”;
String orderNo = “订单号”;
//调用支付工具类,获取支付的url(也就是二维码)
String payAddr = PayUtils.nativePayAddr(total, “用户支付页面显示内容”, notifyUrl, orderNo, attach.toString());
Map<String,String> map=new HashMap<>();
map.put("url",payAddr);
return map;
}
成功支付的回调
public Map<String,String> notifyNative(HttpServletRequest request) {
Transaction transaction = PayUtils.parserTransaction(request);
//解密完成后的数据
TenderTransactionRecord payLog = new TenderTransactionRecord();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
payLog.setPaymentDate(sdf.parse(PayUtils.dealDateFormat(transaction.getSuccessTime())));//支付时间
} catch (ParseException e) {
e.printStackTrace();
}
//payLog.setTenderFee(transaction.getAmount().getTotal());//支付金额
payLog.setTransactionId(transaction.getTransactionId());//微信支付系统生成的订单号
payLog.setOrderNo(transaction.getOutTradeNo());//自己系统的订单号
Map<String,String> map=new HashMap<>();
if ("SUCCESS".equals(transaction.getTradeState().toString())) {
//执行成功业务
map.put("code","SUCCESS");
map.put("message","成功");
} else {
map.put("code","FAIL");
map.put("message","失败");
}
return map;
}