微信公众号支付开发中的坑(Ruby on Rails示例)

前不久产品同事让在我们的应用里实现微信的公众号支付功能,我接手了这个开发任务后马上着手实现。花了一天半的时间把按照微信公众号支付文档把相关代码实现了,本以为胜利在望,没想到后面支付调试花去了我整整一个多周……目前该功能已完成,就把整个过程中遇到的一些坑和需要注意的地方总结一下吧!

设置微信支付测试目录和添加测试微信号

很多人在这儿就踩到坑了,其实要注意的地方就一点:是设置一个目录,支付页面只有在该目录下才能调用内置的H5支付接口进行支付,否则一律会报错。

举个栗子🌰,我希望我的应用内的支付行为发生在地址为http://example.com/orders/pay页面或者http://example.com/orders/pay/1页面,那我就应该在测试目录设置测试授权目录为http://example.com/orders/pay/,注意别漏了后面那个/(目录嘛!),否则那就等着报错吧。

设置好之后在测试白名单里添加你的微信号,这块儿没啥,别填成昵称什么的就行了,基本不会出错。

此外,就是官方说的,不要把测试支付目录和正式支付目录设置成相同的,否则也会支付出错。

获取预支付订单号prepay_id

根据微信公众号支付的业务流程说明,在H5网页发起支付前,需要先调用统一下单接口来获取prepay_id(因为后面h5页面调用微信支付时需要提交一个包含prepay_id的字段package)。获取该字段可以使用wx_pay这个gem,如果你们应用只用公众号支付的话也可以自己照着微信文档去实现了,也不难。

要注意以下几点:

  • 签名时要把值为空的参数排除
  • 发送的xml不用加入声明,根节点为xml,所以使用Hash#to_xml方法会报错,哪怕指定根节点为xml也会因为带入了声明信息而导致支付出错,处理方式拼接字符串还是使用Hash#to_xml后处理多余字符或者其他方式看个人吧。
  • openid是使用支付的公众号下的微信用户的openid,这点当时我查资料时发现竟然也有几个同学会在这块儿出错。微信官方文档描述过,同一个用户在不同公众号下的openid是不同的。值不对的情况下会调用统一下单接口会提示类似appid和openid不匹配的错误。
  • 微信支付成功的时候,微信会向你填写的notify_url发起一个post请求,里面包含了成功支付后的信息,注意是post请求,别在路由里设成了get……

通过config接口注入权限

微信郑重提示:所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用

好了,既然公众号支付接口也是通过微信JS-SDK完成的,那么我们也需要至少在支付页面做好这个准备!

按照上面那个链接里的JSSDK使用步骤,前两步不用多说,第三步里在做注入的时候只有一个signature字段的获取需要注意,该值的获取可参考文档,这块儿逻辑最好单独抽离出来,并且做好缓存防止调用接口的频率超过上限。

H5调起支付API

接口注入权限完成后,就可以发起h5支付了。支付的实现官网给了两个,我使用的是这个,如下:

1
2
3
4
5
6
7
8
9
10
wx.chooseWXPay({
timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '', // 支付签名随机串,不长于 32 位
package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: '', // 支付签名
success: function (res) {
// 支付成功后的回调函数
}
});

另一个实现在这里:微信支付文档里的H5调起支付API很多人说有问题,是旧版的文档,我一开始使用也是有问题,后面就用的第一种实现方式了。

这个步骤里可能会遇到的坑:

  • 参与支付签名的参数有appId、timeStamp、nonceStr、package、signType这五个参数,其中,注意timeStamp中的s一定要大些,但是微信jssdk中所使用的timestamp中的s小写
  • package的值是prepay_id=xxx,而不是直接使用prepay_id的值,并且prepay_id=xxx里的xxx没有引号包裹

具体实现过程

好了,到这儿,零零散散的坑点基本都说完了,再穿起来说一次吧:

首先,预想我们发起支付的页面是orders/pay页面(对应页面文件app/views/orders/pay.html.haml)。该页面我们会放一个表单,里面是支付时的一些信息用于向支付人展示,也包含一些支付订单需要的信息:

1
2
3
4
5
6
= simple_form_for(@order, url: submit_order_orders_path, remote: true) do |f|
= f.input :unit_price, readonly: true, input_html: { value: @product.unit_price }
= f.association :product, as: :hidden
= f.input :amount, required: true
= f.input :total_fee, as: :float
= f.submit 'submit', class: 'ui green button attached', value: t('.submit')

在上述代码可以看到我添加了remote: true选项,了解的同学应该知道我要使用SJR了。关于SJR不了解的同学可以看下一篇开源中国的翻译《服务器端生成的 JavaScript 响应》和Ruby China的一篇帖子《DHH 再次重申,Rails 项目应该拥抱 SJR,别去整啥 JSON-Client render 方案》,讲的不错,我就不献丑了。

在提交表后,orders#submit_order接收参数处理后去创建一条支付订单记录@order,同时调用统一支付订单接口去获取预支付订单信息prepay_id,获取方法上面已经说过,就不再赘述。之后,再生成微信调用H5支付所需信息和微信JSAPI注入配置信息赋值给一个实力变量@wechat_pay_config,这个实例变量存储的是一个hash,看起来大概是这个样子:

