← 上一章:購物車 Part 3 下一章:金流串接(使用 Paypal) →
訂單處理
有了購物車功能,客人順利的下單、結帳,訂單也成立了,接下來就是訂單的處理了。
訂單要處理什麼?
通常一筆訂單上面會有:
- 商品(Product)資訊。
- 訂購數量(quantity)。
- 狀態。
其實訂單跟前面介紹到購物車有點像,以購物車來說,一個 Cart
物件裡面會包含一個以上的 CartItem
,同樣的,訂單(Order)裡可能也會有一個以上的訂單資訊(OrderItem),每筆資訊都包含了商品的資訊、訂購的數量以及訂單的狀態。
這個章節主要介紹的是訂單的「狀態」。訂單的狀態可以很簡單也可以很複雜,訂單的狀態欄位的資訊可選擇使用一般的字串或數字欄位來表示,但在識別上可能不見得方便。另外,有時在改變訂單狀態的同時,還會伴隨著一些變化,例如「當訂單的狀態變成已付款時,要發送手機簡訊給消費者」。
訂單狀態的變化也可能有多種變化,看一下這張圖:
如果依照正常流程,理論上商品需要先「退貨」才能辦理「退款」,但如果是在狀態 2 的「已付款」,還沒來得及出貨結果客人就先按了退款,這時候也是一樣要進行退款流程;或是,商品要辦理退貨,理論上也是要等商品到貨才能退,但可能在運作的過程中客人就想退貨了,也是該進行退貨流程。最重要的是,如果訂單是「待處理」狀態,它不應被直接改成「已到貨」。
如果我們只是用單純的字串在做這個欄位的變化,然後程式裡面是直接修改這個欄位的內容,有機會會造成訂單的靈異現像,例如明明還沒付錢的訂單卻被要求要退款。
有限狀態機
有一個有趣的數學模型:有限狀態機,又簡稱「狀態機」,剛好可以解決我們上面提到的問題。
用平常每天大家都會遇到的「門」當例子,狀態機就是「已經關上的門,只能打開不能關;已經打開的門,只能關不能打開」(廢話);或是開車為例:「在開車打檔的時候,應該是要 1 檔、2 檔、3 檔依續往上打,而不應該是在 4 檔突然打 P 檔」。
在狀態機的模型下,狀態理論上只能順著我們設計的路線走,所以不會有「待處理」的訂單突然就變成「已到貨」狀態的情況發生。
使用 AASM
大概理解「狀態機」的概念後,如果要自己用 if...else...
來設計實作狀態機,可能會非常的辛苦,而且不容易維護。我們可以使用一個名為 AASM 的 Gem 可以很快的幫我們做好這件事。
請把 aasm
放到 Gemfile 裡,存檔後別忘了執行 bundle install
指令。
訂單狀態
AASM 的使用方式滿簡單的,假設有一個叫做 Order
的 Model,我們可以透過 AASM 提供的方法來定義可能發生的「狀態」:
class Order < ApplicationRecord
include AASM
aasm do
state :pending, initial: true
state :paid, :shipping, :delivered, :returned, :refunded
end
end
說明:
state :pending, initial: true
表示是這個 Model 的初始狀態。- 第二個
state
則是列出了除了:pending
之外可能的狀態。 - 在 AASM 這個 Gem 的設計,在資料表中用來存放狀態的欄位名稱預設叫做
aasm_state
,型態是字串,但如果你的 Order Model 的狀態欄位不叫這個名字,可以透過 Migration 改個名字,或是使用column: :state
參數修改預設設定:
class Order < ApplicationRecord
include AASM
aasm column: :state do
state :pending, initial: true
state :paid, :shipping, :delivered, :returned, :refunded
end
end
這樣就可把預設用來存放「狀態」的 aasm_state
欄位名字改成 state
了。
定義事件
除了定義狀態之外,還要再定義一下「事件」。基本上「狀態」是因為觸發了某個「事件」而改變的,不建議也不應該直接修改狀態的內容:
class Order < ApplicationRecord
include AASM
aasm do
state :pending, initial: true
state :paid, :shipping, :delivered, :returned, :refunded
event :pay do
transitions from: :pending, to: :paid
end
event :ship do
transitions from: :paid, to: :shipping
end
event :delivering do
transitions from: :shipping, to: :delivered
end
event :return do
transitions from: [:delivered, :shipping], to: :returned
end
event :refund do
transitions from: [:paid, :returned], to: :refunded
end
end
end
這裡定義了 pay
、ship
、delivering
、return
以及 refund
等事件,其中 return
跟 refund
事件可以在兩個以上的不同的狀態觸發(例如在「已付款」跟「已退貨」都可以變成「已退款」)。
試用
到 rails console
試一下用起來的樣子吧:
$ bin/rails console
>> o1 = Order.create
(0.1ms) begin transaction
SQL (2.2ms) INSERT INTO "orders" ("aasm_state", "created_at", "updated_at") VALUES (?, ?, ?) [["aasm_state", "pending"], ["created_at", 2017-01-12 15:11:52 UTC], ["updated_at", 2017-01-12 15:11:52 UTC]]
(0.7ms) commit transaction
=> #<Order id: 7, product: nil, quantity: nil, aasm_state: "pending", created_at: "2017-01-12 15:11:52", updated_at: "2017-01-12 15:11:52">
使用 Order.create
先隨便建立一組訂單,你會發現這個訂單的狀態是 pending
。AASM 根據你定義的「狀態」也順便送了你一些好用的方法,例如可以問它目前是 pending
嗎?它會告讓你「是」:
>> o1.pending?
=> true
問它目前是不是已付款 paid
狀態嗎?它說「不是」:
>> o1.paid?
=> false
繼續問它,請問這筆訂單現在可以付錢嗎?它說「可以喔」:
>> o1.may_pay?
=> true
那可以出貨嗎?它說「不行」:
>> o1.may_delivering?
=> false
這樣的回答相當合理,因為我們的設定中,沒付錢的訂單本來就不應該直接出貨。既然沒付錢,那現在讓我們觸發 pay
事件來付錢吧:
>> o1.pay
=> true
看一下這筆訂單的狀態:
>> o1
=> #<Order id: 7, product: nil, quantity: nil, aasm_state: "paid", created_at: "2017-01-12 15:11:52", updated_at: "2017-01-12 15:11:52">
它的狀態度成 paid
了,再問問它的狀態:
>> o1.pending?
=> false
>> o1.paid?
=> true
回答都是正確的了。
你在這裡看到的一些問號方法以及 may_
開頭的方法,都是 AASM 在你定義狀態及事件的時候,自動送給你的,相當方便而且防呆。
如果想要在某個事件完成之後接著做另一件事,例如「退款之後發送簡訊通知購買人」,可以在事件裡面加上 after
方法:
class Order < ApplicationRecord
include AASM
aasm do
state :pending, initial: true
state :paid, :shipping, :delivered, :returned, :refunded
#...[略]...
event :refund do
transitions from: [:paid, :returned], to: :refunded
after do
# 發送簡訊通知...
end
end
end
end
這樣就會在做完 refund
退款事件後,接著做 after
方法指定的事了。
使用狀態機來操作訂單的狀態,不僅方便,也可避免直接修改狀態可能打錯字或是其它更嚴重的問題。更多詳細使用方法及 Callback,請見 AASM 的說明頁面。
Comments