🦄 2024 独立开发者训练营,一起创业!查看介绍 / 立即报名(剩余10个优惠名额) →

微信支付:H5 移动端支付实施细节手册


之前我们完成了开发微信支付时需要做的准备,还实施了一个扫码支付功能。 介绍了微信支付的基本流程,还有相关的一些概念。扫码支付比较适用于桌面端的应用,因为支付的时候需要用到微信 App 扫二维码。下面再介绍一种适用于移动端的微信支付方法,就是 H5 支付。用户在移动设备的浏览器上提交支付请求,会调开微信 App 进行支付,支付完成以后又会被重定向到原来的支付页面。

文章有配套视频《微信支付:H5 移动端支付》,订阅宁皓网可以在线学习所有相关的课程。

支付流程

  1. 用户在手机浏览器上,在应用的支付页面提交支付。
  2. 应用请求微信的统一下单接口。
  3. 微信支付系统返回跳转链接。
  4. 应用页面重定向到微信支付返回的跳转链接。
  5. 链接会调开用户的微信 App。
  6. 用户在微信  App 确认并完成支付。
  7. 用户被重定向到原来申请支付时的页面。
  8. 页面可以引导用户查询微信支付订单状态。

开通 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')

在《微信支付:开发准备与实施扫码支付细节手册(下)》里添加的两条路由:

  1. checkout:结账页面。
  2. wxpay/notify:处理微信支付发送过来的支付结果通知。

相比之前,我们在应用里又添加了几条新的路由。

  1. checkout/pay:用户点击支付页面上的确认支付按钮,会请求这个地址。它做的事主要是去组织 H5 支付需要的数据,然后请求微信支付统一下单接口,返回跳转链接。
  2. checkout/query:支付完成以后,可以查询微信支付交易状态。比如我们可以在支付页面上显示一个对话框,提示用户查询微信交易的状态。
  3. 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 这个控制器里了。

  1. wxPaySign:用来生成微信支付签名。
  2. wxPayNotify:处理微信支付发送过来的支付结果通知。
  3. 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>&times;</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' }

相关资源

  1. H5 支付官方文档
  2. 微信支付:开发准备与实施扫码支付细节手册(上)
  3. 微信支付:开发准备与实施扫码支付细节手册(下)
  4. 视频课程:《微信支付:开发准备与扫码支付
微信支付
微信好友

用微信扫描二维码,
加我好友。

微信公众号

用微信扫描二维码,
订阅宁皓网公众号。

240746680

用 QQ 扫描二维码,
加入宁皓网 QQ 群。

统计

15260
分钟
0
你学会了
0%
完成

社会化网络

关于

微信订阅号

扫描微信二维码关注宁皓网,每天进步一点