用户在微信应用内部的浏览器打开我们应用的支付页面,页面会去请求一个登录凭证(code),通过这个登录凭证又会得到用户的 Openid,这个 Openid 就是加密之后的用户的微信号。
这个时候用户就可以按一下支付按钮请求支付,我们的应用会组织好好需要调用统一下单带的数据,去获取到一个微信支付的预支付。然后我们的应用会组织好公众号支付需要的一些参数, 再把它们返回给应用的前端,再由前端去调用微信的 JSAPI 来请求支付。请求支付会调起用户的微信支付功能,用户可以确认并且完成支付 。
支付成功以后,会把用户带回到我们指定的页面上,在页面上可以引导用户查询交易状态,如果查询的结果是 SUCCESS,可以把用户带到一个成功提示的页面。这个就是我们要实现的一个微信支付的公众号支付功能。
配置
在公众平台的功能设置里,可以设置一下网页授权域名。
在微信支付的开发设置里,设置一下公众号支付里的支付授权目录。
微信用户的 OpenID
实现公众号微信支付的关键一步就是获取到用户的 OpenID。这个 OpenID 的值会包含在用户的相关信息里面,在公众号里你可以向用户申请授权得到用户的信息。用户同意以后(如果请求得到用户相关信息会提示授权,如果只是请求 OpenID 的话不会出现授权提示),微信会给我们返回微信用户的相关信息,在这个相关信息里面,就会有这个 OpenID。
OpenID 其实就是加密之后的用户的微信号,在同一个应用之下,用户的 OpenID 是唯一的。不过要注意你可能会在同一主体下创建多个应用,比如网站应用,或者一些小程序什么的。微信用户在同一主体下的不同的应用里面,他们的 OpenID 是不一样的。
如果你想识别唯一的用户,那你需要开通并认证微信的开放平台,然后在开放平台去绑定同一主体下的不同的应用。这样你就有能力得到用户的 UnionID,你可以使用这个 UnionID 的值来确定某个特定的用户,因为在你绑定在开放平台里的不同的应用下面,用户的 UnionID 的值是一样的。
登录凭证(code)
想要获取用户的信息需要使用 access_token 去换,得到这个 access_token 得先有个 code,这个 code 就是我们说的登录凭证。获取到这个 code 我们得先去准备一些数据。先组织好获取登录凭证需要的一些数据,带着这些数据把用户重定向到微信授权地址上。如果没问题,会被重定向回我们自己指定的页面上,重定向回来的时候,地址上就会带着我们需要用的登录凭证。
配置
在项目里可以先去创建一个新的配置文件,文件的名字是 wexin.js,在里面可以存储一些跟微信相关的配置数据。这些数据我们会在项目的其它地方用到。下面是文件里面的内容,暂时添加了一个 open.auth,它的值是微信登录授权用的一个地址。在请求获取登录凭证的时候会用到这个地址。
config/weixin.js:
'use strict' module.exports = { open: { auth: 'https://open.weixin.qq.com/connect/oauth2/authorize' } }
获取登录凭证
获取登录凭证需要的代码,我们可以放在 CheckoutController 里面的 render 这个方法里。它个方法目前渲染的就是结账页面(/checkout),用户在访问这个页面的时候,可以试着去获取登录凭证。
app/Controllers/Http/CheckoutController.js:
/** * 结账页面。 * @param {Object} view * @return 渲染结账页面视图。 */ async render ({ view, request, response, session }) { /** * 获取申请 access_token 需要的 code。 */ const code = request.input('code') logger.debug('code: ', code) const appid = Config.get('wxpay.appid') if (!code) { const redirect_uri = `https://${ request.hostname() }${ request.url() }` const response_type = 'code' const scope = 'snsapi_base' const openAuthUrlParams = { appid, redirect_uri, response_type, scope } const openAuthUrlString = queryString.stringify(openAuthUrlParams) const openAuthApi = Config.get('weixin.open.auth') const openAuthUrl = `${ openAuthApi }?${ openAuthUrlString }` return response.redirect(openAuthUrl) } return view.render('commerce.checkout') }
在上面的 render 方法里, 我们先把 view,request,response,还有 session 解构出来。方法的一开始,添加了一个 code,表示登录凭证,使用 request.input 方法可以得到请求里面包含的数据。成功得到登录凭证返回到这个页面的时候,地址里面会包含 code 这个数据的值,它就是我们需要的登录凭证。
const code = request.input('code') logger.debug('code: ', code)
然后又去判断了一下,如果访问 checkout 页面的时候,没有得到 code 的值,就去组织好请求登录凭证需要的数据,然后重定向到微信授权地址。重定向的时候,用到了 response.redirect。
return response.redirect(openAuthUrl)
获取登录凭证需要的数据
请求登录凭证需要几个数据,这些数据要使用地址查询符(Query String)的形式发送到微信授权地址。 下面是几个必要的数据:
- appid:公众号应用 ID。
- redirect_uri:重定向到的地址,得到登录凭证以后会被重定向到这里设置的地址上。
- response_type:这里要设置成 code,因为我们需要的是登录凭证。
- scope:可以是 snsapi_base 或者 snsapi_userinfo,因为我们只想得到微信用户的 OpenID,所以可以设置成 snsapi_base,如果想获取用户的其它的信息,可以设置成 snsapi_userinfo。
我们把上面这些数据放在了一个叫 openAuthUrlParams 的对象里,然后又把这个对象转换成了地址查询符的形式。把转换之后的数据跟微信授权地址拼接在一起,组织成一个最终要重定向到的地址(openAuthUrl)。
测试
把应用的 /checkout 页面地址发送给微信用户,用户在微信内部浏览器打开这个页面以后,我们再回到项目里面检查 app.log 这个日志文件。你会发现,第一次访问 /checkout 页面的时候,要求输出的 code(登录凭证)的值是 undefined,这样就会把用户重定向到微信授权地址,成功以后,再次返回 /checkout 页面,这时候请求里面就会带着 code 的值了。所以第二条记录里面,code 是有值的。
app.log
10:08:25 DEBUG - code: undefined 10:08:39 DEBUG - code: 011O1Na51dTrvM1Z7Ja51XNOa51O1Nai
获取 access_token
有了登录凭证(code)以后,下一步就可以去请求得到 access_token,这里面会包含 access_token 本身,还有我们需要的微信用户的 OpenID。
配置
先添加两个配置,一个是 appSecret,它是公众账号的应用密钥,你在公众平台的管理后台可以得到这个密钥的值。我们可以把这个密钥的值放在项目的 .env 文件里面,这里我给它起了一个名字叫 WXMP_APP_SECRET,也就是在 .env 文件里,添加一个叫 WXMP_APP_SECRET 的东西,对应的值就是公众平台里面的应用的密钥。
然后再修改配置文件,添加一个 appSecret,对应的值可以使用 Env.get 去得到 .env 文件里面添加的环境变量的值。在 weixin.js 这个配置文件里,我又添加了一个 api.accessToken,它的值是请求 access_token 的时候需要用的接口的地址。
config/weixin.js:
'use strict' const Env = use('Env') module.exports = { appSecret: Env.get('WXMP_APP_SECRET'), open: { auth: 'https://open.weixin.qq.com/connect/oauth2/authorize' }, api: { accessToken: 'https://api.weixin.qq.com/sns/oauth2/access_token' } }
控制器
请求 access_token 需要的代码我们也可以把它们放在 CheckoutController 里的 render 方法里面。要做的就是先去组织好请求 access_token 需要的数据,把数据转换成地址查询符的形式,然后把转换之后的数据跟 access_token 接口地址拼接在一起,再用一个 HTTP 客户端(比如 axios)去请求这个地址。得到的响应里面,就会包含 access_token 相关的数据。
改造 render 方法,添加下面这些代码:
/** * 获取 access_token */ const secret = Config.get('weixin.appSecret') const grant_type = 'authorization_code' const accessTokenUrlParams = { appid, secret, grant_type, code } const accessTokenUrlString = queryString.stringify(accessTokenUrlParams) const accessTokenApi = Config.get('weixin.api.accessToken') const accessTokenUrl = `${ accessTokenApi }?${ accessTokenUrlString }` const wxResponse = await axios.get(accessTokenUrl) logger.debug('accessToken: ', wxResponse.data) session.put('accessToken', wxResponse.data)
请求 access_token 需要的数据
- appid:公众号应用 ID。
- secret:公众号应用密钥。
- grant_type:授权类型,这里要设置成 authorization_code。
- code:登录凭证。
我把上面这些数据放在了一个叫 accessTokenUrlParams 的对象里,又把这个对象转换成了地址查询符形式的字符串,然后把它跟 access_token 拼接在了一起,接着用 axios 的 get 方法去请求最终组织好的地址。得到的响应给它起了个名字叫 wxResponse,它里面的 data 属性的数据就是我们需要的东西。
我们把需要的数据输出到了应用的日志文件里了,然后又把这个数据放在了 session 里面存储起来了。这样在支付的时候,可以读取用户 session 里的数据,获取到我们需要使用的 openid 的值。
app.log
15:09:08 DEBUG - accessToken: { access_token: '7_1_zu2EpTHYS6gNn88xZyY6zfIypl-69QVXh3Ya7pcFtVV9P6IIE3Qj182S0f27lowQeOOpAj8U4r4nEQpAmiFeDu_JKVK7I6g0EEJElyl6o', expires_in: 7200, refresh_token: '7_J-g1AtVc0K89_9v8XME_kFhhC4Nes5Jt3YNR51BJ-XPKykQ77z6QsG-f-pf9vfgcaMxfUm43o9ymx-kvlkocH_6KhGUf3OPHt3XssU4jNNc', openid: 'osbKIjtJPwZzfMea5X7Q_q2tH_EU', scope: 'snsapi_base', unionid: 'oUJ7H0caaUFoviKZrU9-DFHvtOdo' }
统一下单
跟其它的微信支付方式一样,支付的时候我们得去请求微信支付的统一下单接口。这里需要改动几个地方,请求统一下单的代码是在 CheckoutController 里的 pay 这个方法里面。
app/Controllers/Http/CheckoutController.js:
async pay ({ request, session }) { ... /** 支付类型 */ const trade_type = 'JSAPI' ... /** 微信用户 openid */ const accessToken = session.get('accessToken') const openid = accessToken.openid ... /** * 准备支付数据。 */ let order = { ... openid } ... }
使用公众号支付方式的时候,调用统一下单接口,trade_type 的值应该设置成 JSAPI。还有就是需要带着微信用户的 openid,这个值我们之前已经得到了,并且把它存储在了用户的 session 里了,这里我们用 session.get 得到了存储在 session 里的 accessToken,它里面的 openid 属性的值就是我们需要的 openid。
公众号支付
同样是在这个 pay 这个方法里,继续去添加一些代码。去准备 JSAPI 需要的一些参数数据,组织好这些数据以后,把它响应给应用的前端,在应用的前端可以调用 JSAPI 去请求支付。
/** * JSAPI 参数 */ const timeStamp = moment().local().unix() const prepay_id = data.prepay_id let wxJSApiParams = { appId: appid, timeStamp: `${ timeStamp }`, nonceStr: nonce_str, package: `prepay_id=${ prepay_id }`, signType: 'MD5' } const paySign = this.wxPaySign(wxJSApiParams, key) wxJSApiParams = { ...wxJSApiParams, paySign } /** * 为前端返回 JSAPI 参数, * 根据这些参数,调用微信支付功能。 */ return wxJSApiParams
JSAPI 参数数据
- appId:公众号应用 ID。
- timeStamp:时间戳。
- nonceStr:随机数。
- package:它的值应该使用这样的形式 `prepay_id=${ prepay_id }` ,prepay_id 的值就是请求统一下单接口返回来的数据里面的预支付的 id 号。
- signType:签名类型,可以是 MD5。
- sign:签名。根据微信支付提供的规则算出签名的值。这个签名的值要根据一些特定的数据生成,这里这些数据就是 JSAPI 需要的一些参数的值(除 sign 本身)。
最后得到的参数数据的名字叫 wxJSApiParams,我们把这个数据响应给了前端。在前端,点击确认支付以后,会用 Ajax 的形式请求应用的支付接口,这个接口会使用 CheckoutController 里的这个 pay 方法来处理,所以这个方法里返回的东西,就是给前端提供的响应数据。
请求支付
在结账页面上的自定义脚本里面,添加一个新的方法(public/main.js):
/** * JSAPI 微信支付。 * * @param {Object} wxJSApiParams 支付需要的参数数据。 */ const wxPay = (wxJSApiParams) => { WeixinJSBridge.invoke( 'getBrandWCPayRequest', wxJSApiParams, (response) => { console.log(response) } ) }
改造前端确认支付按钮的事件处理(public/main.js):
/** * 请求支付。 */ $('#pay').click(() => { $.ajax({ url: '/checkout/pay', method: 'POST', data: { _csrf }, success: (response) => { console.log(response) if (response) { modalQuery.modal() localStorage.setItem('#modal-query', 'show') wxPay(response) } }, error: (error) => { console.log(error) } }) })
上面这块请求支付的代码,就是用户点击了结账页面上的确认支付按钮以后的事件处理。这里我们用 ajax 请求 /checkout/pay 页面,页面用的处理方法是 CheckoutController 里的 pay 方法,这个方法会给我们准备好 JSAPI 需要的参数数据。得到的这些数据就是 response 。
然后我们把请求支付的功能放在了一个自定义的方法里面,名字是 wxPay,在这个方法里使用了微信内部浏览器特有的 WeixinJSBridge.invoke 请求支付。
相关资源
- 《微信支付:公众号支付》视频。
- 公众号支付开发文档。