痛點長這樣
電商後台的需求,隔幾週就來一個同款:
- 「退款超過一千元要客服主管核准」
- 「商品定價低於成本(負毛利)要營運主管簽核」
- 「大額出庫單要倉管加財務兩關」
第一個需求,你在退款流程裡加個 status = PENDING_APPROVAL 加一支核准 API,收工。第二個,在商品那邊再寫一套。第三個來的時候你發現:三套「送審→通知審批人→通過/駁回→留紀錄」的 code 長得 87% 像,卻散在三個模組各自維護——然後 PM 說「駁回要能填理由」,你要改三個地方。
這就是該抽「審批流引擎」的訊號:審批是流程,不是業務。退款、商品、出庫是業務,各自不同;「走關卡」這件事是通用的,應該只寫一次。
通用審批引擎的四塊積木
我自己的電商後端有一個 workflows 模組就是這樣抽的,四個模型講完:
1. Workflow(流程定義):「負毛利商品審核」是一個 workflow,定義它有幾關、順序如何。流程定義是資料不是 code——加一關是後台設定,不是改版上線。
2. Step(關卡):每一關定義「誰可以審」。指定審批人有三種常見策略,我的系統三種都支援:
- 指定角色(ROLE):「任何營運主管都可以審」——最常用,人員異動不用改流程
- 指定個人(USER):「一定要財務長本人」
- 部門主管(DEPARTMENT_MANAGER):「送審人的直屬主管」——動態解析,每個人送出去找到的審批人不同
每關還有一個容易被忘掉的欄位:逾時策略(auto_approve_on_timeout)。審批最大的隱形成本是「卡在某人身上三天沒人動」——逾時自動通過、自動升級給上一層、或發提醒,設計時就要選。
3. Request(一次送審):關鍵設計在這——它跟業務對象的關聯用 GenericFK(一個能指向「任意類型資料」的外鍵:存「對象類型+對象 ID」兩個欄位,而不是寫死指向商品表或退款表)。這就是引擎能通用的原因:今天審商品、明天審出庫單,引擎一行不改。Request 上再帶個 progress(走到第幾關)。
4. Log(審批紀錄):誰、哪一關、通過還是駁回、理由、時間。審批的本質是「找人負責」,沒有紀錄的審批等於沒審。
業務端怎麼接:兩個掛勾點
引擎通用了,業務端只剩兩件事:
- 送審時機:業務規則判斷「這筆要不要審」(例如毛利算出來是負的)→ 建一筆 Request、把業務對象標成「審核中」。
- 審完回呼:最後一關通過(或任一關駁回)時,引擎通知業務端做後續——商品上架、退款放行、或退回修改。這裡用事件(發「審核通過」事件讓業務端訂閱)比直接呼叫乾淨,引擎不必知道每種業務通過後要幹嘛。
注意「審核中」的狀態放業務對象身上(商品的 status),「走到第幾關」放 Request 身上——兩台狀態機,各管各的(為什麼要拆,見〈狀態機設計〉那篇)。
什麼時候不要用引擎
反方向也要講。只有一種審批、而且就一關(「退款要主管按一下」)——直接一個狀態欄位+一支 API 就好,引擎是第三次重複才值得的投資。判斷句:「流程會不會被非工程師改?」 會(PM 想自己在後台調關卡)→ 引擎;不會、而且只有一處 → 寫死沒有罪。
相關
- 「審核中」跟「第幾關」為什麼是兩台狀態機 → 狀態機設計
- 審完通知業務端的事件作法 → 15 三個 async primitive 的 event 段