本文旨在為基于Odoo 18平臺開發一款符合中國用戶習慣的、功能強大的通用工作流審批模塊提供一份全面的技術實現與產品設計方案。該模塊的核心特性包括:為最終用戶設計的圖形化流程設計器、對任意Odoo模型的普適性、復雜的審批節點邏輯(如會簽、條件分支、匯報線查找)、流程中動態操作(如加簽、轉簽),以及與釘釘、企業微信的深度無縫集成。將從系統總體架構出發,深入探討工作流引擎核心、圖形化設計器實現、高級審批路由策略、動態干預機制、本土化生態集成,并最終落腳于性能、安全與可擴展性的長遠考量,為項目的成功實施提供堅實的技術藍圖與決策依據。
第一章:系統總體架構與技術選型
本章宏觀定義模塊的系統邊界、核心組件及其交互方式,旨在構建一個高內聚、低耦合、可擴展的系統架構。
1.1 系統邊界與模塊定位
該模塊將作為 Odoo 的一個獨立應用 (App) 進行開發,遵循 Odoo 的模塊化設計規范。其系統邊界清晰,主要包含以下幾個層面:
- 核心引擎層 (Core Engine Layer): 負責工作流的定義、實例化、狀態流轉和生命周期管理。此層與具體業務模型完全解耦,是整個系統的基石。
- 前端交互層 (Frontend Interaction Layer): 提供給最終用戶的圖形化流程設計器,以及在業務表單中嵌入的審批進度、操作按鈕等 UI 組件。
- Odoo 集成層 (Odoo Integration Layer): 負責與 Odoo 原生功能進行深度交互,包括但不限于 ORM、業務文檔(任意模型)、權限體系、員工組織架構等。
- 外部生態集成層 (External Ecosystem Integration Layer): 一個可插拔的適配器層,專門處理與釘釘、企業微信等中國本土化應用的對接,包括身份認證、消息推送和外部審批。
1.2 架構模式:前后端分離
為應對圖形化流程設計器復雜的前端交互需求,并保證未來技術棧的靈活性,我們采用前后端分離的架構模式。
- 后端 (Backend): 基于 Odoo 的 Python 環境,負責提供 RESTful API。這些 API 將封裝所有工作流相關的業務邏輯,包括流程引擎的驅動、權限校驗、數據持久化等。后端將是所有業務規則和狀態的唯一權威來源。
- 前端 (Frontend): 流程設計器將作為一個獨立的單頁面應用 (SPA) 開發,通過 API 與后端進行數據交換。這種模式允許我們選用最適合圖形化、高交互場景的前端框架(詳見第三章),而不受 Odoo 原生 OWL 框架的限制。審批相關的組件(如審批歷史、操作按鈕)則可以作為獨立的 Web Components 或深度集成的 OWL 組件嵌入到 Odoo 的標準視圖中。
1.3 核心組件交互協議
- 設計器與后端: 設計器通過拖拽生成的流程圖,將被序列化為一個標準化的 JSON 對象,通過 RESTful API 發送至后端。后端接收此 JSON,解析并存儲為流程定義(
workflow.definition
)。 - 業務表單與后端: 當用戶在某個業務單據(如銷售訂單)上觸發工作流時,前端組件將攜帶單據的
model
和res_id
調用后端的流程啟動 API。后續的審批、駁回等操作同樣通過 API 完成。 - 引擎與 Odoo ORM: 工作流引擎在執行過程中,會頻繁通過 Odoo ORM 與其他模塊交互。例如,在條件節點,引擎需要讀取關聯業務單據的字段值;在審批節點,需要查詢
hr.employee
模型來確定審批人。 - 引擎與外部生態: 當需要推送通知或進行外部審批時,引擎會調用外部生態集成層提供的統一接口,該層再將請求適配并發送至釘釘或企業微信的 API。
1.4 技術選型初步考量
- 后端語言: Python 3.x (Odoo 18 環境)
- 數據庫: PostgreSQL (Odoo 默認)
- 工作流引擎架構: 活動驅動 (Activity-based) 結合 事件溯源 (Event Sourcing) 思想(詳見第二章)。
- 前端框架 (流程設計器): Vue 3 或 React。雖然 Odoo 18 的 OWL 2 性能有所提升,但 Vue/React 擁有更龐大的生態系統、更成熟的圖形庫和更豐富的社區支持,對于構建復雜的圖形化設計器而言,優勢顯著(詳見第三章)。
- 圖形庫 (流程設計器): AntV G6 或 LogicFlow,兩者均提供強大的自定義節點和布局功能(詳見第三章)。
第二章:通用工作流引擎核心設計
本章深入探討驅動所有流程運轉的后端引擎,其核心在于構建一個與業務無關、高度可擴展、理論堅實的數據模型和狀態機。
2.1 引擎架構選型:活動驅動vs. 有限狀態機
在工作流引擎的底層架構選擇上,我們面臨兩種主流范式:有限狀態機 (FSM) 和活動驅動 (Activity-Based)。
- 有限狀態機 (FSM):
- 原理: 模型由一組明確定義的狀態(State)和在這些狀態之間轉換的事件(Event/Transition)組成。流程的演進是事件驅動的。
- 優點: 簡單、直觀,對于狀態和轉換路徑固定的簡單流程,實現清晰。Python 庫如
pytransitions
提供了輕量級的實現。 - 缺點: 靈活性和擴展性受限。對于復雜的業務流程,如并行審批(會簽)、條件分支、動態加簽、子流程等,FSM 的描述能力會變得非常復雜和臃腫。狀態和轉換通常需要硬編碼,難以實現用戶通過圖形化界面動態定義流程。
- 活動驅動 (Activity-Based):
- 原理: 流程被看作是一系列相互連接的活動(Activity)或任務(Task)的集合。流程的推進是基于活動的完成。這種模型與 BPMN (Business Process Model and Notation) 標準高度契合。
- 優點: 高度靈活和富有表現力。能夠原生支持復雜的流程結構,如并行網關、包容性網關、事件、子流程、調用活動等。這與我們圖形化設計器的目標完全一致,用戶在畫布上拖拽的節點(審批、抄送、條件判斷)可以直接映射為后端的活動。
- 缺點: 實現相對 FSM 更復雜,需要更完善的數據模型來描述流程定義和實例。
結論: 考慮到本項目需要支持用戶自定義的復雜流程,我們將采用活動驅動的架構。這種架構能夠完美支撐圖形化設計、條件路由、并行處理等高級功能。我們將參考成熟的開源 Python 工作流引擎 SpiffWorkflow
的設計思想,它提供了對 BPMN 核心元素的強大支持,并擁有活躍的社區,其架構設計經過了長期驗證。
2.2 核心數據模型設計
為實現與業務的解耦,我們需要設計一套通用的數據模型來描述和執行工作流。
模型 (Model) | 描述 | 關鍵字段 |
| 流程定義 |
|
| 流程實例 |
|
| 任務實例 |
|
| 流轉實例 |
|
| 操作日志 |
|
2.3 流程狀態機與事件溯源
雖然我們選擇了活動驅動架構,但每個流程實例和任務實例的生命周期本身可以用一個簡單的狀態機來管理。更重要的是,我們將引入事件溯源 (Event Sourcing) 的設計思想來增強系統的可審計性、可靠性和可追溯性。
- 傳統狀態更新: 直接在數據庫中
UPDATE
workflow.instance
或workflow.task
的state
字段。這種方式簡單直接,但會丟失歷史狀態變更的上下文。 - 事件溯源方法:
- 不直接修改狀態:
workflow.instance
和workflow.task
的狀態字段將不再被直接更新。 - 記錄不可變事件: 任何導致狀態變更的操作(如“啟動流程”、“審批通過”、“駁回”、“加簽”)都將被記錄為一個不可變的事件并存入
workflow.log
表。這個日志表就是事件流 (Event Stream)。 - 狀態重建: 任何時候,一個流程實例或任務的當前狀態都可以通過從頭到尾重放 (Replay) 其關聯的事件流來確定。
- 快照優化: 為了避免每次查詢狀態都重放整個事件流,我們可以定期或在關鍵事件后為流程實例創建狀態快照 (Snapshot),并將其存儲在
workflow.instance
表中。這樣,狀態重建只需從最近的快照開始重放后續事件即可。
- 不直接修改狀態:
事件溯源帶來的優勢:
- 完美的審計日志:
workflow.log
表天然成為一個完整、不可篡改的審計日志,詳細記錄了流程的每一步演變。 - 強大的調試與追溯能力: 可以輕松實現“時間旅行”調試,查看流程在任何歷史時間點的狀態。
- 簡化的動態干預邏輯: “撤回”等復雜操作可以被建模為發布一個新的“撤回事件”,而不是去復雜地修改和刪除現有數據。這使得業務邏輯更清晰。
- 高可靠性: 即使應用崩潰,只要事件被成功記錄,就可以通過重放事件來恢復到正確的狀態,確保了流程的持久執行。
這個設計借鑒了像 Temporal/Cadence 這類現代工作流引擎的思想,將為我們的系統帶來極高的健壯性和可維護性。
第三章:圖形化流程設計器
本章專注于最終用戶進行流程設計的核心界面,詳細規劃其技術實現,旨在提供一個直觀、強大且易于擴展的可視化工具。
3.1 前端框架選型:Odoo OWL 2 vs. Vue 3
構建一個功能豐富的圖形化設計器,前端框架的選擇至關重要。我們對 Odoo 18 的原生框架 OWL 2 和主流框架 Vue 3 進行了深入比較。
對比維度 | Odoo OWL 2 | Vue 3 | 結論與建議 |
性能與體積 | 輕量級 (~<20kb gzipped),專為 Odoo 優化,響應式狀態管理 ( | 性能頂尖,通過 tree-shaking 最小化包體積 (~16kb),擁有更成熟的虛擬 DOM 和編譯時優化。 | 兩者性能均可滿足需求,但 Vue 在通用 Web 應用場景下的性能基準測試中通常表現更優。 |
Odoo 集成度 | 原生集成,無縫銜接。可直接調用 Odoo RPC 服務,使用 Odoo 的資源管理、翻譯、權限體系。組件開發遵循 Odoo 規范。 | 需要通過 RESTful API 與 Odoo 后端通信。集成需要額外工作,如處理認證、數據格式轉換等。 | OWL 集成最簡單。但前后端分離架構下,API 通信是標準實踐,Vue 的集成成本可控。 |
生態系統與UI庫 | 生態系統局限于 Odoo 社區,缺乏大型、成熟的第三方組件庫和圖形庫。UI 元素需自行構建或依賴 Odoo 提供的基本組件。 | 極其龐大和活躍的生態。擁有海量高質量的 UI 庫 (Element Plus, Ant Design Vue, Naive UI) 和專業的圖形/圖表庫。 | Vue 勝出。對于圖形化設計器這種復雜應用,豐富的生態意味著可以快速集成成熟的解決方案,避免重復造輪子。 |
開發效率與工具鏈 | 開發工具相對基礎,依賴 Odoo 的資源加載機制。社區資源和解決方案較少。 | 擁有 Vite、Vue CLI、Vue DevTools 等現代化、高效的開發工具鏈,社區龐大,問題解決和學習資源豐富。 | Vue 勝出。強大的工具鏈和社區支持能顯著提升開發效率和項目質量。 |
高級渲染與可移植性 | 主要用于構建 Odoo 標準的 Web 界面,對 Canvas/WebGL 的直接支持和封裝較少。 | 可通過 | Vue 勝出。對于未來可能需要的高性能渲染或將設計器嵌入其他系統的場景,Vue 提供了更好的靈活性。 |
最終技術決策:
盡管 OWL 2 在 Odoo 內部集成上具有天然優勢,但考慮到圖形化流程設計器是一個高度復雜、交互密集的獨立應用,我們強烈建議采用 Vue 3。其龐大的生態系統、成熟的工具鏈以及在構建復雜單頁面應用方面的卓越表現,將為項目帶來更高的開發效率、更強的可擴展性和更優的用戶體驗。通過標準化的 RESTful API 與 Odoo 后端通信,可以完全規避集成上的挑戰。
3.2 圖形庫選型:AntV G6 vs. LogicFlow
在選擇了 Vue 3 框架后,我們需要一個強大的圖形庫來構建設計器的核心——畫布。AntV G6 和 LogicFlow 是兩個優秀的選擇。
對比維度 | AntV G6 (by Ant Group) | LogicFlow (by DiDi) | 結論與建議 |
核心定位 | 通用的圖可視化與圖分析引擎,功能全面,支持流程圖、腦圖、ER 圖等多種場景。 | 專注于流程圖、邏輯編排場景的編輯器,API 設計更貼近業務流程。 | 兩者均符合需求。LogicFlow 更聚焦,G6 更通用、功能更強大。 |
自定義節點能力 | 極其強大。支持通過 JS 對象、SVG、甚至直接嵌入 React/Vue 組件 ( | 良好。通過繼承 | G6 勝出。直接嵌入 Vue 組件的能力是殺手級特性,它使得構建復雜配置面板的節點變得異常簡單和高效。 |
數據綁定與序列化 | 數據模型靈活,節點數據可為任意 JS 對象。序列化為 JSON 需要自行實現邏輯,從圖數據中提取所需屬性。 | 采用 MVVM 架構,模型與視圖分離,數據綁定更清晰。提供適配器 API,方便將內部數據格式轉換為標準格式(如 BPMN JSON)。 | LogicFlow 的架構設計更貼近數據驅動,但 G6 的靈活性也足以實現同樣的目標。 |
輔助功能 | 非常成熟。提供豐富的內置插件和行為,如 Minimap、Grid、History (Undo/Redo)、Tooltip、ContextMenu 等,且高度可定制。 | 同樣提供 Control (縮放、撤銷/重做)、Minimap、Menu 等核心插件,功能完善。 | 兩者均能滿足需求,G6 的插件生態和可配置項似乎更為豐富。 |
性能與生態 | 針對大規模圖(數千節點)有性能優化策略。作為 AntV 的一部分,生態系統龐大,社區活躍,文檔詳盡。 | 性能良好,專注于流程圖場景。生態相對較小,但增長迅速,文檔清晰。 | G6 在處理大規模圖和生態成熟度上略有優勢。 |
最終技術決策:
我們推薦使用 AntV G6。其最核心的優勢在于無與倫比的自定義節點能力,特別是能夠將整個 Vue 組件作為節點的一部分進行渲染。這將極大地簡化“審批節點”、“條件節點”等復雜節點的開發,我們可以直接在節點上嵌入表單、下拉框、規則編輯器等豐富的 UI 元素,為用戶提供極致的配置體驗。
3.3 設計器實現方案
- 項目結構:
- 創建一個獨立的 Vue 3 + Vite 項目。
- 使用 Pinia 進行狀態管理,存儲畫布數據、當前選中節點信息等。
- 使用 Axios 或 Fetch API 與 Odoo 后端進行通信。
- 核心組件:
- 畫布 (Canvas): 基于 AntV G6 初始化一個 Graph 實例,配置 Grid、Minimap、History 等插件。
- 節點面板 (Node Palette): 左側的組件欄,展示可拖拽的節點類型(審批、抄送、條件、開始、結束等)。通過 HTML5 的拖放 API 實現將節點拖拽到畫布上。
- 屬性面板 (Property Panel): 右側的配置區域。當用戶選中畫布上的一個節點或邊時,此面板會動態渲染出對應的配置表單。例如,選中“審批節點”,則顯示審批人類型、審批方式等配置項。
- 自定義節點實現 (以審批節點為例):
- 使用 G6 的
registerNode
API 注冊一個新的節點類型,例如approval-node
。 - 利用
@antv/g6-vue-node
(社區或自研),將一個 Vue 組件作為節點的shape
。 - 這個 Vue 組件將負責渲染節點的視覺表現(如圖標、標題、當前審批人摘要)。
- 當用戶點擊節點上的“配置”按鈕或選中節點時,通過事件總線或狀態管理,通知右側的屬性面板加載該節點的詳細配置組件。
- 使用 G6 的
- 數據流與序列化:
- 用戶在畫布上的所有操作(添加/刪除節點、連接、修改屬性)都會實時更新 G6 的圖數據模型。
- 當用戶點擊“保存”時,調用 G6 的
graph.save()
方法,得到一個包含所有節點和邊信息的 JSON 對象。 - 對該 JSON 進行預處理,提取出后端工作流引擎需要的核心信息(節點ID、類型、位置、配置數據、連接關系),形成我們在第二章定義的
definition_json
格式。 - 將此
definition_json
通過 API 發送至 Odoo 后端進行持久化。
第四章:高級審批節點與動態路由策略
本章聚焦于實現復雜的審批節點邏輯,這是模塊區別于簡單工作流的核心競爭力。我們將深入探討條件分支、審批人矩陣和組織架構集成的實現算法與數據庫層面的優化。
4.1 條件分支:基于表單字段的動態路由
條件分支節點允許流程根據業務單據的當前數據走向不同的路徑。
- 前端配置: 在圖形化設計器的條件分支節點配置面板中,用戶可以添加多條流出路徑。每條路徑關聯一個或多個條件表達式。
- UI 設計: 提供一個規則構建器界面,允許用戶選擇關聯表單的任意字段(通過 API 從后端獲取該 Odoo 模型的字段列表
fields_get
),選擇操作符(如>
、<
、=
、in
、contains
),并輸入比較值。 - 示例:
[ 金額 > 10000 ] AND [ 費用類型 = '差旅費' ]
- UI 設計: 提供一個規則構建器界面,允許用戶選擇關聯表單的任意字段(通過 API 從后端獲取該 Odoo 模型的字段列表
- 后端實現:
- 表達式解析: 當流程執行到條件節點時,引擎從
definition_json
中提取出各分支的條件表達式。 - 數據獲取: 使用
res_model
和res_id
,通過 Odoo ORM 的browse()
和read()
方法獲取當前業務單據的字段值。 - 安全求值: 必須使用一個安全的表達式求值引擎來執行判斷,嚴禁使用
eval()
。Odoo 原生的safe_eval
或domain
表達式解析器是理想的選擇。我們可以將前端配置的規則轉換為 Odoo 的 Domain 元組格式,例如[('amount_total', '>', 10000), ('expense_type', '=', 'travel')]
。 - 路由決策: 引擎逐一評估每個分支的條件。一旦找到第一個滿足條件的分支,流程就沿著該路徑繼續;如果配置了“其他”或“默認”路徑,在所有條件都不滿足時,流程將走向該路徑。
- 表達式解析: 當流程執行到條件節點時,引擎從
4.2 審批人矩陣:會簽、或簽、順序簽
審批節點需要支持多種協作模式。
- 前端配置: 在審批節點的屬性面板中,提供配置項:
- 審批人: 可指定具體員工、部門、職位,或動態查找(如匯報線)。
- 審批方式:
- 會簽 (AND): 需要所有審批人同意,流程才繼續。任一駁回,流程即終止或駁回。
- 或簽 (OR): 任一審批人同意,流程即繼續。所有人都駁回,流程才駁回。
- 順序簽: 審批人按指定順序逐一審批。
- 后端實現:
- 任務生成: 當流程流轉到審批節點時,引擎根據配置解析出所有需要參與審批的用戶列表。
- 會簽邏輯:
- 為列表中的每個審批人創建一個狀態為
pending
的workflow.task
實例。 - 當一個
task
被審批通過時,將其狀態更新為completed
。 - 引擎設置一個監聽器,檢查與該審批節點關聯的所有
task
是否都已completed
。全部完成后,才將流程推向下一節點。 - 如果任何一個
task
被駁回,引擎立即將所有其他相關的pending
任務置為cancelled
,并將流程實例狀態標記為rejected
。
- 為列表中的每個審批人創建一個狀態為
- 或簽邏輯:
- 同樣為每個審批人創建
workflow.task
。 - 當任何一個
task
被審批通過時,引擎立即將所有其他相關的pending
任務置為cancelled
,并將流程推向下一節點。 - 只有當所有
task
都被駁回時,流程實例才被標記為rejected
。
- 同樣為每個審批人創建
- 順序簽邏輯:
- 只為列表中的第一個審批人創建
workflow.task
。 - 當該
task
被審批通過后,引擎再為列表中的下一個審批人創建新的task
。 - 循環此過程,直到列表中的所有人都審批完畢。
- 任何一個
task
被駁回,流程即終止。
- 只為列表中的第一個審批人創建
4.3 組織架構集成:基于匯報線的動態審批人查找
這是中國特色審批流中最核心和復雜的功能之一,要求系統能根據員工的匯報關系自動找到上級審批人。
- 前端配置: 在審批人配置中,提供“匯報線”選項,并允許設置:
- 查找起點: 通常是“單據創建人”。
- 終止條件: “直至職位為‘總監’的審批人”、“直至匯報層級為 3”、“直至部門負責人”等。
- 后端實現與數據庫優化:
這是一個典型的在樹狀結構(組織架構)中進行遞歸查詢的場景。在 Odoo 中,hr.employee
和 hr.department
通常通過 parent_id
或 manager_id
字段構成層級關系。
方案一:Odoo ORM parent_of
(利用 _parent_store
)
- 原理: 在
hr.employee
模型的manager_id
字段上設置_parent_store = True
。Odoo 會自動創建一個parent_path
字段,并物化存儲每個員工到根節點的路徑。 - 查詢: 可以使用 Odoo 的
parent_of
域操作符來查詢所有上級。 - 優點: ORM 原生支持,符合 Odoo 開發規范,自動處理路徑維護。
- 缺點: 對于非常深或復雜的查詢邏輯(如“直到某職位”),可能需要多次查詢或在 Python 端進行額外處理,性能可能不是最優。
- 原理: 在
方案二:原生 SQL WITH RECURSIVE
CTE (Common Table Expression)
- 原理: 直接在數據庫層面使用遞歸查詢一次性獲取完整的匯報鏈。
- 優點: 性能極高。將遞歸邏輯下推到數據庫,避免了 Python 與數據庫之間的多次往返通信,網絡延遲和應用服務器負載都顯著降低。查詢邏輯可以非常靈活,在 SQL 中直接實現復雜的終止條件。
- 缺點: 繞過了 Odoo ORM 的權限檢查,需要謹慎處理。SQL 語句相對復雜。
示例 SQL 查詢 (查找員工 ID 為 start_employee_id
的所有上級):
WITH RECURSIVE reporting_line AS (-- Anchor member: the starting employeeSELECTid,name,parent_id,job_id,1 AS levelFROMhr_employeeWHEREid = %(start_employee_id)sUNION ALL-- Recursive member: join with the parentSELECTe.id,e.name,e.parent_id,e.job_id,rl.level + 1FROMhr_employee eJOINreporting_line rl ON e.id = rl.parent_idWHERErl.parent_id IS NOT NULL -- Avoid infinite loop if root is reached
)
SELECT id, name, job_id, level FROM reporting_line;
數據庫索引策略:
- B-Tree 索引: 必須在
hr_employee
表的id
和parent_id
列上建立標準的 B-Tree 索引。這是加速JOIN
操作和遞歸查詢性能的關鍵。 - BRIN 索引: 對于超大規模(數十萬員工)且數據按某個字段(如創建時間)物理有序的表,BRIN 索引在特定范圍查詢下可能占用更少空間。但對于此處的遞歸連接查詢,B-Tree 仍然是首選。
- B-Tree 索引: 必須在
替代數據模型考量 (針對超大規模組織):
- 物化路徑 (Materialized Path): Odoo 的
_parent_store
實際上就是此模型的實現。對于絕大多數場景,這已足夠高效。 - 嵌套集 (Nested Set): 讀性能(特別是查詢整個子樹)極高,但寫操作(增刪改員工)成本巨大,需要更新大量節點的左右值。對于組織架構不頻繁變動的場景可以考慮,但在 Odoo 中實現復雜,需要大量自定義邏輯。
- 物化路徑 (Materialized Path): Odoo 的
最終技術決策:
我們將混合使用方案一和方案二。
- 對于簡單的層級查找,優先使用 Odoo ORM 的
parent_of
,保持代碼的 Odoo 風格。 - 對于性能敏感、邏輯復雜的匯報線查找(特別是“直至某級別/職位”),封裝一個直接執行
WITH RECURSIVE
CTE 原生 SQL 查詢的函數。這將是確保系統在復雜組織架構下依然保持高性能響應的關鍵優化點。
第五章:流程中動態干預與異常處理機制
本章專門討論在流程執行過程中可能發生的特殊操作與自動化管理,旨在通過引入成熟的設計模式,構建一個健壯、靈活且可審計的動態干預系統。
5.1 動態干預的實現:命令模式
“加簽”、“轉簽”、“委托”、“撤回”等操作本質上是對一個正在運行的流程實例狀態的修改。為了優雅地處理這些操作,我們將引入命令模式。
- 核心思想: 將每一個動態干預操作封裝成一個獨立的對象(命令對象)。這個對象包含了執行該操作所需的所有信息(如操作類型、目標任務、參與者、備注等)。
- 架構設計:
Command
(接口): 定義一個所有命令類都必須實現的接口,至少包含execute()
和undo()
方法。ConcreteCommand
(具體命令類):AddSignerCommand(instance, task, signers, type)
TransferCommand(instance, task, from_user, to_user)
DelegateCommand(instance, from_user, to_user, start_date, end_date)
WithdrawCommand(instance, task)
Invoker
(調用者): 流程操作的入口,例如用戶點擊 UI 上的“加簽”按鈕。它會創建一個具體的命令對象并調用其execute()
方法。它不關心命令是如何被執行的。Receiver
(接收者): 真正的操作執行者,即我們的工作流引擎。execute()
方法內部會調用引擎的相應服務來修改流程狀態。History
(歷史記錄): 調用者每執行一個命令,就將其推入一個歷史堆棧。
- 命令模式帶來的優勢:
- 解耦: 將操作的發起者與執行者完全解耦,使得添加新的動態操作變得非常容易,只需增加一個新的命令類即可,符合開閉原則。
- 可撤銷/重做 (Undo/Redo): 通過實現
undo()
方法,并利用歷史堆棧,可以輕松實現操作的撤銷。例如,WithdrawCommand
的undo()
方法可以重新激活被撤回的任務。 - 日志與審計: 每個命令對象本身就是一個完整的操作記錄,可以被序列化并存入
workflow.log
,提供了極佳的審計追蹤能力。 - 宏命令與隊列: 可以將多個命令組合成一個宏命令,實現事務性操作。也可以將命令放入隊列中,實現異步執行或延遲執行。
5.2 狀態快照與恢復:備忘錄模式
在執行一些高風險或復雜的動態干預操作(如“撤回并允許修改已審批節點”)之前,我們需要一種機制來保存流程的當前狀態,以便在操作失敗或需要回滾時能夠安全恢復。備忘錄模式是實現此功能的完美選擇。
- 核心思想: 在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。
- 架構設計:
Originator
(發起人):WorkflowInstance
對象。它知道如何保存和恢復自己的狀態。它會創建一個包含其當前狀態的Memento
對象。Memento
(備忘錄): 一個簡單的值對象,用于存儲WorkflowInstance
的狀態快照。這個快照可能是一個序列化后的 JSON 字符串,包含了所有任務的狀態、處理人、表單數據摘要等。其內部數據對外部是隱藏的。Caretaker
(管理者): 我們的工作流引擎或命令對象。在執行WithdrawCommand
之前,Caretaker
會向WorkflowInstance
請求一個Memento
并將其保存起來。如果需要回滾,Caretaker
會將這個Memento
交還給WorkflowInstance
,由后者自行恢復狀態。
- 應用場景:
當一個審批人想要撤回他已經批準的某個節點時,系統后臺執行以下步驟:
WithdrawCommand
被創建。- 在
execute()
方法中,首先調用工作流實例的createMemento()
方法,獲取當前流程狀態的快照。 - 將此快照與該命令關聯并存儲。
- 執行真正的撤回邏輯(例如,將后續任務置為
cancelled
)。 - 如果后續操作(如允許修改表單)出現問題,或用戶決定取消撤回,可以調用
undo()
方法,該方法會從存儲中取出備忘錄,并調用工作流實例的restoreFromMemento()
方法,將流程精確地恢復到撤回操作之前的狀態。
5.3 復雜回滾與補償:Saga 模式
對于跨多個服務或涉及復雜補償邏輯的“撤回”操作,簡單的命令撤銷可能不足以保證數據一致性。例如,一個審批通過后,可能已經觸發了向 ERP 系統創建發貨單、向財務系統預扣款等外部操作。此時,撤回審批需要逆向操作這些外部系統。Saga 模式為這種分布式事務提供了強大的解決方案。
- 核心思想: 將一個長事務分解為一系列本地事務,每個本地事務都有一個對應的補償事務。如果任何本地事務失敗,Saga 會按相反順序執行補償事務,以保證最終一致性。
- 應用場景 (撤回已觸發外部系統的審批):
- Saga 開始: 用戶點擊“撤回”。
- 本地事務 1: 在工作流引擎中,將后續任務置為
cancelled
。- 補償事務 1: 重新激活這些任務。
- 本地事務 2: 調用 ERP 系統的 API,取消已創建的發貨單。
- 補償事務 2: 重新調用 API 創建發貨單。
- 本地事務 3: 調用財務系統的 API,釋放預扣款。
- 補償事務 3: 重新調用 API 預扣款。
如果“取消發貨單”失敗,Saga 協調器會觸發“補償事務 1”,即重新激活工作流中的后續任務,使整個系統狀態回滾到撤回操作之前。
- 實現方式: 我們可以采用編排 (Orchestration) 方式,由工作流引擎自身扮演 Saga 協調器的角色,按順序調用本地事務,并在失敗時調用補償事務。
5.4 自動化處理機制
為防止流程因無人處理而停滯,需要建立自動化的催辦和超時處理機制。
- 實現方案: 利用 Odoo 的計劃任務 (Scheduled Actions / Cron Jobs)。
- 催辦: 創建一個定時任務(如每小時執行一次),掃描所有處于
pending
狀態超過一定時間(如 24 小時)的workflow.task
。對這些任務的當前處理人,通過郵件、Odoo 內部通知或釘釘/企微消息發送催辦提醒。 - 超時自動處理: 創建另一個定時任務,掃描
pending
狀態超過更長時間(如 72 小時)的任務。根據流程定義中該節點的超時策略(可配置為:自動同意、自動駁回、轉交給其上級),執行相應的自動化操作。這同樣可以通過創建一個系統觸發的Command
對象來實現,以保持邏輯的一致性和可審計性。
- 催辦: 創建一個定時任務(如每小時執行一次),掃描所有處于
5.5 補充思考:函數式編程與不可變數據結構
為了從根本上簡化狀態管理和并發控制,我們可以借鑒函數式編程中的不可變性 (Immutability) 思想。
- 原理: 任何對流程狀態的修改,都不會在原地改變現有狀態對象,而是返回一個全新的狀態對象。
- 優勢:
- 線程安全: 不可變對象可以被多個線程安全地共享,無需加鎖,從根本上消除了競態條件。
- 歷史追溯: 每次變更都產生一個新版本的狀態,歷史記錄被完整保留,天然支持版本控制和“時間旅行”調試。
- 邏輯簡化: 動態干預操作的函數成為純函數(給定輸入,總有相同輸出),沒有副作用,極大地降低了代碼的復雜度和出錯的可能性。
雖然在 Python 中完全實現不可變性有一定成本,但我們可以將此思想應用于核心的流程狀態管理中,例如,將流程實例的狀態設計為一個不可變的字典或數據類,每次變更都生成一個新的實例,這將使我們的引擎更加健壯和可預測。
第六章:中國本土化生態集成層
本章將設計一個獨立的、可插拔的集成層,專門處理與釘釘和企業微信的對接,以滿足中國用戶的移動辦公習慣,實現無縫的跨平臺審批體驗。
6.1 統一身份認證與單點登錄 (SSO)
目標是讓用戶在釘釘/企業微信工作臺內,無需輸入 Odoo 的用戶名密碼,即可直接訪問審批頁面并進行操作。
6.1.1 核心流程:基于 OAuth 2.0 的免登授權
我們將遵循釘釘/企業微信開放平臺提供的標準 OAuth 2.0 授權碼模式。以釘釘為例:
- 前端獲取臨時授權碼 (
authCode
):- 在嵌入到釘釘工作臺的 H5 微應用頁面中,前端 JavaScript 需要引入釘釘的 JSAPI 庫 (
dingtalk-jsapi
)。 - 頁面加載時,調用
dd.runtime.permission.requestAuthCode
方法。此方法會向釘釘客戶端請求一個臨時的、一次性的授權碼authCode
。 - 注意:
authCode
有效期很短(通常為5分鐘),且只能使用一次。
- 在嵌入到釘釘工作臺的 H5 微應用頁面中,前端 JavaScript 需要引入釘釘的 JSAPI 庫 (
- 后端交換用戶信息:
- 前端將獲取到的
authCode
通過 API 發送給 Odoo 后端。 - Odoo 后端接收到
authCode
后,使用預先配置好的應用AppKey
(Client ID) 和AppSecret
(Client Secret),調用釘釘的/rpc/oauth2/dingtalk_app_user.json
或類似接口。 - 釘釘服務器驗證
authCode
和應用憑證后,會返回該用戶的身份信息,其中最重要的是userId
(企業內唯一標識) 和unionId
(跨企業唯一標識)。
- 前端將獲取到的
- 用戶映射與會話建立:
- Odoo 后端拿到釘釘
userId
后,需要在res.users
表中查找是否存在與之映射的 Odoo 用戶。 - 映射策略: 我們將在
res.users
模型上新增一個字段,如dingtalk_user_id
,用于存儲釘釘的用戶 ID。 - 查找邏輯:
- 如果找到匹配的用戶,則認為身份驗證成功。Odoo 后端生成一個標準的 Odoo 會話 (session),并將 session ID 返回給前端。前端后續所有對 Odoo API 的請求都攜帶此 session ID。
- 如果未找到匹配用戶,可以根據預設策略進行處理:
- 自動創建用戶: 根據從釘釘獲取的基本信息(姓名、手機號等)在 Odoo 中自動創建一個新用戶并建立映射。
- 引導綁定: 跳轉到一個綁定頁面,讓用戶手動登錄其已有的 Odoo 賬戶,完成一次性綁定。
- 用戶同步: 為保證映射的有效性,需要一個后臺任務,定期從釘釘通訊錄同步員工信息(姓名、部門、職位、
userId
)到 Odoo 的hr.employee
和res.users
表。
- Odoo 后端拿到釘釘
6.1.2 架構方案:直接集成 vs. 獨立身份提供商 (IdP)
- 方案 A:直接集成
- 描述: 在 Odoo 中直接編寫與釘釘、企業微信 API 對接的代碼。
- 優點: 架構簡單,開發快速,不引入新的系統依賴。
- 缺點: 每增加一個新的平臺(如飛書),都需要重寫一套類似的認證和同步邏輯。身份管理邏輯分散在各個應用中,難以統一管理安全策略(如 MFA)。
- 方案 B:引入獨立身份提供商 (IdP) 作為中間件
- 描述: 部署一個獨立的 IdP 服務(如開源的 Keycloak,或商業的 Authing、阿里云 IDaaS)。
- 上游: IdP 配置為與釘釘、企業微信等身份源對接。
- 下游: Odoo 配置為信任該 IdP,通過標準的 OpenID Connect (OIDC) 或 SAML 協議進行認證。
- 流程: 用戶在釘釘中訪問 -> 重定向到 IdP -> IdP 通過釘釘完成認證 -> IdP 向 Odoo 頒發
id_token
-> Odoo 驗證id_token
并建立會話。 - 優點:
- 高度可擴展: 新增身份源或應用時,只需在 IdP 中進行配置,Odoo 端代碼無需改動。
- 集中式安全管理: 可以在 IdP層面統一實施 MFA、風險檢測、密碼策略等高級安全控制。
- 真正的 SSO: 用戶登錄一次 IdP,即可訪問所有集成的應用,體驗更佳。
- 標準化: 遵循 OIDC/SAML 標準,架構清晰,易于維護。
- 缺點: 增加了架構復雜性,需要部署和維護一個額外的 IdP 服務。
- 描述: 部署一個獨立的 IdP 服務(如開源的 Keycloak,或商業的 Authing、阿里云 IDaaS)。
技術決策:
- 短期/MVP 版本: 采用方案 A (直接集成),快速實現核心功能。
- 長期/企業級部署: 強烈建議演進到方案 B (獨立 IdP)。這將為企業構建一個統一、安全、可擴展的身份認證中臺,是現代企業 IT 架構的最佳實踐。我們的集成層應設計為可插拔,以便未來平滑過渡到 IdP 方案。
6.2 消息推送服務
目標是將待辦、已辦、抄送、駁回等審批通知實時推送到用戶的釘釘/企業微信。
- 封裝統一接口: 在集成層中,創建一個統一的消息推送服務,如
notification_service.push(user_ids, message_content)
。 - 適配器模式: 該服務內部包含針對不同平臺的適配器 (
DingTalkAdapter
,WeComAdapter
)。 - 調用時機: 工作流引擎在狀態流轉的關鍵節點(如生成新任務、流程結束)會調用此服務。
- 消息格式: 消息內容應設計為卡片式消息 (Action Card),不僅包含文本信息,還應包含一個“立即處理”的按鈕,點擊后可直接跳轉到 H5 審批頁面。
- 用戶 ID 處理: 推送服務需要將 Odoo 的
user.id
轉換為對應平臺的userId
。這依賴于 6.1 節中建立的用戶映射關系。
6.3 外部應用內審批
目標是讓用戶在釘釘/企業微信客戶端內,即可完成審批操作,無需跳轉到 Odoo PC 端。
- H5 審批頁面:
- 開發一個輕量級的、移動端優先的 H5 頁面,專門用于展示和處理單個審批任務。
- 該頁面通過 URL 參數接收
task_id
。 - 頁面加載時,首先執行 6.1 節的 SSO 流程,獲取 Odoo 會話。
- 然后使用
task_id
和獲取到的會話,調用 Odoo 后端 API,拉取任務詳情(包括關聯的業務單據信息、審批歷史等)。 - 表單展示: 為了在移動端展示業務單據的關鍵信息,可以設計一個“表單摘要”視圖。后端提供一個 API,可以根據流程定義中的配置,返回指定業務單據的幾個關鍵字段的值。
- 操作按鈕: 頁面提供“同意”、“駁回”、“轉簽”等操作按鈕。點擊后,調用 Odoo 后端的相應 API,并附上審批意見。
- 數據同步: 操作成功后,后端工作流引擎狀態實時更新,數據一致性得到保證。
- 小程序 (可選):
- 優勢: 相比 H5,小程序能提供更接近原生的用戶體驗,性能更好,且能訪問更多的平臺原生能力。
- 劣勢: 開發和維護成本更高,需要為不同平臺(釘釘小程序、微信小程序)分別開發。
- 建議: 在項目初期,優先實現 H5 方案,因為它跨平臺且開發成本低。待業務成熟、對體驗要求更高時,再考慮投入開發小程序。
第七章:性能、安全性與可擴展性考量
本章將前瞻性地分析并提出解決方案,以確保模塊在復雜業務場景和大規模數據下的長期穩定、高效與安全。
7.1 性能優化
隨著流程實例和操作日志的不斷累積,數據庫性能將成為系統的主要瓶頸。我們需要從索引、分區和架構層面進行深度優化。
7.1.1 數據庫索引策略
workflow.log
表的優化:- 此表將是寫入最頻繁、數據量增長最快的表。其
comment
、action_details
等字段可能包含非結構化數據,非常適合使用 JSONB 類型存儲。 - GIN 索引: 必須在存儲操作詳情的 JSONB 字段上創建 GIN 索引,以加速對特定鍵值的查詢。
jsonb_path_ops
vsjsonb_ops
: 如果 JSON 結構相對固定,查詢路徑明確(如按user_id
或action_type
搜索),應優先使用jsonb_path_ops
操作符類創建 GIN 索引。它生成的索引更小,查詢性能更好。jsonb_ops
: 如果需要對未知的鍵或值進行模糊搜索,則使用默認的jsonb_ops
。
- 表達式索引 (Index on Expression): 對于 JSONB 字段中頻繁用于精確匹配或排序的頂級鍵(如
timestamp
,operator_id
),創建 B-Tree 表達式索引通常比 GIN 索引更高效。- 示例:
CREATE INDEX ON workflow_log (((log_data->>'timestamp')::timestamptz));
- 示例:
CREATE INDEX ON workflow_log ((log_data->>'operator_id'));
- 示例:
- 此表將是寫入最頻繁、數據量增長最快的表。其
workflow.task
表的優化:- 在
state
、assignees
、instance_id
等高頻查詢字段上建立 B-Tree 索引,以加速待辦事項的查詢。
- 在
7.1.2 大數據量下的日志表管理:表分區 (Table Partitioning)
當 workflow.log
表的數據量達到千萬甚至上億級別時,單一表的查詢和維護(如 VACUUM
)性能會急劇下降。必須采用表分區。
- 分區策略:
- 按時間范圍分區 (Range Partitioning): 這是最適合日志數據的策略。可以按月或按季度創建一個新的分區表。
CREATE TABLE workflow_log PARTITION BY RANGE (created_at);
CREATE TABLE workflow_log_2025_q3 PARTITION OF workflow_log FOR VALUES FROM ('2025-07-01') TO ('2025-10-01');
- 按時間范圍分區 (Range Partitioning): 這是最適合日志數據的策略。可以按月或按季度創建一個新的分區表。
- 分區優勢:
- 分區裁剪 (Partition Pruning): 當查詢帶有時間范圍時,PostgreSQL 查詢優化器會自動跳過不相關的分區,極大地提升查詢速度。
- 高效數據歸檔: 當歷史數據不再需要頻繁訪問時,可以直接
DETACH
舊的分區,或將其移動到廉價的存儲介質,而無需執行緩慢的DELETE
操作。
- 自動化管理: 使用
pg_partman
擴展或自定義cron
腳本,可以實現分區的自動創建和舊分區的歸檔/刪除。
7.1.3 讀寫分離架構 (Read/Write Splitting)
對于極致性能要求的場景,特別是當復雜的日志分析和報表查詢開始影響 Odoo 主業務的響應時,應考慮將日志數據同步到專門的分析型數據庫。
- 架構: PostgreSQL (Odoo DB) -> Debezium (CDC) -> Kafka -> ClickHouse/Elasticsearch
- Debezium: 作為一個開源的變更數據捕獲 (Change Data Capture) 工具,它通過監聽 PostgreSQL 的邏輯復制流 (WAL log),可以實時捕獲
workflow.log
表的行級INSERT
操作。 - Kafka: Debezium 將捕獲到的變更事件作為消息發布到 Kafka 主題中,作為高可靠的緩沖層。
- 分析型數據庫:
- ClickHouse: 一個列式存儲數據庫,對于大規模日志數據的聚合分析查詢性能極佳。非常適合用于生成統計報表(如流程平均耗時、節點瓶頸分析)。
- Elasticsearch: 一個強大的搜索引擎,擅長全文搜索和復雜的半結構化數據查詢。非常適合用于構建一個高級的、可交互的日志搜索界面。
- Debezium: 作為一個開源的變更數據捕獲 (Change Data Capture) 工具,它通過監聽 PostgreSQL 的邏輯復制流 (WAL log),可以實時捕獲
- 優勢: 這種架構實現了徹底的讀寫分離。Odoo 的主數據庫只負責核心的 OLTP(在線事務處理)負載,保證了審批操作的低延遲。而所有復雜的、資源消耗大的 OLAP(在線分析處理)查詢都在外部的專用系統上進行,兩者互不干擾。
7.2 權限與安全性
- 流程定義權限:
- 創建 Odoo 的
ir.model.access.csv
和ir.rule
記錄,定義哪些用戶組(如“流程管理員”)可以創建、修改、刪除workflow.definition
。 - 應提供精細化的權限控制,例如,部門管理員只能設計本部門相關的流程。
- 創建 Odoo 的
- 流程實例權限:
- 可見性: 默認情況下,只有流程的參與者(創建人、當前審批人、已審批人、抄送人)才能看到流程實例的詳情。
- 操作權限: 只有被分配到某個
workflow.task
的用戶才能執行該任務的審批操作。 - 管理員權限: 超級管理員或流程管理員應有權限查看和干預(如強制終止、修改審批人)所有流程實例。
- API 安全:
- 所有暴露給前端的 RESTful API 都必須經過 Odoo 的會話驗證和權限檢查。
- 對于敏感操作的 API,應增加額外的安全措施,如請求頻率限制。
- OAuth 安全:
- 嚴格遵守 OAuth 2.0 安全最佳實踐,
AppSecret
等憑證絕不能泄露到前端。 - 在配置 OAuth 客戶端時,使用最小權限原則,只申請必要的
scope
。
- 嚴格遵守 OAuth 2.0 安全最佳實踐,
7.3 未來擴展性
- 版本化:
workflow.definition
模型已包含version
字段。當一個已激活的流程被修改時,應創建一個新的版本,而不是直接修改舊版本。舊版本應繼續用于處理已啟動的流程實例,而新發起的流程則使用新版本。這確保了流程變更的平滑過渡。 - 跨組織流程: 當前架構主要面向單一組織。未來可擴展支持集團公司下的跨法人實體流程,這需要在用戶和組織架構模型中引入更復雜的邏輯。
- AI 輔助:
- [預測] AI 輔助流程配置: 未來可以引入 AI 模型,根據用戶輸入的自然語言描述(如“幫我創建一個金額大于一萬需要總監審批的報銷流程”),自動生成流程圖的草稿。
- [預測] 異常檢測與優化建議: AI 可以分析海量的
workflow.log
數據,自動識別流程瓶頸(如某個節點平均耗時過長)、預測超時風險,并向管理員提出流程優化建議。
- 微服務化:
- [預測] 如果工作流引擎的負載變得極其巨大,可以考慮將其從 Odoo 的單體應用中剝離出來,作為一個獨立的微服務部署。這個微服務可以由更適合高并發的語言(如 Go, Java)編寫,并通過 gRPC 或 REST API 與 Odoo 主應用進行通信。當前的 API 驅動架構為這種未來的演進奠定了良好基礎。