🦄 2024 独立开发者训练营,一起创业!查看介绍 / 立即报名 →

微信支付:开发准备与实施扫码支付细节手册(下)

图:Anna Paschenko

微信扫码支付,就是在应用上生成一个支付用的二维码,用户可以扫描二维码确认并完成支付。扫码支付提供了两种模式,我们要用的是第二种模式。

搭建微信支付开发环境,参考:《微信支付:开发准备与实施扫码支付细节手册(上)

文章有一个配套的视频版本,宁皓网会员可以在线观看《微信支付:开发准备与扫码支付

支付流程

微信扫码支付模式二

  1. 请求预支付:应用为用户生成订单以后,根据订单内容向微信支付系统请求一个预支付的链接。
  2. 生成二维码:应用得到了链接,根据链接内容生成支付用的二维码图像展示在支付页面上。
  3. 确认并完成支付:用户用微信扫描支付二维码,确认并完成支付。
  4. 接收并处理支付结果:支付完成以后,微信支付系统会通知我们的应用支付的结果。应用可以验证支付结果是否有效,再去执行对应的动作,比如更新用户的订单状态。
  5. 回复检查支付结果的状态:最后要回复一下微信支付系统我们自己验证的支付结果的状态,可以是 SUCCESS(成功)😀,也可以是 FAIL(失败)💀。

注意事项

实施微信支付功能并不难,只是有很多细节需要注意。在请求得到预支付的时候,你要准备很多数据,请求的时候要带着这些数据。这些数据有些是微信支付提供给我们的,比如公众账号ID(appid),商户号(mch_id)。有些数据是跟订单或商品相关的,比如订单号(out_trade_no),商品描述(body),商品金额(total_fee)等等。还有些数据我们要自己现去生成,比如随机字符串(nonce_str),还有签名(sign)。

数据格式

还要注意微信支付的请求或响应的数据都要使用 xml 格式的。也就是如果你要把数据发送给微信支持系统,你要把数据的格式转换成 xml。从微信支付那里获取到的数据也是 xml 格式的,在应用里想要使用它们,你需要把 xml 格式的数据转换成应用能懂的数据格式。比如,我们会在 Node.js 应用里实施微信支付,所以数据的转换就是把 JavaScript 的对象(object)转换成 xml  ,还需要把 xml 转换成 JavaSciprt 的对象。

接口

微信支付提供了一些支付的方式,比如公众号支付,扫码支付,APP 支付等等,支付方式确定了微信支付的流程。另外,还有一些 API 接口, 比如下单,查询,退款,这些功能都有一个对应的 API 接口。文章里我们暂时只关注支付功能,所以只需要用到 统一下单 这个 API 。在请求预支付的时候,请求的就是这个统一下单接口。

请求预支付时需要的数据

请求统一下单接口获取预支付,请求的时候需要带着一些数据。先去准备这些数据,这些数据就是在统一下单接口里面规定好的(除了 key,它是在创建签名时需要用的密钥)。下面我们暂时只准备必填的数据,具体可用的数据,你要参考官方文档,去查看统一下单的文档。

账户

  1. appid:公众账号 ID
  2. mch_id:商户号

appidmch_id 是成功开通微信支付以后的邮件里,或者微信支付的后台可以找到。

支付

  1. trade_type:交易类型

如果选择扫码支付功能,trade_type 的值应该是 NATIVE。

商品 / 订单

  1. out_trade_no:商户订单号
  2. body:商品描述
  3. total_fee:标价金额
  4. product_id:商品 ID

这些东西是跟商品或者订单相关的。

应用

  1. notify_url:通知地址

支付成功以后微信支付系统会把结果发送到 notify_url 设置的地址。这个地址我们应该在应用里自己去定义,它的功能就是得到请求里的数据,也就是支付结果。然后验证结果是否有效,再去做出对应的动作,比如更新用户在应用上提交的订单的状态。

签名

  1. nonce_str:随机字符串
  2. key:密钥
  3. sign:签名

在请求预支付的时候,我们要带的数据里面要包含一个签名。这个签名的算法是微信支付系统规定好的,我们要按照这个规定,一步一步地算出这个签名。

日志服务

先在应用里添加一个日志服务,可以方便做调试,这个日志服务可以把指定和信息输出到一个文本文件里。

安装

npm install log4js --save

服务

在应用里,创建一个文件。app/Services/Logger.js:

const log4js = require('log4js')

log4js.configure({
  appenders: {
    file: {
      type: 'file',
      filename: 'app.log',
      layout: {
        type: 'pattern',
        pattern: '%r %p - %m',
      }
    }
  },
  categories: {
    default: {
      appenders: ['file'],
      level: 'debug'
    }
  }
})

