
系列:30 天打造企業級 PLM|面向:後端
Day 9 的流程引擎會產生簽核任務,但「這張單該給誰簽?」還沒回答。答案藏在部門、類別、金額級距的交叉矩陣裡,而且組織三個月改一次。把簽核人寫死在關卡設定裡的系統,每次改組都要 IT 加班。今天講兩個資料驅動的解法:Approver Matrix 矩陣路由,與 External Lookup 外部人事系統即時查詢。
Approver Matrix 維護頁實機畫面:

在以前 Oracle Agile PLM 中,如果要實現動態簽核路由(例如依金額指派主管、呼叫外部 HR 人事系統),唯一的途徑是撰寫客製化的 Process Extension (Java PX) 或 Event Handler。
這種作法在 J2EE 容器下有很大的隱患:
STUCK Thread 告警,甚至拖垮整台應用伺服器。Mini-PLM 的解法是將規則與整合徹底「資料化」與「標準化」:自家規則走 Approver Matrix 多維度矩陣(管理員自己改,即刻生效),外部人事走 REST External Lookup(具備連線逾時保護與錯誤隔離)。
實際資料表 exp_approver_matrix 的結構,就是 Excel 的正規化:
exp_approver_matrix
form_type_id ─┐
workflow_id ├─ 適用範圍(哪種表單、哪條流程、哪一關)
step_id ─┘
dept_code ─┐
category ├─ 比對維度(部門、類別、金額級距、優先級)
amount_band │
priority_level ─┘
approver_user_id ─┬─ 命中後指派(指定人 或 指定群組,二擇一)
approver_group_id─┘
role ← 簽核角色(Approver / Observer / Notifier)
order_by ← 同關多人時的順序
有個取捨值得說:維度欄位(dept_code / category / amount_band / priority_level)允許留空,留空等於萬用字元,查找時填了才比對。這讓一張矩陣同時容納精準規則與 fallback 規則,比對不到專屬規則時退回通用規則。值的來源是表單欄位,Day 4 的 metadata 又出現了:表單上「部門」「類別」欄位的值,就是矩陣查找的 key。
簽核矩陣在處理專案組織規則,但「部門主管是誰」這種規則還是在人事系統。External Lookup 模式下,簽核人指派變成一次 HTTP 呼叫:送出 lookupKey(如員工工號加層級),人事系統回簽核人清單。系統上有三件事要顧好。
API 的定義要嚴:400(請求格式錯)與 404(查無此人)語意要分開。曾把 lookup-not-found 與其他錯誤混用同一個 exception,前端無法正確顯示,後來拆成專屬例外與 spec 對齊的錯誤 body。lookupKey 要正規化:trim、大小寫、空值,外部整合的 key 髒一點點就 miss。timeout 與失敗策略要先定:外部系統掛了,單子是卡住還是走 fallback?這是商業決策(呼應 Day 12 的失敗策略)。
依賴外部人事系統的功能怎麼開發測試?使用 Mock Server 腳本 mock-lookup-server.ps1,一支 PowerShell 起在 :8888 的假人事系統,回固定測資。前後端不用等真系統的測試環境,E2E 可重複(真系統的資料會變),而且 mock 本身就是 API 的可執行版本。凡外部依賴,必有 mock,這條紀律讓 Day 20 的 E2E 測試成為可能。

表單推進到關卡
→ 關卡設定的簽核人來源 = MATRIX
→ 取表單欄位值組出查找條件(formType + workflow + step + dept/category/...)
→ 精準規則優先、空維度規則兜底(NULLS LAST 排序)
→ 命中列展開成人(指定人直接加;指定群組展開群組成員)
→ initStepActions 產生 Action(接回 Day 9)
實作細節兩個:群組展開時要去重,同一人透過兩條規則命中只加一次;NULLS LAST 讓精準規則排在兜底規則前面,這行為直接寫進 repository 的 @Query,因為它是規則語意的一部分,不能靠預設排序碰運氣。
targetStep
Long 用 == 比:小 ID 測試全過,真實 ID 全 miss矩陣查找要比對 formTypeId,當時寫成 a.getFormTypeId() == b.getFormTypeId()。單元測試全綠,接上真實資料卻一筆都命中不了。
Long 是物件,== 比的永遠是參考位址,沒有任何 Java 版本改過這個語意。會讓人誤以為它能比值的,是 JLS 規定的自動裝箱快取:Long.valueOf() 對 -128 ~ 127 這段回傳同一顆共用實例,所以測資用小 ID 時 == 碰巧成立。
Long a = 127L, b = 127L;
System.out.println(a == b); // true ← 落在快取區間,同一顆實例造成的假象
Long c = 128L, d = 128L;
System.out.println(c == d); // false ← 真相:兩次裝箱是兩顆物件
危險就在這裡:它不是「完全不能用」,而是「小數字看起來能用」。測試資料的 ID 幾乎都從 1 開始編,正好整段躲在快取區間內,於是這個 bug 天生對單元測試免疫,只在正式庫的六位數 ID 上發作。
更麻煩的是 Long 的快取範圍是寫死的,不像 Integer 還能用 -XX:AutoBoxCacheMax 調整——連「把參數調大讓它一致地錯」這條路都沒有。
本專案編譯目標是 Java 8(<java.version>1.8</java.version>),沒有型別系統或編譯器警告會幫你擋,只能靠紀律:包裝型別一律 equals(),或用 Objects.equals() 順便吃掉 null。反過來說,primitive long 用 == 是對的,所以真正該警惕的時機是「entity 的 ID 欄位為了可為 null 而宣告成 Long」的那一刻。
簽核路由做的事說白了,就是把治理規則從程式裡搬出來:矩陣承接自家規則,Lookup 承接外部權威,mock 保障開發迭代。不過還有一類更動態的指派,「簽核者的主管自動加簽」這種依前一關結果決定下一關的規則,矩陣表達不了。那是 Trigger 引擎的領域。明日 Day 11:可設定式自動化(上)。