唠叨几句
因为被微信那个破烂文档坑了我两个星期,导致项目进度慢了很多。本来微信的 API 的确是设计得烂,但烂我也觉得不要紧了,文档也烂那我就真的火了,跟人捉迷藏似的东一块西一块(玩猜谜吗你)。这里也记录一下我做开发遇到的坑。
如何申请公众号以及商户平台不在本文范畴内,因为项目经理已经拿到这这些东西了,我所做的就是完成代码的编写。
环境
这里使用了 com.github.binarywang
的 jar,下面默认都是在这个前期下讨论。其他自己实现的或者其他人的库请配合文档和其他人分享的资料看。
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>2.8.0</version>
</dependency>
正文
首先有一个十分重要的提醒:
配合前端开发人员测试微信支付的时候千万千万不能用微信的沙箱。
那个沙箱没有他们文档说的那么厉害,只能确认微信回调我们服务器没问题,不能用来模拟测试整个支付流程,而且这个沙箱设计得十分垃圾,连沙箱都算不上(支付金额只允许用例内的。你见过只能画指定几个图案的沙箱吗??),所以还是乖乖地测一次给一分钱吧。
开发
一般开发主要用到微信的两种支付方式
- JSAPI : 用于在微信自己的浏览器里面唤起微信支付
- NATIVE : 用于扫码支付
JSAPI 不用多说,就是触发之后会唤起微信的支付对话框给用户选择支付与否。
NATIVE 就是生成订单之后给用户用微信扫码付款的。这个方式我看了文档,微信给本来的设计貌似是给那些自动贩卖机用的,不过稍微改变一下使用方法就可以适用于任意扫码支付。
JSAPI 和 NATIVE 两种支付方式均使用统一下单接口先获取微信预付款订单信息,然后再进行剩下的操作。两者传参几乎一样,不同的是:
- JSAPI 需要传入 openId,NATIVE 不需要。
- NATIVE 需要传入 productId,JSAPI 不需要。
上面所说的稍微改变一下使用方法,就是在 NATIVE 支付的时候 productId 使用自己的订单 ID 就好了。
NATIVE 支付方式
首先说这个支付方式是因为,这个方式很简单,而且我也很推荐用这个,但扫码就需要用另外一台手机了。
根据微信的文档向统一下单接口传入相应的参数之后,就得到预付款订单了。这里得到的预付款订单包含了参数 codeUrl
,传这个给前端开发的去生成二维码或者自己服务器端生成二维码都可以,扫码之后就可以用微信付款。付款成功之后,微信会通过预先指定的回调 API 发送订单支付的结果。根据结果完成自己的订单逻辑这个就不说了。
JSAPI 支付方式
我觉得这个就是最坑爹的地方了。
首先传入参数之后,微信返回了预付款订单信息,然后这个信息需要返回给前端。但是,这之前需要对预付款订单的某些字段拼接起来,作一次签名,签名需要严格按照字段序排序以及注意大小写:
appId
nonceStr
package
signType
timeStamp
StringBuilder params = new StringBuilder()
.append("appId=").append(wxPayService.getConfig().getAppId()).append("&")
.append("nonceStr=").append(nonceStr).append("&")
.append("package=").append("prepay_id=").append(orderResult.getPrepayId()).append("&")
.append("signType=").append("MD5").append("&")
.append("timeStamp=").append(timestamp);
//appId={appId}&nonceStr={nonceStr}&package=prepay_id={prepayId}&signType=MD5&timeStamp={timestamp}
(真心对微信大小写随便来表示很无语)其中
nonceStr
prepay_id
需要与微信返回的预付款订单内的一致。timeStamp
也要传给前端,到时候前端需要把这个 timeStamp
传进唤起微信支付对话框的函数。
拼接好这个之后,再在后面拼接参数 key
并进行一次 MD5。
params.append("&").append("key=").append(wxPayService.getConfig().getMchKey());
String prepay_sign = DigestUtils.getMD5(true, params.toString());
所以所需要传给前端的参数如下:
{
"appId":"你的 appId",
"nonceStr":"订单内的 nonceStr",
"timeStamp":"订单参数签名的时间",
"prepay_sign":"订单签名结果"
}
这时候不要急着去唤起微信的支付窗口,因为还有后面一系列步骤。
这里需要注意这些返回的参数:
nonceStr
timeStamp
这两个参数在整个支付的过程中要一致,而且参数大小写也需要注意。流程内的 API 有的地方给弄驼峰命名法有的地方则用全小写。
然后,前端需要再拿当前调用 JSAPI 支付的浏览器地址栏的路径,向服务器请求一个签名,这个签名就是前端唤起 JSAPI 所需要的签名,我这里请求的 API 以及示例如下:
POST -> https://shinonometn.com/WC/ticket
{
"url":"https://shinonometn.com/?#/order/11",
"nonceStr":"8897djsk09ll",
"timeStamp":1560789
}
url 那里一定一定要注意,对于 SPA 应用,路由前缀那里,绝对不能只有一个#
,微信的这个安全机制很傻屄。首先你需要去商户平台那里注册 JSAPI 支付允许的“支付目录”(我晕,目录),然后在调用 JSAPI 支付的时候他们会校验你地址栏”在“不”在“已注册的“支付目录”,不在就拒绝下单。我猜他们这个机制是做给服务器端渲染页面的应用做的:你会发现你的 SPA 应用拿到的 URL 经常跟他们微信拿到的 URL 不匹配,从而一直提示你 URL 未注册 然后拒绝下单。这其实不算坑,文档在很隐蔽的地方提及需要前端拿这个 sign 去调用 JSAPI 支付 and 支付前需要调用 config 一次才是坑死人。
这个签名是这样的,如下参数全小写,严格按照字典顺序排序:
-
jsapi_ticket
(我一直不知道这个东西的存在,因为文档里面没有提及) -
noncestr
(小写,小心) -
timestamp
(小写,小心) -
url
(就是上面提及的 URL)
然后如此拼接:
jsapi_ticket={你拿到的 JSAPI TICKET}&noncestr={订单上的 nonceStr}×tamp={你订单的 timeStamp}&url={URL}
//不算大括号,只是为了好看加上去的
然后对这这个拼接好的字符串,SHA1 一次,拿小写的字符串,返回给前端,那么前端就可以很愉快地填上对应的参数去 wx.config 一下,唤起微信支付窗口了。
那么这个 URL 在商户平台注册的时候需要注意什么呢?对于服务器端渲染页面,你需要填写支付页面的地址,删掉最后的”目录“:
//订单支付页面
https://shinonometn.com/order/pay/1
//注册的 URL
https://shinonometn.com/order/pay/
对于 SPA (单页应用)来说,你只需要填写你的应用地址,路由那里怎么方便怎么做手脚。
//带上路由的 SPA 的页面
https://shinonometn.com/?#/order/pay/1
^我就弄了个问号
//注册的 URL
https://shinonometn.com/
唉,就是因为 JSAPI 的沙雕设计我加班到凌晨 2 点陪前端的人调试。