一、吐槽篇
一个字——坑!两个字——很坑!三个字——非常坑!首先,微信支付接口作为微信开发接口的一部分,竟然有一本书那么厚的官方文档,共36页,更重要的是,这36页还不能把开发的流程说清楚,描述过于分散,过度分类,导致遇到一个问题的时候很难定位,虽然文档前面给了时序图,但是开发流程的时序化仍然不够,让人觉得十分混乱。本来接手这个任务的时候时间就非常的紧,想着找个demo撑死一两天就完事了,没想到最终耗费了接近一个星期,除去中间别的事耽误的时间也用了整整五天的时间,在网上查找资料的时候也看到很多人说一个问题卡好几天甚至几周的情况,实在是难以接受。
二、开发流程
我这里就一并按顺序总结了。首先我用的是JS-SDK,即官方文档所描述的方式去做,必须说明的是,虽然是JS-SDK,但是整个签名过程不可能只在JS里完成,由于安全性问题,微信提供的接口JS是无法跨域调用的,必须在后台进行调用再将结果发给前台。OK,我的开发的宏观情况是,前端使用JS-SDK,后端使用JAVA tomcat。
总体流程是:①授权部分:后端通过微信接口拿到access_token,再拿到ticket(jsapi_ticket),给前台,前台通过js-sdk将jsapi_ticket,noncestr,timestamp,url签名,获取授权。
②支付部分:前端获取code,给后端,后端通过code访问微信接口拿到openid,通过openid等一堆key信息和回调url拿到prepayid,把prepayid,timestamp,noncestr给前台,前台通把这些东西以及签名提交,发起支付。
③回调部分:支付成功后,支付部分填写的url的对应方法会执行,拿到后从参数中拿到支付成功后的信息,校验签名,发送成功结果给支付发起端。
④完成部分:前端收到成功的消息,自定义跳转页面。
1、授权部分
js部分:
wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名,见附录1 jsApiList: ["chooseWXPay"] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 });
这是发起授权申请的部分,signature需要签名的字段包括:
"jsapi_ticket="+ticket+"&noncestr="+noncestr+"×tamp="+timestamp+"&url="+url;
说明:
①jsapi_ticket获取:
需要调用接口获取,调用接口不能用js(跨域),所以在后台进行,接口调用使用https://api.weixin.qq.com/cgi-bin/ticket/getticket,GET方法即可,参数是"access_token="+access_token +"&type=JSAPI"
②access_token获取:
调用https://api.weixin.qq.com/cgi-bin/token,同样使用GET方法,参数是"appid="+appid+"&secret="+secret+"&grant_type=client_credential",只要参数不错,就能拿到jsapi_ticket了。
(后面获取opendid的时候也会拿到一个access_token,但不是同一个!)
③nonceStr、timestamp在JS生成即可,记得签名时候的timestamp和nonceStr要和参数填写的一样。
④签名规则:
就是在上面那一串东西后面加上"&key=xxxx",然后MD5加密处理后,变成大写,key是商户的key,千万别搞错了,不是appkey。规则具体看这里。
⑤appId,这里I是大写的
⑥url的获取:
这个url不需要urlencode,但是必须是当前发起支付页的url,若url中有#号,只取#前面的。下面代码直接拿走吧!
location.href.split("#")[0]
⑦invalid signature:如⑥所说,url输入的不对,url要动态获取
⑧invalid url domain :url和appid没绑定,要在微信公众号那里的功能配置里配置,别把安全域名和业务域名搞混了……(我就在这里卡了半天)
配置成功后,就会进入
wx.ready(function(){
}
这个函数里面,并且如果debug打开,能看到configok的反馈。
2、支付部分
为了确保wx.config成功后再支付(否则不可能支付),请把下面函数放到wx.ready里面:
wx.chooseWXPay({ timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: '', // 支付签名随机串,不长于 32 位 package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' paySign: '', // 支付签名 success: function (res) { // 支付成功后的回调函数 } });
首先说明填写要怎么填,别的应该都好理解,package这个东西有点不明所以,这个东西官方文档叫做扩展包,但是在整个官方文档里却又找不到扩展包要填什么,只知道肯定要填"package=prepay_id=xxxx",我翻查了旧版微信支付的demo等,知道了这个扩展包主要是填写一些选填的东西,例如币种、编码等,但这并不是必须的,用最简单的"package=prepay_id=xxxx"填写就可以了。
先给出要签名的字符串列表
String signstr = "appid="+appid+"&body="+body+"&mch_id="+mch_id+"&nonce_str="+nonce_str+"¬ify_url="+ notify_url +"&openid="+openid+"&out_trade_no="+out_trade_no+"&spbill_create_ip="+spbill_create_ip+"&total_fee="+total_fee+"&trade_type=JSAPI"+"&key="+appkey;
说明
①签名必须在后端完成,前端timestamp和nonceStr必须和后端签名时用的值一致。
②notify_url不需要urlencode,回调地址最好不要加参数(回调地址是支付成功后通知的那个地址接口,接口内容自己完成)
③openid的获取:
可以看官方文档,这些文档都不是一个文档,所以很容易找不到。
具体做法:
首先,在客户端点击支付后,必须先跳转到这个地址:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
1)redirect_uri,是你使用JS-SDK的页面的地址,也就是wx.chooseWXPay所在页面的地址,必须进行urlencode,跳转过去后能在url中尾部看到?code=CODE&state=STATE,把CODE从url中取出来。
2)使用GET方法调用这个接口https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code,把刚取到的code填进去,还有别的参数不要填错了。
3) 返回的json格式中就能取到openid了。
④prepayid的获取:
这部分仍然在后台进行,使用post并且是XML格式发送数据,也就是说发送一段XML格式的字符串就是了,格式如下:
String text = "<xml>"+ "<appid>"+appid+"</appid>"+ "<body>"+body+"</body>"+ "<mch_id>"+mch_id+"</mch_id>"+ " <nonce_str>"+nonce_str+"</nonce_str>"+ " <notify_url>"+notify_url+"</notify_url>"+ "<openid>"+openid+"</openid>"+ "<out_trade_no>"+out_trade_no+"</out_trade_no>"+ "<spbill_create_ip>"+spbill_create_ip+"</spbill_create_ip>"+ "<total_fee>"+total_fee+"</total_fee>"+ "<trade_type>"+"JSAPI"+"</trade_type>"+ "<sign>"+sign+"</sign>"+ "</xml>";
说明:
1)spbill_create_ip是支付终端的IP地址
2)notify_url不用urlencode
3)total_fee不能是小数,因为单位是分
4)签名上面已经给了,就是用key,value方式把除了sign以外的所有字符串连起来,最后加上"&key=xxx",然后MD5再转大写。
5)out_trade_no,这个是自己标记自己商品号的一个号,不能重复,所以建议用时间戳+一些自己的规则。
6)body是中文时报签名错误:首先确保中文是utf-8,然后转成iso8859-1,可以用下面这句代码:
text = new String(text.getBytes(), "iso8859-1");
注意:①假如是自己在本地调用接口,写死一个中文去测试,请确保你的页面编码是utf-8,像我这种不太注意的,本来页面用了GBK编码,所以怎么试都不成功,怎么查编辑文件用什么编码,自行谷歌百度自己的编辑器的设置编码在哪里。
②tomcat的编码,如果是从ajax把中文的body传到后台,请注意tomcat的编码设置,确保拿到的中文是utf-8。
7)上面的做好了,就可以拿到prepayid了,将prepayid和自己生成的nocestr和timestamp以及刚才生成的签名sign传回给前台,并且填到wx.chooseWXPay里,尝试支付。
8)“商户签名错误”:假如已经拿验算工具查过,签名是正确的,仍然报错,那必然是这个原因——timeStamp,S是大写的!!。虽然官网已经做出了提醒,但是绕了那么多圈之后你早就不记得这件事了,或者说没搞清楚,没错,签名的时候timeStamp的S必须是大写的,然后前台的wx.chooseWXPay里的timestamp是小写的,就是那么的神奇!
9)当前的页面URL未注册:
微信公众账户里的微信支付-支付测试,看看填写的地址是不是支付所在的地址。
10)系统繁忙,正在升级,请稍后再试:
基本上不会是系统繁忙,这是我整个过程遇到微信接口唯一的一次报错错误,请检查签名参数,着重检查中文编码问题。
3、支付部分
终于,突过重重难关,弹出支付图标并且可以支付了,但是怪并没有打完,你的回调接口可能还没完善,这篇文章对回调部分的描述挺不错的,网上能下到JAVA的DEMO,但有些部分已经不能用了,主要不能用的部分是拿到数据的时候需要用inputstream去处理,而不能通过request直接读取。
回调要要做的事:
①拿到反馈数据
只要支付成功了,微信会推送一个XML格式的数据过来,JAVA请用下面的代码解析出来
public static byte[] readBytes(InputStream in) throws IOException { byte[] temp = new byte[in.available()]; byte[] result = new byte[0]; int size = 0; while ((size = in.read(temp)) != -1) { byte[] readBytes = new byte[size]; System.arraycopy(temp, 0, readBytes, 0, size); result = SystemUtil.mergeArray(result,readBytes); } return result; }
②校验签名
微信支付传过来数据,但未必是对的,所以把里面的签名拿出来,把别的变量拿出用keyvalue同样的方式加密再和签名对比,一样的就是对的。
③返回SUCCESS
④微信会在30分钟内进行8次回调,这是担心你网络问题等拿不到支付后的数据,所以你要判断这个东西是否已经成功处理过了
⑤处理:不外乎就是拿出订单号等,写入自己的数据库里。
4、完成部分
wx.chooseWXPay({ timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: '', // 支付签名随机串,不长于 32 位 package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' paySign: '', // 支付签名 success: function (res) { // 支付成功后的回调函数 } });
看到“支付成功后的回调函数”了吗?就在这里面写完成后跳转的页面,location.href=xxx,完结了~
大功告成……唉,真不好意思说大功,像支付宝一样的话也就一天半天的事,这个却整了那么久。
给官方的建议是,Demo能否出现在显眼的位置,能否做一个针对DEMO的文档或者说明(例如这个demo是适合哪个版本的都没说),对DEMO做相应修改或者填写各种key和id就行了,顶多再教一下配置些什么,毕竟很多东西都是死的,不需要用户全部敲一遍,只不过按照规则填就是了。