Native支付流程介绍https://pay.weixin.qq.com/docs/merchant/products/native-payment/development.html
支付的时候需要四个角色:微信支付用户、微信客户端、商户后台系统、微信支付系统
当用户点击了“确认支付”,商户后台系统就会生成一笔订单,并且这笔订单信息会在t_order_info表中进行存储
商户后台系统生成完订单之后,会调用微信支付系统的统一下单API,并且微信支付系统此时会生成一笔预支付交易,然后响应给商户后台系统一个预支付交易链接(code_url)。我们会使用一个二维码生成工具将code_url生成一个二维码图片,用户就可以在商户后台系统中看到这个支付二维码图片
用户便可以使用微信客户端的扫一扫功能扫描支付二维码,扫码的过程会直接提交给微信的支付系统,微信支付系统收到用户的扫码请求后会先验证链接的有效性,验证链接有效性后会要求用户进行授权(用户授权的过程就是用户确认支付输入密码的过程)
用户输入密码确认支付,向微信支付平台提交支付授权,微信支付系统再验证授权,完成支付交易
支付交易完成后,会将支付成功或失败的结果通过微信或短信的形式返回给微信客户端,用户就可以看到一个支付交易结果页面
与此同时,我们商户后台系统会接收到微信支付系统的一个异步通知(回调通知),我们商户系统接收到这个通知之后,我们的商户系统需要根据通知里面的响应结果修改我们商户后台系统中的订单状态与订单结果。商户后台系统修改完订单状态后会告诉微信支付系统,我们处理完订单了(我们的回调通知已经成功的接收了)。
但是在异步通知的发送和响应的过程中,有可能会出现网络异常的情况,从而导致发送请求或响应请求失败。也就是说用户收到支付成功的通知,但是商户系统没有收到支付成功的信息。
为了避免上面的情况,我们可以主动调用查询订单的API。
官方地址:Native下单 - Native支付 | 微信支付商户文档中心 (qq.com)
@Slf4j
@CrossOrigin //跨域注解
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "微信支付")
public class WxPayController {
@Autowired
private WxPayService wxPayService;
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("native/{productId}")
public R nativePay(@PathVariable Long productId) {
log.info("productId - " + productId);
//Map集合要返回支付二维码连接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
//我们在R类上添加了@Accessors(chain = true)注解,表示可以链式操作
//链式操作有什么好处/作用? 可以链式操作 R.ok().setData(map).setCode(); 并且这样后Set方法也会给我们返回一个R对象,更方便
return R.ok().setData(map);
}
}
Native开发指引
文档地址:Native下单API
文档中有请求示例和应答示例
Code_url的有效期是两个小时,我们可以把二维码的地址存储起来
我们依然按照 1.1 中的时序图来完成,首先要生成订单
通过本接口来生成支付链接参数code_url,然后将该参数值生成二维码图片展示给用户。用户在使用微信客户端扫描二维码后,可以直接跳转到微信支付页面完成支付操作
下单Service
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private OrderInfoService orderInfoService;
@Autowired
private CloseableHttpClient httpClient;
/**
* 创建订单,调用Native支付接口
*
* @param productId 商品ID
* @return 支付二维码url
*/
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
// TODO 生成订单
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
if (orderInfo != null && !StringUtils.isNullOrEmpty(orderInfo.getCodeUrl())) {
log.info("二维码订单已经存在-"+orderInfo.getCodeUrl());
// 之后再支付这个商品的时候,直接通过url生成二维码扫描即可
HashMap<String, Object> returnMap = new HashMap<>();
returnMap.put("codeUrl", orderInfo.getCodeUrl());
return returnMap;
}
// TODO 调用统一下单API
//请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 请求body参数(这个地方封装成一个对象再转JSON也是没问题的)
//Gson gson = new Gson();
Map paramsMap = new HashMap<>();
//公众号ID
paramsMap.put("appid", wxPayConfig.getAppid());
//直连商户号
paramsMap.put("mchid", wxPayConfig.getMchId());
//商品描述
paramsMap.put("description", orderInfo.getTitle());
//商户订单号
paramsMap.put("out_trade_no", orderInfo.getOrderNo());
//通知地址(整个支付流程完成后,微信平台要通知我们的商户平台),这个地址其实是我们内网穿透的地址
// 这个地方拼接完成就是https://500c-219-143-130-12.ngrok.io//api/wx-pay/native/notify,这个接口是我们自己定义的
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
//订单金额
Map amountMap = new HashMap<>();
//订单总金额
amountMap.put("total", orderInfo.getTotalFee());
//货币类型 人民币
amountMap.put("currency", "CNY");
paramsMap.put("amount", amountMap);
log.info("Native统一支付下单请求数据 - " + JSONObject.toJSONString(paramsMap));//{"amount":{"total":1,"currency":"CNY"},"mchid":"1558950191","out_trade_no":"ORDER_20231105014402447","appid":"wx74862e0dfcf69954","description":"test","notify_url":"https://06ca-240e-444-10-1f3f-513c-1cc6-c851-ccba.ngrok-free.app/api/wx-pay/native/notify"}
//请求体
StringEntity entity = new StringEntity(JSONObject.toJSONString(paramsMap), "utf-8");
//JSON类型的请求数据
entity.setContentType("application/json");
//说明请求的请求体
httpPost.setEntity(entity);
//设置请求头Accept,意思是希望接收的请求数据也是JSON
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
//CloseableHttpClient对象自带签名和验签,所以我们直接执行execute方法就好了
CloseableHttpResponse response = httpClient.execute(httpPost);
log.info("Native统一支付下单响应数据 - " + response); //HttpResponseProxy{HTTP/1.1 200 OK [Server: nginx, Date: Fri, 03 Nov 2023 07:11:23 GMT, Content-Type: application/json; charset=utf-8, Content-Length: 52, Connection: keep-alive, Keep-Alive: timeout=8, Cache-Control: no-cache, must-revalidate, X-Content-Type-Options: nosniff, Request-ID: 089BBA92AA0610E50318F7C58C5820F79B0D28B25F-0, Content-Language: zh-CN, Wechatpay-Nonce: e71434e9c3a974376ceb078ddfd17271, Wechatpay-Signature: FXlbxm0OvGeUF08kxIdCPLH1fFP/lc9RwoJ2iLK4iYimRKFNUbs1xp7Z/7nYmI5G1+hCZcYGWAWWmwPQ8p7z1tE8C3Y78fbStOBKzt/fVhQnEG/cR7HiFrVN60F7zhOecx8bmDYqhBA2k+7BU1bhJcXEwSR+6PMv0BVERL6crk60ngCxZge3+fSz3lNxk42IDQKjM9rI/YHK/uyCi1YNuOQKs8EZEgArBJ7yjek884OT0RMWwPnRf9KVoiJoHeS1dMf3jJqDBXL/0ap+pXgSwh0XK/YKNg788kZwSjENzLRi8NR7QjjfF2pWaUHN2OIabYzvYuWhIEZeECw7kdDGsQ==, Wechatpay-Timestamp: 1698995483, Wechatpay-Serial: 70806CE61990E2AAF559EC92EBD2058E6FA733EB, Wechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048] org.apache.http.entity.BufferedHttpEntity@1b094186}
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {//处理成功
log.info("success,return body = " + EntityUtils.toString(response.getEntity()));//{"code_url":"weixin://wxpay/bizpayurl?pr=IcXgoEAzz"}
} else if (statusCode == 204) {//处理成功,但是没有返回值
log.info("success");
} else {
log.info("Native下单失败 响应码:" + statusCode + ",响应体 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
//关闭
response.close();
//连接不能关闭,因为我们是将httpClient注入了Spring容器
//假如说写了下面的代码,向微信支付平台发送一条请求后httpClient将会关闭,那我们之后再向微信支付平台发送请求的时候就会出现java.lang.IllegalStateException: Connection pool shut down异常
//httpClient.close();
}
//将相应结果转换成JSON
JSONObject responseJson = JSONObject.parseObject(EntityUtils.toString(response.getEntity()));
HashMap<String, Object> returnMap = new HashMap<>();
returnMap.put("codeUrl", responseJson.get("code_url"));
//也要返回订单号,前端之后会向后端传输此参数,判断用户是否下单
returnMap.put("orderNo", orderInfo.getOrderNo());
// 将二维码的地址存储起来,因为有效期为两个小时,两个小时如果没下单还是可以扫的
orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), responseJson.get("code_url").toString());
return returnMap;
}
}
创建订单Service
Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Autowired
private ProductMapper productMapper;
/**
* 创建订单
* 我们并不是每次请求都需要创建订单的
*/
@Override
public OrderInfo createOrderByProductId(Long productId) {
// TODO 查找已经存在但是未支付的订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);
if (orderInfo != null) {
return orderInfo;
}
Product product = productMapper.selectById(productId);
orderInfo = new OrderInfo();
orderInfo.setTitle(product.getTitle());
orderInfo.setProductId(productId);
// 订单金额(单位是分)
orderInfo.setTotalFee(product.getPrice());
// 调用工具类生成订单号
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());
// 订单金额
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
// TODO 订单信息存入数据库
// baseMapper指的就是当前OrderInfo的Mapper
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 查找已经存在但是未支付的订单
*/
private OrderInfo getNoPayOrderByProductId(Long productId) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id", productId);
queryWrapper.eq("order_status", OrderStatus.NOTPAY.getType());
// queryWrapper.eq("user_id", userId);
return baseMapper.selectOne(queryWrapper);
}
/**
* 存储订单二维码
*/
@Override
public void saveCodeUrl(String orderNo, String codeUrl) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCodeUrl(codeUrl);
baseMapper.update(orderInfo, queryWrapper);
}
}
@Resource
private OrderInfoService orderInfoService;
@ApiOperation("订单列表")
@GetMapping("/list")
public R list(){
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
/**
* 查询订单列表,并倒序查询
*
* @return
*/
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<OrderInfo>().orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}
我们打开支付二维码后,我们的客户端浏览器是如何去判断是否已经扫码完成并且支付成功?
当用户支付成功后,我们不能一直展示下面这个页面,而是要展示一个支付成功的页面
这个时候前端其实要加一个定时器的,来定时查询我们的支付订单是否支付成功,我们的后端也要配合做一个接口来查询订单是否支付成功
后端实现查询订单状态的接口
/**
* 查询本地订单状态
* @param orderNo
* @return
*/
@ApiOperation("查询本地订单状态")
@GetMapping("/query-order-status/{orderNo}")
public R queryOrderStatus(@PathVariable String orderNo){
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if(OrderStatus.SUCCESS.getType().equals(orderStatus)){
return R.ok().setMessage("支付成功"); //支付成功
}
// 支付中的代码是101,前端会对101状态码进行判断。
// 如果前端获取到的状态码是101的话,就会执行定时查单的任务,一直到状态码等于0为止,就可以做页面的跳转了
return R.ok().setCode(101).setMessage("支付中......");
}
如果我们不支付的话,它会一直请求上面这个接口
下单成功后就会跳转到以下页面
Native关闭订单接口Native关闭订单接口官方文档
当创建订单后,用户不想再购买了,用户是可以取消订单的
编写取消订单接口
/**
* 用户取消订单
* @param orderNo 要取消的订单的订单号
* @return
*/
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) {
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok();
}
/**
* 用户取消订单
*
* @param orderNo 订单号
*/
@Override
public void cancelOrder(String orderNo) throws IOException {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);// CANCEL("用户已取消")
}
private void closeOrder(String orderNo) throws IOException {
log.info("关单接口的调用 - 订单号 - " + orderNo);
String httpPostUrl = String.format("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close", orderNo);
log.info("请求地址-" + httpPostUrl);
//TODO 创建HttpPost对象
HttpPost httpPost = new HttpPost(httpPostUrl);
//TODO 请求body参数(这个地方封装成一个对象再转JSON也是没问题的)
Map<String,String> paramsMap = new HashMap<>();
paramsMap.put("mchid", wxPayConfig.getMchId());
//TODO 将请求参数设置到请求对象中
StringEntity entity = new StringEntity(JSONObject.toJSONString(paramsMap), "utf-8");
//JSON类型的请求数据
entity.setContentType("application/json");
//说明请求的请求体
httpPost.setEntity(entity);
//设置请求头Accept,意思是希望接收的请求数据也是JSON
httpPost.setHeader("Accept", "application/json");
//TODO 完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {//处理成功
log.info("200 - success,return body = " + EntityUtils.toString(response.getEntity()));//{"code_url":"weixin://wxpay/bizpayurl?pr=IcXgoEAzz"}
} else if (statusCode == 204) {//处理成功,但是没有返回值
log.info("204 - success");
} else {
log.info("Native取消订单失败 响应码:" + statusCode + ",响应体 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
//关闭
response.close();
}
//因为没有响应数据,所以不需要处理响应数据
}
梳理的太乱了,别看了
官网
官网:签名生成-接口规则 | 微信支付商户平台文档中心 (qq.com)
微信支付API v3 要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized
我们代码中没有签名这一步是因为在我们引入的SDK中帮我们进行签名了
但是也有出现的可能性,比如提供的一些基本参数不正确,商户API证书序列号不正确或者错误的APPID
下面就是微信平台规定的签名串的具体格式
我们可以向微信平台发送一个请求,并且查看debug日志
我们向微信平台正式发送请求之前首先要做构造签名串的处理
//HTTP请求方法
authorization message=[POST
//URL
/v3/pay/transactions/native
//请求时间戳
1699111801
//随机字符串
nTvnmJcxbiljU0Fs8g6hle8nmi1L5uX1
//请求报文主体
{"amount":{"total":1,"currency":"CNY"},"mchid":"1558950191","out_trade_no":"ORDER_20231104233000813","appid":"wx74862e0dfcf69954","description":"test","notify_url":"https://06ca-240e-444-10-1f3f-513c-1cc6-c851-ccba.ngrok-free.app/api/wx-pay/native/notify"}
]
SHA256摘要算法之后,再进行RSA私钥非对称加密,最后再进行Base64编码,最后便是签名值
设置HTTP头
在HTTP头中添加一个Authorization头,存放认证类型和签名信息
下面这一块就是签名信息
authorization token=[mchid="1558950191",nonce_str="nTvnmJcxbiljU0Fs8g6hle8nmi1L5uX1",timestamp="1699111801",serial_no="34345964330B66427E0D3D28826C4993C77E631F",signature="SHD04aKOoIPP+HbRXvukRPe3uQkpiGgtF6Qtqd8pg08vJmSz5xHqvdB26icoUp6yzbkKh8oKMOnptbxmcOJ0SJJ9Rt99Nnq3A7koiMbTfyqi4IQtwj2nNYk1EDKjmVRqoRSl3lrCI5Y+cpxEzZQnkuT2RK+JvCCR1D2oWFORlP7hgdVOr2u2z6SkKPA0nY0v6aIwuMsjYKwUUjsLBmqRrdn6gseil68hO3K0jP/Fv1RkKb4mVYfh22cegP5J/BGm/51BdwZ6vkmlEGy+hvR9Y8bSuobBvDuE7Pfxn2zrQotNBc0tCib2JbfXBayX0w9tGrkZXclBqzmBBhUmYrLoJA=="]
下面便是设置HTTP请求头
认证类型就是告诉微信平台我们使用什么算法进行的签名
如下面所示的信息,便是控制台中的内容
其中WECHATPAY2-SHA256-RSA2048
认证类型,后面的都是签名信息
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1558950191",nonce_str="nTvnmJcxbiljU0Fs8g6hle8nmi1L5uX1",timestamp="1699111801",serial_no="34345964330B66427E0D3D28826C4993C77E631F",signature="SHD04aKOoIPP+HbRXvukRPe3uQkpiGgtF6Qtqd8pg08vJmSz5xHqvdB26icoUp6yzbkKh8oKMOnptbxmcOJ0SJJ9Rt99Nnq3A7koiMbTfyqi4IQtwj2nNYk1EDKjmVRqoRSl3lrCI5Y+cpxEzZQnkuT2RK+JvCCR1D2oWFORlP7hgdVOr2u2z6SkKPA0nY0v6aIwuMsjYKwUUjsLBmqRrdn6gseil68hO3K0jP/Fv1RkKb4mVYfh22cegP5J/BGm/51BdwZ6vkmlEGy+hvR9Y8bSuobBvDuE7Pfxn2zrQotNBc0tCib2JbfXBayX0w9tGrkZXclBqzmBBhUmYrLoJA=="
其实到这个地方便组装完成了一个Http 请求
这个链接中有示例代码
下面这个是HTTP发送的实际的请求头
下面是商户接收到微信平台的响应/请求时如何验签的?
官方网址签名验证-接口规则 | 微信支付商户平台文档中心 (qq.com)
我们之前代码中没做是因为在SDK中默认帮我们做了
如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。
同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送
微信支付API v3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名
微信平台的公钥要从微信平台给我们的证书中得到,并且微信平台的证书只提供API接口下载获取平台证书列表-文档中心-微信支付商户平台 (qq.com)
http-outgoing-2 << Wechatpay-Signature: XzeRGVBTp0uzi1WKVrJyc66zyJ9IGJcPY/0J2FZPXY/tRSLBaLogyQM8XVPrbdImxNu1/HTjA3q4lmo1oVrpiw09OJxE8lbYNN+r/30gwFvaricYHfRaazbLLMkBfLyCXeOe+MHOqAKJeIa4i/lffT02JeMr90ZrZHJ453coaPhB0RHaQt+GkYj9mCy56XgOtzietth143mBxVjboTeskG9r2BDPHw+6LP8+oFvoadSqHHlmOoJ5zZRwbSMhnb0ZrW9IQvTYK8n5cN+cBJIfrGCYe1OHn+56nXDy85aGrvAzmxlz07Ho2lbDCM9z4aMyQAIVUIaYIGD8zJZFj99jFg==
验签的流程如下所示
上面三个串都可以在我们的控制台找到,应答主体其实就是我们所需要的响应体
控制台中如下所示
然后,把签名base64解码后保存为文件signature.txt
最后,验证签名
获取平台证书
调用API接口进行下载证书
为什么获取的是证书列表,而不是一个证书?
微信办法的证书是有有效期的,假如平台证书过期后会出现一个空档期,在这个空档期内没法进行证书的下载,也没有办法进行公钥获取与验签
所以一般情况下会有一个规范,在启用新的平台证书前,微信支付会提前24小时把新证书加入到平台证书列表中,也就是说在这24小时内,平台证书会存在两个,一个是将要过期的旧证书,一个是将要启用的新证书
也就是说在这24小时内,用两个微信平台证书的任意序列号都行
我们首先看一下我们配置类中配置的签名验证器,主要看一下ScheduledUpdateCertificatesVerifier对象
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier() {
log.info("获取签名验证器");
//TODO 获取商户私钥
//参数1:商户私钥文件在哪里
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//TODO 私钥签名对象
//参数1:商户API证书序列号, 参数2:商户私钥文件
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//TODO 身份认证对象
//参数1:商户号,参数2:私钥签名对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
//TODO 使用定时更新的签名验证器,不需要传入证书
//参数1:身份认证对象,参数2:商户对称加密的密钥
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
在类中有下面一个方法,initCertManager方法中调用了init方法,其中第三个参数传递了一个枚举值,其值是60(分钟),含义就是每隔一个小时就会检查一下是否有新的微信平台证书,如果有新的平台证书的话,就会进行下载