摘要:使用觸發器自動根據微信支付回調更新可以保證無論何種情況下,數據中保存的都是最終用戶實際支付的金額。想要實現這個功能,則要將觸發器和云函數進行搭配使用了。
本文主要側重于講述小程序在線支付功能中的編程思想和編程模式,并在必要的地方提供關鍵代碼示例。(文末也將附上關鍵的 js 代碼)
為方便演示,這里將實現一個最簡單的虛擬商品的訂單支付功能,訂單略去了收貨地址和多規格、多數量的情況,示例中僅討論在商品詳情頁中直接創建訂單并發起支付的情況。需要分別定義 Product 表和 Order 表進行數據存取,在 BaaS 后臺中創建兩張數據表。
一、數據表結構設計Product 表:
數據表錄入權限:所有人
數據行讀寫權限:創建者可寫,所有人可讀
Order 表:
數據表錄入權限:所有人
數據行讀寫權限:創建者可寫,創建者可讀
商品的訂單結算和支付流程一般包括“創建訂單 -> 支付 -> 更新訂單狀態”三個步驟。下文中將分析幾種實現該流程的方案,供我們一起探討。
二、客戶端創建訂單,客戶端更新訂單狀態我們先來看下只在客戶端中如何處理這些邏輯。
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。
這意味著,不花一分錢也能將訂單變為已支付。在生產環境中,任何情下都不應該使用這種支付流程。
三、客戶端創建訂單,觸發器更新訂單狀態基于這種情況,你或許會想:既然由客戶端來更新訂單狀態會引起安全問題,又沒有后端開發者參與,要怎么做?
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 表的數據創建后不會由外部更改,提高了數據的安全性。
四、云函數創建訂單,觸發器更新訂單狀態細心的讀者可能發現了除了 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 表的權限控制和數據的準確性得到了安全的保障。
五、云函數創建訂單,云函數校驗并更新訂單狀態我們再來研究一下代碼,在 pay 這個方法中 wx.BaaS.pay(params) 所做的事情實際上是發起一個請求,獲取 BaaS 系統返回的支付解密數據,然后使用這些支付解密數據調用微信客戶端的支付功能,最終由用戶輸入密碼完成支付。
同理,根據客戶端提供的數據都不可信的原則,這個請求中 params 參數時的數據同樣可以被偽造,比如修改掉 totalCost 的值,也會導致最終支付的金額跟實際應該支付的金額不一值,根據之前觸發器的設定,雖然會如實地記錄了最終支付的金額,可以為后臺追溯金額異常的訂單提供依據,但是并不會阻止訂單更新為已支付的狀態。
當用戶支付成功后,我們更希望在更新訂單狀態前可以先進行支付數據的校驗,校驗不通過則不更新訂單狀態。想要實現這個功能,則要將觸發器和云函數進行搭配使用了。
先將觸發器的動作類型改為云函數:
微信支付成功后會觸發調用 verifyPayment 云函數:
客戶端的代碼保持不變,此時整個流程是:調用 createOrder 云函數創建訂單,拿到創建訂單成功的回調數據后,發起支付,支付成功之后,由觸發器自動調用 verifyPayment 云函數,校驗實付金額是否跟該商品的價格一致,若一致則更新該訂單為已支付狀態。
在 verifyPayment 云函數中只考慮了校驗實付金額這一個維度,在實際開發中應綜合考慮更多維度來確保數據準確,在此不再展開討論。
至此,本文完成了一個小程序在線支付的案例,介紹了如何借助 BaaS 平臺最快地實現小程序在線支付功能,通過開發過程中發現的各種安全問題,迭代出四種不同的實現方案,一步步完善支付功能的安全性,最后得出一個最快最安全實現小程序在線支付的方案。
六、商品詳情頁和云函數 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() }
知曉云是國內首家專注于小程序開發的后端云服務。使用知曉云,小程序開發快人一步。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102966.html
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數據結構,大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數據結構,大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數據結構,大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:云計算這個詞出現至今,一直是科技技術領域的熱門。混合云雖然很便捷,但是由于它是不同的云組合起來的云計算環境,企業在管理時會碰到不好管理的問題。以下五個步驟,可以幫助您又快又好地管理混合云。云計算這個詞出現至今,一直是科技技術領域的熱門。云計算又分為公有云、私有云和混合云,近兩年,混合云因為具有靈活性強的特點,成為眾多企業的首選,企業借助混合云,可以根據業務需求進行云上遷移。 混合云雖然...
閱讀 1686·2021-09-22 10:02
閱讀 1930·2021-09-02 15:40
閱讀 2835·2019-08-30 15:55
閱讀 2242·2019-08-30 15:44
閱讀 3593·2019-08-30 13:18
閱讀 3223·2019-08-30 11:00
閱讀 1944·2019-08-29 16:57
閱讀 564·2019-08-29 16:41