循环日程

Odoo 循环会议提醒为什么不是“每个实例各挂一个定时器”:calendar.recurrence、下一个事件选择与 trigger 接力讲透

结合 calendar.recurrence 与 calendar_event 源码,讲清 Odoo 为什么只给每条 recurrence 挂一个 trigger,如何选择未来最近实例、更新 trigger_id,以及拆分/截断循环后提醒为什么会跟着重排。

Odoo 开发 协同办公
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

看到“循环会议提醒”,很多人会下意识想成这样:

每个会议实例都挂一个定时器,到了时间再提醒。

但 Odoo 的 calendar.recurrence 不是这么组织的。 它的核心思路是:

一条 recurrence 管一整串事件,一个 trigger 只服务一个 recurrence 的未来提醒。

这样做的好处很明显:

  • 不会给每个实例都造一个独立调度负担;
  • 修改循环规则时,能统一重算未来 occurrences;
  • 遇到拆分、截断、时区变化时,链路更容易维护。

1. recurrence 不是“事件的附件”,而是规则本体

calendar.recurrence 里最重要的字段是:

  • base_event_id
  • calendar_event_ids
  • event_tz
  • rrule
  • rrule_type
  • interval
  • count
  • until
  • trigger_id

你可以把它理解成:

  • base_event_id:这条循环的代表事件
  • calendar_event_ids:所有已经生成出来的实例
  • rrule:iCalendar 语义下的规则串
  • trigger_id:负责后续提醒的调度锚点

rrule 是一个 computed + inverse 字段。 _compute_rrule() 会把表单上的周几、月第几周、结束日期这些参数序列化成规则串; _inverse_rrule() 又能把规则串反解析回来。

这说明 Odoo 不只是在 UI 上“选个重复频率”,而是在维护一套可序列化、可反序列化的 recurrence 规则


2. Odoo 不是只保存规则,还会把未来实例真正算出来

_apply_recurrence() 是真正把规则变成事件的地方。

它的逻辑大致是:

  1. 找到 base_event_id
  2. 计算这条 recurrence 应该覆盖的时间区间;
  3. 把已经存在且范围匹配的事件保留;
  4. 对缺失的 range 用 copy_data() 造新事件;
  5. 对不再符合规则的事件做 detach。

这一步很重要,因为 recurrence 不是“纯公式视图”。 Odoo 会真的生成事件实例,方便:

  • 参会人状态跟踪
  • 单次例外编辑
  • 通知发送
  • 日程界面渲染

也就是说,规则是“母体”,事件实例是“落地结果”。


3. 为什么一个 recurrence 只需要一个 trigger

_setup_alarms() 是这篇文章最值得看的地方。

它不是给每个事件都建 trigger,而是先做一件事:

  • flush calendar.eventrecurrence_idstart
  • 用 SQL 选出每条 recurrence 中未来最近的一个 event
  • 再把这些 event 交给 calendar.event._setup_alarms()
  • 最后把返回的 trigger_id 写回 recurrence

SQL 里有一个很关键的片段:

SELECT DISTINCT ON (recurrence_id) id event_id, recurrence_id
FROM calendar_event
WHERE start > %s
  AND id IN %s
ORDER BY recurrence_id, start ASC;

这代表什么?

  • 每条 recurrence 只挑一个“下一场要提醒的事件”
  • 不是所有实例一起挂调度
  • 当这个 trigger 被消费后,系统再根据 recurrence 的后续事件重建下一批提醒

这是一种很典型的链式调度设计。

它省资源,也更容易在循环规则变化后统一重算。


4. 拆分循环和截断循环,其实是在重写规则边界

会议不是永远不变的。 当你只想改“从某个实例开始”的后续内容时,Odoo 不能直接把整条 recurrence 全改掉。

这时就会用到:

  • _split_from(event, recurrence_values=None)
  • _stop_at(event)

_split_from() 会先把当前 recurrence 在目标 event 处截断,然后复制出一个新的 recurrence,新的 base event 从这个实例开始。

_stop_at() 则负责:

  • 找出从指定时间点开始的后续事件;
  • detach 这些事件;
  • 把旧 recurrence 的 until 调整到前一天。

如果截断后已经没有事件了,它甚至会直接删除 recurrence。

这说明 Odoo 对循环会议的理解不是“一个永远增长的列表”,而是:

规则边界可以被切开,未来实例可以被重新定义。


5. 时区和 DST 是这类代码最容易翻车的地方

calendar.recurrence 里有一段很值得注意的逻辑:_get_start_of_period()

它会根据 recurrence 类型去算周期起点:

  • weekly:按语言里的周起始日回退
  • monthly:回到月初
  • 其他类型则保持原值

然后它还会检查 DST 差异。

这是为了避免时区切换时,重复事件在某些时间点“多出一份”或者“少一份”。

这类 bug 在日历系统里特别常见:

  • 本地时间看着没问题;
  • UTC 落到另一日;
  • 周期回算时又多了一次偏移。

Odoo 的处理方式不是“假装没有时区问题”,而是明确检测并做保护。


6. 为什么这套设计比“每个实例一个定时器”更稳

这套设计的优势很实用:

  • 规则变化时,能统一改 recurrence;
  • 未来实例按需生成,不用一开始就爆炸式创建;
  • trigger 只跟最近一次提醒绑定,调度规模更小;
  • 拆分和截断可以把“从某个点开始”当成一次明确的边界操作。

但它也要求你在排障时换一种思维方式:

  • 不要只看单个 event;
  • 要看 recurrence 的规则串、base event、trigger_id;
  • 如果提醒没触发,先看未来最近实例有没有被正确挑出来;
  • 如果改了一次循环后后续全变了,要先看是不是触发了 split/stop 边界。

结论

calendar.recurrence 不是“复制一堆事件”这么简单。 它是一个把:

  • 规则序列化
  • 未来实例生成
  • 单一 trigger 调度
  • 拆分和截断
  • 时区与 DST 保护

串起来的循环日程引擎。

真正看懂它,你就会明白:

Odoo 不是在管理一堆重复事件,而是在管理“重复规则如何被安全地落到具体实例上”。

DISCUSSION

评论区

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