const logger = log4js.getLogger()

module.exports = logger

使用

CheckoutController.js 文件的顶部,导入创建的日志服务:

const logger = use('App/Services/Logger')

输出日志信息到日志文件里(app.log),可以使用下面这样的形式:

logger.debug('日志数据')

配置文件

跟微信支付相关的一些数据可以保存在一个配置文件里。创建一个配置文件 wxpay.js,放在应用的 config 目录的下面。

配置

config/wxpay.js:

'use strict'

const Env = use('Env')

module.exports = {
  // 公众账号ID
  appid: Env.get('WXPAY_APP_ID'),

  // 商户号
  mch_id: Env.get('WXPAY_MCH_ID'),

  // 密钥
  key: Env.get('WXPAY_KEY'),

  // API
  api: {
    // 统一下单
    unifiedorder: 'https://api.mch.weixin.qq.com/pay/unifiedorder'
  },

  // 通知地址
  notify_url: 'https://wxpay-dev-demo.ninghao.net/wxpay/notify'
}

环境变量

注意上面有些配置的值用到了 Env.get 方法获取到,是因为这些数据比较敏感,不应该直接保存在配置文件里。所以我们把它放在了环境变量里,Env.get 可以从环境变量里获取到值。在项目下面的 .env 文件里,存储的就是一些环境变量。

.env

...

WXPAY_APP_ID=wx5826s149db20f28e
WXPAY_MCH_ID=1328538902
WXPAY_KEY=3fx1815ed8bf4908sde12287eb6a7f92
.env 文件不会在项目的版本控制里出现。

使用配置

下面可以在之前创建的 CheckoutController 里面用一下刚才定义的这些配置。

app/Controllers/Http/CheckoutController.js:

先在文件里导入 Config:

const Config       = use('Config')

然后把 CheckoutControllerrender 方法,改成下面这样:

  async render ({ view }) {
    // 公众账号 ID
    const appid = Config.get('wxpay.appid')

    // 商户号
    const mch_id = Config.get('wxpay.mch_id')

    // 密钥
    const key = Config.get('wxpay.key')
 
    // 通知地址
    const notify_url = Config.get('wxpay.notify_url')

    // 统一下单接口
    const unifiedOrderApi = Config.get('wxpay.api.unifiedorder')
  }

这里就是用了 Config.get 方法,得到了 wxpay 这个配置文件里面的对应的配置数据。

商品 / 订单

再去准备商品  / 订单相关的数据,这些数据我们直接手工设置一下。在实际应用中,这些数据你应该根据应用的购物车系统去生成。在 CheckoutControllerrender 方法里,再添加下面这些代码:

 // 商户订单号
 const out_trade_no = moment().local().format('YYYYMMDDHHmmss')

 // 商品描述
 const body = 'ninghao'

 // 商品价格
 const total_fee = 3

 // 支付类型
 const trade_type = 'NATIVE'

 // 商品 ID
 const product_id = 1

out_trade_no 是订单号,每笔交易的订单号都应该不一样。这里我用了 moment 得到了当前时间,用它作为订单号。这个 moment 要去安装一下,打开命令行,然后在项目目录的下面,执行:

npm install moment --save

然后在 CheckoutController 文件的顶部导入 moment,添加下面这行代码:

const moment       = use('moment')

签名

现在可以去生成签名。签名的计算方法是微信支付规定的:

  1. 给数据排序。
  2. 把数据转换成地址查询符字符串。
  3. 在转换后的字符串结尾添加 key(密钥)。
  4. 用 md5 处理字符串。
  5. 将处理结果全部转换成大写字母。

先再准备一个随机字符(nonce_str),可以为项目安装一个 randomstring 这个 package:

npm install randomstring --save

然后在 CheckoutController.js 文件的顶部,导入 randomstring,添加代码:

const randomString = use('randomstring')

render 方法里,再添加一个 nonce_str

 // 随机字符
 const nonce_str = randomString.generate(32)

准备签名数据

render 方法里,再去准备一下参与签名的数据,可以放在一个 object 里面:

    // 统一下单
    let order = {
      appid,
      mch_id,
      out_trade_no,
      body,
      total_fee,
      trade_type,
      product_id,
      notify_url,
      nonce_str
    }

签名方法

CheckoutController 这个类里面添加一个生成签名用的方法,名字是 wxPaySign,代码如下:

  wxPaySign (data, key) {
    // 1. 排序
    const sortedOrder = Object.keys(data).sort().reduce((accumulator, key) => {
      accumulator[key] = data[key]
      // logger.debug(accumulator)
      return accumulator
    }, {})

    // 2. 转换成地址字符
    const stringOrder = queryString.stringify(sortedOrder, null, null, {
      encodeURIComponent: queryString.unescape
    })

    // 3. 结尾加上密钥
    const stringOrderWithKey = `${ stringOrder }&key=${ key }`

    // 4. md5 后全部大写
    const sign = crypto.createHash('md5').update(stringOrderWithKey).digest("hex").toUpperCase()

    return sign
  }

