框架深潜

Odoo Google Calendar 同步为什么难点不只在 OAuth:sync token、循环事件重写与 organizer 权限边界讲透

很多人觉得 Google Calendar 接入的核心就是拿到 OAuth token,但真正让同步稳定的,是更底层的几条规则:写库成功后才 post-commit 推送、sync token 失效时全量重拉、循环事件在 Odoo 与 Google 之间如何重新生成、以及 guest readonly 事件为什么必须由 organizer 身份修改。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Google Calendar 同步在 Odoo 里,真正难的不是 OAuth 授权页,而是如何避免双写、漏写、重复创建和错误改写别人事件

/home/ubuntu/odoo-temp/addons/google_calendar/models/google_sync.pycalendar.pycalendar_recurrence_rule.pyres_users.py 来看,这个模块最关键的不是“能不能调到 Google API”,而是四条边界:

  1. 只有数据库事务真正提交后,才允许把变更推到 Google。
  2. 增量同步依赖 sync token,失效时必须退回 full sync。
  3. 循环事件不是简单复制,而是有独立的 recurrence 建模与 ID 生成规则。
  4. 不是 organizer 的用户,不能随便改受限事件,否则会制造重复或 403。

为什么 Odoo 坚持 post-commit 同步

google_sync.py 最值得先看的不是 API 调用,而是 after_commit 装饰器。

源码注释写得很明确:

  • Google API 请求要在当前事务结束后再发;
  • 这样只有 Odoo 数据库里的变更真的落盘,才会推送给 Google;
  • 尤其是事件创建,否则 Odoo 事务崩了但 Google 已经建了事件,就会出现重复。

这条规则非常重要。

很多系统接第三方日历时,会在保存事件的同一个事务里同步远端,看上去“实时”,但一旦本地事务后面失败,远端已经写成功,就会产生经典脏数据。

Odoo 在这里的选择很成熟:

宁可同步稍晚一步,也不要让本地失败、远端成功。

所以 Calendar 同步首先是一套事务边界设计,不是一个 OAuth 小功能。

need_sync + google_id 说明同步不是一次动作,而是持续对象关系

抽象模型 google.calendar.sync 上有两个关键字段:

  • google_id
  • need_sync

这两个字段放在一起,传递了一个非常清楚的思想:

  • google_id 表示这个 Odoo 对象已经和 Google 某个对象建立映射;
  • need_sync 表示它当前还有待推送的本地变更。

于是同一个事件可能经历:

  • 本地新建,待插入 Google;
  • 本地更新,待 patch 到 Google;
  • 本地归档,待删除 Google;
  • Google 改动后,再反向写回 Odoo。

换句话说,Odoo 不是在“调用一次同步接口”,而是在维护一份长期存在的双边映射状态

sync token 不是优化项,而是增量同步的锚点

res_users.py_sync_request() 里,是否 full sync 取决于:

full_sync = not bool(self.sudo().google_calendar_sync_token)

也就是说,google_calendar_sync_token 不是可有可无的缓存,而是:

  • 有 token:按增量拉;
  • 没 token:全量重拉;
  • token 失效:捕获 InvalidSyncToken 后重新 full sync。

这就解释了为什么有些团队会发现:

  • 平时同步很快;
  • 某次异常后突然大量重扫;
  • 看起来像“Google 又抽风了”。

其实不是。 很多时候是增量锚点失效后,系统主动回退到全量校正

这反而是稳健设计,因为继续相信一枚已经失效的 sync token,才真的危险。

循环事件为什么总是最容易把同步搞乱

在 Odoo 里,循环事件不是“主事件 + N 条普通子事件”那么简单。

calendar.recurrence 被单独继承了 google.calendar.sync,说明 recurrence 本身就是一个要同步的对象。

更关键的是 _get_event_google_id() 的规则:

  • recurrence 自身有一个 google_id
  • 每个实例事件的 Google id 由 recurrence_id + 起始时间 组合生成;
  • 如果是全天事件,用日期;
  • 如果是非全天事件,用 UTC 紧凑时间戳。

这意味着:

