Casha | 2. RBAC, Menu & Order api 等設計
Casha 的 admin-portal 提供了一套能配置餐廳權限與設定菜單的功能, 今天就來介紹一下 admin-portal 的幾個核心設計:
- 管理平台, 餐廳, 分店的 RBAC 機制
- 座位與菜單的設計
最後再花約一半的篇幅介紹 order api。
開始前提醒, 因為篇幅過長的原因, 若有興趣可以直接到我的 git doc 內觀看
RBAC
先看 ER Model:

以 ER Model 習慣的講法:
一個平台能有多個用戶, 每個用戶對應一組帳號
每個帳號可以綁定一個或多個角色
每個角色可以綁定多個權限
每個權限會對應到一組 api 在 crud 上的操作, 並對應用戶在後台的功能清單
在 cahsa admin-portal 之中, 依照使用者的身分, 分為三個預設角色:
- 平台管理員
- 餐廳管理員
- 分店管理員
這三個預設的角色具備全部該範圍下的全部權限, 當平台管理員收到註冊請求後創立餐廳管理員, 該帳號即綁定餐廳全數權限, 同理, 餐廳在創建分店時, 亦生成預設的分店管理員帳號, 後續再由這些管理員創建同犯完角色時, 去綁定他們設定的角色與權限。
Note
1. Spring Security
角色所綁定的權限(Perminssion)會對應一支 api, 故在開發新功能的時候, 會需要將設定的 Perminssion Code 寫入 Controller 的 @PreAuthorize 內。
1 |
|
2. User Session
在整個 admin portal 中, 只有少部分 api 如登入, 註冊等不需要身分驗證, 而身分驗證成功後, 會在 redis 內創建 user session, 後續的 api 則在 gateway 驗證 jwt 成功後, 將 userAccount 放入 Header 後往 Bff 發送, 並在 Bff 層從 redis 內解析, 故在做相關的查詢的時候, 請求不需要特別帶 branch 或 restaurant Id。
1 |
|
3. 身分驗證流程
前面提到的 gateway 和 redis 等後踢前的效果, 核心是由 Auth-Service 頒發 jwt, 後續都由 gateway 進行驗證, 不用每次都再轉發到 Auth-Service 內, 但 gateway 和 auth-service 就要管理同一套 salt。

座位與菜單

1. 座位狀態
不得不說, 在思考設計的時候才發現分店管理座位狀態的設計是 F&B 系統非常核心的一塊。
一般常見的小型店面, 只要不須帶位的, 通常都會將 qrcode 貼在桌上, 所以點餐流程只需要集中處裡訂單即可。而需要帶位的店面則在 pos 上還需要仰賴座位狀態進行帶位, 常見的業務方法會即時生成一個共用的 qrcode 共同管理訂單狀態。
因此在設計後端座位管理的時候就要考量到, 這一套服務是要提供給上述哪一種需求的客戶, 又或是提供狀態選項, 平台全包。很不巧的我選擇了後者, 發現越做越複雜, 目前程式是依照 branch.visit_mode 來管理是否有追桌的行為, , 現在只先實作 DISABLED 不追桌, 若未來有時間可以再 refactor 與完善一下。
用戶掃碼
下面這張圖是其中一間分店第二桌的 QR Code, 在完成座位設定後生成, 其 url 為
在 url 中後面是該座位的 token, 經解密後可以取得 branch & table Id, 在解析完成後攜帶分店資訊返回前端, 再轉導到菜單進行菜單查詢。

2. 菜單設計
Casha 的菜單設計為 一個時間區間下所設定的單品與分類組合, 單品與分類為多對一, 相對直覺, 而菜單在建立後還需要配置各個版本, 給定菜單啟用的時間, 以及該時端要配置的分類與單品。
關於菜單的快取設計

