参考文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_0.shtml
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。
Native支付适用于PC网站、实体店单品或订单、媒体广告支付等场景
用户扫描商户展示在各种场景的二维码进行支付,具体操作流程如下:
参考文档:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_1.shtml#top
注意:appid 和 mchid 是必须的。
API v3密钥主要用于平台证书解密、回调信息解密,具体使用方式可参见接口规则文档中证书和回调报文解密章节。
注意:这里我们设置的是 API v3密钥,不是 API v2密钥。
在【证书工具】-“生成证书”环节,已完成申请证书流程,点击“查看证书文件夹”,查看已生成的证书文件。
恭喜,到此一切准备就绪!
为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。
相较于之前的微信支付API,主要区别是:
所有的API请求必须使用HTTPS。
微信支付API v3使用 JSON 作为消息体的数据交换格式,图片上传API除外。请求须设置HTTP头部:
Content-Type: application/json
Accept: application/json
微信支付API v3使用HTTP状态码来表示请求处理的结果。
环境:
需要提前准备以下的材料
参考地址:https://gitee.com/binary/weixin-java-tools
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-payartifactId>
<version>4.2.0version>
dependency>
其他
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
<scope>providedscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.7.21version>
dependency>
修改 application.yml 文件
wx:
pay:
appId: 请填写您的appId
mchId: 请填写您的mchId
apiV3Key: 请填写您的apiV3Key
privateCertPath: 请填写您的apiclient_cert.pem所在文件(绝对路径或相对路径)
privateKeyPath: 请填写您的apiclient_key.pem所在文件(绝对路径或相对路径)
notifyUrl: 请填写您的支付成功回调地址(必须https开头且公网可访问)
新增两个配置文件,完成支付环境初始化
读取配置文件的内容
package com.ray.weixin.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* wxpay pay properties.
*
* @author Binary Wang
*/
@Data
@ConfigurationProperties(prefix = "wx.pay")
public class WxPayPropertiesV3 {
/**
* 设置微信公众号或者小程序等的appid
*/
private String appId;
/**
* 微信支付商户号
*/
private String mchId;
/**
* apiV3 秘钥值
*/
private String apiV3Key;
/**
* apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateCertPath;
/**
* apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径
*/
private String privateKeyPath;
/**
* 该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。
* 请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。
* 回调URL示例:“https://pay.weixin.qq.com/wxpay/pay.action”
*/
private String notifyUrl;
}
完成支付环境初始化
package com.ray.weixin.config;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Binary Wang
*/
@Configuration
@ConditionalOnClass(WxPayService.class)
@EnableConfigurationProperties(WxPayPropertiesV3.class)
@AllArgsConstructor
public class WxPayConfiguration {
private WxPayPropertiesV3 properties;
@Bean
@ConditionalOnMissingBean
public WxPayService wxService() {
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl()));
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(false);
// 微信支付接口请求实现类,默认使用Apache HttpClient实现
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
其中 WxPayServiceImpl 是微信支付接口请求实现类,默认使用Apache HttpClient实现。
package com.ray.weixin.utils;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @ClassName: WxOrderUtils
* @Description: 微信订单工具类
* @Author: Ray
* @Date: 2022-02-16 9:28
*/
@Slf4j
public class WxOrderUtils {
public static final String ORDER_PAY_PREFIX = "ray_pay_";
public static final String ORDER_REFUND_PREFIX = "ray_refund_";
public static final String FORMAT = "yyyyMMddHHmmssSSS";
/**
* 生成商户订单号
*
* @return
*/
public static String genOutTradeNo() {
String order = ORDER_PAY_PREFIX + new SimpleDateFormat(FORMAT).format(new Date());
log.debug("生成商户订单号: [{}]", order);
return order;
}
/**
* 生成退款订单号
*
* @return
*/
public static String genRefundOrder() {
String order = ORDER_REFUND_PREFIX + new SimpleDateFormat(FORMAT).format(new Date());
log.debug("生成退款订单号: [{}]", order);
return order;
}
}
package com.ray.weixin.utils;
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @ClassName: HtmlUtils
* @Description: 请求头工具类
* @Author: Ray
* @Date: 2022-02-10 8:48
*/
@Slf4j
public class HtmlUtils {
/**
* 获取请求头信息
*
* @return
*/
public static String fetchRequest2Str(HttpServletRequest request) {
String reqStr = null;
BufferedReader streamReader = null;
try {
streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
reqStr = responseStrBuilder.toString();
log.info("Request Received is \n" + reqStr);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (streamReader != null) {
streamReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return reqStr;
}
/**
* 获取请求头签名
*
* @return
*/
public static SignatureHeader fetchRequest2SignatureHeader(HttpServletRequest request) {
SignatureHeader signatureHeader = new SignatureHeader();
signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
signatureHeader.setTimeStamp(request.getHeader("Wechatpay-TimeStamp"));
return signatureHeader;
}
}
package com.ray.common.core.domain;
import java.util.HashMap;
/**
* @Description: 微信支付V3 通知应答
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
* @Author: Ray
* @Date: 2022-02-13 9:39
**/
public class WxPayResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
private static final String FAIL = "FAIL";
private static final String SUCCESS = "SUCCESS";
public static WxPayResult error() {
WxPayResult r = new WxPayResult();
r.put("code", FAIL);
r.put("message", "失败");
return r;
}
public static WxPayResult ok() {
WxPayResult r = new WxPayResult();
r.put("code", SUCCESS);
r.put("message", "成功");
return r;
}
@Override
public WxPayResult put(String key, Object value) {
super.put(key, value);
return this;
}
}
package com.ray.common.exception;
/**
* 支付异常类
*
* @author Ray
*/
public class WeixinPayException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public WeixinPayException() {
super("支付异常,请联系管理员");
}
public WeixinPayException(String msg) {
super(msg);
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
参考文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
商户后台系统先调用微信支付的Native下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
code_url有效期为2小时,过期后扫码不能再发起支付。
所以我们在处理订单的时候,如果是重复的订单,可以直接返回有效期内的二维码,不用再次请求。
package com.ray.weixin.service;
import com.ray.weixin.utils;
import com.ray.common.utils.StringUtils;
import com.ray.qhummingbird.domain.PayOrder.PayOrderEntity;
import com.ray.qhummingbird.domain.UserPayOrder.UserPayOrderCreateInfoVo;
import com.ray.qhummingbird.domain.UserPayOrder.UserPayOrderEntity;
import com.ray.qhummingbird.domain.UserPayOrderLog.UserPayOrderLogEntity;
import com.ray.weixin.config.WxPayPropertiesV3;
import com.ray.weixin.domain.UserPayOrderLogRefundDto;
import com.ray.weixin.utils.HtmlUtils;
import com.ray.weixin.utils.WxOrderUtils;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName: WxPayRemoteService
* @Description: 微信支付远程服务
* @Author: Ray
* @Date: 2022-02-10 14:09
*/
@Service
@Slf4j
public class WxPayRemoteService {
@Autowired
private WxPayService wxService;
@Autowired
private WxPayPropertiesV3 properties;
/**
* @Description: 调用下单API
* @Author: Ray
* @Date: 2022-02-10 14:54
**/
public Map<String, Object> createOrderV3() throws WxPayException {
log.debug("创建订单 >>>>> ");
// 商品描述
String description = "自定义商品描述";
// 商户订单号
String outTradeNo = WxOrderUtils.genOutTradeNo();
// 总金额,单位分
Integer total = 1;
WxPayUnifiedOrderV3Request request = this.genWxPayUnifiedOrderV3Request(description, outTradeNo, total);
String qrCode = wxService.createOrderV3(TradeTypeEnum.NATIVE, request);
// 返回需要的信息
HashMap<String, Object> map = new HashMap<>();;
map.put("qrCode", qrCode);
map.put("outTradeNo", outTradeNo);
return map;
}
/**
* @Description: 组装请求数据
* @Author: Ray
* @Date: 2022-02-10 14:30
**/
private WxPayUnifiedOrderV3Request genWxPayUnifiedOrderV3Request(String description, String outTradeNo, Integer total) {
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
// 商品描述
request.setDescription(description);
// 商户订单号
request.setOutTradeNo(outTradeNo);
// 通知地址
request.setNotifyUrl(properties.getNotifyUrl());
// 订单金额
WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
// 总金额,单位分
amount.setTotal(total);
// 货币类型,选填
amount.setCurrency("CNY");
request.setAmount(amount);
return request;
}
}
package com.ray.weixin.feign;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.ray.common.core.controller.BaseController;
import com.ray.common.core.domain.R;
import com.ray.common.core.domain.WxPayResult;
import com.ray.common.exception.WeixinPayException;
import com.ray.common.utils.StringUtils;
import com.ray.weixin.service.WxPayRemoteService;
import com.ray.weixin.utils.WxOrderUtils;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName: WxPayFeign
* @Description: 微信支付接口
* @Author: Ray
* @Date: 2022-02-08 15:10
*/
@RestController
@RequestMapping("/feign/wxPay")
public class WxPayFeign extends BaseController {
@Autowired
private WxPayRemoteService wxPayRemoteService;
/**
* 生成商户订单号
*/
@GetMapping("genOutTradeNo")
public String genOutTradeNo() {
return WxOrderUtils.genOutTradeNo();
}
/**
* 生成退款订单
*/
@GetMapping("genOutRefundNo")
public String genOutRefundNo() {
return WxOrderUtils.genRefundOrder();
}
/**
* 调用统一下单接口
*
*/
@GetMapping("createOrderV3")
public R createOrderV3() {
try {
logger.debug("调用统一下单接口 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
// 先查询数据,如果存在并且二维码在有效期内,则直接返回(省略)
// 调用下单API
HashMap<String, Object> map = wxPayRemoteService.createOrderV3();
// 商品订单号
String outTradeNo = map.get("outTradeNo");
// 二维码
String qrCode = map.get("qrCode");
// 保存数据(省略)
// 返回数据
return R.data(map);
} catch (WxPayException e) {
e.printStackTrace();
throw new WeixinPayException(e.getErrCodeDes());
}
return R.error();
}
}
文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
微信支付通过支付通知接口将用户支付成功消息通知给商户
tip: 如果没有公网地址的,可以使用内网穿透。
注意:
• 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
• 如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。
**特别提醒:**商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
支付通知的返回参数如下:
/**
* @ClassName: WxPayRemoteService
* @Description: 微信支付远程服务
* @Author: Ray
* @Date: 2022-02-10 14:09
*/
@Service
@Slf4j
public class WxPayRemoteService {
@Autowired
private WxPayService wxService;
@Autowired
private WxPayPropertiesV3 properties;
// 忽略部分代码
/**
* @Description: 解析支付结果v3通知
* @Author: Ray
* @Date: 2022-02-10 14:10
**/
public WxPayOrderNotifyV3Result parseOrderNotifyV3Result(HttpServletRequest request) throws WxPayException {
// 解析支付结果v3通知
return wxService.parseOrderNotifyV3Result(
HtmlUtils.fetchRequest2Str(request),
HtmlUtils.fetchRequest2SignatureHeader(request)
);
}
}
/**
* @ClassName: WxPayFeign
* @Description: 微信支付接口
* @Author: Ray
* @Date: 2022-02-08 15:10
*/
@RestController
@RequestMapping("/feign/wxPay")
public class WxPayFeign extends BaseController {
@Autowired
private WxPayRemoteService wxPayRemoteService;
private final ReentrantLock lock = new ReentrantLock();
// 忽略部分代码
/**
* @Description: 支付成功异步通知
* @Author: Ray
* @Date: 2022-02-13 9:37
**/
@PostMapping("/native/notify")
public WxPayResult nativeNotify(HttpServletRequest request) {
logger.debug("支付成功异步通知 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
if (lock.tryLock()) {
try {
// 解析支付结果v3通知
WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = wxPayRemoteService.parseOrderNotifyV3Result(request);
// 获取基本信息
String tradeState = wxPayOrderNotifyV3Result.getResult().getTradeState();
String tradeStateDesc = wxPayOrderNotifyV3Result.getResult().getTradeStateDesc();
String outTradeNo = wxPayOrderNotifyV3Result.getResult().getOutTradeNo();
// 获取用户支付订单信息(省略)
// 如果已经处理,直接返回成功标识
if (StringUtils.isNotBlank(user.getTradeState()) &&
WxPayConstants.WxpayTradeStatus.SUCCESS.equals(user.getTradeState())) {
// 返回成功标识
return WxPayResult.ok();
}
// 业务逻辑...(省略)
// 返回成功标识
return WxPayResult.ok();
} catch (WxPayException e) {
logger.error(e.getMessage());
// 返回失败标识
return WxPayResult.error();
} finally {
lock.unlock();
}
}
// 返回失败标识
return WxPayResult.error();
}
}
参考文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
查询订单可通过微信支付订单号和商户订单号两种方式查询,两种查询方式返回结果相同
/**
* @ClassName: WxPayRemoteService
* @Description: 微信支付远程服务
* @Author: Ray
* @Date: 2022-02-10 14:09
*/
@Service
@Slf4j
public class WxPayRemoteService {
@Autowired
private WxPayService wxService;
@Autowired
private WxPayPropertiesV3 properties;
// 忽略部分代码
/**
* @Description: 查询订单API
* @Author: Ray
* @Date: 2022-02-10 15:18
**/
public WxPayOrderQueryV3Result queryOrderV3(String transactionId, String outTradeNo) throws WxPayException {
WxPayOrderQueryV3Result wxPayOrderQueryV3Result = this.wxService.queryOrderV3(transactionId, outTradeNo);
log.debug("查询订单结果 >>>>> " + wxPayOrderQueryV3Result);
return wxPayOrderQueryV3Result;
}
}
/**
* @ClassName: WxPayFeign
* @Description: 微信支付接口
* @Author: Ray
* @Date: 2022-02-08 15:10
*/
@RestController
@RequestMapping("/feign/wxPay")
public class WxPayFeign extends BaseController {
@Autowired
private WxPayRemoteService wxPayRemoteService;
// 忽略部分代码
/**
* 调用查询订单接口
*
*/
@GetMapping("queryOrderV3/{outTradeNo}")
public R queryOrderV3(@PathVariable("outTradeNo") String outTradeNo) {
try {
logger.debug("调用查询订单接口 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
// 查询订单
WxPayOrderQueryV3Result wxPayOrderQueryV3Result = wxPayRemoteService.queryOrderV3("", outTradeNo);
// 保存数据(省略)
// 返回数据
HashMap<String, Object> map = new HashMap<>();
map.put("tradeState", wxPayOrderQueryV3Result.getTradeState());
map.put("tradeStateDesc", wxPayOrderQueryV3Result.getTradeStateDesc());
return R.data(map);
} catch (WxPayException e) {
e.printStackTrace();
throw new WeixinPayException(e.getErrCodeDes());
}
return R.error();
}
}
其他接口的使用方法相似,这里就不演示了。