【實戰教程】只需三步,用雲函數又快又安全地實現小程序支付


本文主要側重於講述小程序在線支付功能中的編程思想和編程模式,並在必要的地方提供關鍵代碼示例。(文末也將附上關鍵的 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()
}

知曉雲是國內首家專一於小程序開發的後端雲服務。使用知曉雲,小程序開發快人一步。

相關文章
相關標籤/搜索