摘要:使用觸發(fā)器自動根據(jù)微信支付回調(diào)更新可以保證無論何種情況下,數(shù)據(jù)中保存的都是最終用戶實際支付的金額。想要實現(xiàn)這個功能,則要將觸發(fā)器和云函數(shù)進行搭配使用了。
本文主要側(cè)重于講述小程序在線支付功能中的編程思想和編程模式,并在必要的地方提供關(guān)鍵代碼示例。(文末也將附上關(guān)鍵的 js 代碼)
為方便演示,這里將實現(xiàn)一個最簡單的虛擬商品的訂單支付功能,訂單略去了收貨地址和多規(guī)格、多數(shù)量的情況,示例中僅討論在商品詳情頁中直接創(chuàng)建訂單并發(fā)起支付的情況。需要分別定義 Product 表和 Order 表進行數(shù)據(jù)存取,在 BaaS 后臺中創(chuàng)建兩張數(shù)據(jù)表。
一、數(shù)據(jù)表結(jié)構(gòu)設(shè)計Product 表:
數(shù)據(jù)表錄入權(quán)限:所有人
數(shù)據(jù)行讀寫權(quán)限:創(chuàng)建者可寫,所有人可讀
Order 表:
數(shù)據(jù)表錄入權(quán)限:所有人
數(shù)據(jù)行讀寫權(quán)限:創(chuàng)建者可寫,創(chuàng)建者可讀
商品的訂單結(jié)算和支付流程一般包括“創(chuàng)建訂單 -> 支付 -> 更新訂單狀態(tài)”三個步驟。下文中將分析幾種實現(xiàn)該流程的方案,供我們一起探討。
二、客戶端創(chuàng)建訂單,客戶端更新訂單狀態(tài)我們先來看下只在客戶端中如何處理這些邏輯。
1) 創(chuàng)建訂單:Order 表中創(chuàng)建一條新記錄,status 字段默認值為 "no_paid",保存訂單金額,商品快照和商品 id 以及訂單創(chuàng)建者,其中訂單創(chuàng)建者由 BaaS 的用戶系統(tǒng)自動處理,值為創(chuàng)建訂單的用戶 id:
/** * 創(chuàng)建訂單處理函數(shù) */ 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", } // 客戶端創(chuàng)建訂單,客戶端更新訂單狀態(tài) 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)支付:調(diào)用 BaaS SDK 提供的支付方法 wx.BaaS.pay,調(diào)起微信支付: /** * 發(fā)起微信支付 * @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)更新訂單狀態(tài):支付成功后,更新 status 字段值為 "paid",并更新微信支付序列號: /** * 更新訂單狀態(tài) * 僅在由客戶端更新訂單狀態(tài)時使用 * @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() }
我們從整體上來看支付流程,便能發(fā)現(xiàn)訂單狀態(tài)實質(zhì)上是由客戶端中 updateOrder 方法發(fā)起請求來進行更新的。
而這一情況將導(dǎo)致極大的安全隱患。因為從原則上來說,我們認為來自客戶端的信息都是不可信的,訂單狀態(tài)很容易被偽造出的一個請求跳過支付直接將狀態(tài)更新為 "paid",并更新一個假的 transaction_no。
這意味著,不花一分錢也能將訂單變?yōu)橐阎Ц?。在生產(chǎn)環(huán)境中,任何情下都不應(yīng)該使用這種支付流程。
三、客戶端創(chuàng)建訂單,觸發(fā)器更新訂單狀態(tài)基于這種情況,你或許會想:既然由客戶端來更新訂單狀態(tài)會引起安全問題,又沒有后端開發(fā)者參與,要怎么做?
BaaS 平臺中觸發(fā)器和云函數(shù)可以幫你解決這個問題。它們可以完成這種非客戶端的處理邏輯,同時使用它們的時候跟開發(fā)后端應(yīng)用又有很大的不同。
首先來看一下觸發(fā)器(Trigger),觸發(fā)器是一種當觸發(fā)條件被滿足,將會執(zhí)行觸發(fā)器中的事先定義的動作,定義好的動作可以是操作數(shù)據(jù)庫或者調(diào)用云函數(shù)。
我們希望當支付完成之后,觸發(fā)器可以幫我們自動地操作數(shù)據(jù)庫,更新訂單對應(yīng)的 status 和 transaction_no 字段。觸發(fā)器設(shè)置如下:
「觸發(fā)類型」選擇微信支付回調(diào),條件是支付成功后執(zhí)行觸發(fā)器。一般觸發(fā)器類型常見的還有操作數(shù)據(jù)表,定時任務(wù)等,分別對應(yīng)操作數(shù)據(jù)表后觸發(fā)和定時觸發(fā)。
「動作」定義了觸發(fā)器將要執(zhí)行的操作,這里是更新 Order 數(shù)據(jù)表對應(yīng)的 status、total_cost 和 transaction_no 字段。更多觸發(fā)器的具體細節(jié),不同平臺的實現(xiàn)有所不同,在此不展開討論。
借助觸發(fā)器,客戶端創(chuàng)建訂單成功后不需要再調(diào) updateOrder 方法,Order 訂單的數(shù)據(jù)會自動更新成支付成功對應(yīng)的狀態(tài):
/** * 創(chuàng)建訂單處理函數(shù) */ createOrderHandle() { ... // 與上文相同 // 客戶端創(chuàng)建訂單,觸發(fā)器自動更新訂單狀態(tài) 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 數(shù)據(jù)行讀寫權(quán)限是創(chuàng)建者可寫的,意味著創(chuàng)建者可以對數(shù)據(jù)進行任意操作,將更新訂單狀態(tài)的工作交給觸發(fā)器后,Order 表的 ACL 數(shù)據(jù)行讀寫權(quán)限應(yīng)設(shè)置為「不可寫」,保證 Order 表的數(shù)據(jù)創(chuàng)建后不會由外部更改,提高了數(shù)據(jù)的安全性。
四、云函數(shù)創(chuàng)建訂單,觸發(fā)器更新訂單狀態(tài)細心的讀者可能發(fā)現(xiàn)了除了 status 和 transacton_no 字段外,還由觸發(fā)器自動更新了 total_cost 字段,保存的是實際支付的金額。
這就引出了另外一個問題,雖然現(xiàn)在不能通過客戶端修改訂單狀態(tài),但是創(chuàng)建訂單的所有數(shù)據(jù)仍是由客戶端發(fā)起請求,在請求參數(shù)中定義的,這種方式同樣很容易被人篡改數(shù)據(jù),比如 1000 元的商品可以被更改成 1 元甚至 0 元,造成只需要花很少的錢就可以買到高價值的商品。
使用觸發(fā)器自動根據(jù)微信支付回調(diào)更新 total_cost 可以保證無論何種情況下,數(shù)據(jù)中保存的都是最終用戶實際支付的金額。雖然這種方式可以事后幫助我們發(fā)現(xiàn)訂單金額異常的問題,但還是不能解決在創(chuàng)建訂單時金額被篡改的問題,這又要如何解決呢?
這時候創(chuàng)建訂單的功能應(yīng)該交給后端邏輯去做了,在 BaaS 平臺中就需要用到云函數(shù)了,云函數(shù)又被稱為 FaaS(Functions as a Service)函數(shù)即服務(wù)。
云函數(shù)是一段可以部署在服務(wù)端的代碼,關(guān)鍵詞是一段代碼,而不是一整套的后端邏輯,它本質(zhì)上就是函數(shù)而已,特別是對于運行在 node.js 環(huán)境下的云函數(shù)來說,它跟平常所寫的 JavaScript 代碼幾乎一模一樣,對前端開發(fā)者來說非常容易上手。云函數(shù)可以由 SDK 或觸發(fā)器調(diào)用,也可以在云函數(shù)之間相互調(diào)用。
為了避免創(chuàng)建訂單時客戶端數(shù)據(jù)篡改或商品信息不能實時同步的問題,我們將創(chuàng)建訂單的邏輯遷移到 BaaS 平臺的云函數(shù)中:
關(guān)注「知曉云」微信公眾號,在微信后臺回復(fù)「創(chuàng)建訂單」,獲取完整的【創(chuàng)建訂單】云函數(shù)源碼。
調(diào)用該云函數(shù)時傳入商品 id,云函數(shù)先查出此商品的具體信息,再使用該商品信息來創(chuàng)建訂單,整個過程在 BaaS 平臺的云函數(shù)系統(tǒng)中完成,保證了數(shù)據(jù)的準確性。支付完成后,觸發(fā)器同樣會自動更新訂單狀態(tài)??蛻舳酥惺褂?invokeFunction 方法調(diào)用云函數(shù):
/** * 創(chuàng)建訂單處理函數(shù) */ createOrderHandle() { ... // 與上文相同 // 使用云函數(shù)創(chuàng)建訂單,觸發(fā)器更新訂單狀態(tài) 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" }) }) }
由于創(chuàng)建訂單和更新訂單的操作已經(jīng)分別交由云函數(shù)和觸發(fā)器處理了,為了更好的安全性,Order 表的數(shù)據(jù)創(chuàng)建權(quán)限和修改權(quán)限都不應(yīng)該對客戶端開放。
需要額外說明的是,而觸發(fā)器和云函數(shù)系統(tǒng)級別的操作,相當于擁有最高權(quán)限,所以我們這里相當于禁止了客戶端中除了讀取數(shù)據(jù)外的所有操作,也就使得 Order 表的權(quán)限控制和數(shù)據(jù)的準確性得到了安全的保障。
五、云函數(shù)創(chuàng)建訂單,云函數(shù)校驗并更新訂單狀態(tài)我們再來研究一下代碼,在 pay 這個方法中 wx.BaaS.pay(params) 所做的事情實際上是發(fā)起一個請求,獲取 BaaS 系統(tǒng)返回的支付解密數(shù)據(jù),然后使用這些支付解密數(shù)據(jù)調(diào)用微信客戶端的支付功能,最終由用戶輸入密碼完成支付。
同理,根據(jù)客戶端提供的數(shù)據(jù)都不可信的原則,這個請求中 params 參數(shù)時的數(shù)據(jù)同樣可以被偽造,比如修改掉 totalCost 的值,也會導(dǎo)致最終支付的金額跟實際應(yīng)該支付的金額不一值,根據(jù)之前觸發(fā)器的設(shè)定,雖然會如實地記錄了最終支付的金額,可以為后臺追溯金額異常的訂單提供依據(jù),但是并不會阻止訂單更新為已支付的狀態(tài)。
當用戶支付成功后,我們更希望在更新訂單狀態(tài)前可以先進行支付數(shù)據(jù)的校驗,校驗不通過則不更新訂單狀態(tài)。想要實現(xiàn)這個功能,則要將觸發(fā)器和云函數(shù)進行搭配使用了。
先將觸發(fā)器的動作類型改為云函數(shù):
微信支付成功后會觸發(fā)調(diào)用 verifyPayment 云函數(shù):
客戶端的代碼保持不變,此時整個流程是:調(diào)用 createOrder 云函數(shù)創(chuàng)建訂單,拿到創(chuàng)建訂單成功的回調(diào)數(shù)據(jù)后,發(fā)起支付,支付成功之后,由觸發(fā)器自動調(diào)用 verifyPayment 云函數(shù),校驗實付金額是否跟該商品的價格一致,若一致則更新該訂單為已支付狀態(tài)。
在 verifyPayment 云函數(shù)中只考慮了校驗實付金額這一個維度,在實際開發(fā)中應(yīng)綜合考慮更多維度來確保數(shù)據(jù)準確,在此不再展開討論。
至此,本文完成了一個小程序在線支付的案例,介紹了如何借助 BaaS 平臺最快地實現(xiàn)小程序在線支付功能,通過開發(fā)過程中發(fā)現(xiàn)的各種安全問題,迭代出四種不同的實現(xiàn)方案,一步步完善支付功能的安全性,最后得出一個最快最安全實現(xiàn)小程序在線支付的方案。
六、商品詳情頁和云函數(shù) js 代碼商品詳情頁 js 代碼
/** 商品詳情頁 js 代碼 **/ const productTableId = 12345678 const orderTableId = 123456789 Page({ data: { product: {} }, onLoad(options) { // 設(shè)置默認的商品 id,方便調(diào)試 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() } } }) }, /** * 創(chuàng)建訂單處理函數(shù) */ 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", } // 客戶端創(chuàng)建訂單,客戶端更新訂單狀態(tài) // 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" }) // }) // 客戶端創(chuàng)建訂單,觸發(fā)器更新訂單狀態(tài) // return createObject.set(data).save().then(res => { // this.order = res.data || {} // return this.pay(this.order) // }).then(res => { // wx.navigateTo({ url: "../order/order" }) // }) // 使用云函數(shù)創(chuàng)建訂單,觸發(fā)器或云函數(shù)更新訂單狀態(tài) 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" }) }) }, /** * 發(fā)起微信支付 * @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 }) }, /** * 更新訂單狀態(tài) * @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() } })
創(chuàng)建訂單云函數(shù)
/** 創(chuàng)建訂單云函數(shù) **/ 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() }
校驗并更新訂單狀態(tài)云函數(shù)
/** 校驗并更新訂單狀態(tài)云函數(shù) **/ 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() }
知曉云是國內(nèi)首家專注于小程序開發(fā)的后端云服務(wù)。使用知曉云,小程序開發(fā)快人一步。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/102966.html
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數(shù)據(jù)結(jié)構(gòu),大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數(shù)據(jù)結(jié)構(gòu),大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:又快又好巧用打造你的實用折線圖最終效果本示例利用官方示例改造而成,生成帶圖示的折線圖,標出各折線的名稱,可以篩選想要顯示的折線。了解了上折線圖的數(shù)據(jù)結(jié)構(gòu),大家也就明白了顯示一條折線,即是添加隱藏一條折線,即是將其去除。 又快又好!巧用ChartJS打造你的實用折線圖 最終效果 showImg(https://segmentfault.com/img/bVq52r); 本示例利用官方示例...
摘要:云計算這個詞出現(xiàn)至今,一直是科技技術(shù)領(lǐng)域的熱門。混合云雖然很便捷,但是由于它是不同的云組合起來的云計算環(huán)境,企業(yè)在管理時會碰到不好管理的問題。以下五個步驟,可以幫助您又快又好地管理混合云。云計算這個詞出現(xiàn)至今,一直是科技技術(shù)領(lǐng)域的熱門。云計算又分為公有云、私有云和混合云,近兩年,混合云因為具有靈活性強的特點,成為眾多企業(yè)的首選,企業(yè)借助混合云,可以根據(jù)業(yè)務(wù)需求進行云上遷移。 混合云雖然...
閱讀 1805·2021-09-22 10:02
閱讀 1973·2021-09-02 15:40
閱讀 2868·2019-08-30 15:55
閱讀 2283·2019-08-30 15:44
閱讀 3621·2019-08-30 13:18
閱讀 3250·2019-08-30 11:00
閱讀 1976·2019-08-29 16:57
閱讀 587·2019-08-29 16:41