很多人第一次接触 Odoo 企业版 Marketing Automation,脑子里对应的往往是 Mailchimp 式理解:
- 拉一个人群;
- 定时发几封邮件;
- 根据打开/点击继续发下一封。
这个理解有一定表象,但没说到它的架构重点。
真正看 marketing_campaign.py、marketing_activity.py、marketing_participant.py 后,你会发现 Odoo 做的不是“邮件序列器”,而是一套 参与者执行图:
- campaign 定义目标对象和总过滤条件;
- activity 定义节点、延迟和触发边;
- participant 代表某条业务记录进入这张图;
- trace 代表“这个参与者在这个节点上的待执行/已执行轨迹”。
所以 Marketing Automation 的本质不是“定时发消息”,而是“给一批记录跑一张带触发边和时间延迟的执行图”。
一、campaign 的核心不是名字,而是“目标模型 + 去重语义”
marketing.campaign 上最关键的不是标题,而是:
model_iddomainunique_field_idmarketing_activity_idsparticipant_ids
这说明 campaign 不是邮件容器,而是:
- 面向哪个业务模型跑;
- 用什么 domain 选人;
- 需要按哪个字段去重;
- 跑哪些节点。
为什么 unique_field_id 很重要
源码注释说得很直白:
- 它用于避免基于某个字段处理重复记录;
- 比如 email 相同的联系人,只想进一次流程。
这说明 Odoo 在 campaign 层就考虑了:
自动化流程的对象不是“数据库行”,而是“业务身份”。
否则同邮箱多个记录会被反复轰炸。
二、为什么 activity 不是列表项,而是图节点
marketing.activity 不只是一个“第 1 封邮件 / 第 2 封邮件”的顺序项。
它同时有:
parent_idchild_idstrigger_typeinterval_number / interval_typevalidity_durationactivity_domain
这已经非常像图结构了。
节点如何连接?
begin:流程起点;activity:接在另一个活动后;mail_open / mail_not_open / mail_click / mail_reply ...:按邮件行为触发。
这说明营销自动化不是线性序列,而是:
- 一条记录进入后,可能沿不同分支继续;
- 某些节点只在父节点满足特定行为后生成子轨迹。
activity 的本质是带触发条件的节点,不是普通待发任务。
三、为什么 participant 是核心,而不是 mailing trace
marketing.participant 是整个模块很容易被忽视、但其实最重要的模型之一。
它绑定的是:
campaign_idmodel_nameres_idtrace_idsstate
这意味着 Odoo 不是让每次邮件直接盯着业务对象跑,而是先造一个 participant 代理层。
这个代理层解决了什么?
- 把一个业务记录和某个 campaign 的关系固定下来;
- 允许同一业务记录进入不同 campaign 而互不冲突;
- 允许在 participant 层维护运行中 / 已完成 / 已移除状态;
- 让后续 traces 全挂在这个 participant 身上。
所以 participant 不是冗余表,而是 流程实例。
四、为什么 begin 活动不是 campaign 启动时一次性群发,而是 participant 创建时生成 trace
marketing.participant.create() 里做了一件很关键的事:
- 为所有
trigger_type='begin'的 activity 生成 trace; - schedule_date = 当前时间 + 活动延迟;
- 并触发 cron 在相应时间点执行。
这说明 campaign 本身不是“发出去”的主体。
真正被安排进时间线的是:
- 某个 participant
- 在某个 activity
- 的一条 trace
因此系统实际调度的最小单位是 trace,不是 activity,也不是 campaign。
这很像任务图调度器,而不是群发器。
五、为什么改了 activity 后要 require_sync
marketing.activity.write() 和 marketing.campaign.action_update_participants() 这条链很重要。
如果活动配置变了,比如:
- 延迟改了;
- 过滤条件改了;
- 新增了活动节点;
系统不会假装历史 trace 自动天然就一致。
它会:
- 把活动标成
require_sync=True; - campaign 上出现
require_sync; - 需要显式执行
action_update_participants()去重排 traces。
这说明 Odoo 把活动图和已生成执行轨迹分开看:
- 图变了,不代表旧轨迹自动安全;
- 需要一次同步操作,把计划层修改投影到实例层。
sync 的本质是“重新对齐流程定义和已落地执行图”。
这很专业,也很克制。
六、为什么 domain 会沿父节点继承叠加
_compute_inherited_domain() 会把:
- campaign.domain
- 当前 activity.activity_domain
- 所有祖先 activity 的 domain
全部 AND 在一起。
这说明一个子节点能执行,不只是看自己的条件,而是:
- 它所在整条路径上的限制都得满足。
这有什么好处?
这样你在父节点定义的筛选,会自然向下游分支继承,而不用每个子节点手动重复抄一遍。
也就是说,营销图上的过滤不是局部 patch,而是 路径约束的累积。
七、为什么 allowed_parent_ids 要按 trigger 类型限制
并不是所有 activity 都能接在任意父节点后面。
源码会根据 trigger_type 限制 allowed_parent_ids:
- 如果触发条件是邮件打开、点击、回复;
- 那父节点就必须是 email 类活动;
- 否则流程图语义不成立。
这说明 Odoo 并不允许用户随意乱连图。
节点图不是随便画出来的 DAG,而是带业务类型约束的触发图。
这保证了流程图的因果关系是真实可执行的。
八、为什么 participant 完成不是看时间,而是看还有没有 scheduled traces
participant.check_completed() 的逻辑很朴素但很关键:
- 只要这个 participant 已经没有
state='scheduled'的 traces; - 就可以把 participant 标成
completed。
这说明参与者完成不是:
- 跑满了多少天;
- 执行了多少节点;
- 或某个最终节点显式打标。
而是:
执行图上已经没有未来待跑的节点。
这是很图论的完成定义,和普通邮件营销工具很不一样。
九、为什么删除业务记录要把 participant 标成 unlinked
如果底层业务对象没了,participant 不会假装还能继续跑。
action_set_unlink() 会:
- 把 participant 状态置为
unlinked; - 把未执行 traces 改成
canceled; - 原因写成
Record deleted。
这说明 Odoo 对自动化很谨慎:
- 不是目标记录没了还继续乱发;
- 而是显式终止并保留取消痕迹。
十、实战里最容易误解的 4 个点
误区 1:campaign 就是邮件活动集合
不对。
它定义的是目标模型、过滤条件和去重语义。
误区 2:activity 是线性序列步骤
不是。
它们是带触发边的节点图。
误区 3:改了活动配置会自动改好所有实例
不会。
需要 sync 把定义层变更映射到 traces。
误区 4:participant 只是中间表
也不对。
它本质上是“业务记录在某个 campaign 中的一次流程实例”。
总结
把 marketing_automation 源码串起来后,你会发现 Odoo 企业版做的根本不是“定时群发”。
它真正做的是一张可执行营销图:
- 用 campaign 定义目标模型、过滤和去重;
- 用 activity 定义节点、延迟和触发边;
- 用 participant 表示流程实例;
- 用 trace 表示实例在节点上的待执行轨迹;
- 用 sync 在定义层变更后重排执行图;
- 用 completed / unlinked / canceled 精细收口实例生命周期。
所以 Odoo 企业版 Marketing Automation 的本质,不是“自动发邮件”,而是“围绕业务记录运行一张带时间和行为触发的流程图”。
这才是这部分源码最值得学的地方。
参考源码
- enterprise/marketing_automation/models/marketing_campaign.py
- enterprise/marketing_automation/models/marketing_activity.py
- enterprise/marketing_automation/models/marketing_participant.py
DISCUSSION
评论区