NextShop:一個在意「同時發生」的全端電商 Demo
2026年3月26日

為什麼做這個專案
購物網站的畫面不難做,難的是畫面背後那些「不能出錯」的地方:兩個人同時買最後一件商品、使用者連按兩次結帳、金流通知比訂單還早到……
NextShop 是一個前後端分離的電商 Demo,重點不在功能多,而是結帳流程的一致性:庫存不超賣、訂單不重複、付款通知不重複處理。
技術架構
| 項目 | 使用技術 |
|---|---|
| 前端 | Next.js 15 + React 19 + TypeScript,部署在 Vercel |
| 狀態管理 | Zustand(購物車) |
| 後端 | Spring Boot 3 + Spring Data JPA + Spring Security,Docker 部署在 Render |
| 資料庫 | PostgreSQL(Neon) |
| 驗證 | JWT + 會輪替的 Refresh Token |
| 金流 | Stripe PaymentIntent(Demo 模式)+ Webhook |
| API 文件 | springdoc-openapi(Swagger) |
主要功能
- 商品與分類:分類支援階層、排序與分頁,商品可有多張圖片
- 購物車:用 UUID 識別,數量會自動合併
- 結帳與訂單:訂單有待付款、已付款、失敗、取消、退款五種狀態,每次狀態變化都會記錄成事件
- 金流:Stripe 付款與 Webhook,可儲存付款方式
- 會員:登入、角色權限、收件地址管理
- 後台:商品、分類、使用者、訂單管理,管理員的每個操作都會留下稽核紀錄
- 排程:每分鐘處理逾時未付款的訂單,每小時檢查一次庫存與訂單是否一致
三個讓結帳「不會出錯」的設計
1. 扣庫存只用一條 SQL:不超賣
常見的錯誤寫法是「先查庫存,夠的話再扣」。兩個請求同時進來時,可能都查到還有 1 件,結果兩邊都扣成功。
這裡把檢查和扣除合併成一條 UPDATE,只有庫存足夠時才會更新:
@Query("""
UPDATE Inventory i SET i.stock = i.stock - :qty
WHERE i.product.id = :productId
AND i.stock >= :qty
""")
int deductStockAtomic(@Param("productId") Long productId, @Param("qty") int qty);資料庫保證這條語句是原子操作。回傳更新筆數為 0,就代表庫存不足,直接拋出例外並讓整筆交易回滾。
2. 冪等結帳:按兩次也只有一張訂單
前端每次結帳都會帶一個 requestId。後端先用它查有沒有已經建立的訂單,有的話直接回傳原本那一張:
return orderRepository.findByRequestIdWithItems(request.getRequestId())
.map(OrderResponse::from)
.orElseGet(() -> processCheckout(request));網路重試、使用者連點,都不會產生重複訂單。取消訂單時退回庫存也用一個 stockRestored 旗標保護,同一張訂單不會被退兩次。
3. Webhook 去重:同一個付款通知只處理一次
Stripe 的 Webhook 可能重送,也可能比訂單資料更早抵達。處理順序是:
- 驗證簽章,確認通知真的來自 Stripe
- 用事件 ID 檢查是否處理過,處理過就直接回 OK
- 如果找不到對應的訂單,先存進「待處理」佇列並發出警示,而不是直接丟掉
event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
if (webhookEventRepository.existsByEventId(event.getId())) {
return "OK";
}前端的購物車只存 cartUuid,不用流水號 ID,避免被人用遞增的數字猜到別人的購物車。
開發過程中踩到的坑
- 連線池的 prepared statement 錯誤:資料庫走 PgBouncer 這類連線池時,JDBC 預設的 prepared statement 快取會出錯。最後在 JDBC 設定裡關掉快取(
prepareThreshold=0)才解決。 - 壓力測試:用 k6 腳本打結帳 API,20 個併發請求很穩定;拉到 50 時,免費方案資料庫的連線數先用完了。所以後端加了一個 Semaphore 限流,超過就回傳 429,保護資料庫不被打爆。
- 免費方案的休眠:後端放在 Render 免費方案,閒置一段時間就會休眠。後來資料庫也遇到免費方案自動暫停,於是搬到閒置時只休眠、有請求就自動喚醒的 Neon。
小結
這個專案讓我體會到:電商的難度不在頁面,而在同時發生的事情。原子操作、冪等、去重,這些設計在畫面上完全看不到,卻決定了一個系統能不能被信任。
後端在免費方案上,閒置後第一次開啟需要約 50 秒喚醒。