本文旨在为基于Odoo 18平台开发一款符合中国用户习惯的、功能强大的通用工作流审批模块提供一份全面的技术实现与产品设计方案。该模块的核心特性包括:为最终用户设计的图形化流程设计器、对任意Odoo模型的普适性、复杂的审批节点逻辑(如会签、条件分支、汇报线查找)、流程中动态操作(如加签、转签),以及与钉钉、企业微信的深度无缝集成。将从系统总体架构出发,深入探讨工作流引擎核心、图形化设计器实现、高级审批路由策略、动态干预机制、本土化生态集成,并最终落脚于性能、安全与可扩展性的长远考量,为项目的成功实施提供坚实的技术蓝图与决策依据。
本章宏观定义模块的系统边界、核心组件及其交互方式,旨在构建一个高内聚、低耦合、可扩展的系统架构。
该模块将作为 Odoo 的一个独立应用 (App) 进行开发,遵循 Odoo 的模块化设计规范。其系统边界清晰,主要包含以下几个层面:
为应对图形化流程设计器复杂的前端交互需求,并保证未来技术栈的灵活性,我们采用前后端分离的架构模式。
workflow.definition
)。model
和 res_id
调用后端的流程启动 API。后续的审批、驳回等操作同样通过 API 完成。hr.employee
模型来确定审批人。本章深入探讨驱动所有流程运转的后端引擎,其核心在于构建一个与业务无关、高度可扩展、理论坚实的数据模型和状态机。
在工作流引擎的底层架构选择上,我们面临两种主流范式:有限状态机 (FSM) 和活动驱动 (Activity-Based)。
pytransitions
提供了轻量级的实现。结论: 考虑到本项目需要支持用户自定义的复杂流程,我们将采用活动驱动的架构。这种架构能够完美支撑图形化设计、条件路由、并行处理等高级功能。我们将参考成熟的开源 Python 工作流引擎 SpiffWorkflow
的设计思想,它提供了对 BPMN 核心元素的强大支持,并拥有活跃的社区,其架构设计经过了长期验证。
为实现与业务的解耦,我们需要设计一套通用的数据模型来描述和执行工作流。
模型 (Model) |
描述 |
关键字段 |
|
流程定义 |
|
|
流程实例 |
|
|
任务实例 |
|
|
流转实例 |
|
|
操作日志 |
|
虽然我们选择了活动驱动架构,但每个流程实例和任务实例的生命周期本身可以用一个简单的状态机来管理。更重要的是,我们将引入事件溯源 (Event Sourcing) 的设计思想来增强系统的可审计性、可靠性和可追溯性。
UPDATE
workflow.instance
或 workflow.task
的 state
字段。这种方式简单直接,但会丢失历史状态变更的上下文。workflow.instance
和 workflow.task
的状态字段将不再被直接更新。workflow.log
表。这个日志表就是事件流 (Event Stream)。workflow.instance
表中。这样,状态重建只需从最近的快照开始重放后续事件即可。事件溯源带来的优势:
workflow.log
表天然成为一个完整、不可篡改的审计日志,详细记录了流程的每一步演变。这个设计借鉴了像 Temporal/Cadence 这类现代工作流引擎的思想,将为我们的系统带来极高的健壮性和可维护性。
本章专注于最终用户进行流程设计的核心界面,详细规划其技术实现,旨在提供一个直观、强大且易于扩展的可视化工具。
构建一个功能丰富的图形化设计器,前端框架的选择至关重要。我们对 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 后端通信,可以完全规避集成上的挑战。
在选择了 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 元素,为用户提供极致的配置体验。
registerNode
API 注册一个新的节点类型,例如 approval-node
。@antv/g6-vue-node
(社区或自研),将一个 Vue 组件作为节点的 shape
。graph.save()
方法,得到一个包含所有节点和边信息的 JSON 对象。definition_json
格式。definition_json
通过 API 发送至 Odoo 后端进行持久化。本章聚焦于实现复杂的审批节点逻辑,这是模块区别于简单工作流的核心竞争力。我们将深入探讨条件分支、审批人矩阵和组织架构集成的实现算法与数据库层面的优化。
条件分支节点允许流程根据业务单据的当前数据走向不同的路径。
fields_get
),选择操作符(如 >
、<
、=
、in
、contains
),并输入比较值。[ 金额 > 10000 ] AND [ 费用类型 = '差旅费' ]
definition_json
中提取出各分支的条件表达式。res_model
和 res_id
,通过 Odoo ORM 的 browse()
和 read()
方法获取当前业务单据的字段值。eval()
。Odoo 原生的 safe_eval
或 domain
表达式解析器是理想的选择。我们可以将前端配置的规则转换为 Odoo 的 Domain 元组格式,例如 [('amount_total', '>', 10000), ('expense_type', '=', 'travel')]
。审批节点需要支持多种协作模式。
pending
的 workflow.task
实例。task
被审批通过时,将其状态更新为 completed
。task
是否都已 completed
。全部完成后,才将流程推向下一节点。task
被驳回,引擎立即将所有其他相关的 pending
任务置为 cancelled
,并将流程实例状态标记为 rejected
。workflow.task
。task
被审批通过时,引擎立即将所有其他相关的 pending
任务置为 cancelled
,并将流程推向下一节点。task
都被驳回时,流程实例才被标记为 rejected
。workflow.task
。task
被审批通过后,引擎再为列表中的下一个审批人创建新的 task
。task
被驳回,流程即终止。这是中国特色审批流中最核心和复杂的功能之一,要求系统能根据员工的汇报关系自动找到上级审批人。
这是一个典型的在树状结构(组织架构)中进行递归查询的场景。在 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
字段,并物化存储每个员工到根节点的路径。parent_of
域操作符来查询所有上级。方案二:原生 SQL WITH RECURSIVE
CTE (Common Table Expression)
示例 SQL 查询 (查找员工 ID 为 start_employee_id
的所有上级):
WITH RECURSIVE reporting_line AS (
-- Anchor member: the starting employee
SELECT
id,
name,
parent_id,
job_id,
1 AS level
FROM
hr_employee
WHERE
id = %(start_employee_id)s
UNION ALL
-- Recursive member: join with the parent
SELECT
e.id,
e.name,
e.parent_id,
e.job_id,
rl.level + 1
FROM
hr_employee e
JOIN
reporting_line rl ON e.id = rl.parent_id
WHERE
rl.parent_id IS NOT NULL -- Avoid infinite loop if root is reached
)
SELECT id, name, job_id, level FROM reporting_line;
数据库索引策略:
hr_employee
表的 id
和 parent_id
列上建立标准的 B-Tree 索引。这是加速 JOIN
操作和递归查询性能的关键。替代数据模型考量 (针对超大规模组织):
_parent_store
实际上就是此模型的实现。对于绝大多数场景,这已足够高效。最终技术决策:
我们将混合使用方案一和方案二。
parent_of
,保持代码的 Odoo 风格。WITH RECURSIVE
CTE 原生 SQL 查询的函数。这将是确保系统在复杂组织架构下依然保持高性能响应的关键优化点。本章专门讨论在流程执行过程中可能发生的特殊操作与自动化管理,旨在通过引入成熟的设计模式,构建一个健壮、灵活且可审计的动态干预系统。
“加签”、“转签”、“委托”、“撤回”等操作本质上是对一个正在运行的流程实例状态的修改。为了优雅地处理这些操作,我们将引入命令模式。
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()
方法,并利用历史堆栈,可以轻松实现操作的撤销。例如,WithdrawCommand
的 undo()
方法可以重新激活被撤回的任务。workflow.log
,提供了极佳的审计追踪能力。在执行一些高风险或复杂的动态干预操作(如“撤回并允许修改已审批节点”)之前,我们需要一种机制来保存流程的当前状态,以便在操作失败或需要回滚时能够安全恢复。备忘录模式是实现此功能的完美选择。
Originator
(发起人): WorkflowInstance
对象。它知道如何保存和恢复自己的状态。它会创建一个包含其当前状态的 Memento
对象。Memento
(备忘录): 一个简单的值对象,用于存储 WorkflowInstance
的状态快照。这个快照可能是一个序列化后的 JSON 字符串,包含了所有任务的状态、处理人、表单数据摘要等。其内部数据对外部是隐藏的。Caretaker
(管理者): 我们的工作流引擎或命令对象。在执行 WithdrawCommand
之前,Caretaker
会向 WorkflowInstance
请求一个 Memento
并将其保存起来。如果需要回滚,Caretaker
会将这个 Memento
交还给 WorkflowInstance
,由后者自行恢复状态。当一个审批人想要撤回他已经批准的某个节点时,系统后台执行以下步骤:
WithdrawCommand
被创建。execute()
方法中,首先调用工作流实例的 createMemento()
方法,获取当前流程状态的快照。cancelled
)。undo()
方法,该方法会从存储中取出备忘录,并调用工作流实例的 restoreFromMemento()
方法,将流程精确地恢复到撤回操作之前的状态。对于跨多个服务或涉及复杂补偿逻辑的“撤回”操作,简单的命令撤销可能不足以保证数据一致性。例如,一个审批通过后,可能已经触发了向 ERP 系统创建发货单、向财务系统预扣款等外部操作。此时,撤回审批需要逆向操作这些外部系统。Saga 模式为这种分布式事务提供了强大的解决方案。
cancelled
。
如果“取消发货单”失败,Saga 协调器会触发“补偿事务 1”,即重新激活工作流中的后续任务,使整个系统状态回滚到撤回操作之前。
为防止流程因无人处理而停滞,需要建立自动化的催办和超时处理机制。
pending
状态超过一定时间(如 24 小时)的 workflow.task
。对这些任务的当前处理人,通过邮件、Odoo 内部通知或钉钉/企微消息发送催办提醒。pending
状态超过更长时间(如 72 小时)的任务。根据流程定义中该节点的超时策略(可配置为:自动同意、自动驳回、转交给其上级),执行相应的自动化操作。这同样可以通过创建一个系统触发的 Command
对象来实现,以保持逻辑的一致性和可审计性。为了从根本上简化状态管理和并发控制,我们可以借鉴函数式编程中的不可变性 (Immutability) 思想。
虽然在 Python 中完全实现不可变性有一定成本,但我们可以将此思想应用于核心的流程状态管理中,例如,将流程实例的状态设计为一个不可变的字典或数据类,每次变更都生成一个新的实例,这将使我们的引擎更加健壮和可预测。
本章将设计一个独立的、可插拔的集成层,专门处理与钉钉和企业微信的对接,以满足中国用户的移动办公习惯,实现无缝的跨平台审批体验。
目标是让用户在钉钉/企业微信工作台内,无需输入 Odoo 的用户名密码,即可直接访问审批页面并进行操作。
6.1.1 核心流程:基于 OAuth 2.0 的免登授权
我们将遵循钉钉/企业微信开放平台提供的标准 OAuth 2.0 授权码模式。以钉钉为例:
authCode
):
dingtalk-jsapi
)。dd.runtime.permission.requestAuthCode
方法。此方法会向钉钉客户端请求一个临时的、一次性的授权码 authCode
。authCode
有效期很短(通常为5分钟),且只能使用一次。authCode
通过 API 发送给 Odoo 后端。authCode
后,使用预先配置好的应用 AppKey
(Client ID) 和 AppSecret
(Client Secret),调用钉钉的 /rpc/oauth2/dingtalk_app_user.json
或类似接口。authCode
和应用凭证后,会返回该用户的身份信息,其中最重要的是 userId
(企业内唯一标识) 和 unionId
(跨企业唯一标识)。userId
后,需要在 res.users
表中查找是否存在与之映射的 Odoo 用户。res.users
模型上新增一个字段,如 dingtalk_user_id
,用于存储钉钉的用户 ID。userId
)到 Odoo 的 hr.employee
和 res.users
表。6.1.2 架构方案:直接集成 vs. 独立身份提供商 (IdP)
id_token
-> Odoo 验证 id_token
并建立会话。技术决策:
目标是将待办、已办、抄送、驳回等审批通知实时推送到用户的钉钉/企业微信。
notification_service.push(user_ids, message_content)
。DingTalkAdapter
, WeComAdapter
)。user.id
转换为对应平台的 userId
。这依赖于 6.1 节中建立的用户映射关系。目标是让用户在钉钉/企业微信客户端内,即可完成审批操作,无需跳转到 Odoo PC 端。
task_id
。task_id
和获取到的会话,调用 Odoo 后端 API,拉取任务详情(包括关联的业务单据信息、审批历史等)。本章将前瞻性地分析并提出解决方案,以确保模块在复杂业务场景和大规模数据下的长期稳定、高效与安全。
随着流程实例和操作日志的不断累积,数据库性能将成为系统的主要瓶颈。我们需要从索引、分区和架构层面进行深度优化。
7.1.1 数据库索引策略
workflow.log
表的优化:
comment
、action_details
等字段可能包含非结构化数据,非常适合使用 JSONB 类型存储。jsonb_path_ops
vs jsonb_ops
: 如果 JSON 结构相对固定,查询路径明确(如按 user_id
或 action_type
搜索),应优先使用 jsonb_path_ops
操作符类创建 GIN 索引。它生成的索引更小,查询性能更好。jsonb_ops
: 如果需要对未知的键或值进行模糊搜索,则使用默认的 jsonb_ops
。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
)性能会急剧下降。必须采用表分区。
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');
DETACH
旧的分区,或将其移动到廉价的存储介质,而无需执行缓慢的 DELETE
操作。pg_partman
扩展或自定义 cron
脚本,可以实现分区的自动创建和旧分区的归档/删除。7.1.3 读写分离架构 (Read/Write Splitting)
对于极致性能要求的场景,特别是当复杂的日志分析和报表查询开始影响 Odoo 主业务的响应时,应考虑将日志数据同步到专门的分析型数据库。
workflow.log
表的行级 INSERT
操作。ir.model.access.csv
和 ir.rule
记录,定义哪些用户组(如“流程管理员”)可以创建、修改、删除 workflow.definition
。workflow.task
的用户才能执行该任务的审批操作。AppSecret
等凭证绝不能泄露到前端。scope
。workflow.definition
模型已包含 version
字段。当一个已激活的流程被修改时,应创建一个新的版本,而不是直接修改旧版本。旧版本应继续用于处理已启动的流程实例,而新发起的流程则使用新版本。这确保了流程变更的平滑过渡。workflow.log
数据,自动识别流程瓶颈(如某个节点平均耗时过长)、预测超时风险,并向管理员提出流程优化建议。