Menu 是一個快取優先讀取 (Cache First) 的設計, 當第一個人訪問該 QR Code 並獲取菜單後, 菜單的內容與訊息便會儲存在 Redis Cache 內, 並且設定 180 ± (180 * 20%) 秒的 TTL (Time To Live), 來分散尖峰時刻 Cache 同時過期導致大量用戶請求湧入造成快取雪崩的風險; 而後面的用戶則會快取命中 (Cache Hit), 直接取用快取資料。
快取未命中 (Cache Miss) 時, 處理競爭鎖與重建快取 (Lock Competition & Cache Rebuilding), 生成一個全域唯一的 lockToken(UUID),用於安全地釋放鎖, 並利用 Lua SET lockKey lockToken NX EX LOCK_EXPIRE_SECONDS 指令(經由 setIfAbsent 方法)原子性地嘗試獲取鎖, 同時給鎖設定一個較短的過期時間(3秒),防止持有鎖的執行緒崩潰導致鎖永遠無法釋放(死鎖)。
- 若成功獲取鎖, 先處理雙重檢查 (Double-Check), 避免成功獲取鎖的極短時間內,已經有另一個執行緒完成了快取重建並釋放了鎖;接著通過 Feign Client 呼叫 store-service 的 /resolveActive API 來獲取最新的菜單資料(也是整個流程中最耗時、成本最高的操作), 成功後將 Rs 轉換與寫入快取, 設置 TTL 抖動並返回結果。
- 若未能獲取鎖, 意味著已經有另一個執行緒正在重建快取, 退避等待 80ms 後再次嘗試讀取, 若等待後,快取依然為空(例如:持有鎖的執行緒執行非常慢,或在寫入快取前失敗了),則執行降級策略不再等待,而是直接降級呼叫下游服務。
最後當前成功獲取了鎖的執行緒,必須在 finally 程式碼塊中執行鎖釋放邏輯。
詳細內容可以到 git doc 內觀看
3. order api
點餐的流程起自 用戶掃碼到顯示訂單完成, 前置作業要完成菜單的配置。上述提到用戶在掃碼後獲取餐廳與分店等訊息, 並取得菜單資訊,下一步便能在菜單上點餐並前往購物車結帳。
目前的設計沒有真正串接金流, 而是在前端自行發送一個 mock 的付款按鈕, 所以核心付款的 api 會是:
- /cis/order/create: 創建訂單, 支付前準備, 同時會最終確認存貨是否充足
- /cis/payments/mock-callback: 模擬支付方付款成功時的 callbacl method
由於篇幅限制, 下面會稍微簡化, 若想看更詳細的流程歡迎到 git doc 內觀看
1. order/create
這個 API 的核心目的是:處理使用者在點選「下單」或「結帳」按鈕後、實際發起支付前的所有準備工作。它是一個集「存貨驗證」、「最終計價」、「庫存預留」和「訂單創建」於一體的原子性操作。
該 api 在 BFF 層僅做轉發, 在 Biz 主要職責可分解為:
- 最終報價與鎖庫: 由 order-service 呼叫下游 store-service 進行最終的存貨檢查與計價,並預留庫存。
- 冪等性保證:防止因客戶端重試(如網路抖動、用戶雙擊)而導致重複下單。
- 創建訂單:將訂單資訊(含快照)持久化到資料庫,狀態為「已下單,未支付」(PLACED, UNPAID)。
- 綁定預留:將臨時的庫存預留令牌與剛創建的正式訂單綁定,為後續的支付成功/失敗處理做準備。
- 支付超時管理:發送一個延遲消息,設定支付時限(如15分鐘),超時後系統將自動取消訂單並釋放預留的庫存。
其最終目標是生成一個待支付的訂單,並確保在支付過程中,用戶所見的價格和庫存是確定的、被臨時鎖定的。

2. payments/mock-callback
理支付提供商發送的非同步支付結果通知,並根據結果更新訂單狀態、驅動後續業務流程(如庫存結算或釋放)。
它是一個冪等的操作,需要妥善處理以下情況:
- 支付成功:將訂單狀態更新為「已支付」,並觸發庫存預留轉為實際銷售的流程。
- 用戶取消:用戶在支付過程中主動取消,立即釋放預留的庫存。
- 支付失敗:支付因各種原因失敗,保持訂單為「未支付」狀態,等待超時流程來處理。
- 重複通知:支付提供商可能重複發送通知,API 必須能識別並正確處理已處理過的訂單。
全流程
最後來張超大的流程圖。

結語
花了滿長的篇幅介紹 order & payment 的機制, 現在 Jmeter 壓力測試業在如火如荼的進行中, 測試後除了記錄觀測平台外, 也能用測資來調整後續報表的設計。
預計下一篇會介紹 Jmeter 的實作和 Grafana/Prometheus 的設置, 並看看在當前的設計下, 我的服務表現如何 (汗…)。
