先说结论
Odoo 的 microsoft_calendar 模块,看上去像一个“把会议同步到 Outlook”的集成功能;但从源码看,它真正处理的是一个很棘手的双向同步问题:
- 本地事务提交节奏与远端 Graph API 调用节奏并不相同;
- 增量同步依赖 delta token,而 token 会失效;
- 循环事件在 Outlook 侧有很多限制,不能随便在 Odoo 里改;
- 同一个会议同时存在 Microsoft event id、iCalUId、seriesMasterId 等多层身份。
所以,这个模块最值得看的不是“怎么接 Microsoft”,而是双向日历同步如何尽量避免状态打架。
need_sync_m 是整套同步系统的心跳字段
在多个模型里你都能看到 need_sync_m:
calendar.eventcalendar.recurrencemicrosoft.calendar.sync
它不是一个普通“是否已同步”的展示位,而是待同步意图。
源码里的模式大致是:
- 本地事件被创建、修改、删除时,若属于微软同步字段,且当前同步处于活动状态,就把
need_sync_m=True; - 当某些变更是从微软端反向拉回 Odoo 时,会带
dont_notify=True并把need_sync_m=False,避免回写风暴; - 循环事件的特殊展开与映射也会反复调整这个标记。
这说明 Odoo 并不是“每改一次就立刻强推 Graph API”,而是在维护一个可重试、可去抖的本地待同步集合。
这非常重要,因为日历同步天然会遇到:
- 多人编辑;
- 批量写入;
- 事务回滚;
- 网络失败;
- 远端对象已不存在。
如果没有 need_sync_m 这一层,本地变更和远端同步会很快缠成一团。
为什么 post-commit 是这类集成的生命线?
microsoft_sync.py 里有一个关键点:使用了 self.env.cr.postcommit.add。
这件事很关键,甚至可以说是整套设计的安全底座。
为什么不能在事务里直接强推 Outlook?
设想一个场景:
- 用户保存会议;
- Odoo 在数据库事务尚未提交时,就先调用 Microsoft Graph 创建或更新远端事件;
- 结果本地事务后面又因为别的字段校验失败而回滚了。
这时你就会得到一个非常尴尬的结果:
- Outlook 里已经有了新状态;
- Odoo 本地却没有这次变更。
这就是典型的分布式一致性问题。
Odoo 的做法是:
- 本地先完成事务;
- 提交成功后,再触发对微软端的同步动作。
这并不能让双向同步变成强一致,但至少守住了一个核心原则:
不要让一个最终会回滚的本地状态先污染外部系统。
对所有 SaaS 集成来说,这都是非常重要的设计经验。
delta token 为什么既高效又脆弱?
res_users.py 与 utils/microsoft_calendar.py 里可以看到,Odoo 会保存 microsoft_calendar_sync_token,并通过 Graph 的 delta 机制拉增量事件。
这很高效,因为它避免了每次全量扫用户日历。
但源码也明确处理了 InvalidSyncToken:
- 如果旧 token 失效,就退回
get_events(token=token)做无旧 token 的同步,再获取新的 next sync token。
这告诉我们什么?
- delta token 不是永久凭证;
- 它是一个“仅在当前服务端历史窗口内有效”的增量游标;
- 一旦服务端历史被裁剪、上下文变化、或者 token 过旧,客户端就必须接受一次更重的重新对齐。
这也是为什么同步系统里,能够优雅回退到全量重建,比“增量模式平时很快”更重要。
为什么循环事件不让你在 Odoo 里随便建、随便改?
如果当前同步活跃,源码会明确阻止:
- 在 Odoo 中直接创建 recurrent events;
- 在 Odoo 中直接更新 recurrence;
- 某些已同步循环事件的删除/归档操作。
报错信息也很坦率:
- 由于 Outlook Calendar 的限制,循环事件应直接在 Outlook 里创建或更新。
很多用户第一次看到这类限制会觉得“不够智能”,但实际上这是在尊重对端系统的真实约束。
源码里提到的典型限制包括:
- Outlook 对 recurrence exception 的移动有边界,不能把一次 occurrence 挪到前一场之前或后一场之后;
- Outlook 对循环事件的修改容易触发垃圾通知/垃圾同步;
- Odoo 若强行本地改,很容易把整组 recurrence 弄乱。
这也是为什么模块会建议:
- 单次例外可以谨慎处理;
- 真正的 recurrence 结构变更,尽量回到 Outlook 端操作。
这不是功能欠缺,而是把高风险操作放回更权威的一侧。
一个会议为什么要有三个 ID?
在微软日历同步里,常见三个标识:
microsoft_idms_universal_event_id(iCalUId)microsoft_recurrence_master_id(seriesMasterId)
很多人会问,一个会议为什么不能只用一个 ID?
因为它们解决的问题不同:
microsoft_id
更像 Graph API 当前对象的具体实例标识。
iCalUId
是跨日历、跨副本更稳定的“通用事件身份”。源码甚至会在某些场景通过 iCalUId 去反查真实 event id。
seriesMasterId
是循环事件体系里的主系列身份,用来把单个 occurrence 重新挂回母体结构。
如果没有这三层身份,以下问题会很难处理:
- 同一会议被不同日历副本感知;
- 循环事件拆分为主事件与实例;
- 某次同步拿到的是 occurrence,而不是 master;
- 本地和远端需要重新匹配未映射事件。
所以这不是 ID 设计过度,而是日历同步本身比普通业务单据复杂得多。
为什么“第一次同步日期”也要被单独记录?
源码里有 microsoft_calendar.sync.first_synchronization_date 相关逻辑。
它的作用是:
- 当系统进入同步后,用一个时间边界限制后续要纳入的本地事件范围;
- 避免把过久远的历史会议全量推送到 Outlook,造成噪音和性能压力。
这一步很有产品感。
因为日历不是发票。旧会议大量回灌到外部系统,通常没有意义,反而会制造:
- 同步耗时变长;
- 历史提醒被重建;
- 用户误以为旧事项被重新安排。
所以首同步日期本质上是在定义:从什么时候开始,把 Odoo 当成外部日历的有效历史来源。
同步暂停与停止不是一回事
源码里 _get_microsoft_sync_status() 会区分:
sync_activesync_pausedsync_stopped- 以及未配置 / 未授权场景
这区分非常有必要。
paused
更像系统级开关:
- 暂时别同步;
- 但连接与状态还在;
- 恢复后可以继续。
stopped
更像用户或系统显式断开:
- 不再认为当前连接是活跃同步关系;
- 某些本地记录会停止继续向外推。
如果把这两者混成一个布尔值,就很难做好:
- 运维临时停同步;
- 用户主动断开;
- 恢复策略;
- 批量重启同步。
实战里最该注意的 5 个坑
1. 把事务内外边界想简单了
日历同步最怕本地还没 commit,外部已经更新成功。post-commit 不是锦上添花,而是底线。
2. 以为 delta token 永远有效
一旦 token 失效,必须允许系统重新全量对齐,而不是硬顶着旧 token 重试。
3. 低估循环事件复杂度
循环事件不是“多条普通会议”,它在 Outlook 里有非常强的结构约束。
4. 不理解多层 ID 的必要性
只靠一个 event id,很难正确追踪 recurrence、重映射与跨副本匹配。
5. 把 need_sync_m 当成展示字段
它其实是本地同步队列的一部分,是整个双向集成稳定性的关键。
最后一句
Microsoft Calendar 模块最成熟的地方,不在于“能同步会议”,而在于它承认了一件事:
日历双向同步本质上不是接口调用问题,而是一致性问题。
所以它才会:
- 用
need_sync_m缓冲本地意图; - 用 post-commit 保护事务边界;
- 用 delta token 提升效率,同时接受失效回退;
- 对循环事件保持克制;
- 用多层 ID 管理远端对象身份。
这套设计不花哨,但非常靠谱。真正长期稳定的集成,往往就是靠这些看似保守的边界守出来的。
DISCUSSION
评论区