SpringBoot 2 集成微信扫码支付

前言

该文主要是手把手教你如何在SpringBoot 中集成微信扫码支付,以及集成的过程需要注意的问题事项。另外需要感谢 vbirdbest 关于微信支付和支付宝支付相关包博客总结。因为文中很多地方参考了vbirdbest的博客。 vbirdbest 博主关于支付宝和微信相关总结GitHub地址vbirdbest总结已经很好了,那么我为什么要在写一篇呢?我想通过另一种角度带你如何看微信文档并能自己实现它。以及我们需要注意的问题。毕竟涉及支付不能太马虎。

准备工作

在开发微信支付功能之前,首先要确保你的微信公众号是服务号,因为只有服务号才有申请微信支付的权限。

申请服务号支付权限,相当于在微信商户平台申请与之对应的商户平台账号。当商户平台帐号申请完毕后,微信会给你绑定的邮箱发送一个包含商户号的邮件,这个商户号很重要,后面微信支付 API 调用会用到。

扫码支付还需要在微信商户平台-账户设置-安全设置-api安全 设置一个32位 Key。这个Key是用来生成验证的sign使用的。具体使用后面会介绍到。

详细操作请直接参考 vbirdbest 博主的博客:Spring Boot入门教程(三十九):微信支付集成-申请服务号和微信支付:

查看微信开发文档

访问:https://pay.weixin.qq.com/wiki/doc/api/index.html 如下图所示,选择 Native 支付
在这里插入图片描述
扫码支付有2中模式:

  • 模式一:需要设置回调 URL 使用流程比较复杂。
  • 模式二:无需设置回调 URL 使用流程比较简单。

该文介绍使用的是模式二的方式,模式二业务流程说明如下:

(1)商户后台系统根据用户选购的商品生成订单。

