活动提醒链路

Odoo 活动提醒为什么不会对所有报名人同时轰炸:event.mail、cron 分批与 slot 定时链路讲透

很多人以为 Odoo 的活动邮件就是“设个模板,到点群发”。但 event.mail、event.mail.registration、event.mail.slot 和 event_mail_scheduler 实际拼出了一条会分模式、分场次、分批次、可续跑的调度链。看懂它,活动提醒才不会发早、发漏或一次打爆系统。

其他
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 12 阅读

先说结论

Odoo 的活动提醒不是一个“到点把模板发出去”的平面动作,而是一套按触发类型分流、按场次拆分、按批量限流、按 cron 续跑的调度系统。

addons/event/models/event_mail.py 往下看,官方其实把提醒分成了三类:

  • 报名后触发after_sub
  • 围绕活动开始/结束触发before_eventafter_event_startbefore_event_endafter_event
  • 多场次活动的 slot 级触发:每个 event.slot 单独算时间

所以你在界面里看到的一条 Mail Schedule,后面不一定只对应一次发送。它可能会继续长出:

  • 一批 event.mail.registration,表示“针对某个报名人的待发送任务”
  • 一批 event.mail.slot,表示“针对某个场次的局部调度器”
  • 多次 cron 续跑,直到把所有该发的人都处理完

这也是为什么一个大型活动不会因为几千个报名人就直接在一轮 cron 里卡死。

第一层:为什么 event.mail 不是“邮件记录”,而是“调度规则”

event.mail 的关键字段不是正文,而是时间和触发语义:

  • interval_nbr
  • interval_unit
  • interval_type
  • scheduled_date
  • template_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 保存报名人级别的待发任务。它至少做了三件事:

  1. scheduler_idregistration_id 绑起来
  2. _compute_scheduled_date()registration.create_date 重新算每个人自己的触发时间
  3. mail_sent 防止重复发送

这就意味着:

  • 同一个活动、同一条提醒规则,可以对应很多行报名人调度记录
  • 晚报名的人,哪怕活动相同,发送时间也会不同
  • cron 中断后,下次可以从未发送且已到时的记录继续跑

这不是“实现细节”,而是 Odoo 让活动提醒可恢复、可扩展的基础。

第三层:为什么 before/after event 模式又回到了“批量按活动发”

如果不是 after_sub,系统会走 _execute_event_based()_execute_slot_based()

这里的思路和报名后触发完全不同:

  • 先找出这次该通知的报名人
  • 再按照批次拆开
  • 每一批直接渲染并发送
  • last_registration_id 记录进度

_execute_event_based() 里有两个非常实战的参数:

  • mail.batch_size
  • mail.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_id
  • scheduled_date
  • last_registration_id
  • mail_done
  • mail_count_done

event_mail_slot.py 里的 _compute_scheduled_date() 用的是:

  • start_datetime 处理 before_event / after_event_start
  • end_datetime 处理 before_event_end / after_event

所以在多场次活动里,同一条提醒配置不会只算出一个时间,而是会被展开成每个场次各自的时间点

这就是很多人误会的地方:

你配置的是一条规则,系统执行时却会展开成一组 slot 级子任务。

第五层:为什么 draft / cancel 报名人不会被发到

无论是活动级还是报名级发送,Odoo 都会显式跳过无效报名状态。

_execute_event_based() 里,查询报名人的 domain 会排除:

  • draft
  • cancel

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

结果验证了几件很关键的事:

  1. 报名后即时邮件不是无限发,而是会被 cron_limit 截断
  2. 超出的部分会等待下一轮 cron
  3. draft / cancel 不会进入发送集合
  4. 每条报名后规则都会生成自己的 event.mail.registration
  5. mail_count_done 只统计已经跑完的那一部分

这说明官方在设计时,优先考虑的是大批量活动下的可控性,不是“代码逻辑最短”。

实战里最容易踩的 4 个坑

1)把 event.mail 当成发送日志

不是。

它是“策略 + 状态汇总”,不是每个收件人的发送明细。报名人级明细在 event.mail.registration,slot 级展开在 event.mail.slot

2)以为多场次活动只会按活动总时间算一次

不是。

多场次会按 slot 展开,各自算 scheduled_date。如果你只盯着活动主记录,很容易误判“为什么某些提醒还没发”。

3)看到没全发完,就以为 cron 坏了

未必。

先看:

  • mail.batch_size
  • mail.render.cron.limit
  • last_registration_id
  • cron 是否被 _trigger() 续调

很多“漏发”其实只是还没跑完下一轮

4)忘了活动结束后的边界

源码里有明确判断:

  • 某些“开始前”的提醒,如果活动或 slot 已经过期,就不会再补发

这属于非常合理的防呆:

Odoo 宁愿不补发一封已经失去业务意义的提醒,也不愿事后突然给用户发一封“昨天活动开始前 1 天提醒”。

该怎么排查活动提醒问题

推荐顺序:

  1. 先看 event.mailinterval_typeinterval_unitscheduled_date
  2. 再看活动是否 is_multi_slots
  3. 如果是报名后触发,看 event.mail.registration 是否生成、mail_sent 是否变化
  4. 如果是多场次,看 event.mail.slot 是否生成、每个 slot 的 scheduled_date 是否正确
  5. 再看 last_registration_idmail_count_done
  6. 最后看 cron 参数是否把任务分批切开了

这个顺序能避免你一上来就怀疑模板、SMTP 或队列,而忽略最前面的业务分流逻辑。

最后一句

Odoo 的活动提醒机制厉害的地方,不是“能发邮件”,而是它把提醒拆成了规则、报名人任务、场次任务和 cron 续跑四层。

所以真正该理解的不是“模板什么时候发”,而是:

这条提醒到底是按活动发、按报名人发,还是按 slot 发;它在大批量场景下怎样保证既不漏、也不把系统拖死。

一旦看懂这条链,活动提醒的大多数怪现象都会变得非常可解释。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。