customers └─ 1:N orders ├─ 1:N order_items │ └─ N:1 product_variants ├─ 1:N order_status_history │ └─ N:1 admin_users(actor_id 可空) ├─ 1:N order_event_history │ └─ N:1 admin_users(actor_id 可空) └─ 1:N order_coupons ├─ N:1 coupons(coupon_id 可空) └─ 1:1 coupon_claims
coupons └─ 1:N coupon_claims └─ 1:0..1 order_coupons
total 訂單實付總額 GREATEST(subtotal + shipping_fee - discount, 0)
refund_status 退款狀態,預設 ‘none’, (requested、approved、processing、refunded、rejected、failed。)
status 訂單履約狀態, (unshipped, shipped, completed, cancelled, returned)
product_stock_reservations.expires_at 對齊。
idx_orders_checkout_expiry(unpaid 且 expires 非空)order_id 所屬訂單 ID idx_order_items_order:order_id
variant_id 商品規格/SKU ID idx_order_items_variant:variant_id
occurred_at 狀態發生時間 actor_id 操作者 ID,可空 idx_order_status_history_actor:actor_id
note 狀態變更備註,可空 刪除訂單主檔時,自動刪除對應的訂單歷程 idx_order_status_history_order_time:(order_id, occurred_at)
source_history_id 原 order_status_history.id;UNIQUE,
order_id 所屬訂單 ID; ON UPDATE CASCADE、ON DELETE CASCADE。 idx_order_event_history_order_time:(order_id, occurred_at)
occurred_at 事件發生時間;
actor_id 操作者 ID,可空; ON UPDATE CASCADE、ON DELETE RESTRICT。 idx_order_event_history_actor:(actor_id)
discount_value 折扣數值, discount_value <= 100 (percent) discount_value > 0
order_id 套用優惠券的訂單 ID coupon_id 優惠券ID;刪除優惠券時設為 NULL,交易快照會保留。 idx_order_coupons_coupon:coupon_id
coupon_id 被領取的優惠券 ID ON UPDATE CASCADE,ON DELETE RESTRICT UNIQUE(coupon_id, customer_id) idx_coupon_claims_coupon_status(coupon_id, status)
customer_id 領取優惠券的會員 ID ON UPDATE CASCADE,ON DELETE RESTRICT。 UNIQUE(coupon_id, customer_id) idx_coupon_claims_customer_status(customer_id, status)
status 領券狀態,預設 claimed, - claimed: 已領取,尚未使用 - consumed:已使用 - revoked: 已撤銷 - expired: 已失效
claimed_at 領券時間。預設 now()。 consumed_at、revoked_at 不得早於 claimed_at。
consumed_at 優惠券實際使用時間,可為 NULL。 - status = consumed 時必須有值。 - status = claimed 時必須為 NULL。
revoked_at 優惠券撤銷或失效時間,可為 NULL。 - status = revoked 或 expired 時必須有值。 - status = claimed、consumed 時必須為 NULL。
前台套用優惠券與下單
pages/checkout.html
↓
載入 js/data-paths.js
載入 js/api-mock.js
載入 js/components/coupons.js
載入 js/pages/checkout.js
[pages/checkout.html 第 335~345 行]
↓
initCheckoutPage()
[js/pages/checkout.js 第 69 行]
↓
_initCheckoutCoupon()
[js/pages/checkout.js 第 86、386 行]
↓
YuruiCoupons.loadCoupons()
[js/components/coupons.js 第 23 行]
↓
API.coupons.getAll()
[js/api-mock.js 第 487 行]
↓
DataPaths.coupons
js/data-paths.js 第 30 行
↓
data/promotions/coupons.json
使用者輸入折扣碼後:
_applyCheckoutCouponCode()
[js/pages/checkout.js 第 409 行]
↓
YuruiCoupons.validateCoupon()
[js/components/coupons.js 第 97 行]
↓
檢查券碼、status、會員生日/首購資格、minimum amount
↓
calculateAppliedCoupons()
[js/components/coupons.js 第 82 行]
↓
將套用中的券碼保存到 localStorage
[js/components/coupons.js 第 140 行]
建立 Checkout Session 後: confirmOrderBtn ↓ _handleConfirmOrder() ↓ _buildCheckoutRequest() ↓ 只建立 variantId、quantity、shipping、paymentMethod、idempotencyKey ↓ API.checkout.createSession(request) ↓ Spring Boot 從 PostgreSQL 建立快照並重算 pricing ↓ 暫存 sessionStorage.lastCheckoutSession ↓ 以 CheckoutSession.pricing 覆蓋摘要,等待 I-7 付款下一步
draft 可 PATCH 收件資料與付款方式,不清空購物車。ready_to_pay 顯示後端金額與 checkout_expires_at 倒數。主動取消或逾時會清除前端 Session;訂單取消與庫存釋放仍由後端交易負責。
orders、order_items 與庫存保留,前端不建立訂單 ID 或交易快照。coupon_claims,也不讓前端折扣覆蓋後端 pricing。localStorage.mockOrders,不把 CheckoutSession 當成 Legacy Order。會員中心讀取訂單
pages/member-center.html
↓
initMemberCenterComponent()
[pages/member-center.html 第 50 行]
↓
loadData()
[js/components/member-center.js 第 966 行]
↓
API.orders.getByCustomerId(customerId)
[js/components/member-center.js 第 986 行]
↓
API.orders.getAll()
[js/api-mock.js 第 405 行]
↓
data/commerce/orders.json
+ localStorage.mockOrders
↓
依 customerId 篩選
後台訂單列表與狀態變更
AdminAPI.useBackend = false
[admin/js/admin-api.js 第 16~19 行]
↓
loadOrders()
↓
直接讀 DataPaths.orders
[admin/js/orders.js 第 947 行]
↓
data/commerce/orders.json
出貨:
點擊 .btn-ship-order
[admin/js/orders.js 第 239 行]
↓
修改 ordersCache 中的 order.status = shipped
↓
push 一筆 order.history
[admin/js/orders.js 第 247~260 行]
↓
AdminAPI.orders.ship()
[admin/js/orders.js 第 265 行]
↓
因 useBackend = false,只回傳 mock Promise
[admin/js/admin-api.js 第 31~40 行]
後台優惠券管理
initDiscounts()
[admin/js/discounts.js 第 91 行]
↓
useBackend = false
↓
讀 data/promotions/coupons.json
[admin/js/discounts.js 第 94~101 行]
↓
新增、停用、刪除只修改 couponsCache 與畫面
↓
AdminAPI.coupons.*
↓
回傳 mock Promise,不寫資料庫
中風險:orders.status 與狀態歷程沒有同步保護
後台使用 /api/admin/orders 查詢訂單,列表先對 order ID 分頁,再載入表頭摘要;商品與狀態歷程只在詳情讀取。這可避免 order_items 或 order_status_history 將列表資料列放大。
履約狀態固定為 unshipped → shipped → completed。線上付款必須先由可信付款流程標記 paid;COD 可以 unpaid 出貨,完成時於同一交易標記 paid。Admin 不提供任意 payment、refund 或 status PATCH。