提前说明:此博客使用的是V3版本,只是做了微信支付的接入,没有业务逻辑处理说明。
支付准备
1.获取商户号
微信商户平台:微信支付 - 中国领先的第三方支付平台 | 微信支付提供安全快捷的支付方式 步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取appid
微信公众平台:微信公众平台 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.获取商户证书
登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取V3密钥
获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
pom引入
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
<!--实体对象工具类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!--web应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置文件:
wxpay: # APIv3密钥 api-v3-key: # APPID appid: # 商户ID mch-id: # 商户API证书序列号 mch-serial-no: # 接收支付结果通知地址 notify-domain: https://127.0.0.1/pay/wxNativeNotify # 退款回调通知 refund-notify-domain: https://127.0.0.1/pay/wxRefundNotify # 商户私钥文件路径 private-key-path: D:\key\apiclient_key.pem
回调路径需要配置访问权限,需要自己做本地路径映射
私钥文件路径放到自己本地可以访问的地方就行
对应的配置类:
@Component
@Data
@ConfigurationProperties(prefix = "wxpay")
public class WechatpayConfig {
// 商户ID
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 接收结果通知地址
private String notifyDomain;
// 退款通知
private String refundNotifyDomain;
}
支付
官方支付文档
Native下单 - Native支付 | 微信支付商户文档中心
微信有5钟支付场景,我使用的是扫码支付
import lombok.Data;
/**
* @Author: zm
* @Description: 微信订单实体类
* @Date: 2024/10/8 15:05
*/
@Data
public class WechatPayModel {
//商品描述
private String description;
//商户订单号 (唯一)
private String outTradeNo;
//附加数据,会在支付通知中返回
private String attach;
//金额实体
private Amount amount;
//微信支付订单号 返回
private String transactionId;
/**
* 交易状态,枚举值:
* SUCCESS:支付成功
* REFUND:转入退款
* NOTPAY:未支付
* CLOSED:已关闭
* REVOKED:已撤销(仅付款码支付会返回)
* USERPAYING:用户支付中(仅付款码支付会返回)
* PAYERROR:支付失败(仅付款码支付会返回)
*/
private String tradeState;
/**
* 退款状态
* SUCCESS:退款成功
* CLOSED:退款关闭
* PROCESSING:退款处理中
* ABNORMAL:退款异常
*/
private String refundState;
/**
* 订单金额实体类
*/
@Data
public class Amount{
//订单金额 单位为分
private Integer total;
//CNY:人民币,境内商户号仅支持人民币
private String currency;
}
}
import com.ruoyi.zm.propertie.WechatpayConfig;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.Properties;
/**
* @Author: zm
* @Description: 支付配置公共类
* @Date: 2024/9/29 15:23
*/
@Configuration
public class CommonUtils {
@Resource
private WechatpayConfig wxPayConfig;
/**
* 初始化商户配置
* @return
*/
@Bean
public RSAAutoCertificateConfig getConfig(){
return new RSAAutoCertificateConfig.Builder()
.merchantId(wxPayConfig.getMchId())
.privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
.merchantSerialNumber(wxPayConfig.getMchSerialNo())
.apiV3Key(wxPayConfig.getApiV3Key())
.build();
}
}
# 支付引入相关的包 import cn.hutool.core.util.ObjectUtil; import com.ruoyi.common.core.constant.HttpStatus; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.zm.dto.WechatPayModel; import com.ruoyi.zm.propertie.WechatpayConfig; import com.wechat.pay.java.core.RSAAutoCertificateConfig; import com.wechat.pay.java.core.exception.HttpException; import com.wechat.pay.java.core.exception.MalformedMessageException; import com.wechat.pay.java.core.exception.ServiceException; import com.wechat.pay.java.core.exception.ValidationException; 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.PrepayRequest; import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse; import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest; import com.wechat.pay.java.service.refund.RefundService; import com.wechat.pay.java.service.refund.model.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map;
扫码预下单
AjaxResult是我自己封装的返回处理,你们替换成自己的就可以
/**
* 微信支付 扫码预下单
* @param wechatPayModel 参数实体
* @author: zm
* @date: 2024/10/8
* @rerturn: java.lang.String
*/
public AjaxResult nativePay(WechatPayModel wechatPayModel){
if(StringUtils.isAnyEmpty(wechatPayModel.getOutTradeNo(),wechatPayModel.getDescription())){
return AjaxResult.error("订单号或描述为空");
}
if(StringUtils.isAnyEmpty(wechatPayModel.getAmount().getTotal().toString())){
return AjaxResult.error("金额为空,请检查参数");
}
PrepayResponse response;
try{
// 构建service
NativePayService service = new NativePayService.Builder().config(config).build();
// request.setXxx(val)设置所需参数,具体参数可见Request定义
PrepayRequest request = new PrepayRequest();
Amount amount = new Amount();
amount.setTotal(wechatPayModel.getAmount().getTotal());
request.setAmount(amount);
request.setAppid(payConfig.getAppid());
request.setMchid(payConfig.getMchId());
request.setDescription(wechatPayModel.getDescription());
request.setNotifyUrl(payConfig.getNotifyDomain());
request.setOutTradeNo(wechatPayModel.getOutTradeNo());
request.setAttach(wechatPayModel.getAttach());
// 调用下单方法,得到应答
response = service.prepay(request);
}catch (HttpException e) { // 发送HTTP请求失败
log.error("微信下单发送HTTP请求失败,错误信息:{}", e.getHttpRequest());
return AjaxResult.error("下单失败");
} catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500
log.error("微信下单服务状态错误,错误信息:{}", e.getErrorMessage());
return AjaxResult.error("下单失败");
} catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败
log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:{}", e.getMessage());
return AjaxResult.error("下单失败");
}
return AjaxResult.success(response);
}
支付回调
回调请求工具类
/**
* 获取请求体
*/
public static String getRequestBody(HttpServletRequest request) throws IOException {
ServletInputStream stream;
BufferedReader reader = null;
StringBuilder sb = new StringBuilder();
try {
stream = request.getInputStream();
// 获取响应
reader = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
throw new IOException("读取返回支付接口数据流出现异常!");
} finally {
assert reader != null;
reader.close();
}
return sb.toString();
}
/**
* 支付回调方法
* 业务信息不在这里写,回调地址只返回了业务订单号
* @param request 请求参数
* @author: zm
* @date: 2024/10/8
* @rerturn: java.lang.String
*/
public synchronized Map<String,String> payNativeNotify(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.info("------收到支付通知------");
Map<String, String> map = new HashMap<>();//应答对象
// 请求头Wechatpay-Signature
String signature = request.getHeader("Wechatpay-Signature");
// 请求头Wechatpay-nonce
String nonce = request.getHeader("Wechatpay-Nonce");
// 请求头Wechatpay-Timestamp
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 微信支付证书序列号
String serial = request.getHeader("Wechatpay-Serial");
// 签名方式
String signType = request.getHeader("Wechatpay-Signature-Type");
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serial)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(getRequestBody(request))
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
// 以支付通知回调为例,验签、解密并转换成 Transaction
log.info("验签");
Transaction transaction;
try {
// 以支付通知回调为例,验签、解密并转换成 Transaction
transaction = parser.parse(requestParam, Transaction.class);
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
response.setStatus(500);
log.error("sign verification failed", e);
map.put("code", "FAIL");
map.put("message","失败");
return map;
}
log.info("验签成功!支付回调结果:{}", transaction.toString());
//处理业务逻辑
WechatPayModel payModel=new WechatPayModel();
payModel.setOutTradeNo(transaction.getOutTradeNo());
payModel.setTransactionId(transaction.getTransactionId());
payModel.setTradeState(transaction.getTradeState().name());
// 处理成功,返回 200 OK 状态码
map.put("code", "SUCCESS");
response.setStatus(200);
return map;
}
支付查询
/**
* 根据商户订单号查询订单
* @param model 请求参数
* @author: zm
* @date: 2024/10/9
* @rerturn: java.lang.String
*/
public AjaxResult queryOrderByOrderNo(WechatPayModel model){
log.info("微信订单查询开始:商户订单号:{}",model.getOutTradeNo());
QueryOrderByOutTradeNoRequest queryRequest = new QueryOrderByOutTradeNoRequest();
queryRequest.setMchid(payConfig.getMchId());
queryRequest.setOutTradeNo(model.getOutTradeNo());
try {
NativePayService service = new NativePayService.Builder().config(config).build();
Transaction result = service.queryOrderByOutTradeNo(queryRequest);
log.info("订单查询成功,查询返回数据:{}",result.toString());
//支付成功
WechatPayModel payModel=new WechatPayModel();
payModel.setOutTradeNo(result.getOutTradeNo());
payModel.setTransactionId(result.getTransactionId());
payModel.setTradeState(result.getTradeState().name());
return AjaxResult.success(payModel);
} catch (ServiceException e) {
log.error("订单查询失败,返回码:{},返回信息:{}", e.getErrorCode(), e.getErrorMessage());
return AjaxResult.error("订单查询失败!");
}
}
退款申请
退款申请挺复杂,可以支持部分退款,我这里没有做部分退款,是全部退款的例子。
/**
* 微信退款申请
* @param model 请求参数
* @author: zm
* @date: 2024/10/9
* @rerturn: java.lang.String
*/
public AjaxResult wxCreateRefund(WechatPayModel model){
//现在只有全额退款,所以没有重新生成退款流水号,如果部分退款,那么退款流水号需要改成调用端传输退款流水号
String refundNo="REFUND_"+model.getOutTradeNo();
if(ObjectUtil.isNull(model.getAmount().getTotal())){
return AjaxResult.error("退款金额不能为空!");
}
log.info("微信发起退款申请:商户订单号[{}],退款单号[{}]",model.getOutTradeNo(),refundNo);
try {
// 构建退款service
RefundService service = new RefundService.Builder()
.config(config)
.build();
CreateRequest request = new CreateRequest();
// 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
request.setOutTradeNo(model.getOutTradeNo());
request.setOutRefundNo(refundNo);
AmountReq amount = new AmountReq();
amount.setTotal(Long.valueOf(model.getAmount().getTotal()));
amount.setRefund(Long.valueOf(model.getAmount().getTotal()));
amount.setCurrency("CNY");
request.setAmount(amount);
//退款通知回调
request.setNotifyUrl(payConfig.getRefundNotifyDomain());
//接收退款返回参数
Refund refund = service.create(request);
log.info("退款返回信息:{}", refund);
WechatPayModel payModel=new WechatPayModel();
payModel.setOutTradeNo(refund.getOutTradeNo());
payModel.setTransactionId(refund.getTransactionId());
payModel.setRefundState(refund.getStatus().name());
if (refund.getStatus().equals(Status.SUCCESS) || refund.getStatus().equals(Status.PROCESSING)) {
return AjaxResult.success(payModel);
}else if (refund.getStatus().equals(Status.ABNORMAL)) {
return AjaxResult.error(HttpStatus.ERROR,"退款失败,账户异常!");
} else if (refund.getStatus().equals(Status.CLOSED)) {
return AjaxResult.error(HttpStatus.ERROR,"用户余额不足或者订单超过退款期限!");
}else{
log.info("退款失败!,退款返回信息:{}", refund);
return AjaxResult.error(HttpStatus.ERROR,"退款失败,未知异常!");
}
} catch (ServiceException e) {
log.error("退款失败!,错误信息:{}", e.getMessage());
return AjaxResult.error("退款失败!");
} catch (Exception e) {
log.error("服务返回成功,返回体类型不合法,或者解析返回体失败,错误信息:{}", e.getMessage());
return AjaxResult.error("退款失败!");
}
}
退款通知
/**
* 退款通知
* @param request 请求参数
* @author: zm
* @date: 2024/10/9
* @rerturn: java.lang.String
*/
public void refundNotify(HttpServletRequest request) {
try{
log.info("------收到退款通知------");
// 请求头Wechatpay-Signature
String signature = request.getHeader("Wechatpay-Signature");
// 请求头Wechatpay-nonce
String nonce = request.getHeader("Wechatpay-Nonce");
// 请求头Wechatpay-Timestamp
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 微信支付证书序列号
String serial = request.getHeader("Wechatpay-Serial");
// 签名方式
String signType = request.getHeader("Wechatpay-Signature-Type");
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(serial)
.nonce(nonce)
.signature(signature)
.timestamp(timestamp)
.signType(signType)
.body(getRequestBody(request))
.build();
// 初始化 NotificationParser
NotificationParser parser = new NotificationParser(config);
// 以支付通知回调为例,验签、解密并转换成 Transaction
log.info("退款验签");
RefundNotification refund=null;
try {
// 以支付通知回调为例,验签、解密并转换成 Transaction
refund = parser.parse(requestParam, RefundNotification.class);
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
e.printStackTrace();
log.error("sign verification failed", e);
}
if(refund!=null){
log.info("验签成功!-退款回调结果:{}", refund.toString());
//parse.getRefundStatus().equals("SUCCESS");说明退款成功
WechatPayModel payModel=new WechatPayModel();
payModel.setOutTradeNo(refund.getOutTradeNo());
payModel.setTransactionId(refund.getTransactionId());
payModel.setRefundState(refund.getRefundStatus().name());
//业务逻辑
}else{
log.error("验签解析失败!");
}
} catch (Exception e) {
e.printStackTrace();
log.error("退款回调失败!错误信息:{}", e.getMessage());
}
}
业务调试
postman调用,下单返回的二维码路径我没有写前端,我是用的在线工具生成的,偷懒的可以用在线工具生成,勤快的小伙伴可以自己写写前端生成二维码。
好了,微信支付到此就完事了,有疑问可以留言。