【实战教程】只需三步,用云函数又快又安全地实现小程序支付


本文主要侧重于讲述小程序在线支付功能中的编程思想和编程模式,并在必要的地方提供关键代码示例。(文末也将附上关键的 js 代码)前端

为方便演示,这里将实现一个最简单的虚拟商品的订单支付功能,订单略去了收货地址和多规格、多数量的状况,示例中仅讨论在商品详情页中直接建立订单并发起支付的状况。须要分别定义 Product 表和 Order 表进行数据存取,在 BaaS 后台中建立两张数据表。node

1、数据表结构设计

Product 表:数据库

数据表录入权限:全部人编程

数据行读写权限:建立者可写,全部人可读小程序

Order 表:segmentfault

数据表录入权限:全部人后端

数据行读写权限:建立者可写,建立者可读安全

商品的订单结算和支付流程通常包括“建立订单 -> 支付 -> 更新订单状态”三个步骤。下文中将分析几种实现该流程的方案,供咱们一块儿探讨。微信

2、客户端建立订单,客户端更新订单状态

咱们先来看下只在客户端中如何处理这些逻辑。并发

1) 建立订单:Order 表中建立一条新记录,status 字段默认值为 "no_paid",保存订单金额,商品快照和商品 id 以及订单建立者,其中订单建立者由 BaaS 的用户系统自动处理,值为建立订单的用户 id:

/**
 * 建立订单处理函数
 */
createOrderHandle() {
  const orderTableId = 12345678
  const tableObject = new wx.BaaS.TableObject(orderTableId)
  const createObject = tableObject.create()


  const product = this.data.product
  const data = {
    product_id: product.id,
    product_snapshot: product,
    total_cost: product.price,
    status: 'no_paid',
  }


  // 客户端建立订单,客户端更新订单状态
  return createObject.set(data).save().then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(transactionNo => {
    return this.updateOrder(transactionNo)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })


2)支付:调用 BaaS SDK 提供的支付方法 wx.BaaS.pay,调起微信支付:

/**
 * 发起微信支付
 * @param {Object} order
 */
pay(order) {
  const product = this.data.product
  const orderTableId = 12345678
  const params = {
    totalCost: order.total_cost,
    merchandiseDescription: product.title,
    merchandiseSchemaID: orderTableId,
    merchandiseRecordID: order.id,
    merchandiseSnapshot: product,
  }
  return wx.BaaS.pay(params).then(res => {
    return res.transaction_no
  })
}


3)更新订单状态:支付成功后,更新 status 字段值为 "paid",并更新微信支付序列号:

/**
 * 更新订单状态
 * 仅在由客户端更新订单状态时使用
 * @param {String} transaction_no 支付成功后由微信返回的微信支付序列号
 */
updateOrder(transaction_no) {
  const orderTableId = 12345678
  const tableObject = new wx.BaaS.TableObject(orderTableId)
  const recordId = this.order.id
  const record = tableObject.getWithoutData(recordId)


  record.set('status', 'paid')
  record.set('transaction_no', transaction_no)
  return record.update()
}

咱们从总体上来看支付流程,便能发现订单状态实质上是由客户端中 updateOrder 方法发起请求来进行更新的。

而这一状况将致使极大的安全隐患。由于从原则上来讲,咱们认为来自客户端的信息都是不可信的,订单状态很容易被伪造出的一个请求跳过支付直接将状态更新为 'paid',并更新一个假的 transaction_no。

这意味着,不花一分钱也能将订单变为已支付。在生产环境中,任何情下都不该该使用这种支付流程。

3、客户端建立订单,触发器更新订单状态

基于这种状况,你或许会想:既然由客户端来更新订单状态会引发安全问题,又没有后端开发者参与,要怎么作?

BaaS 平台中触发器和云函数能够帮你解决这个问题。它们能够完成这种非客户端的处理逻辑,同时使用它们的时候跟开发后端应用又有很大的不一样。

