参考资料:产品中心 - 微信支付商户平台 (qq.com)
付款码支付、JSAPI支付、小程序支付、Native支付、APP支付、刷脸支付
用户展示微信钱包内的“付款码”给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。
特点:用户在客户端输入支付金额
在微信小程序平台内实现支付的功能。
Native支付是指商户展示支付二维码,用户再用微信“扫一扫”完成支付的模式。这种方式适用于PC网站。
特点:商家预先指定支付金额
商户通过在移动端独立的APP应用程序中集成微信支付模块,完成支付。
用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。
微信支付企业主流的API版本有v2和v3,课程中我们使用微信支付APIv3。
V2和V3的比较
相比较而言,APIv2比APIv3安全性更高,但是APIv2中有一些功能在APIv3中尚未完整实现,具体参考如下API字典页面:API字典概览 | 微信支付商户平台文档中心 (qq.com)
如果需要独立申请和开通微信支付功能,可以参考如下官方文档。开通微信支付后,才能获取相关的开发参数以及商户公钥和商户私钥文件。
参考资料:微信支付接入指引 - 微信支付商户平台 (qq.com)
在service-order
服务的resources目录中创建wxpay.properties
这个文件定义了在“接入指引”的步骤中我们提前准备的微信支付相关的参数,例如商户号、APPID、API秘钥等等
# 微信支付相关参数 wxpay: mch-id: 1558950191 #商户号 mch-serial-no: 34345964330B66427E0D3D28826C4993C77E631F # 商户API证书序列号 private-key-path: D:/project/yygh/cert/apiclient_key.pem # 商户私钥文件 api-v3-key: UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B # APIv3密钥 appid: wx74862e0dfcf69954 # APPID notify-url: https://7d92-115-171-63-135.ngrok.io/api/order/wxpay/payment/notify # 接收支付结果通知地址 notify-refund-url: http://agxnyzl04y90.ngrok.xiaomiqiu123.top/api/order/wxpay/refunds/notify # 接收退款结果通知地址
将商户私钥文件复制到配置文件指定的目录下:
资料:资料>微信支付>商户证书>apiclient_key.pem
private-key-path: D:/project/yygh/cert/apiclient_key.pem # 商户私钥文件
参考文档:APIv3证书与密钥使用说明
一个完整的请求和响应的流程:
参考文档:SDK&工具
我们可以使用官方提供的 SDK wechatpay-java
在service-order微服务中添加依赖:
<!--微信支付APIv3--> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>0.2.6</version> </dependency>
在config 包中 创建 WxPayConfig.java
package com.atguigu.syt.order.config; @Configuration @PropertySource("classpath:wxpay.properties") //读取配置文件 @ConfigurationProperties(prefix="wxpay") //读取wxpay节点 @Data public class WxPayConfig { // 商户号 private String mchId; // 商户API证书序列号 private String mchSerialNo; // 商户私钥文件 private String privateKeyPath; // APIv3密钥 private String apiV3Key; // APPID private String appid; // 接收支付结果通知地址 private String notifyUrl; // 接收退款结果通知地址 private String notifyRefundUrl; }
在 API 请求过程中,客户端需使用微信支付平台证书,验证服务器应答的真实性和完整性。我们使用自动更新平台证书的配置类 RSAAutoCertificateConfig
。每个商户号只能创建一个 RSAAutoCertificateConfig
。
在WxPayConfig
中添加如下方法:
/** * 获取微信支付配置对象 * @return */ @Bean public RSAAutoCertificateConfig getConfig(){ return new RSAAutoCertificateConfig.Builder() .merchantId(mchId) .privateKeyFromPath(privateKeyPath) .merchantSerialNumber(mchSerialNo) .apiV3Key(apiV3Key) .build(); }
RSAAutoCertificateConfig
通过 RSAAutoCertificateProvider
自动下载微信支付平台证书。 同时,RSAAutoCertificateProvider
会启动一个后台线程,定时更新证书(目前设计为60分钟),以实现证书过期时的新老证书平滑切换。
常见错误:引入商户私钥后如果项目无法启动,则需要升级JDK版本,并重新配置idea编译和运行环境到最新版本的JDK。建议升级到1.8.0_300以上
参考文档:业务流程时序图
在service-order中创建FrontWXPayController
package com.atguigu.syt.order.controller.front; @Api(tags = "微信支付接口") @RestController @RequestMapping("/front/order/wxpay") public class FrontWXPayController { @Resource private WxPayService wxPayService; @Resource private AuthContextHolder authContextHolder; @ApiOperation("获取支付二维码url") @ApiImplicitParam(name = "outTradeNo",value = "订单号", required = true) @GetMapping("/auth/nativePay/{outTradeNo}") public Result<String> nativePay(@PathVariable String outTradeNo, HttpServletRequest request, HttpServletResponse response) { //校验用户登录状态 authContextHolder.checkAuth(request, response); String codeUrl = wxPayService.createNative(outTradeNo); return Result.ok(codeUrl); } }
SDK参考代码:wechatpay-java/NativePayServiceExample.java at main · wechatpay-apiv3/wechatpay-java · GitHub
具体代码示例如下:
Native下单API参数参考:Native下单API
接口:OrderInfoService
/** * 根据订单号获取订单 * @param outTradeNo * @return */ OrderInfo selectByOutTradeNo(String outTradeNo);
实现:OrderInfoServiceImpl
@Override public OrderInfo selectByOutTradeNo(String outTradeNo) { LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(OrderInfo::getOutTradeNo, outTradeNo); return baseMapper.selectOne(queryWrapper); }
接口:WxPayService
package com.atguigu.syt.order.service; public interface WxPayService { /** * 获取支付二维码utl * @param outTradeNo * @return */ String createNative(String outTradeNo); }
实现:WxPayServiceImpl
package com.atguigu.syt.order.service.impl; @Service @Slf4j public class WxPayServiceImpl implements WxPayService { @Resource private RSAAutoCertificateConfig rsaAutoCertificateConfig; @Resource private WxPayConfig wxPayConfig; @Resource private OrderInfoService orderInfoService; @Override public String createNative(String outTradeNo) { // 初始化服务 NativePayService service = new NativePayService.Builder().config(rsaAutoCertificateConfig).build(); // 调用接口 try { //获取订单 OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo); PrepayRequest request = new PrepayRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 request.setAppid(wxPayConfig.getAppid()); request.setMchid(wxPayConfig.getMchId()); request.setDescription(orderInfo.getTitle()); request.setOutTradeNo(outTradeNo); request.setNotifyUrl(wxPayConfig.getNotifyUrl()); Amount amount = new Amount(); //amount.setTotal(orderInfo.getAmount().multiply(new BigDecimal(100)).intValue()); amount.setTotal(1);//1分钱 request.setAmount(amount); // 调用接口 PrepayResponse prepayResponse = service.prepay(request); return prepayResponse.getCodeUrl(); } catch (HttpException e) { // 发送HTTP请求失败 // 调用e.getHttpRequest()获取请求打印日志或上报监控,更多方法见HttpException定义 log.error(e.getHttpRequest().toString()); throw new GuiguException(ResultCodeEnum.FAIL); } catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500 // 调用e.getResponseBody()获取返回体打印日志或上报监控,更多方法见ServiceException定义 log.error(e.getResponseBody()); throw new GuiguException(ResultCodeEnum.FAIL); } catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败 // 调用e.getMessage()获取信息打印日志或上报监控,更多方法见MalformedMessageException定义 log.error(e.getMessage()); throw new GuiguException(ResultCodeEnum.FAIL); } } }
可见,使用 SDK 并不需要计算请求签名和验证应答签名。
创建api/wxpay.js
import request from '~/utils/request' export default { //获取支付二维码url nativePay(outTradeNo) { return request({ url: `/front/order/wxpay/auth/nativePay/${outTradeNo}`, method: 'get' }) }, }
引入api
import wxpayApi from '~/api/wxpay'
添加data数据
codeUrl: null, //微信支付二维码 isPayShow: false, //不显示登录二维码组件 payText: '支付'
修改按钮
将 <div class="v-button" @click="pay()">支付</div> 修改为 <div class="v-button" @click="pay()">{{payText}}</div>
添加方法
//支付 pay() { //防止重复提交 if(this.isPayShow) return this.isPayShow = true this.payText = '支付中.....' //显示二维码 wxpayApi.nativePay(this.orderInfo.outTradeNo).then((response) => { this.codeUrl = response.data this.dialogPayVisible = true }) }, //关闭对话框 closeDialog(){ //恢复支付按钮 this.isPayShow = false this.payText = '支付' }
用二维码组件替换img图片
<img src="二维码链接" alt="" /> 替换成 <qriously :value="codeUrl" :size="220"/>
参考文档:微信支付查单接口
商户可以主动调用微信支付查单接口,同步订单状态。
调用查询订单接口,如果支付成功则更新订单状态
并添加交易记录
,如果支付尚未成功则轮询查单
OrderInfoService接口
/** * 根据订单号更新订单状态 * @param outTradeNo * @param status */ void updateStatus(String outTradeNo, Integer status);
OrderInfoServiceImpl实现
@Override public void updateStatus(String outTradeNo, Integer status) { LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(OrderInfo::getOutTradeNo, outTradeNo); OrderInfo orderInfo = new OrderInfo(); orderInfo.setOrderStatus(status); baseMapper.update(orderInfo, queryWrapper); }
PaymentInfoService接口
/** * 保存交易记录 * @param orderInfo * @param transaction */ void savePaymentInfo(OrderInfo orderInfo, Transaction transaction);
实现:PaymentInfoServiceImpl
@Override public void savePaymentInfo(OrderInfo orderInfo, Transaction transaction) { PaymentInfo paymentInfo = new PaymentInfo(); paymentInfo.setOrderId(orderInfo.getId()); paymentInfo.setPaymentType(PaymentTypeEnum.WEIXIN.getStatus()); paymentInfo.setOutTradeNo(orderInfo.getOutTradeNo());//数据库字段的长度改成32 paymentInfo.setSubject(orderInfo.getTitle()); paymentInfo.setTotalAmount(orderInfo.getAmount()); paymentInfo.setPaymentStatus(PaymentStatusEnum.PAID.getStatus()); paymentInfo.setTradeNo(transaction.getTransactionId()); paymentInfo.setCallbackTime(new Date()); paymentInfo.setCallbackContent(transaction.toString()); baseMapper.insert(paymentInfo); }
FrontWXPayController
@ApiOperation("查询支付状态") @ApiImplicitParam(name = "outTradeNo",value = "订单id", required = true) @GetMapping("/queryPayStatus/{outTradeNo}") public Result queryPayStatus(@PathVariable String outTradeNo) { //调用查询接口 boolean success = wxPayService.queryPayStatus(outTradeNo); if (success) { return Result.ok().message("支付成功"); } return Result.ok().message("支付中").code(250); }
接口:WxPayService
/** * 查询订单支付状态 * @param outTradeNo * @return */ boolean queryPayStatus(String outTradeNo);
实现:WxPayServiceImpl
@Resource private PaymentInfoService paymentInfoService; @Override public boolean queryPayStatus(String outTradeNo) { // 初始化服务 NativePayService service = new NativePayService.Builder().config(rsaAutoCertificateConfig).build(); // 调用接口 try { QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义 request.setOutTradeNo(outTradeNo); request.setMchid(wxPayConfig.getMchId()); // 调用接口 Transaction transaction = service.queryOrderByOutTradeNo(request); Transaction.TradeStateEnum tradeState = transaction.getTradeState(); //支付成功 if(tradeState.equals(Transaction.TradeStateEnum.SUCCESS)){ OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo); //更新订单状态 orderInfoService.updateStatus(outTradeNo, OrderStatusEnum.PAID.getStatus()); //记录支付日志 paymentInfoService.savePaymentInfo(orderInfo, transaction); //通知医院修改订单状态 //组装参数 HashMap<String, Object> paramsMap = new HashMap<>(); paramsMap.put("hoscode", orderInfo.getHoscode()); paramsMap.put("hosOrderId", orderInfo.getHosOrderId()); paramsMap.put("timestamp", HttpRequestHelper.getTimestamp()); paramsMap.put("sign", HttpRequestHelper.getSign(paramsMap, "8af52af00baf6aec434109fc17164aae")); //发送请求 JSONObject jsonResult = HttpRequestHelper.sendRequest(paramsMap, "http://localhost:9998/order/updatePayStatus"); //解析响应 if(jsonResult.getInteger("code") != 200) { log.error("查单失败," + "code:" + jsonResult.getInteger("code") + ",message:" + jsonResult.getString("message") ); throw new GuiguException(ResultCodeEnum.FAIL.getCode(), jsonResult.getString("message")); } //返回支付结果 return true;//支付成功 } return false;//支付中 } catch (HttpException e) { // 发送HTTP请求失败 // 调用e.getHttpRequest()获取请求打印日志或上报监控,更多方法见HttpException定义 log.error(e.getHttpRequest().toString()); throw new GuiguException(ResultCodeEnum.FAIL); } catch (ServiceException e) { // 服务返回状态小于200或大于等于300,例如500 // 调用e.getResponseBody()获取返回体打印日志或上报监控,更多方法见ServiceException定义 log.error(e.getResponseBody()); throw new GuiguException(ResultCodeEnum.FAIL); } catch (MalformedMessageException e) { // 服务返回成功,返回体类型不合法,或者解析返回体失败 // 调用e.getMessage()获取信息打印日志或上报监控,更多方法见MalformedMessageException定义 log.error(e.getMessage()); throw new GuiguException(ResultCodeEnum.FAIL); } }
utils/request.js的响应拦截器中增加对250状态的判断
}else if (response.data.code !== 200) { 修改为 }else if (response.data.code !== 200 && response.data.code !== 250) {
在api/wxpay.js中添加的方法
//查询订单 queryPayStatus(outTradeNo) { return request({ url: `/front/order/wxpay/queryPayStatus/${outTradeNo}`, method: 'get' }) },
js中修改pay方法:每隔3秒查单
添加queryPayStatus方法
完善closeDialog方法:停止定时器
//发起支付 pay(){ if(this.isPayShow) return this.isPayShow = true this.payText = '支付中.....' wxpayApi.nativePay(this.orderInfo.outTradeNo).then((response) => { this.codeUrl = response.data this.dialogPayVisible = true //每隔3秒查单 this.timer = setInterval(()=>{ console.log('轮询查单:' + new Date()) this.queryPayStatus() }, 3000) }) }, //查询订单状态 queryPayStatus(){ wxpayApi.queryPayStatus(this.orderInfo.outTradeNo).then(response => { if(response.code == 250){ return } //清空定时器 clearInterval(this.timer) window.location.reload() }) }, //关闭对话框 closeDialog(){ //停止定时器 clearInterval(this.timer) //恢复支付按钮 this.isPayShow = false this.payText = '支付' }
//应答超时 //设置响应超时,支付成功后没有及时响应,可能继续轮询查单,此时数据库会记录多余的支付日志 try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); } //返回支付结果 return true;//支付成功
支付成功后,判断订单状态:
OrderInfo orderInfo = orderInfoService.selectByOutTradeNo(outTradeNo); //处理支付成功后重复查单 //保证接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的 Integer orderStatus = orderInfo.getOrderStatus(); if (OrderStatusEnum.UNPAID.getStatus().intValue() != orderStatus.intValue()) { return true;//支付成功、关单、。。。 } //更新订单状态 //记录支付日志 ......