方法接受 datakey 两个参数。data 是要处理的数据,它应该是一个对象。key 是微信支付用的密钥,登录到微信支付系统后台可以去设置这个密钥的值。

1,排序

在方法里,先根据传递进来的 data ,去生成一个排序之后的新的排序之后的对象。

2,转换

把对象转换成地址查询符,这里用到了 Node.js 里的 querystring 这个模块提供的功能。在文件的顶部要去导入这个模块:

const queryString = use('querystring')

3,加密钥

在字符的结尾再添加上密钥的值,这个密钥你可以在微信支付管理后台去设置。

4,md5 处理后全部大写

用 md5 处理字符,再把结果转换成全部大写字母。这里用到了 Node.js 的 crypto 模块,所以要在文件顶部导入这个模块:

const crypto = use('crypto')

生成签名

现在可以在 render 方法里,使用上面定义的 wxPaySign 方法去生成签名数据了。方法需要的两个参数,提前已经在 render 方法里准备好了。

const sign = this.wxPaySign(order, key)

签名也要作为请求统一下单接口的时候带着的数据,所以我们得到了签名值以后,可以把它放到要发送的数据里。这里可以重新设置一下之前定义的 order 的值 。

   order = {
      xml: {
        ...order,
        sign
      }
    }

注意上面我们重新定义 order 的时候,在它里面先添加了一个 xml 属性,这个属性一会在把数据转换成 xml 格式的时候会用到,它会被转换成一组 <xml> 标签,里面会包装着要发送的具体的数据。...order 的意思是把之前 order 里的东西放进来,... 是 spread 操作符。然后又在对象里添加了一个 sign,它是刚才我们生成的签名的值。

转换成 xml

现在已经准备好了请求统一下单接口的时候要带着的数据,下面要做的是把这些数据转换成 xml 格式的。这里要用到一个 xml-js,先去安装一下它:

npm install xml-js --save

然后在 CheckoutController.js 文件的顶部导入这个模块:

const convert = use('xml-js')

在控制器的 render 方法里可以去转换数据:

    // 转换成 xml 格式
    const xmlOrder = convert.js2xml(order, {
      compact: true
    })

上面用到了 xml-js 里的 js2xml 这个方法,它可以把 object 格式的数据转换成 xml 格式的数据。比如:

{
  xml: {
    appid: 'xxx'
  }
}

上面这个对象转换成 xml 以后,会变成:

<xml>
  <appid>xxx</appid>
</xml>

请求统一下单接口

现在我们就真正准备好了请求统一下单接口需要的数据,这样就可以去请求统一下单接口来得到预支付的链接了。在 Node.js 应用里发出 HTTP 请求,可以使用 axios 这个包。先去安装一下它:

npm install axios --save

导入包:

const axios = use('axios')

发出请求

CheckoutController.js 里面的 render 方法里,可以去执行请求动作,然后把得到的响应交给了 wxPayResponse

    // 调用统一下单接口
    const wxPayResponse = await axios.post(unifiedOrderApi, xmlOrder)

注意 axios 会返回 Promise ,所以在它前面可以用一个 await 去等待执行的结果。之前我们在这行代码所在的 render 方法的前面已经添加了一个 async,意思就是这个 render 方法里有一些异步动作。

async render ({ view }) {
  ...
}

处理响应

