真正的协作式 Spreadsheet,从来不是“大家同时打开同一份表”。enterprise/spreadsheet_edition/models/spreadsheet_mixin.py 和 spreadsheet_cell_thread.py 显示,Odoo 企业版关心的是:每条编辑消息基于哪个 revision、乱序时怎么拒绝、什么时候要生成 snapshot、评论线程如何跟随权限一起移动。
一、协作入口先判断消息顺序,不是先写数据库
dispatch_spreadsheet_message() 的文档注释写得非常直白:协作消息最重要的是顺序。每条消息都带 serverRevisionId,服务器会检查它是否等于当前已知 revision;不一致就拒绝。
这一步决定了 Odoo 不会为了“看起来流畅”接受乱序修改。因为在协作表格里,一次错序写入就足以让后面的公式、注释和选区全部失真。
二、revision 和 snapshot 分工不同
_save_concurrent_revision() 用于保存新的 revision,它要求父 revision 关系成立;save_spreadsheet_snapshot() 则会创建新的 snapshot UUID,再通过 dispatch_spreadsheet_message() 把 snapshot 作为一次特殊 revision 发送进去。
这说明 revision 更像增量日志,snapshot 更像阶段性快照。企业协作里两者缺一不可:
- 只有 revision,没有 snapshot,历史回放会越来越重;
- 只有 snapshot,没有 revision,细粒度协作与回溯就会丢失。
三、冲突不是“最后保存者获胜”,而是明确报错重试
save_spreadsheet_snapshot() 若发现 dispatch_spreadsheet_message() 返回 false,会直接抛出并发更新错误,让用户重试。restore_spreadsheet_version() 也会在恢复前检查写权限,并在恢复后让其他协作者感知到一个他们预期之外的 snapshot,从而主动刷新。
这套机制非常成熟:它不假装冲突不存在,而是把冲突显式暴露出来。对企业协作来说,这比“静默吞掉别人修改”安全得多。
四、评论线程不是表格附属品,而是带权限的入口
spreadsheet_cell_thread.py 里,get_spreadsheet_access_action() 会先拿到关联记录,再检查是否拥有 read access;没有权限时直接回主页。_check_access() 也会顺着关联表格对象再做一次读权限校验。
这意味着单元格评论不是一个独立的“聊天角落”。如果你对底层表格没有读权限,就不能因为收到评论提醒而侧面闯进去。
五、fork / restore 说明协作并不排斥分叉
fork_history() 会复制 spreadsheet、复制 revision,到目标版本后再清掉评论;restore_spreadsheet_version() 则是截断后续 revision,再把某个版本重新立成新的 snapshot。业务上这说明 Odoo 不只是支持“共同编辑”,也支持“从某个历史点分支出去”和“回滚到稳定版本”。
实战注意事项
- 不要把 revision 当普通日志表:它是协作顺序控制的核心。
- 遇到并发报错要尊重重试机制:不要试图跳过 revision 校验。
- 评论权限要跟底表权限一起设计:单元格 thread 也可能泄露业务上下文。
- 为关键模板准备 snapshot 节点:这样回滚和 fork 才真正可用。
新手误区
- 误以为多人在线编辑只靠 websocket 推送。
- 误以为 snapshot 就是自动保存。
- 误以为评论通知可以绕过文档权限。
- 误以为版本恢复只是把旧 JSON 覆盖回去。
主要源码参考
enterprise/spreadsheet_edition/models/spreadsheet_mixin.pyenterprise/spreadsheet_edition/models/spreadsheet_cell_thread.py
DISCUSSION
评论区