首先来看一下触发器(Trigger),触发器是一种当触发条件被知足,将会执行触发器中的事先定义的动做,定义好的动做能够是操做数据库或者调用云函数。

咱们但愿当支付完成以后,触发器能够帮咱们自动地操做数据库,更新订单对应的 status 和 transaction_no 字段。触发器设置以下:



「触发类型」选择微信支付回调,条件是支付成功后执行触发器。通常触发器类型常见的还有操做数据表,定时任务等,分别对应操做数据表后触发和定时触发。

「动做」定义了触发器将要执行的操做,这里是更新 Order 数据表对应的 status、total_cost 和 transaction_no 字段。更多触发器的具体细节,不一样平台的实现有所不一样,在此不展开讨论。

借助触发器,客户端建立订单成功后不须要再调 updateOrder 方法,Order 订单的数据会自动更新成支付成功对应的状态:

/**
 * 建立订单处理函数
 */
createOrderHandle() {
  ... // 与上文相同
  
  // 客户端建立订单,触发器自动更新订单状态
  return createObject.set(data).save().then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })
}

值得注意的是,上面介绍的第一种方案中 Order 表的 ACL 数据行读写权限是建立者可写的,意味着建立者能够对数据进行任意操做,将更新订单状态的工做交给触发器后,Order 表的 ACL 数据行读写权限应设置为「不可写」,保证 Order 表的数据建立后不会由外部更改,提升了数据的安全性。

4、云函数建立订单,触发器更新订单状态

细心的读者可能发现了除了 status 和 transacton_no 字段外,还由触发器自动更新了 total_cost 字段,保存的是实际支付的金额。

这就引出了另一个问题,虽然如今不能经过客户端修改订单状态,可是建立订单的全部数据还是由客户端发起请求,在请求参数中定义的,这种方式一样很容易被人篡改数据,好比 1000 元的商品能够被更改为 1 元甚至 0 元,形成只须要花不多的钱就能够买到高价值的商品。

使用触发器自动根据微信支付回调更新 total_cost 能够保证不管何种状况下,数据中保存的都是最终用户实际支付的金额。虽然这种方式能够过后帮助咱们发现订单金额异常的问题,但仍是不能解决在建立订单时金额被篡改的问题,这又要如何解决呢?

这时候建立订单的功能应该交给后端逻辑去作了,在 BaaS 平台中就须要用到云函数了,云函数又被称为 FaaS(Functions as a Service)函数即服务。

云函数是一段能够部署在服务端的代码,关键词是一段代码,而不是一整套的后端逻辑,它本质上就是函数而已,特别是对于运行在 node.js 环境下的云函数来讲,它跟日常所写的 JavaScript 代码几乎如出一辙,对前端开发者来讲很是容易上手。云函数能够由 SDK 或触发器调用,也能够在云函数之间相互调用。

为了不建立订单时客户端数据篡改或商品信息不能实时同步的问题,咱们将建立订单的逻辑迁移到 BaaS 平台的云函数中:

关注「知晓云」微信公众号,在微信后台回复「建立订单」,获取完整的【建立订单】云函数源码。

调用该云函数时传入商品 id,云函数先查出此商品的具体信息,再使用该商品信息来建立订单,整个过程在 BaaS 平台的云函数系统中完成,保证了数据的准确性。支付完成后,触发器一样会自动更新订单状态。客户端中使用 invokeFunction 方法调用云函数:

/**
 * 建立订单处理函数
 */
createOrderHandle() {
  ... // 与上文相同
  
  // 使用云函数建立订单,触发器更新订单状态
  wx.BaaS.invokeFunction('createOrder', {
    product_id: this.data.product.id
  }).then(res => {
    this.order = res.data || {}
    return this.pay(this.order)
  }).then(res => {
    wx.navigateTo({ url: '../order/order' })
  })
}