如果一切正常,请求统一下单接口返回的响应的数据(wxPayResponse.data),像下面这样:

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
  ...
  <code_url><![CDATA[weixin://wxpay/bizpayurl?pr=yCYZHGS]]></code_url>
</xml>

这里有我们需要生成支付二维码用到的 code_url。但是响应回来的数据是 xml 格式的,所以我们不能直接用,需要转换一下它。

转换

    const _prepay = convert.xml2js(wxPayResponse.data, {
      compact: true,
      cdataKey: 'value',
      textKey: 'value'
    }).xml

    const prepay = Object.keys(_prepay).reduce((accumulator, key) => {
      accumulator[key] = _prepay[key].value
      return accumulator
    }, {})

转换之后的响应数据交给了 prepay ,它里面的东西应该像下面这样:

{ 
  return_code: 'SUCCESS',
  return_msg: 'OK',
  ...
  code_url: 'weixin://wxpay/bizpayurl?pr=yCYZHGS' 
}

这个响应里的 code_url 的值,就是支付二维码表示的数据。

生成二维码

有了微信支付系统给我们的应用返回来的 code_url,就可以基于这个 code_url 去生成一个二维码图像,再把这个图像交给支付页面的视图去用。

安装

生成二维码图像需要用到一个包:

npm install qrcode --save

导入

const qrcode = use('qrcode')

使用

CheckoutControllerrender 方法里生成二维码图像的 Data Url :

   // 生成二维码链接
    const qrcodeUrl = await qrcode.toDataURL(prepay.code_url, { width: 300 })

传递给视图

return view.render('commerce.checkout', { qrcodeUrl })

修改视图

resources/views/commerce/checkout.edge

<img class="card-img-top" src="https://ninghao.net/%7B%7B%20%3Cstrong%3EqrcodeUrl%3C/strong%3E%20%7C%7C%20assetsUrl%28%27something-wrong.png%27%29%20%7D%7D">

现在访问 checkout 页面的时候,就会显示一个支付用的二维码了。打开微信,可以扫描这个二维码完成支付,完成以后微信支付系统会通知我们的应用支付的结果。

处理支付结果通知

得到微信发送过来的支付结果以后,要验证支付结果的签名,还有用户支付的金额。然后要把验证的结果回复给微信支付系统,可以回复 SUCCESS(成功),或者 FAIL(失败)。

修改配置

一开始我们就在应用里定义了通知的地址 wxpay/notify,现在要去修改一下应用的 config/shield.js,在 filterUris 里面,添加一个 '/wxpay/notify'。意思就是去告诉应用,/wxpay/notify 不需要 csrf 保护,不然的话微信支付系统发过来的支付结果我们没法用。

  csrf: {
    enable: true,
    methods: ['POST', 'PUT', 'DELETE'],
    filterUris: ['/wxpay/notify'],
    cookieOptions: {
      httpOnly: false,
      sameSite: true,
      path: '/',
      maxAge: 7200
    }
  }

处理支付结果的方法

下面修改一下 CheckoutController 里的 wxPayNotify 这个方法:

  wxPayNotify ({ request }) {
    logger.warn('--------------------------------------------------------------------------')
    logger.info(request)

    const _payment = convert.xml2js(request._raw, {
      compact: true,
      cdataKey: 'value',
      textKey: 'value'
    }).xml

    const payment = Object.keys(_payment).reduce((accumulator, key) => {
      accumulator[key] = _payment[key].value
      return accumulator
    }, {})

    logger.info('支付结果:', payment)

    const paymentSign = payment.sign

    logger.info('结果签名:', paymentSign)

    delete payment['sign']

    const key = Config.get('wxpay.key')

    const selfSign = this.wxPaySign(payment, key)

    logger.info('自制签名:', selfSign)

    const return_code = paymentSign === selfSign ? 'SUCCESS' : 'FAIL'

    logger.debug('回复代码:', return_code)

    const reply = {
      xml: {
        return_code,
      }
    }

    return convert.js2xml(reply, {
      compact: true
    })
  }

方法收到的 request 里面,有支付结果数据,是在 _raw 这个属性里面。但是数据格式是 xml ,所以我们要去转换一下它。转换之后的结果交给了 payment

然后把支付结果里的签名提取出来交给了 paymentSign。接着我们要自己去算出一个签名,再去比较结果里的签名,看看是否一致。自己算签名的时候去掉了数据里的 sign 属性的值。然后用 wxPaySign 这个方法去计算签名。

根据比对签名的结果,确定 return_code 的值,它就是我们要给微信回复的信息。回复的信息应该是 xml 格式的数据,所以我们用 js2xml 方法转换了一下。方法最终 return 的东西,就是给微信支付系统做的回复。

项目代码

文章项目里的代码全部在 github ,ninghao/ninghao-sandbox-v2 这个仓库里,注意项目的分支是 wxpay

克隆项目

git clone git@github.com:ninghao/ninghao-sandbox-v2.git
cd ninghao-sandbox-v2
git branch -a
git checkout -b wxpay remotes/origin/wxpay
npm install
cp .env.example .env
adonis key:generate

设置

打开项目以后,在项目根目录下的 .env 文件里,添加下面这些配置,注意把配置对应的值设置成你自己的:

WXPAY_APP_ID=wx58263149db20f28e
WXPAY_MCH_ID=1328508902
WXPAY_KEY=3fa1815e38bf4908sse12287eb6a7f92
WXPAY_NOTIFY_URL=https://sandbox.ninghao.net/wxpay/notify
微信支付
微信好友

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

微信公众号

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

240746680

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

统计

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

社会化网络

关于

微信订阅号

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