1
2
3
4
5
6
7
8
9
{
appId: 'appid',
timeStamp: Time.now.to_i,
nonceStr: SecureRandom.hex,
package: "prepay_id=#{prepay_id}",
signType: 'MD5',
signature: 'signature',
paySign: 'sign'
}

然后orders#submit_order里面的事儿就处理完了,最后就是执行views/orders/submit_order.js.coffee里的内容里,也就是发起H5支付的代码,看起来大概是这样:

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
#将实际发起支付的代码包装成一个方法方便调用
pay = ->
wx.chooseWXPay
#注意timestamp中的s是小写……
timestamp: '<%=j @wechat_pay_config[:timeStamp].to_s%>'
nonceStr: '<%=j @wechat_pay_config[:nonceStr]%>'
package: '<%=j @wechat_pay_config[:package]%>'
signType: '<%=j @wechat_pay_config[:signType]%>'
paySign: '<%=j @wechat_pay_config[:paySign]%>'
success: (res) ->
# 支付成功后则重定向到@order的详情页
Turbolinks.visit '<%=j url_for(@order)%>'
fail: (res) ->
# do something...
cancel: (res) ->
# do something...

if wx?
wx.config
debug: false
appId: '<%=j @wechat_pay_config[:appId]%>'
timestamp: '<%=j @wechat_pay_config[:timeStamp].to_s%>'
nonceStr: '<%=j @wechat_pay_config[:nonceStr]%>'
signature: '<%=j @wechat_pay_config[:signature]%>'
jsApiList:[
'chooseWXPay'
]
#如果生成的订单记录@order有效则发起H5支付调用
<% if @order.valid? %>
pay()
#如果记录无效则alert出错误信息
<% else %>
alert('<%=j @order.errors.full_messages.join(" ")%>')
<% end %>

然后支付成功(注意:是付款成功,而不是调出支付界面成功)后,微信会向之前填写的notify_url这个链接发起一个post请求,你需要处理这个请求并返回success字符串,否则微信会不停发起这个请求共计八次(具体重试通知方案以官方为准),代码大致如下:

1
2
3
4
5
6
# app/controller/orders_controller.rb

def notify
# deal with data from wechat notify
render text: 'success', layout: false
end

微信发送到这个notify url里的内容包含支付银行类型、商户订单号、支付时间、微信订单号等信息,各位看着情况处理吧,只用最后返回一个success字符串就好!

最后再强调一下注意的地方:

  • 公众平台设置的支付目录,是目录,所以必须要以反斜杠/结尾
  • 跟上面一条有关系,假如你支付目录设定的是http://example.com/orders/pay/的话,而你实际发起支付的页面文件是app/views/orders/pay.html.haml的话,那你页面地址正常情况下就是/orders/pay,但是请注意,这样是会支付失败的,你必须让你的地址变为/orders/pay/才行,解决办法有人推荐在url后拼接一个/,不过rails路由里的get方法后可以增加一个option`trailing_slash: true`,即最终看起来类似get 'pay', trailing_slash: true,然后在所有使用get请求方式访问pay action后的页面地址都会自动在地址末尾加上/,问题解决
  • 如果你不是像我这样使用的sjr发起支付,那么就一定要注意,在进入发起支付页面前,至少要先访问另外一个页面假如叫a页面,在a页面放一条进入实际支付页面的链接,这样点过去之后才能够正常发起支付,否则假如你是在聊天窗口发了一条链接然后直接从连天窗口点进支付页面的话,这个支付十有八九是会出错的,这个暂时还不知道为什么,微信文档里也没提,我看很多人都是在这儿被坑了很久(我就是在这儿被坑了几乎一个周,之前把支付页面地址发在聊天窗口,直接点进去后准备支付,死活调试不通过,最后从另一个页面进入,马上搞定,真心无语……)
  • 调用统一下单发送的报文体,使用Hash#to_xml方法的话记得处理掉声明信息
  • 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符

结束语

回头看看,其实整个支付开发过程不算麻烦,至少公众号支付没有说麻烦到无法开发。很多人在这儿遇到问题其实还是微信文档里有很多疏漏和前后不一致的地方,“微信文档是个坑”这说法不是没道理的……反正遇到问题多思考、多尝试、多查、多跟其他人交流,相信就算是神坑也不是多大的问题,祝大家代码无bug… : )

p.s. 整篇文章中提到的代码和一些流程还有很多要完善的地方,比如我使用sjr完成支付会因为我从用户点击提交表单到后台调用微信H5支付处理了大量的数据(生成本地支付订单、获取微信jsticket、调用统一下单支付api、包装支付配置参数)可能让用户的体验会有那么一点点差、还有处理微信支付成功后的通知需要考虑安全性等等等等,都有很大的优化空间,只不过这篇文章主要还是重在抛砖引玉,让大家注意有哪些坑,毕竟抛却这些坑,整个过程的实现对于一般的同学还是没什么难度的。废话到此,以后有空会补一个demo,不过可能没那么快,如果有谁有什么疑问或者意见的话,欢迎点击左侧的邮件图标沟通交流!: )

p.p.s. 微信web开发者工具已经放出了,大家可以到该链接下载使用,妈妈再也不怕我调试微信发脾气啦!