(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;

(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。

(4)商户后台系统根据返回的code_url生成二维码。

(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。

(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。

(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。

(8)微信支付系统根据用户授权完成支付交易。

(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。

(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。

(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。

(12)商户确认订单已支付后给用户发货。

了解完业务流程后,需要查看微信统一下单API 文档。这里需要提示的是,这个统一下单API一定要好好阅读。

统一下单API 大致信息如下:

  • URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder 不需要证书
  • 处了总金额类型是int外,其他参数一律是String
  • 必须的参数有:服务商的 appid、商户号(mch_id)、随机字符串 nonce_str、签名 sign、商品描述 body、商户订单号 out_trade_no、总金额 total_fee、终端IP spbill_create_ip、通知地址(回调地址) notify_url 、交易类型 trade_type。

需要注意的内容如下:

  • 总金额是以分计算的,需要我们将金额进行转换
  • sign是将参数字典排序然后最后拼接Key(微信商户平台-账户设置-安全设置-api安全 设置一个32位 Key)进行MD5或HMAC-SHA256 进行加密的结果。
  • 通知地址 notify_url 必须是外网访问URL,并且不能携带参数。

签名算法和生成随机数算法规则: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3

需要使用的工具如下:
(签名校验工具): https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1

根据文档编写最简代码实现扫码支付

阅读完API 我们就可以开始写调用微信支付的测试代码了。其实微信支付的调用,就是把我们的商品信息以及商品的价格数据发送给微信,然后微信在把支付的code返回。我们根据支付code生成二维码后让用户扫码,用户付款成功后通过通知地址 notify_url 回调我们的系统,通知系统支付成功。

在写代码之前,先吐槽一下微信。我记得我们公司当时在对接微信时,微信连 Java 的 SDK 都没有。有啥问题也联系不上客服,当时对接的时候出现问题只能百度。还有就是文档啥的,跟阿里比差远了。废话少说开始我们的测试代码编写了。

创建 WeiXinPayTestController 然后定义 nativePay 访问方法

public class WeiXinPayTestController {
	
	private static final String UNIFIEDORDERURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
	private static final String GETSIGNKEYURL = "https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey";
	private static final String SANDBOXUNIFIEDORDERURL = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder";
	private static final String ORDERQUERYURL = "https://api.mch.weixin.qq.com/pay/orderquery";
	
	@Autowired
	private WeiXinPayProperties weiXinPayProperties;
	
	@Autowired
	private RestTemplate restTemplate;
	/**
	 * 正式支付
	 * @return
	 */
	@RequestMapping("/nativePay")
	public String nativePay(){
	}

我尝试用沙盒进行支付结果各种密钥不对,最后只好暂时先放弃了。

WeiXinPayProperties 是通过SpringBoot @ConfigurationProperties定义的配置文件类具体内容如下:

@Component
@ConfigurationProperties(prefix="wx.pay")
public class WeiXinPayProperties {
	
	/**合作身份者ID */
	private String appid;
	/** 商户号 */
	private String mchId;
	/** 商户号密钥 */
	private String appsecret;
	/** API 密钥 商户后台配置的一个32位的key 微信商户平台-账户设置-安全设置-api安全 */
	private String key;
	/**是否使用沙箱*/
	private String useSandbox;
	/** 沙箱环境API 密钥  */
	private String sandboxKey;
	/** 回调地址 */
	private String notifyUrl;
	省略get and set方法
}

微信支付Api的调用我们通过 RestTemplate 来完成。

在 nativePay 方法内配置微信的基础信息:公众账号ID、商户号,随机字符串、终端IP、交易类型

Map<String, String> requestData = new HashMap<String, String>();
requestData.put("appid", weiXinPayProperties.getAppid());//公众账号ID
requestData.put("mch_id", weiXinPayProperties.getMchId());//商户号
requestData.put("nonce_str", RandomUtil.randomString(15));//随机字符串 32位以内
requestData.put("spbill_create_ip", "15.23.160.111");
requestData.put("trade_type", "NATIVE");//交易类型 扫码支付

spbill_create_ip APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。

配置微信支付自定义支付信息参数

requestData.put("attach", "附加数据远洋返回");
requestData.put("body", "订单号 BW_000001");//商品简单描述
requestData.put("out_trade_no", "BW_000001");//商户订单号
requestData.put("total_fee", WeiXinUtil.getMoney("0.01"));//标价金额 按照分进行计算
requestData.put("notify_url", "www.beiwaiclass.com");//通知地址 异步接收微信支付结果通知的回调地址必须外网访问 不能携带参数

配置微信支付sign信息参数

String sign = null;
String payUrl = null;
if(Boolean.valueOf(weiXinPayProperties.getUseSandbox())){
	 sign = WeiXinUtil.generateSign(requestData,weiXinPayProperties.getKey());//生成签名
	 payUrl = UNIFIEDORDERURL;
}else{
	 sign = WeiXinUtil.generateSign(requestData,weiXinPayProperties.getSandboxKey());//生成签名
	 payUrl = SANDBOXUNIFIEDORDERURL;
}

requestData.put("sign", sign);

将配置Map信息转换成String

String mapToXmlStr = XmlUtil.mapToXmlStr(requestData, "xml");

将mapToXmlStr作为参数调用微信统一下单Api

	HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_XML);
		HttpEntity<String> formEntity = new HttpEntity<>(mapToXmlStr, headers);
		ResponseEntity<String> postForEntity = restTemplate.postForEntity(payUrl, formEntity, String.class);
		
//获取微信返回的信息
String returnXmlString = postForEntity.getBody();
Map<String, Object> xmlToMap = XmlUtil.xmlToMap(returnXmlString);
String returnCode = (String)xmlToMap.get("return_code");
if("SUCCESS".equals(returnCode)){
	String codeUrl = (String)xmlToMap.get("code_url");
	return codeUrl;
}

前台访问的页面(gotoNativePage.html)代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body >

<h3>购买商品:苹果</h3>
<h3>价格:20</h3>
<h3>数量:10个</h3>
<div id="qrcodeCanvas"></div>

<button style="width: 8%; height: 30px; alignment: center; background: #b49e8f" onclick="commitOrder()">生成支付二维码</button>
<script type="text/javascript" src="/sbe2/jquery-1.8.3.min.js"></script>
<script type="text/javascript" src="/sbe2/jquery.qrcode.js"></script>
<script type="text/javascript" src="/sbe2/qrcode.js"></script>
<script>
    function commitOrder() {
        $.ajax({
            type: "POST",
            url: "http://localhost:8090/sbe2/wx/naitvePay",
            data: null,
            success: function(data) {
            	jQuery('#qrcodeCanvas').qrcode({
            		text: data,
            		typeNumber:-10
            	});	
            }

        })
    }
</script>


</body>
</html>

测试结果:
http://localhost:8090/sbe2/gotoNativePage.html
点击生成支付二维码会生成要支付的二维码在数量下面。
在这里插入图片描述
支付回调的代码:

		/**
	 * 支付回调
	 * @throws Exception 
	 */
	@RequestMapping("/wxNotify")
	public void wxNotify(HttpServletRequest request) throws Exception{
		Map<String, String> parseNotifyParameter = parseNotifyParameter(request);
		String sign = WeiXinUtil.generateSign(parseNotifyParameter,weiXinPayProperties.getKey());//生成签名
		if(sign.equals(parseNotifyParameter.get("sign"))){
			//支付成功
		}
	}
    /**
     * 从request的inputStream中获取参数
     * @param request
     * @return
     * @throws Exception
     */
    public Map<String, String> parseNotifyParameter(HttpServletRequest request) throws Exception {
        InputStream inputStream = request.getInputStream();
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length = 0;
        while ((length = inputStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, length);
        }
        outSteam.close();
        inputStream.close();

        // 获取微信调用我们notify_url的返回信息
        String resultXml = new String(outSteam.toByteArray(), "utf-8");
        Map<String, String> notifyMap = WeiXinUtil.xmlToMap(resultXml);
        return notifyMap;
    }

支付回调需要外网,如果你想进行测试可以进行本地域名映射到外网进行整体的测试。因为账号我无法在进行更改,所以就没有做。但是代码是没有啥大问题的。

使用官方 SDK 实现扫码支付

我们已经简单的完成微信支付的集成,但是个人建议使用官方的SDK ,因为其他的SDK可能有XXE漏洞。关于XXE漏洞官方处理方案 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5
在这里插入图片描述
下载JAVA版的SDK,然后导入到将SDK 通过 mvn install 安装到本地仓库中。然后再SpringBoot中引入 SDK 的依赖。
我下载的 SDK 版本是:3.0.9

		<dependency>
		    <groupId>com.github.wxpay</groupId>
    		<artifactId>wxpay-sdk</artifactId>
    		<version>3.0.9</version>
    	</dependency>

你也可以根据自己的需要对SDK进行修改。
依赖添加完毕后第一步需要集成 微信SDK的配置抽象类:

@Component
@ConfigurationProperties(prefix="wxpay")
public class WeiXinOfficPayProperties extends WXPayConfig {

	private String appID;
	private String mchID;
	private String key;
	private InputStream certStream;
	private int httpConnectTimeoutMs;
	private int httpReadTimeoutMs;
	private IWXPayDomain WXPayDomain;
}

微信扫码支付代码:

	/**
	 * 微信扫码支付
	 * @return
	 */
	@RequestMapping("/naitvePay")
	public String naitvePay(){
		try {
			WXPay wxpay = new WXPay(weiXinPayProperties);
			 Map<String, String> data = new HashMap<String, String>();
		        data.put("body", "购买苹果10个");
		        data.put("out_trade_no", "2016090910595900000012");
		        data.put("device_info", "");
		        data.put("fee_type", "CNY");
		        data.put("total_fee", "1");
		        data.put("spbill_create_ip", "123.12.12.123");
		        data.put("notify_url", "http://www.example.com/wxpay/notify");
		        data.put("trade_type", "NATIVE");  // 此处指定为扫码支付
		        data.put("product_id", "12");
		        Map<String, String> resp = wxpay.unifiedOrder(data);
		        String returnCode = resp.get("return_code");
				if("SUCCESS".equals(returnCode)){
					String codeUrl = resp.get("code_url");
					return codeUrl;
				}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

微信支付的回调:

/**
	 * 微信支付成功后回调地址
	 * @param request
	 */
	@RequestMapping("/wxNotify")
	public void wxNotify(HttpServletRequest request) {
		
		try {
			Map<String, String> notifyMap = WeiXinPaySupportUtil.getNotifyParameter(request);
			WXPay wxpay = new WXPay(weiXinPayProperties);
	        if (wxpay.isPayResultNotifySignatureValid(notifyMap)) {
	            // 签名正确
	            // 进行处理。
	            // 注意特殊情况:订单已经退款,但收到了支付结果成功的通知,不应把商户侧订单状态从退款改成支付成功
	        }
	        else {
	            // 签名错误,如果数据里没有sign字段,也认为是签名错误
	        }
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

进行测试:
http://localhost:8090/sbe2/gotoNativePage2.html
在这里插入图片描述

需要说明的是官方 3.0.9 Java SDK 正式支付生成 sign 默认使用的是 HMAC-SHA256

小结

微信支付集成并不是很难,但是集成的时候有很多地方要注意:

  • 首先是 sign 的生成需要排序传递参数,但是不包含 sign 本身这个字段。
  • 回调地址 notify_url 切记需要外网访问,写完回调的方法后建议自己先调用一下看看能不能调用成功。
  • 订单在重复支付的时候,要保证支付的参数和第一次支付一致,否则会生成code失败。
  • 如果没有使用微信支付官方SDK,要注意XXE漏洞。也可以直接通过前台轮询进行查询的方式判断是否支付成功。
  • 后台要添加手动判断订单是否支付成功,并且同步的操作。防止订单支付成功,用户没有够买到的情况。

代码示例

具体代码示例请查看 :
博客的读者可以通过查看下面仓库的中的模块工程名: spring-boot-2.x-wxpay

Github:spring-boot-2.x-wxpay
如果您对这些感兴趣,欢迎 star、或转发给予支持!转发请标明出处!

参考文献

Spring Boot入门教程(四十一):微信支付集成-扫码支付:

Spring Boot入门教程(三十九):微信支付集成-申请服务号和微信支付:

发布了136 篇原创文章 · 获赞 502 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/ljk126wy/article/details/99554766
今日推荐