iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

團購訂單有五種狀態。狀態本身不難,難的是每個狀態允許哪些操作——這件事若沒有明確定義,會散落成各處的 if 判斷。

狀態 意義 可下單/改單 可取消 可扣款
OPEN 開團中 ○ ○ ✗
CLOSED 已截止 ✗ ○ ○
SETTLED 已結算 ✗ ✗ ✗
CANCELLED 已取消 ✗ ✗ ✗
FAILED 結算失敗 ✗ ✗ ○(可以重試扣款)

以流程來看每個狀態之間的關係與轉換大概如下圖:

https://ithelp.ithome.com.tw/upload/images/20260927/2016866785s1hiGaan.png

※注意: SETTLED 不能回到任何狀態、CANCELLED 是終點、OPEN 不能直接跳到 SETTLED。

FAILED 為什麼必須是獨立狀態?

最初的設計只有「成功」與「失敗」兩種結果,用一個 boolean 就夠,問題是萬一「有人餘額不足」這件事:

  • 它不是成功——錢沒扣到。
  • 它不是結束——使用者儲值之後應該還能重試。
  • 它也不是 CLOSED——CLOSED 意味著「尚未嘗試扣款」,但這張單已經試過了。

強行塞進既有狀態的結果,是程式裡開始出現這種東西:

if (status == CLOSED && hasAttemptedPayment && !allDebited) { ... }

當你需要用「狀態 + 額外旗標」來描述一個情況,那個情況就應該是一個獨立狀態。 加一個列舉值的成本,遠低於讓每處判斷都得知道那些旗標的組合。

FAILED 獨立出來之後,重試邏輯只需要問一個問題:狀態是不是 FAILED 或 CLOSED?

狀態檢查寫在操作的入口

每個會改變狀態的操作,第一件事就是檢查當前狀態是否允許:

if (order.getStatus() == OrderStatus.SETTLED) {
    throw new ConflictException("該訂單已結算,無法進行付款");
}
if (order.getStatus() == OrderStatus.OPEN) {
    throw new IllegalStateException("訂單尚未截止");
}
if (order.getStatus() != OrderStatus.CLOSED && order.getStatus() != OrderStatus.FAILED) {
    throw new IllegalStateException("訂單狀態不允許結算");
}

訂單狀態使用enum,透過 MyBatis-Plus 的 @EnumValue 以字串形式存進資料庫(orders.status 為 VARCHAR(20))。

public enum OrderStatus {
    OPEN("OPEN"), CLOSED("CLOSED"), SETTLED("SETTLED"),
    CANCELLED("CANCELLED"), FAILED("FAILED");

    @EnumValue
    private final String value;
}

不用 TINYINT 存序數,字串的可讀性較高,且日後若在中間插入一個新狀態,所有既有資料的意義會整批位移,而且不會有任何錯誤提示。字串則是自我描述的——直接查資料庫就看得懂,也不怕重新排序。

代價是儲存空間略大與比較稍慢,對這個量級的資料完全不構成問題。


上一篇
Day 12|換掉網址裡的 ID,就看得到別人的餘額?!
系列文
做一個團購後端,順便搞懂那些事 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言