POS 防重同步

Odoo POS 为什么不怕前台重复点同步:uuid、pos_reference 与幂等防重链路讲透

很多人担心 POS 断网重传、多端并发或收银员反复点同步时会不会把订单写重。Odoo 真正依赖的不是“大家别重复点”,而是 uuid 唯一约束、draft 订单可覆写、已落定订单忽略重复同步,以及 relations_uuid_mapping 的后补关联。本文结合 point_of_sale 源码讲清 POS 为什么能在脏网络里尽量保持幂等,以及遇到重复单疑云时该怎么查。

POS
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 4 阅读

POS 最让人紧张的场景之一,不是收款失败,而是“会不会记重单”。

比如这些现场问题:

  • 断网后补传时,店员连点几次同步;
  • 同一订单在两台设备上都试过提交;
  • 浏览器卡住后刷新,前台又把那张单重新送了一次;
  • 支付完成后,又收到一份迟到的旧 payload。

很多人以为 Odoo 之所以没那么容易写重,是因为前端刚好没重复提交。其实恰恰相反,源码默认就假设前端和网络都可能“不干净”,所以它在后端做了一整套“先认人,再决定是更新、创建还是忽略”的幂等保护。

先说结论:Odoo POS 处理重复同步的关键,不是 pos_reference 好不好看,而是 uuid 这一层的唯一身份。 pos_reference 更像票号;真正的“这是不是同一张前端订单”判断,主要靠 uuid

第一层防线:uuid 不是装饰字段,而是唯一键

pos.orderpos.order.linepos.payment 上,源码都定义了 uuid 字段,并加了唯一约束。

这件事的含义非常直接:

  • 前端生成的数据不是只靠数据库自增 id 认身份;
  • 即使网络重试、设备重发,后端也能先用 uuid 识别“你是不是之前那张单”。

所以如果你想理解 Odoo POS 的幂等思路,第一句就该记住:

数据库里的主键是服务器内部身份,uuid 才是前后端协作时的跨端身份。

这也是为什么升级脚本里甚至专门有 post-deduplicate-uuids.py。Odoo 很清楚:只要 uuid 混乱,整条同步链的防重能力就会变差。

第二层防线:sync_from_ui() 不是盲目 create,而是先找旧单

sync_from_ui() 的主线很清楚:

  1. 逐张接收前端订单;
  2. 先调用 _get_open_order(order),按 uuid 查现有订单;
  3. 如果找到且状态还是 draft,就走更新;
  4. 如果找不到,才创建;
  5. 如果找到了,但状态已经不是 draft,通常忽略这次重复同步。

也就是说,Odoo 后端不是“收到一张单就插一张单”,而是:

  • 同一个 uuid + 仍可修改 → 更新;
  • 同一个 uuid + 已经落定 → 忽略重复;
  • 完全陌生的 uuid → 新建。

这就是它在脏网络环境下尽量保持幂等的核心骨架。

为什么只有 draft 单允许被覆写

源码对 draft 非常宽容,对非 draft 很克制。

原因很现实:POS 前台在订单还没正式完成时,收银员可能不断修改:

  • 增减商品;
  • 改客户;
  • 改付款;
  • 调整备注;
  • 重传未完成订单。

所以 draft 单是可以被后续同步覆盖的。

但一旦订单已经 paiddoneinvoiced,Odoo 就开始把它当成业务事实,而不是前端草稿。此时重复同步再来,系统宁可忽略,也不愿意把已落定订单再按旧 payload 改写一遍。

_process_order() 里还有一层“先保存行和付款,再更新主单”

在更新已有订单时,源码有个容易被忽略的细节:

  • 先处理 linespayment_ids
  • 再写其他字段;
  • 并把 uuidaccess_token 之类保护起来,不让重复覆盖乱改。

这样做不是啰嗦,而是在防止订单状态变化后,再去改子表时出现不一致。

尤其是支付相关字段,如果先把主单状态写成 paid,再去碰 payment lines,很多边界会变得危险。

relations_uuid_mapping 在解决什么问题

很多人以为只要订单本身有 uuid 就够了。其实 POS 还有一个更深的难点:

前端一次同步时,很多关联对象也可能还是“前端临时身份”。

所以 _process_order() 里会处理 relations_uuid_mapping

  • 先用各模型的 uuid 找到真正记录;
  • 再把 many2one / one2many / many2many 关系补上。

这代表 Odoo 的目标不只是“别重单”,而是“让前后端在异步创建后还能重新对齐关联关系”。

换句话说,幂等不只是订单级别,还是关系级别。

pos_reference 到底是干什么的

pos_reference 在源码里叫 Receipt Number,更像门店和财务看得懂的票号。

它很重要,但它的职责和 uuid 不一样:

  • uuid:系统防重、跨端识别、同步幂等;
  • pos_reference:业务展示、票据追踪、人工沟通。

所以排查重复单时,不要只盯着 pos_reference。两张异常记录看起来票号像,不代表它们一定是同一单;反过来,同一单的同步冲突,关键也往往要回到 uuid

为什么已落定订单的重复同步会被“吃掉”

sync_from_ui() 中有段注释很诚实:理论上不该出现,但实践中会发生,例如某些 tip later 场景。

所以源码对“已存在且非 draft 的订单”采用保守策略:

  • 保留必要的准备变更;
  • 返回已有订单 id;
  • 日志里标记这次 sync 被忽略。

这不是丢数据,而是在明确表达:

这份 payload 太晚了,订单事实已经成立,不能再用它改写。

最容易误解的三件事

误区一:POS 防重主要靠前端别重复提交

不对。后端才是真正的防线,uuid 和状态判断才是核心。

误区二:pos_reference 就是唯一身份

不对。它更像收据号;真正的同步身份主要是 uuid

误区三:重复同步一定会报错

也不对。很多情况下,系统会选择忽略迟到 payload,而不是把前台体验变成显式报错。

实战排错顺序

遇到“怀疑写重单 / 同步重复 / 订单被旧数据覆盖”的问题,建议按这个顺序查:

  1. 先比对异常订单的 uuid 是否相同;
  2. 再看对应订单在第一次同步时的状态,是 draft 还是已 paid/done
  3. 看日志里 sync_from_ui() 是走了 create、update,还是 ignored;
  4. 检查订单行、支付行是否也各自带有稳定 uuid
  5. 如果关系丢了,回查 relations_uuid_mapping 是否正确传入;
  6. 最后再看 pos_reference 是否只是展示层误导,而不是真重复。

最后的结论

Odoo POS 在防重这件事上,最值得肯定的不是“绝不出错”,而是它的设计思路足够现实:

  • 接受网络会重试;
  • 接受前端会重复送;
  • 接受多设备会并发;
  • 但尽量用 uuid + 状态边界 + 关系补链 把重复写入压到最低。

所以别把 POS 的幂等理解成“运气好,大家刚好没重复点”。

它真正依赖的,是后端先识别这是不是同一张单,再决定该更新、创建,还是礼貌地忽略。

DISCUSSION

评论区

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