因为建立订单和更新订单的操做已经分别交由云函数和触发器处理了,为了更好的安全性,Order 表的数据建立权限和修改权限都不该该对客户端开放。

须要额外说明的是,而触发器和云函数系统级别的操做,至关于拥有最高权限,因此咱们这里至关于禁止了客户端中除了读取数据外的全部操做,也就使得 Order 表的权限控制和数据的准确性获得了安全的保障。

5、云函数建立订单,云函数校验并更新订单状态

咱们再来研究一下代码,在 pay 这个方法中 wx.BaaS.pay(params) 所作的事情其实是发起一个请求,获取 BaaS 系统返回的支付解密数据,而后使用这些支付解密数据调用微信客户端的支付功能,最终由用户输入密码完成支付。

同理,根据客户端提供的数据都不可信的原则,这个请求中 params 参数时的数据一样能够被伪造,好比修改掉 totalCost 的值,也会致使最终支付的金额跟实际应该支付的金额不一值,根据以前触发器的设定,虽然会如实地记录了最终支付的金额,能够为后台追溯金额异常的订单提供依据,可是并不会阻止订单更新为已支付的状态。

当用户支付成功后,咱们更但愿在更新订单状态前能够先进行支付数据的校验,校验不经过则不更新订单状态。想要实现这个功能,则要将触发器和云函数进行搭配使用了。

先将触发器的动做类型改成云函数:

微信支付成功后会触发调用 verifyPayment 云函数:

客户端的代码保持不变,此时整个流程是:调用 createOrder 云函数建立订单,拿到建立订单成功的回调数据后,发起支付,支付成功以后,由触发器自动调用 verifyPayment 云函数,校验实付金额是否跟该商品的价格一致,若一致则更新该订单为已支付状态。

在 verifyPayment 云函数中只考虑了校验实付金额这一个维度,在实际开发中应综合考虑更多维度来确保数据准确,在此再也不展开讨论。

至此,本文完成了一个小程序在线支付的案例,介绍了如何借助 BaaS 平台最快地实现小程序在线支付功能,经过开发过程当中发现的各类安全问题,迭代出四种不一样的实现方案,一步步完善支付功能的安全性,最后得出一个最快最安全实现小程序在线支付的方案

6、商品详情页和云函数 js 代码

商品详情页 js 代码

/** 商品详情页 js 代码 **/
const productTableId = 12345678
const orderTableId = 123456789

Page({
  data: {
    product: {}
  },

  onLoad(options) {
    // 设置默认的商品 id,方便调试
    const productId = options.id || '5ade97135acfb521865bf766'
    this.getProductDetail(productId)
  },
  /**
   * 获取商品详情信息
   * @param {String} id
   */
  getProductDetail(id) {
    const tableObject = new wx.BaaS.TableObject(productTableId)
    const query = new wx.BaaS.Query()

    query.compare('id', '=', id)
    tableObject.setQuery(query).find().then(res => {
      const objects = res.data.objects || []
      const product = objects[0] || {}
      this.setData({ product })
    })
  },
  /**
   * 点击当即购买按钮事件
   */
  createOrder(e) {
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          this.createOrderHandle()
        } else {
          wx.BaaS.login()
        }
      }
    })
  },
  

  /**
   * 建立订单处理函数
   */
  createOrderHandle() {
    const tableObject = new wx.BaaS.TableObject(orderTableId)
    const createObject = tableObject.create()

    const product = this.data.product
    const data = {
      product_id: product.id,
      product_snapshot: product,
      total_cost: product.price,
      status: 'no_paid',
    }
    
    // 客户端建立订单,客户端更新订单状态
    // return createObject.set(data).save().then(res => {
    //   this.order = res.data || {}
    //   return this.pay(this.order)
    // }).then(transactionNo => {
    //   return this.updateOrder(transactionNo)
    // }).then(res => {
    //   wx.navigateTo({ url: '../order/order' })
    // })

    // 客户端建立订单,触发器更新订单状态
    // return createObject.set(data).save().then(res => {
    //   this.order = res.data || {}
    //   return this.pay(this.order)
    // }).then(res => {
    //   wx.navigateTo({ url: '../order/order' })
    // })

    // 使用云函数建立订单,触发器或云函数更新订单状态
    wx.BaaS.invokeFunction('createOrder', {
      product_id: this.data.product.id
    }).then(res => {
      this.order = res.data || {}
      return this.pay(this.order)
    }).then(res => {
      wx.navigateTo({ url: '../order/order' })
    })
  },
  /**
   * 发起微信支付
   * @param {Object} order
   */
  pay(order) {
    const product = this.data.product
    const params = {
      totalCost: order.total_cost,
      merchandiseDescription: product.title,
      merchandiseSchemaID: orderTableId,
      merchandiseRecordID: order.id,
      merchandiseSnapshot: product,
    }
    return wx.BaaS.pay(params).then(res => {
      return res.transaction_no
    })
  },
  /**
   * 更新订单状态
   * @param {String} transaction_no 支付成功后返回的微信支付订单号
   */
  updateOrder(transaction_no) {
    const tableObject = new wx.BaaS.TableObject(orderTableId)
    const recordId = this.order.id
    const record = tableObject.getWithoutData(recordId)

    record.set('status', 'paid')
    record.set('transaction_no', transaction_no)
    return record.update()
  }
})

