相比支付宝支付,微信公众号支付的实现以及过程真的是比较复杂,而且坑多,都是血泪史。
首先,需要登录微信公众平台,https://mp.weixin.qq.com
查看微信支付的开发配置,这里就可以看到对应的支付授权目录以及测试目录,可以选择使用线上作为支付测试,但是不推荐。使用测试授权目录时,注意需要设置测试白名单,规定哪些人可以进行支付测试。
当然,我们有微信公众号,就肯定也便拥有了H5网站。公众号支付采用的支付方式属于JSAPI方式,查看JSAPI网页支付是否已经开通了权限,并配置好支付授权目录,该目录必须是发起支付的页面的精确目录,子目录下无法正常调用支付。
根据微信支付的文档,商户系统和微信支付系统主要交互:
- 商户server调用统一下单接口请求订单,api参见公共api【统一下单API】
- 商户server接收支付通知,api参见公共api【支付结果通知API】
- 商户server查询支付结果,api参见公共api【查询订单API】
统一下单API
商户系统先调用该接口在微信支付服务后台生成预支付交易单,商户订单号为商户系统内部的订单号,32个字符内、可包含字母, 其他说明见商户订单号,由商户自定义生成,微信支付要求商户订单号保持唯一性(建议根据当前系统时间加随机序列来生成订单号)。重新发起一笔支付要使用原订单号,避免重复支付;已支付过或已调用关单、撤销(请见后文的API列表)的订单号不能重新发起支付。
统一下单流程完成后,最主要是根据前几步骤生成的相关参数,获取对应的PrepayId,
请求的url为统一下单api:
请求的参数:
<xml> <appid><![CDATA[wx1exxxx]]></appid> <body><![CDATA[JSAPI_payment_test]]></body> <mch_id>1242312122</mch_id> <nonce_str><![CDATA[6aghlqz18duhfebole531dce0r7bw0td]]></nonce_str> <notify_url><![CDATA[http://xxxx.com/xxx]]></notify_url> <openid><![CDATA[ogGCluNRaxBTNFWZzS_kH-rRez_Q]]></openid> <out_trade_no><![CDATA[nraxbtnfwzzskhrrezq1434590817259]]></out_trade_no> <spbill_create_ip><![CDATA[119.161.230.131]]></spbill_create_ip> <total_fee>1</total_fee> <trade_type><![CDATA[JSAPI]]></trade_type> <sign><![CDATA[F415B11A1C1B4894085FD703CBD14B71]]></sign> </xml>
将参数以POST方式发送给统一下单URL,返回值仍然也是xml格式:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> <appid><![CDATA[xxx]]></appid> <mch_id><![CDATA[1212]]></mch_id> <nonce_str><![CDATA[amxU3MOLatSWVzua]]></nonce_str> <sign><![CDATA[E458BE2C4F23C6F22B7561E74F41DEEF]]></sign><result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201506180927207ee0b107300739613144]]></prepay_id> <trade_type><![CDATA[JSAPI]]></trade_type> </xml>
我们只需获取其中的prepay_id即可;如果没有获得,直接返回错误信息。
上面所说的参数中,appid和mch_id属于公众号以及商户号id,申请公众号以及开通支付时就已经确定;notify_url属于微信支付服务器向服务端回调的接口(后续会用到);spbill_create_id是用户的ip;total_fee是付款总额;trade_type如果是公众号支付写死为JSAPI;out_trade_no是商户订单号,可在服务端通过文档中的算法生成即可;nonce_str是本次请求支付的随机字符串,最多32位。
不确定的就只有两项:
openid, 关于公众号中如何获取openid可以查看相关文档,在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。
在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的开发者中心页配置授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL:https://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html,授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。
获取Openid
在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。
公众号支付首先要设置微信支付的链接,通过该链接,就可以调用到微信后端以及服务端,支付的链接格式如下:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri={{url}}%2Fwxpay%2FpayModelAndView?parameterName=${xxxx}&response_type=code&scope=snsapi_base&state=123#wechat_redirect
scope设置为snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),虽然是snsapi_userinfo的时候可以获取更多信息,但需要弹出授权界面,而且我们也不需要获取那么多信息。
实际上需要微信服务器进行回调才能实现,而回调的redirect_uri为:
${url}/wxpay/payModelAndView?
其中appid即为注册的微信公众服务号ID,url参数即为当前网站的url,并带上coach_product_id,传送给回调地址,url需要进行URLDecoder,微信服务器会回调该服务。
通过code换取网页授权access_token
首先请注意,这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。
尤其注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。
redirect_url中就可以获取到对应的request Parameter,其中的code就是我们需要的编码。
获取code后,请求以下链接获取access_token:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
注意此URL必须在微信浏览器中打开,redirect_uri设置为当前controller方法对应的restful接口。
发送过来的code可以通过request.getParameter(“code”)来获取,如果没有生成该code,不能继续进行。
如果请求失败,记得别让用户还能重复使用这个code调用后台代码。也即,到支付页面后不能刷新。
返回的数据为json格式,如下:
{ "access_token": "OezXcEiiBSKSxW0eoylIeGhaJjUxzVpRR4o6hX-jAhOn160_GRNWPwzcWR_QSO4gbjzWHPV6zuNazuJp3spc2gptHLcR-g2QetMKeDGZ3IJD6PbJCf2YKyw6k4aeiFbdJgfJgNBXKfZ0dPb98IKR_w", "expires_in": 7200, "refresh_token": "OezXcEiiBSKSxW0eoylIeGhaJjUxzVpRR4o6hX-jAhOn160_GRNWPwzcWR_QSO4g7r7Y2BQy_p7bmrjxH8YN3scFXn7C4fUnNn9AFDcz_qW5ErAi4Lp9p18PcLv60yUtOBSwd8MfDIKap12lVExOAg", "openid": "ogGCluNRaxBTNFWZzS_kH-rRez_Q", "scope": "snsapi_base" }
其中,我们只需要获取其openid,然后进行下一步操作。
进行签名:sign
就剩下一个参数了sign,属于签名,关于签名的算法见文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3,算法用java进行实现如下:
public String getSign(Map<String, String> items, String APISecret) throws NoSuchAlgorithmException, UnsupportedEncodingException { Map<String, String> tmp = new TreeMap<String, String>(items); StringBuilder sb = new StringBuilder(); for(Map.Entry<String, String> entry : tmp.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if(StringUtils.isEmpty(value)) continue; sb.append(key).append("=").append(value).append("&"); } sb.append("key=").append(APISecret); return publicService.padStr(new BigInteger(1, MessageDigest.getInstance("MD5").digest(sb.toString().getBytes(CharEncoding.UTF_8))).toString(16).toUpperCase(), "0", 32); }
使用TreeMap对key进行字符串字典排序,附加上对应的key,后进行MD5计算并拍成32位并补零然后转成大写。
微信提供相关接口在线签名验证工具:https://pay.weixin.qq.com/wiki/tools/signverify/。
有了签名就可以进行统一下单相关操作了。
生成公众号支付接口所使用的jsapi调起支付的所有参数,返回给前端。参考微信公众平台相关文档:
其中package使用的是上一步的prepay_id=?,本系统中paySign采用MD5算法,其中的paySign采用统一签名生成算法来计算完成。
保存至服务端本地/数据库,页面发起支付
将用户信息,产品信息,生成的订单保存至数据库,以便在我方能够查询到该记录。
//将userId和out_trade_no等信息写入payment_result表 paymentPublicService.insertStubToPaymentResultTable(userId, PaymentResult.CHANNEL.WEIXIN, coachProductId, outTradeNo);
在从服务端转到页面上之后,再发起支付调用,跳转至付款页面
微信支付需要在回调之后跳转至付款页面(通过调试发现,这个最终付款界面还是必须存在的)。
页面中会调用真正的付款功能。
$(function(){ alert("xxxxxxxx"); callPay(); function onBridgeReady(){ WeixinJSBridge.invoke( 'getBrandWCPayRequest', { "appId" : '${appId}', //公e众号名称,由商户传入 "timeStamp": '${timeStamp}', //时间戳,自1970年以来的秒数 "nonceStr" : '${nonceStr}', //随机串 "package" : '${package1}', //预支付ID参数 "signType" : '${signType}', //微信签名方式: "paySign" : '${paySign}' //微信签名 }, function(res){ if(res.err_msg == "get_brand_wcpay_request:ok" ) { alert("支付成功"); window.location.href="/student/student_booking"; }else if(res.err_msg == "get_brand_wcpay_request:cancel" ){ alert("支付过程中用户取消"); window.location.href="student_pay.jsp"; }else{ alert('支付失败'); window.location.href="student_pay.jsp"; } } ); } function callPay(){ if (typeof WeixinJSBridge == "undefined"){ if( document.addEventListener ){ document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); }else if (document.attachEvent){ document.attachEvent('WeixinJSBridgeReady', onBridgeReady); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); } }else{ onBridgeReady(); } } })
生成完成之后,就可以进行支付,支付完成后,微信服务端就会通过设置的notify_url来进行回调通知,此时数据库端的订单信息就可以填充完整。
支付结果通知
发送过来的Request,得到对应的xml
String xml = IOUtils.toString(request.getInputStream(), CharEncoding.UTF_8);
商户处理后同步返回给微信参数,根据回调通知API,需要返回如下xml,才能让微信服务器确认已经接受到notify消息,否则微信服务器会多次retry调用我们的接口:
<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[OK]]></return_msg> </xml>
如果在服务端主动查询订单,可以查看对应的文档来进行操作,这里坑比较少就不详细说明了: