先说结论
Odoo 的活动提醒不是一个“到点把模板发出去”的平面动作,而是一套按触发类型分流、按场次拆分、按批量限流、按 cron 续跑的调度系统。
从 addons/event/models/event_mail.py 往下看,官方其实把提醒分成了三类:
- 报名后触发:
after_sub - 围绕活动开始/结束触发:
before_event、after_event_start、before_event_end、after_event - 多场次活动的 slot 级触发:每个
event.slot单独算时间
所以你在界面里看到的一条 Mail Schedule,后面不一定只对应一次发送。它可能会继续长出:
- 一批
event.mail.registration,表示“针对某个报名人的待发送任务” - 一批
event.mail.slot,表示“针对某个场次的局部调度器” - 多次 cron 续跑,直到把所有该发的人都处理完
这也是为什么一个大型活动不会因为几千个报名人就直接在一轮 cron 里卡死。
第一层:为什么 event.mail 不是“邮件记录”,而是“调度规则”
event.mail 的关键字段不是正文,而是时间和触发语义:
interval_nbrinterval_unitinterval_typescheduled_datetemplate_ref
_compute_scheduled_date() 会根据不同 interval_type 选不同基准时间:
- 报名后触发:拿活动
create_date只作为规则基准,真正按报名时间单独算 - 活动开始前/后:拿
event_id.date_begin - 活动结束前/后:拿
event_id.date_end
然后再用 _INTERVALS 把“几小时 / 几天 / 几周 / 几个月”转成 relativedelta。
这里最容易忽略的一点是:
event.mail更像“发送策略定义”,不是“某一封将要发出的邮件实例”。
真正落到具体报名人,靠的是后面的子模型。
第二层:为什么 after_sub 模式必须单独建 event.mail.registration
如果 interval_type == 'after_sub',execute() 不会走全局活动邮件,而会转去 _execute_attendee_based()。
这是整套机制里最关键的分叉。
原因很简单:
- “活动开始前 1 天提醒”是一个时间点对应一批人
- “报名后 1 小时提醒”是每个报名人各有自己的发送时间
这两种问题根本不是同一种调度模型。
所以 Odoo 专门用 event.mail.registration 保存报名人级别的待发任务。它至少做了三件事:
- 把
scheduler_id和registration_id绑起来 - 用
_compute_scheduled_date()按registration.create_date重新算每个人自己的触发时间 - 用
mail_sent防止重复发送
这就意味着:
- 同一个活动、同一条提醒规则,可以对应很多行报名人调度记录
- 晚报名的人,哪怕活动相同,发送时间也会不同
- cron 中断后,下次可以从未发送且已到时的记录继续跑
这不是“实现细节”,而是 Odoo 让活动提醒可恢复、可扩展的基础。
第三层:为什么 before/after event 模式又回到了“批量按活动发”
如果不是 after_sub,系统会走 _execute_event_based() 或 _execute_slot_based()。
这里的思路和报名后触发完全不同:
- 先找出这次该通知的报名人
- 再按照批次拆开
- 每一批直接渲染并发送
- 用
last_registration_id记录进度
_execute_event_based() 里有两个非常实战的参数:
mail.batch_sizemail.render.cron.limit
前者控制一次渲染/发送多少条,后者控制一轮 cron 最多处理多少报名人。
如果超过 cron_limit,Odoo 不会硬扛到底,而是:
- 只处理一部分
- 调
event.event_mail_scheduler再触发一次 - 靠
last_registration_id从上次断点继续
这套设计很像很多成熟任务队列的“分块续跑”模式。
它解决的不是“能不能发出去”,而是:
发很多的时候,系统还能不能优雅地发。
第四层:多场次活动为什么必须再拆一层 event.mail.slot
一旦 event_id.is_multi_slots 为真,execute() 会优先走 _execute_slot_based()。
这说明 Odoo 很明确地把“多场次活动”当成了另一种时序问题。
原因也很直观:
- 整个活动可能持续很多天
- 但不同场次各有自己的开始和结束时间
- “活动开始前 1 天提醒”在多场次场景里,应该围绕每个 slot 计算,而不是围绕总活动时间计算
因此 event.mail.slot 会为每个场次各建一条局部调度记录,并各自保存:
event_slot_idscheduled_datelast_registration_idmail_donemail_count_done
event_mail_slot.py 里的 _compute_scheduled_date() 用的是:
start_datetime处理before_event/after_event_startend_datetime处理before_event_end/after_event
所以在多场次活动里,同一条提醒配置不会只算出一个时间,而是会被展开成每个场次各自的时间点。
这就是很多人误会的地方:
你配置的是一条规则,系统执行时却会展开成一组 slot 级子任务。
第五层:为什么 draft / cancel 报名人不会被发到
无论是活动级还是报名级发送,Odoo 都会显式跳过无效报名状态。
在 _execute_event_based() 里,查询报名人的 domain 会排除:
draftcancel
在 event.mail.registration.execute() 里,也只会处理:
registration_id.state in ('open', 'done')
这意味着活动提醒面向的是已成立的报名关系,而不是所有草稿数据。
这点非常重要,因为很多实施里会出现:
- 订单未支付,报名还在草稿
- 人员取消报名
- 后台预录但未确认
如果这些都发提醒,用户会非常困惑。
官方源码的选择很保守:先保证业务状态正确,再谈触达。
第六层:为什么测试代码比界面更能说明官方真实意图
test_event_mail_schedule() 其实把这条链路测得很清楚。
测试里故意:
- 建了两条
after_sub - 建了一条
before_event - 建了一条
after_event - 再造 15 个正常报名、1 个 draft、1 个 cancel
- 把
mail.batch_size设成 2 - 把
mail.render.cron.limit设成 10
结果验证了几件很关键的事:
- 报名后即时邮件不是无限发,而是会被
cron_limit截断 - 超出的部分会等待下一轮 cron
- draft / cancel 不会进入发送集合
- 每条报名后规则都会生成自己的
event.mail.registration mail_count_done只统计已经跑完的那一部分
这说明官方在设计时,优先考虑的是大批量活动下的可控性,不是“代码逻辑最短”。
实战里最容易踩的 4 个坑
1)把 event.mail 当成发送日志
不是。
它是“策略 + 状态汇总”,不是每个收件人的发送明细。报名人级明细在 event.mail.registration,slot 级展开在 event.mail.slot。
2)以为多场次活动只会按活动总时间算一次
不是。
多场次会按 slot 展开,各自算 scheduled_date。如果你只盯着活动主记录,很容易误判“为什么某些提醒还没发”。
3)看到没全发完,就以为 cron 坏了
未必。
先看:
mail.batch_sizemail.render.cron.limitlast_registration_id- cron 是否被
_trigger()续调
很多“漏发”其实只是还没跑完下一轮。
4)忘了活动结束后的边界
源码里有明确判断:
- 某些“开始前”的提醒,如果活动或 slot 已经过期,就不会再补发
这属于非常合理的防呆:
Odoo 宁愿不补发一封已经失去业务意义的提醒,也不愿事后突然给用户发一封“昨天活动开始前 1 天提醒”。
该怎么排查活动提醒问题
推荐顺序:
- 先看
event.mail的interval_type、interval_unit、scheduled_date - 再看活动是否
is_multi_slots - 如果是报名后触发,看
event.mail.registration是否生成、mail_sent是否变化 - 如果是多场次,看
event.mail.slot是否生成、每个 slot 的scheduled_date是否正确 - 再看
last_registration_id、mail_count_done - 最后看 cron 参数是否把任务分批切开了
这个顺序能避免你一上来就怀疑模板、SMTP 或队列,而忽略最前面的业务分流逻辑。
最后一句
Odoo 的活动提醒机制厉害的地方,不是“能发邮件”,而是它把提醒拆成了规则、报名人任务、场次任务和 cron 续跑四层。
所以真正该理解的不是“模板什么时候发”,而是:
这条提醒到底是按活动发、按报名人发,还是按 slot 发;它在大批量场景下怎样保证既不漏、也不把系统拖死。
一旦看懂这条链,活动提醒的大多数怪现象都会变得非常可解释。
DISCUSSION
评论区