
系列:30 天打造企業級 PLM|面向:後端|素材:Trigger 規則引擎模組
「送簽時自動帶入部門主管」「放行時自動把狀態欄改成已發行」「金額欄有值時自動加會財務」,每個部門要的自動化都不一樣,而且永遠會長出新的。一條一條寫成程式,系統很快變成 if-else 沼澤。Mini-PLM 的解法是把規則變成資料:觸發點、條件、動作全部可設定,交給管理員自己組,程式這邊只提供一顆引擎。
Trigger 設定頁實機畫面:

TriggerExecutionLog 每次執行都留紀錄,定位是稽核工具,debug 只是順便在以前 Oracle Agile PLM 中,所有自訂自動化邏輯都依賴 Event Framework 與 Process Extension (Java PX / Groovy PX)。
在舊架構下維護自動化,痛點極多:
Mini-PLM 採用宣告式 Trigger 規則引擎,將自動化全部「資料化」:管理員在 UI 就能配置觸發點、條件組與執行動作,並具備沙盒般的失敗策略(WARN_CONTINUE / BLOCK)與完整的 TriggerExecutionLog 稽核日誌。

觸發點(TriggerPoint)= 什麼時候看規則
條件(TriggerCondition)= 什麼情況下算命中
動作(Executor)= 命中了做什麼
五個觸發點,各自綁定執行階段。TriggerPointPhaseMatrix 實碼如下,這張表本身就是設計文件:
map.put(TriggerPointEnum.AFTER_FORM_MODIFIED, TriggerExecutionPhaseEnum.AFTER_COMMIT);
map.put(TriggerPointEnum.BEFORE_ADD_APPROVERS, TriggerExecutionPhaseEnum.IN_TRANSACTION);
map.put(TriggerPointEnum.AFTER_ACTION_APPROVED, TriggerExecutionPhaseEnum.IN_TRANSACTION);
map.put(TriggerPointEnum.AFTER_ADD_APPROVERS, TriggerExecutionPhaseEnum.AFTER_COMMIT);
map.put(TriggerPointEnum.AFTER_SYSTEM_EVENT, TriggerExecutionPhaseEnum.AFTER_COMMIT);
IN_TRANSACTION 跟業務動作同一個交易,可以影響結果、失敗可回滾;AFTER_COMMIT 則是業務先落定、自動化事後跑,失敗不拖累主流程。哪個觸發點掛哪個階段由引擎層寫死,不開放設定,因為這是正確性問題不是偏好問題:加簽核人必須在交易內(要影響本次推進),通知類必須在 commit 後(不能因為通知失敗回滾簽核)。這張矩陣也是 Day 12 交易血淚的預告。

命名這件事同樣踩過一輪。觸發點曾從「系統事件導向」(如 FORM_SAVED)重構為使用者事件導向(AFTER_ACTION_APPROVED):管理員設規則時,腦中的語言是「簽核通過之後」,「XX Service 的 save 之後」對他毫無意義。規則引擎的詞彙表要用設定者的語言。
條件子表用 groupNo 表達布林邏輯(TriggerConditionEvaluator 的 javadoc 即規格):
/**
* 判斷結構化條件是否符合;同 groupNo 採 AND,不同 groupNo 採 OR。
* @return true 表示至少一個群組符合
*/
即 (A AND B) OR (C AND D),不支援任意巢狀括號。這是刻意的表達力上限:DNF(析取範式)足以覆蓋實務上所有規則,而 UI 只需要「群組內加條件、加新群組」兩個動作,管理員不需要懂布林代數。表達力與可維護性只能挑一邊,我挑了可維護性。

TriggerResult result = executorRegistry
.getExecutor(trigger.getActionType())
.execute(trigger, context);
目前兩顆主力 executor:SET_FIELD(改表單欄位值,支援 $NOW token)與 IMPORT_APPROVER(動態加簽核人,22 種來源 resolver:固定名單、群組、表單欄位指到的人、前一關簽核者的主管、Approver Matrix、External Lookup……Day 10 的兩套路由在這裡以 resolver 形式被複用)。新動作型態就是實作介面加註冊一行,引擎本體不動。
每條 trigger 可設定失敗策略,這其實是把商業風險觀資料化:
| 策略 | 行為 | 適用 |
|---|---|---|
| WARN_CONTINUE | 記 log 繼續 | 錦上添花類(自動帶值) |
| DISABLE_ON_ERROR | 壞規則自動下線 | 防止一條爛規則每張單都炸 |
| BLOCK | IN_TRANSACTION 時拋出回滾 | 寧可卡單不可錯簽類 |
「Previous Step」來源模式(取前一關簽核者,再解析其主管,自動加簽主管的關鍵拼圖)曾經整包消失:前端 UI 沒了、後端 resolver 沒了、entity 欄位沒了。查 git log 一無所獲,因為這批檔案從頭到尾是 untracked,開發時一直沒 git add,某次清理工作目錄就蒸發了。
救援過程像資料考古:git fsck --lost-found 撈 dangling blob,靠 IDE 的 local history 與 blob 內容交叉比對,從 85e6efd 等懸空物件把程式碼一塊塊拼回來,再用四個 fix commit 補齊依賴(entity 欄位、repository 查詢、前端型別)。教訓樸素到不好意思寫:功能沒進版控就不存在。長期 WIP 也要 commit,哪怕丟在私人分支;「等做完再一起 add」是整包蒸發的標準前置條件。
觸發點綁死執行階段,正確性不留給設定;條件用 DNF 群組,表達力讓位可維護性;動作走註冊表,留給擴充。今天講的是引擎怎麼組起來,明天講它最要命的部分。Day 12:REQUIRES_NEW vs REQUIRED 的血淚,同一顆引擎裡兩種交易傳播策略並存的原因。