循环实例的身份,不是任意可改的本地主键,而是与 recurrence 骨架和起始时间绑定。

所以一旦你改的是基准时间、RRULE、follow_recurrence 之类的骨架字段,Odoo 很可能不是“原地微调”,而是要重建整组实例。

为什么 recurrence 修改时会出现“先删后建”的感觉

calendar_recurrence_rule.py 里有几段非常关键:

  • 如果同步事件后来变成 recurrence,要把原 Google 单事件删掉;
  • 某些情况下需要保留一个 inactive copy,带着旧 google_id,这样下一轮同步才能正确删远端对象;
  • 如果基准事件时间变了,旧实例会被归档或重算,再重新应用 recurrence。

这解释了实战里一个非常常见的困惑:

“我只是改了循环规则,为什么看起来像整串事件被删了又重建?”

因为在 Google 与 Odoo 双方都要保持 ID 可解释的前提下,某些变更本来就不适合原地 patch

guest readonly 为什么不能硬改

calendar.py_check_modify_event_permission() 明确限制:

  • 如果事件 guests_readonly = True
  • 且当前用户不是 organizer;
  • 又确实修改了需要同步到 Google 的字段;
  • 就直接抛 ValidationError

calendar_attendee.py 里又专门处理了 attendee 状态同步:

  • 不是 organizer 的事件,需要切换到 owner 用户上下文去同步;
  • 因为 Google 对非 organizer 的 patch 限制很严格;
  • 注释甚至直说,哪怕只传 startend 等必须字段,也可能收到 Forbidden for non-organizer

这类限制非常真实。 Google Calendar 不是协作文档,不是每个参与者都能安全改全部字段。

所以 Odoo 在这里的目标不是“谁都能编辑”,而是尽量在不制造重复事件与 403 的前提下保留最少可改能力

all-day recurrence 为何还有专门的重复创建补丁

_handle_allday_recurrences_edge_case() 专门处理一个很细的坑:

  • 新建“全天循环事件”时;
  • 首条事件可能先被当成单事件同步出去;
  • 之后 recurrence 又再生成一份;
  • 结果重复。

因此源码会在这个 edge case 下主动把部分记录 need_sync = False,阻断第一次错误同步。

这类代码特别说明一点:

Calendar 同步的稳定,不靠理想化模型,而靠承认边界条件真的会咬人。

Google 更新回流 Odoo 时,为什么要比较 write_date

_sync_google2odoo() 里采用的是“last updated wins”,但不是盲信 Google。

它会:

  • 先保存 Odoo 本地记录当前的 write_date
  • 再比较 Google event 的 updated 时间;
  • 只有 Google 较新时才反向写入。

这一步是在避免两边同时改时,旧数据把新数据覆盖掉。

当然它也不是绝对完美,源码自己都提醒: 如果 Google 服务器时间与 Odoo 服务器时间差得离谱,仍然可能有争议。 但在大多数正常环境里,这已经是很务实的冲突策略了。

实施时最该记住的 5 条经验

1. 别把 OAuth 当成同步稳定性的主体

OAuth 只是拿到票,稳定性靠的是 post-commit、sync token 和对象映射。

2. 看到大量重拉,不一定是 bug

很可能是 sync token 失效后的 full sync 回退。

3. 循环事件不要想当然地“只改一条字段”

一旦碰到基准时间或规则重写,系统可能必须重建整串事件。

4. 非 organizer 修改失败,是权限边界,不是前端小问题

Google 本来就限制这类操作,Odoo 只是把这条边界显式化。

5. 重复事件很多时候是事务或 recurrence 身份建模问题

不要只盯着 API 返回值,要看本地 need_syncgoogle_id、recurrence 是否被重新建模。

最后一句

Google Calendar 同步最容易被低估的地方,在于它看起来像一个“连接器”,实际上却是一套跨事务、跨身份、跨对象生命周期的协同协议。

把它当成“授权成功就万事大吉”,一定会踩坑; 把它当成“对象映射 + 增量锚点 + recurrence 重写 + organizer 权限边界”的组合,很多复杂现象才会 suddenly make sense。

DISCUSSION

评论区

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