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