看到“循环会议提醒”,很多人会下意识想成这样:
每个会议实例都挂一个定时器,到了时间再提醒。
但 Odoo 的 calendar.recurrence 不是这么组织的。
它的核心思路是:
一条 recurrence 管一整串事件,一个 trigger 只服务一个 recurrence 的未来提醒。
这样做的好处很明显:
- 不会给每个实例都造一个独立调度负担;
- 修改循环规则时,能统一重算未来 occurrences;
- 遇到拆分、截断、时区变化时,链路更容易维护。
1. recurrence 不是“事件的附件”,而是规则本体
calendar.recurrence 里最重要的字段是:
base_event_idcalendar_event_idsevent_tzrrulerrule_typeintervalcountuntiltrigger_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() 是真正把规则变成事件的地方。
它的逻辑大致是:
- 找到
base_event_id; - 计算这条 recurrence 应该覆盖的时间区间;
- 把已经存在且范围匹配的事件保留;
- 对缺失的 range 用
copy_data()造新事件; - 对不再符合规则的事件做 detach。
这一步很重要,因为 recurrence 不是“纯公式视图”。 Odoo 会真的生成事件实例,方便:
- 参会人状态跟踪
- 单次例外编辑
- 通知发送
- 日程界面渲染
也就是说,规则是“母体”,事件实例是“落地结果”。
3. 为什么一个 recurrence 只需要一个 trigger
_setup_alarms() 是这篇文章最值得看的地方。
它不是给每个事件都建 trigger,而是先做一件事:
- flush
calendar.event的recurrence_id和start - 用 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
评论区