之前我们完成了开发微信支付时需要做的准备,还实施了一个扫码支付功能。 介绍了微信支付的基本流程,还有相关的一些概念。扫码支付比较适用于桌面端的应用,因为支付的时候需要用到微信 App 扫二维码。下面再介绍一种适用于移动端的微信支付方法,就是 H5 支付。用户在移动设备的浏览器上提交支付请求,会调开微信 App 进行支付,支付完成以后又会被重定向到原来的支付页面。
文章有配套视频《微信支付:H5 移动端支付》,订阅宁皓网可以在线学习所有相关的课程。
支付流程
- 用户在手机浏览器上,在应用的支付页面提交支付。
- 应用请求微信的统一下单接口。
- 微信支付系统返回跳转链接。
- 应用页面重定向到微信支付返回的跳转链接。
- 链接会调开用户的微信 App。
- 用户在微信 App 确认并完成支付。
- 用户被重定向到原来申请支付时的页面。
- 页面可以引导用户查询微信支付订单状态。
开通 H5 支付
登录到微信支付后台,在产品中心,可以开通微信支付里的 H5 支付功能。

项目代码
应用的代码在 ninghao/ninghao-sandbox-v2 这个仓库的 wxpay-h5 这个分支上。
路由
start/routes.js
Route.get('checkout', 'CheckoutController.render')
Route.post('wxpay/notify', 'CheckoutController.wxPayNotify')
Route.post('checkout/pay', 'CheckoutController.pay')
Route.post('checkout/query', 'CheckoutController.query')
Route.get('checkout/completed', 'CheckoutController.completed')在《微信支付:开发准备与实施扫码支付细节手册(下)》里添加的两条路由:
- checkout:结账页面。
- wxpay/notify:处理微信支付发送过来的支付结果通知。
相比之前,我们在应用里又添加了几条新的路由。
- checkout/pay:用户点击支付页面上的确认支付按钮,会请求这个地址。它做的事主要是去组织 H5 支付需要的数据,然后请求微信支付统一下单接口,返回跳转链接。
- checkout/query:支付完成以后,可以查询微信支付交易状态。比如我们可以在支付页面上显示一个对话框,提示用户查询微信交易的状态。
- checkout/completed:如果查询的交易状态是 SUCCESS,可以把用户带到这个页面上,提示用户完成了交易。
支付
结账页面视图
重新设计一下 checkout 页面视图,在实施扫码支付功能的时候,结账页面上会显示支付用的二维码。使用 H5 支付的时候,我们可以在这个结账页面上显示订单相关的信息,还有一个结账按钮,按下去可以用 Ajax 方式请求支付。
resources/views/commerce/checkout.edge
<div class="container">
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card text-center mt-5 mx-3">
<div class="card-body">
<img src="https://ninghao.net/%7B%7B%20assetsUrl%28%27wxpay.png%27%29%20%7D%7D" alt="" class="w-50">
<div class="card-price my-5 pb-5">
<p class="card-amount"><small>¥</small>0.03</p>
<p class="card-text text-muted"><small>订单金额</small></p>
</div>
<button data-csrf="{{ csrfToken }}" id="pay" class="btn btn-primary btn-block">确认支付</button>
</div>
</div>
</div>
</div>
</div>要注意的是在 确认支付 按钮上添加的 id 属性,等会儿我们可以在页面使用的自定义脚本里面,使用这个 id 属性的值来定位到这个按钮元素(#pay)。还有在按钮上的 data-csrf 属性,对应的值绑定了一个 csrfToken,在页面自定义脚本里面,可以利用这个自定义的 data 属性,得到 csrfToken 的值。应用(Adonis.js)对 POST 类型的请求会做 CSRF 保护,所以提交这种请求的时候,要带着应用生成的 CSRF Token 的值。
样式
视图需要点自定义的样式,在视图使用的布局(layouts.main)里面链接了一个自定义样式表(main.css),在这个样式表里你可以添加自定义的样式。
public/main.css
.card-price {
font-family: "Century Gothic", sans-serif;
font-weight: bold;
}
.card-amount small {
font-size: 16px;
}
.card-amount {
font-size: 32px;
padding: 0;
margin: 0;
}

功能
在之前我们实施扫码支付的时候,功能代码主要都放在了 CheckoutController 这个控制器里了。
- wxPaySign:用来生成微信支付签名。
- wxPayNotify:处理微信支付发送过来的支付结果通知。
- render:请求统一下单接口,生产二维码图像,交给 checkout 页面视图使用。
下面我们要对 render 方法做一些调整,只让它返回 checkout 页面视图,把其余的代码分割成几个方法。
render
显示结账页面。
render ({ view }) {
return view.render('commerce.checkout')
}orderToXML
要发送给微信支付接口的数据需要转换成 xml 格式,单独创建一个方法可以把 object 数据转换成 xml 格式。
orderToXML (order, sign) {
order = {
xml: {
...order,
sign
}
}
// 转换成 xml 格式
const xmlOrder = convert.js2xml(order, {
compact: true
})
return xmlOrder
}xmlToJS
从微信支付那里得到的数据,格式是 xml,要在应用里使用的话需要转换成 object。这块代码可能会重复用到,所以单独定义一个 xmlToJS 方法,功能就是把 xml 数据转换成 Object。
xmlToJS (xmlData) {
const _data = convert.xml2js(xmlData, {
compact: true,
cdataKey: 'value',
textKey: 'value'
}).xml
const data = Object.keys(_data).reduce((accumulator, key) => {
accumulator[key] = _data[key].value
return accumulator
}, {})
return data
}pay
然后再把之前在 render 方法里的其余的代码放到 pay 这个方法里面。它是 checkout/pay 路由使用的处理请求用的方法。
async pay ({ request, session }) {
logger.info('请求支付 ------------------------')
// 公众账号 ID
const appid = Config.get('wxpay.appid')
// 商户号
const mch_id = Config.get('wxpay.mch_id')
// 密钥
const key = Config.get('wxpay.key')
// 商户订单号
const out_trade_no = moment().local().format('YYYYMMDDHHmmss')
session.put('out_trade_no', out_trade_no)
// 商品描述
const body = 'ninghao'
// 商品价格
const total_fee = 3
// 支付类型
const trade_type = 'MWEB'
// 用户 IP
const spbill_create_ip = request.header('x-real-ip')
// 商品 ID
const product_id = 1
// 通知地址
const notify_url = Config.get('wxpay.notify_url')
// 随机字符
const nonce_str = randomString.generate(32)
// 统一下单接口
const unifiedOrderApi = Config.get('wxpay.api.unifiedorder')
let order = {
appid,
mch_id,
out_trade_no,
body,
total_fee,
trade_type,
product_id,
notify_url,
nonce_str,
spbill_create_ip
}
const sign = this.wxPaySign(order, key)
const xmlOrder = this.orderToXML(order, sign)
// 调用统一下单接口
const wxPayResponse = await axios.post(unifiedOrderApi, xmlOrder)
const data = this.xmlToJS(wxPayResponse.data)
logger.debug(data)
return data.mweb_url
}trade_type 设置的是交易类型,跟扫码支付(NATIVE)不同的是,我们把它设置成了 MWEB:
trade_type = 'MWEB'
还有在方法里,我们把生成的订单号保存在了用户的 session 里面了,这样在后面调用微信支付查询订单接口的时候,可以从 session 里面读取订单号,然后组织好查询需要带的数据。
session.put('out_trade_no', out_trade_no)请求支付
在结账页面上有个 确认支付 按钮,按一下它可以请求支付。可以使用表单或者 Ajax 的形式去执行这个动作。下面我们要使用 Ajax 的方法去请求支付,在页面使用的自定义脚本文件里面,可以添加几行代码:
public/main.js
(function() {
'use strict'
// 自定义脚本
}())把代码放在上面的立即执行函数(IIFE)里面:
const _csrf = $('#pay').data('csrf')
$('#pay').click(() => {
$.ajax({
url: '/checkout/pay',
method: 'POST',
data: {
_csrf
},
success: (response) => {
console.log(response)
if (response) {
window.location.href = response
}
},
error: (error) => {
console.log(error)
}
})
})先从 #pay 按钮上得到了 csrfToken 的值。然后找到页面上的 #pay 元素,监听它的点击事件(click)。点击了 确认支付 按钮,就会使用 jQuery 的 Ajax 方法去发出请求。如果你做的是前端应用,可以使用其它的 HTTP 客户端,比如 axios(浏览器与 Node.js 都可以使用它)。效果都差不多,就是对指定的地址发出各种不同类型的 HTTP 请求。
请求成功会调用 success 方法,得到的响应叫 response,它的值应该就是支付跳转用的地址。下面这行代码,会把用户带到得到的这个支付跳转地址上:
window.location.href = response
在 checkout 页面点一下 确认支付 按钮,就会请求 checkout/pay 这个地址,这个地址请求的处理方法用的是 CheckoutController 里的 pay 方法。在这个方法里面,我们组织好了请求带的数据,然后对微信支付系统的统一下单接口发出请求,并且返回了请求得到的 mweb_url,它的值就是支付要跳转的地址。
打开应用日志 app.log,里面会出现请求统一下单接口返回的数据, mweb_url 就是我们需要用的那个跳转地址:
23:45:56 DEBUG - { return_code: 'SUCCESS',
return_msg: 'OK',
appid: 'wx58263139db20f28e',
mch_id: '1528508902',
nonce_str: 'fnAUH4QVn7L2yrQo',
sign: '9FF86FF3EA9C99D78A2C832638E62649',
result_code: 'SUCCESS',
prepay_id: 'wx20180202234556a1bff266860143681661',
trade_type: 'MWEB',
mweb_url: 'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20180202234556a1bff266860143681661&package=3740852957' }试验
在手机设备上打开支付页面,按一下 确认支付,应该就会调用微信支付 App 进行支付了。
查询订单状态
使用我们自己系统内部的订单号(out_trade_no),或者微信支付生成的订单号(transaction_id),调用微信支付订单查询接口,可以查询交易的状态。
查询对话框视图
用户使用 H5 方式支付完成以后会被重定向到原来发起支付时的页面。就是我们的结账页面,在这个页面上,可以显示一个对话框,提示用户查询交易状态(trade_state),如果状态是 SUCCESS,可以再把用户带到一个完成页面。
resources/views/commerce/checkout.edge
<div class="modal" id="modal-query">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">支付结果</h5>
<button class="close" type="button" data-dismiss="modal">
<span>×</span>
</button>
</div>
<div class="modal-body">
<p>支付完成以后,请再确定一下支付结果。</p>
</div>
<div class="modal-footer">
<button id="order-query" class="btn btn-primary btn-block" data-csrf="{{ csrfToken }}">
支付成功
</button>
</div>
</div>
</div>
</div>视图用到了 Bootstrap 框架提供的 Modal 组件。

页面脚本
按下结账页面的 确认支付,成功得到了响应以后,可以再弹出对话框视图。支付完成以后再次跳转到这个结账页面的时候,iOS 设备会刷新这个页面,所以我们需要一种方法记住对话框的开启状态。这里我们把这个开启状态保存在了用户设备的 LocalStorage 里面。
public/main.js
// 找到页面上的对话框元素
const modalQuery = $('#modal-query')
// 对话框完全隐藏时,设置它的开启状态为 hide
modalQuery.on('hidden.bs.modal', () => {
localStorage.setItem('#modal-query', 'hide')
})
// 页面显示时,读取对话框的开启状态
const modalQueryState = localStorage.getItem('#modal-query')
// 如果对话框开启状态为 show,我们就显示它
if (modalQueryState === 'show') {
modalQuery.modal()
}
// 执行查询订单,交易状态为成功,就把用户带到 /checkout/completed 页面。
$('#order-query').click(() => {
$.ajax({
url: '/checkout/query',
method: 'POST',
data: {
_csrf
},
success: (response) => {
switch (response.trade_state) {
case 'SUCCESS':
window.location.href = '/checkout/completed'
break
default:
console.log(response)
}
},
error: (error) => {
console.log(error)
}
})
})完成页面视图
resources/views/commerce/completed.edge
@layout('layouts.main')
@section('content')
<div class="container">
<div class="jumbotron mt-3">
<h1 class="dispay-4">成功啦!</h1>
<p class="lead">您的订单已经完成了。</p>
<p class="head">
<a href="https://ninghao.net/checkout" class="btn btn-primary my-2">返回</a>
</p>
</div>
</div>
@endsection
订单查询功能
实现订单查询功能,主要的代码放在了 CheckoutController 里的 query 这个方法里面,它是 checkout/query 地址的处理方法。按下对话框上的 支付成功 按钮,请求的就是这个地址。
query
在方法里,准备好调用微信支付订单查询接口需要的数据,注意我们在用户的 Session 里面读取了之前在 pay 方法里保存的 out_trade_no 的值。在实际的应用中,我们的应用收到了微信支付结果以后,可以把结果里的微信支付订单号(transaction_id)保存在数据库里。这样在查询交易状态的时候,也可以根据这个微信支付订单号去查询交易状态。
async query ({ session }) {
logger.info('请求查询 -----------------------')
// 公众账号 ID
const appid = Config.get('wxpay.appid')
// 商户号
const mch_id = Config.get('wxpay.mch_id')
// 密钥
const key = Config.get('wxpay.key')
// 商户订单号
const out_trade_no = session.get('out_trade_no')
// 随机字符
const nonce_str = randomString.generate(32)
// 查询订单接口
const orderQueryApi = Config.get('wxpay.api.orderquery')
const order = {
appid,
mch_id,
out_trade_no,
nonce_str
}
const sign = this.wxPaySign(order, key)
const xmlOrder = this.orderToXML(order, sign)
const wxPayQueryResponse = await axios.post(orderQueryApi, xmlOrder)
const result = this.xmlToJS(wxPayQueryResponse.data)
logger.debug(result)
return result
}completed
添加一个 completed 方法,显示一个 commerce.completed 视图。
completed ({ view }) {
return view.render('commerce.completed')
}数据
调用订单查询接口返回的数据:
09:25:42 DEBUG - { return_code: 'SUCCESS',
return_msg: 'OK',
appid: 'wx58263139db20f28e',
mch_id: '1228508902',
nonce_str: 'QqwBIqZDv4ogsgrg',
sign: '68C38CB0738F20D8B2DB6F135121DCA6',
result_code: 'SUCCESS',
openid: 'osbKIjtJPwZzfMea5X7Q_q2tH_EU',
is_subscribe: 'Y',
trade_type: 'MWEB',
bank_type: 'CFT',
total_fee: '3',
fee_type: 'CNY',
transaction_id: '4200000063201802046578989993',
out_trade_no: '20180204092514',
attach: undefined,
time_end: '20180204092536',
trade_state: 'SUCCESS',
cash_fee: '3' }


