iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 16 篇

Day 16|店家改價之後,已經下的單要扣多少?

  • 分享至 

  • xImage
  •  

團主選一家店開團,參與者從菜單點品項。截止時,系統依每個人點餐的金額從餘額扣款,而扣款前這筆金額會先被凍結,這裡其實有兩條很重要的規則:

  1. 使用者下單時看到多少錢,最後就應該扣多少錢。
  2. 已結算訂單的明細,應該能和當時的扣款紀錄對得上。

問題是:如果店家在下單之後改了價格,訂單到底應該看哪一個價格?

直覺做法是只存 menus,order_items 只記住使用者點了哪個菜單項目、點了幾份,價格和品名,需要時再 JOIN menus:

SELECT m.product_name, m.unit_price, oi.quantity,
       m.unit_price * oi.quantity AS subtotal
FROM order_items oi
JOIN menus m ON oi.menu_id = m.menu_id
WHERE oi.order_id = ?

價格(unit_price)和品名(product_name)只存一份,資料比較乾淨,如果店家改名或改價,所有地方 JOIN 到的資料也會自動更新,但是這樣會讓歷史訂單跟著菜單一起變。

先看兩種情況:

1.ord-002 尚未結算,alice 下單時珍珠奶茶是 55 元;店家之後改成 65 元。若重新 JOIN menus,截止時就會算成 65 元,變成「55 元下單、65 元扣款」。

2.ord-001 已經結算,alice 當時買了 70 元的招牌鍋貼和 35 元的酸辣湯,流水帳記錄扣款 105 元;店家之後把鍋貼改成 80 元。重新 JOIN 後,訂單明細會變成 115 元,和實際扣款對不上。

問題的根源其實不是 JOIN,而是兩張表記錄的是不同時間點的事實。

資料表menus負責記錄「這家店現在賣什麼、賣多少?」,而order_items則是負責記錄「使用者當時買了什麼、多少錢?」如果訂單只保存 menu_id,就等於拿「現在的菜單」去回答「當時買了什麼」,因此這裡不能只追求資料正規化,而要保存一份下單當下的快照(snapshot)。

下單時,從 menus 讀出品名和價格,複製到 order_items:

// 4. Insert order item with snapshot
OrderItem item = new OrderItem();
item.setOrderId(request.getOrderId());
item.setUserId(userId);
item.setMenuId(request.getMenuId());
item.setProductName(menu.getProductName());
item.setUnitPrice(menu.getUnitPrice());
item.setQuantity(request.getQuantity());
orderItemMapper.insert(item);

menu_id 仍然保留,因為它可以告訴我們「這個品項原本對應哪個菜單項目」,只是價格和品名不再依賴 menus 的目前狀態。

下單之後,計算也只使用快照

subtotal 由資料庫根據 unit_price * quantity 計算,之後凍結金額、結算扣款、通知訊息與訂單明細,都只讀 order_items:

用途 使用的訂單資料
凍結金額、可用餘額 subtotal
結算扣款 subtotal
結算通知 subtotal
訂單明細 product_name、unit_price、subtotal

這裡有一個容易忽略的設計原則:

下單之後,凡是需要知道「當時買了什麼、多少錢」的地方,都不能再回頭查 menus。

目前專案只有建立訂單時會讀 menus,這也是這個設計成立的前提。如果未來某個查詢又偷偷改成 JOIN menus 取得價格,店家一改價,歷史訂單就會再次被影響。


上一篇
Day 15|分頁查詢:資料變動時,分頁可能位移
下一篇
Day 17|CAS:兩台機器同時扣款會發生什麼事?
系列文
做一個團購後端,順便搞懂那些事 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言