建立订单云函数

/** 建立订单云函数 **/
const productTableId = 12345678
const orderTableId = 123456789

exports.main = function createOrder(event, callback) {
  const {product_id} = event.data
  const user_id = event.request.user.id
  
  getProductDetail(product_id).then(product => {
    return createOrderHandel(product, user_id)
  }).then(res => {
    const order = res.data || {}
    callback(null, order)
  }).catch(err => {
    callback(err)
  })
}

function getProductDetail(id) {
  const tableObject = new BaaS.TableObject(productTableId)

  const query = new BaaS.Query()
  query.compare('id', '=', id)
  return tableObject.setQuery(query).find().then(res => {
    const objects = res.data.objects || []
    const product = objects[0] || {}
    return product
  })
}

function createOrderHandel(product, user_id) {
  const tableObject = new BaaS.TableObject(orderTableId)
  const createObject = tableObject.create()

  const data = {
    product_id: product.id,
    product_snapshot: product,
    total_cost: product.price,
    status: 'no_paid',
    created_by: user_id
  }
  return createObject.set(data).save()
}

校验并更新订单状态云函数

/** 校验并更新订单状态云函数 **/
const productTableId = 12345678
const orderTableId = 123456789

exports.main = function verifyPayment(event, callback) {
  const data = event.data
  const totalCost = data.total_cost
  const orderId = data.merchandise_record_id
  const transactionNo = data.transaction_no
  const merchandiseSnapshot = data.merchandise_snapshot
  const productId = merchandiseSnapshot.id

  getProductDetail(productId).then(product => {
    if (product.price === totalCost) {
      updateOrder(orderId, transactionNo)
    }
  })
}

function getProductDetail(id) {
  const tableObject = new BaaS.TableObject(productTableId)

  const query = new BaaS.Query()
  query.compare('id', '=', id)
  return tableObject.setQuery(query).find().then(res => {
    const objects = res.data.objects || []
    const product = objects[0] || {}
    return product
  })
}

function updateOrder(orderId, transaction_no) {
  const tableObject = new BaaS.TableObject(orderTableId)
  const recordId = orderId
  const record = tableObject.getWithoutData(recordId)

  record.set('status', 'paid')
  record.set('transaction_no', transaction_no)
  return record.update()
}

知晓云是国内首家专一于小程序开发的后端云服务。使用知晓云,小程序开发快人一步。

相关文章
相关标签/搜索