微信支付 1 开发准备 1.1 开发文档 微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
这里用的是微信公众平台,微信登录是微信开发平台,并且不能同一个邮箱,这个微信支付必须是服务号且认证过的。所以鑫哥又没办法自己申请了,还是用的我们的大尚硅谷的,强势给尚硅谷打一波广告…..希望大家去B站关注一波吧…..
1 2 3 4 1. appid:微信公众账号或开放平台APP的唯一标识 2. mch_id:商户号 (配置文件中的partner) 3. partnerkey:商户密钥 4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
1.2 微信支付模式 这是官方的模式二,模式一是用户自己选择支付金额,二维码永久生效
这个模式二是系统生成价格,然后二维码只能使用一次
业务流程说明:
1 2 3 4 5 6 7 8 9 10 11 12 1.商户后台系统根据用户选购的商品生成订单。 2.用户确认支付后调用微信支付【统一下单API】生成预支付交易; 3.微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。 4.商户后台系统根据返回的code_url生成二维码。 5.用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。 6.微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。 7.用户在微信客户端输入密码,确认支付后,微信客户端提交授权。 8.微信支付系统根据用户授权完成支付交易。 9.微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。 10.微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。 11.未收到支付通知的情况,商户后台系统调用【查询订单API】。 12.商户确认订单已支付后给用户发货。
1.3 微信支付SDK 微信支付提供了SDK, 大家下载后打开源码,install到本地仓库。
(鑫哥是没发现有什么用…..倒不如直接看我文档手摸手一步一步带你搭建,最后源码也会放我gitee上)
使用微信支付SDK,在maven工程中引入依赖
1 2 3 4 5 6 <dependency > <groupId > com.github.wxpay</groupId > <artifactId > wxpay-sdk</artifactId > <version > 0.0.3</version > </dependency >
我们主要会用到微信支付SDK的以下功能:
获取随机字符串
1 WXPayUtil.generateNonceStr()
MAP转换为XML字符串(自动添加签名)
1 WXPayUtil.generateSignedXml(param, partnerkey)
XML字符串转换为MAP
1 WXPayUtil.xmlToMap(result)
微信支付需要引入的依赖
1 2 3 4 5 6 <dependency > <groupId > com.github.wxpay</groupId > <artifactId > wxpay-sdk</artifactId > <version > 0.0.3</version > </dependency >
1.4 HttpClient工具类 HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。
HttpClient通俗的讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用HttpClient.
关于HttpClient(原生)具体的使用不属于我们本章的学习内容,我们这里这里为了简化HttpClient的使用,提供了工具类HttpClient(对原生HttpClient进行了封装)
HttpClient工具类代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 public class HttpClient { private String url; private Map<String, String> param; private int statusCode; private String content; private String xmlParam; private boolean isHttps; public boolean isHttps () { return isHttps; } public void setHttps (boolean isHttps) { this .isHttps = isHttps; } public String getXmlParam () { return xmlParam; } public void setXmlParam (String xmlParam) { this .xmlParam = xmlParam; } public HttpClient (String url, Map<String, String> param) { this .url = url; this .param = param; } public HttpClient (String url) { this .url = url; } public void setParameter (Map<String, String> map) { param = map; } public void addParameter (String key, String value) { if (param == null ) param = new HashMap <String, String>(); param.put(key, value); } public void post () throws ClientProtocolException, IOException { HttpPost http = new HttpPost (url); setEntity(http); execute(http); } public void put () throws ClientProtocolException, IOException { HttpPut http = new HttpPut (url); setEntity(http); execute(http); } public void get () throws ClientProtocolException, IOException { if (param != null ) { StringBuilder url = new StringBuilder (this .url); boolean isFirst = true ; for (String key : param.keySet()) { if (isFirst) { url.append("?" ); }else { url.append("&" ); } url.append(key).append("=" ).append(param.get(key)); } this .url = url.toString(); } HttpGet http = new HttpGet (url); execute(http); } private void setEntity (HttpEntityEnclosingRequestBase http) { if (param != null ) { List<NameValuePair> nvps = new LinkedList <NameValuePair>(); for (String key : param.keySet()) { nvps.add(new BasicNameValuePair (key, param.get(key))); } http.setEntity(new UrlEncodedFormEntity (nvps, Consts.UTF_8)); } if (xmlParam != null ) { http.setEntity(new StringEntity (xmlParam, Consts.UTF_8)); } } private void execute (HttpUriRequest http) throws ClientProtocolException, IOException { CloseableHttpClient httpClient = null ; try { if (isHttps) { SSLContext sslContext = new SSLContextBuilder () .loadTrustMaterial(null , new TrustStrategy () { @Override public boolean isTrusted (X509Certificate[] chain, String authType) throws CertificateException { return true ; } }).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory ( sslContext); httpClient = HttpClients.custom().setSSLSocketFactory(sslsf) .build(); } else { httpClient = HttpClients.createDefault(); } CloseableHttpResponse response = httpClient.execute(http); try { if (response != null ) { if (response.getStatusLine() != null ) { statusCode = response.getStatusLine().getStatusCode(); } HttpEntity entity = response.getEntity(); content = EntityUtils.toString(entity, Consts.UTF_8); } } finally { response.close(); } } catch (Exception e) { e.printStackTrace(); } finally { httpClient.close(); } } public int getStatusCode () { return statusCode; } public String getContent () throws ParseException, IOException { return content; } }
2 微信支付二维码生成 2.1 代码实现 index.html 点击跳转到创建订单的 controller 这里订单Id测试的时候必须换!!!! 重复的微信服务器会返回错误提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title th:text ="${title}" > </title > </head > <body > <div > <font color ="black" > <strong > 微信支付测试</strong > </font > <br /> <br /> <a href ="/createOrder?orderNo=000001&total_fee=1" > 创建订单</a > </div > </body >
后端controller (最后复制了一个完整的类,可以直接跳过去粘贴到项目中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @RequestMapping("/createOrder") public String createNatvie (String orderNo, String total_fee ,Model modle) { try { Map m = new HashMap (); m.put("appid" ,"wx74862e0dfcf69954" ); m.put("mch_id" , "1558950191" ); m.put("nonce_str" , WXPayUtil.generateNonceStr()); m.put("body" , "测试一波在线支付" ); m.put("out_trade_no" , orderNo); m.put("total_fee" , total_fee); m.put("spbill_create_ip" , "127.0.0.1" ); m.put("notify_url" , "http://guli.shop/api/order/weixinPay/weixinNotify\n" ); m.put("trade_type" , "NATIVE" ); HttpClient client = new HttpClient ("https://api.mch.weixin.qq.com/pay/unifiedorder" ); client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb" )); client.setHttps(true ); client.post(); String xml = client.getContent(); Map<String,String> resultMap = WXPayUtil.xmlToMap(xml); System.out.println(resultMap); Map map = new HashMap (); map.put("out_trade_no" , orderNo); map.put("result_code" , resultMap.get("result_code" )); map.put("code_url" , resultMap.get("code_url" )); modle.addAttribute("map" ,map); return "pay" ; }catch (Exception e) { e.printStackTrace(); modle.addAttribute("map" ,"adwadwada" ); return "pay" ; } }
创建前端生成二维码的html
有两个js文件 其中一个是生成二维码的qrious.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > 二维码入门Demo</title > <script src ="jquery.min.js" > </script > <script src ="qrious.js" > </script > </head > <body > <div > 二维码付款</div > <img id ="myqrious" > <script th:inline ="javascript" > var code_url = [[${map.code_url }]] console .log (code_url) var qrious = new QRious ({ element :document .getElementById ("myqrious" ), size :250 , level :'H' , value :code_url }); function orderStatus ( ) { $.post ("returnUrl" ,{},function (data ) { if (data==1 ){ window .location .href ="/" } }); }; setInterval ("orderStatus()" ,3000 ) </script > <dev > </dev > </body > </html >
2.2 测试
1 2 3 4 5 6 7 {nonce_str=tZw3Eyx0I2vHEx5r, appid=wx74862e0dfcf69954, sign=ABDF977D369E83174B5170063D31F741, err_code=ORDERPAID, return_msg=OK, result_code=FAIL, err_code_des=该订单已支付, mch_id=1558950191 , return_code=SUCCESS} {nonce_str=gUMCr9HUMjsliHt1, code_url=weixin:
目前项目是支付是完成了,微信是扣钱了,但是数据库状态是什么,成功了吗,而且页面还在这二维码页面,不合适了。
因为这个支付有点绕,所以分开讲,下面就开始解决这些问题。
3 订单状态及回调 查询订单支付状态去微信服务器 因为支付了用户自己是肯定知道了,但是项目里面不知道(微信支付成功会异步回调给项目,但是万一项目宕机了呢,微信默认是会重试三次,如果还没收到需要自己去查询),所以需要自己去微信服务器查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @RequestMapping("/queryPayStatus") @ResponseBody public Map<String, String> queryPayStatus (String orderNo) { try { Map m = new HashMap <>(); m.put("appid" , "wx74862e0dfcf69954" ); m.put("mch_id" , "1558950191" ); m.put("out_trade_no" , orderNo); m.put("nonce_str" , WXPayUtil.generateNonceStr()); HttpClient client = new HttpClient ("https://api.mch.weixin.qq.com/pay/orderquery" ); client.setXmlParam(WXPayUtil.generateSignedXml(m,"T6m9iK73b0kn9g5v426MKfHQH7X8rKwb" )); client.setHttps(true ); client.post(); String xml = client.getContent(); Map<String, String> resultMap = WXPayUtil.xmlToMap(xml); return resultMap; }catch (Exception e) { return null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "transaction_id":"4200000723202008267389085785", "nonce_str":"Nf3taNRG42MDSIhN", "trade_state":"SUCCESS", "bank_type":"OTHERS", "openid":"oHwsHuIJfVKb5N9Yy3fyuzQxa3GE", "sign":"F20256D926418E00DA9D8D1548789276", "return_msg":"OK", "fee_type":"CNY", "mch_id":"1558950191", "cash_fee":"1", "out_trade_no":"00001231301", "cash_fee_type":"CNY", "appid":"wx74862e0dfcf69954", "total_fee":"1", "trade_state_desc":"支付成功", "trade_type":"NATIVE", "result_code":"SUCCESS", "attach":"", "time_end":"20200826211143", "is_subscribe":"N", "return_code":"SUCCESS" }
同步回调 用户扫码之后要修改数据库状态吧,也得给用户跳转到别的页面吧。
前端代码还是之前那个 pay.html,不过用到了后面的监听器了,每隔三秒监听业务状态,发生变化时,直接同步回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <body > <div > 二维码付款</div > <img id ="myqrious" > <script th:inline ="javascript" > var code_url = [[${map.code_url }]] console .log (code_url) var qrious = new QRious ({ element :document .getElementById ("myqrious" ), size :250 , level :'H' , value :code_url }); function orderStatus ( ) { $.post ("returnUrl" ,{},function (data ) { if (data==1 ){ window .location .href ="/" } }); }; setInterval ("orderStatus()" ,3000 ) </script > </body >
1 2 3 4 这里用给i复制代替业务逻辑,i是全局变量,并发直接打到,代码有很严重的问题,必须根据业务自己修改!!! 鑫哥这里只是用这个来代替数据库查询耗时操作 (PS:如果用过支付宝支付的话 支付宝支付有returnUrl直接传递到支付宝服务器,你只需要自己配置好地址就能自动支付完成回调了, 微信需要自己做一个监听器,就这里有点小小的区别)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int i=0 ; @RequestMapping("/returnUrl") @ResponseBody public int updateOrdersStatus () { i++; System.err.println("#############" +i); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) {e.printStackTrace();} if (i==10 ){ i=0 ; return 1 ; } return 0 ; }
异步回调 这个就是直接支付发送到微信服务器,扣款之后,微信服务器给你发送成功与否的消息,想想既让是微信回调,这波肯定得连接外网了,一波localhost肯定给你安排不了了,其中创建订单第一步就会让你发送一个异步回调的地址,如果不写异步回调地址,就只能自己主动去查询了,就是之前那个查询订单支付状态去微信服务器 ,实际开发中肯定项目部署上去,回调域名地址,但我这开发过程中,用个内网穿透也方便调试什么的,美滋滋。鑫哥为了担心有小老弟连这个都没用过,也懒得单独整篇博客了,小题大做,所以直接在这里教学一波。
发布在文章的末尾了 可以自己去看看。
异步回调地址如图(不影响视觉我直接截图了,就是前面的创建订单)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RequestMapping(value = "/notify/url") @ResponseBody public String notifyUrl (HttpServletRequest request) { System.out.println("----------异步回调--------" ); InputStream inStream; try { inStream = request.getInputStream(); ByteArrayOutputStream outSteam = new ByteArrayOutputStream (); byte [] buffer = new byte [1024 ]; int len = 0 ; while ((len = inStream.read(buffer)) != -1 ) { outSteam.write(buffer, 0 , len); } outSteam.close(); inStream.close(); String result = new String (outSteam.toByteArray(), "utf-8" ); Map<String, String> map = WXPayUtil.xmlToMap(result); System.out.println("==" +map); Map respMap = new HashMap (); respMap.put("return_code" ,"SUCCESS" ); respMap.put("return_msg" ,"OK" ); return WXPayUtil.mapToXml(respMap); } catch (Exception e) { e.printStackTrace(); } return null ; }
控制台输出
1 2 3 4 ----------异步回调-------- =={ transaction_id=4200000710202008262414000181 , nonce_str=5459 dab210484b818d8fb3a5819aeec1, bank_type=OTHERS, openid=oHwsHuIJfVKb5N9Yy3fyuzQxa3GE, sign=BB4ADF1E575D353CAF87F95C21C0184C, fee_type=CNY, mch_id=1558950191 , cash_fee=1 , out_trade_no=12312312 , appid=wx74862e0dfcf69954, total_fee=1 , trade_type=NATIVE, result_code=SUCCESS, time_end=20200826225114 , is_subscribe=N, return_code=SUCCESS}
这个接入微信支付可能真的有点小饶,关键上面那张图片兄弟们得理解了。
4.内网穿透 ①目的 让本地运行的项目可以通过外网访问。
②工作机制
③NATAPP内网穿透服务使用
1 2 3 4 5 6 7 8 9 10 [default] authtoken =79 a1980a333f8a5f clienttoken= log =none loglevel =ERROR http_proxy=
启动natapp.exe,如果上面操作成功,会看到下面效果:
④测试效果 [1]启动本地应用 本地应用监听的端口号需要和隧道一致,比如都是8080
[2]通过隧道暴露到外网的域名访问本地应用 例如:http://aatczu.natappfree.cc/apple/to/thymeleaf/page