电商购物车

Odoo 电商购物车为什么不是一张普通报价单:cart、line merge、匿名会话与恢复邮件主链路讲透

很多人把网站购物车理解成“前台先攒几行,最后转成销售单”,但 website_sale 实际上从加购、合并、校验、配送、匿名身份到弃单恢复都已经按销售订单模型在运行。

网站
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 电商里的购物车,从来就不是一个临时前台篮子。

website_sale 的设计是:

购物车本身就是一张处在特定生命周期里的 sale.order

所以从用户第一次点“加入购物车”开始,系统已经在做销售单层面的工作:

  • 找现有行还是新建行;
  • 校验数量是否合法;
  • 变体组合是否存在;
  • 配送费要不要重算;
  • 会话里的购物车数量要不要刷新;
  • 这张单是不是匿名 cart;
  • 后续是否会进入 abandoned cart 恢复邮件链路。

这就是为什么很多电商定制一旦只盯着前台 JS,最后总会踩坑。真正的主链路在 sale.order 模型里。


一、为什么 Odoo 把购物车直接建模成 sale.order

website_sale/models/sale_order.py 看,核心方法全都挂在 sale.order 上:

  • _cart_add()
  • _cart_find_product_line()
  • _cart_update_line_quantity()
  • _verify_cart_after_update()
  • _check_cart_is_ready_to_be_paid()
  • _filter_can_send_abandoned_cart_mail()

这说明 Odoo 的思路不是“前台先随便存一下,结账时再生成销售单”,而是:

  • 从一开始就用销售订单承载电商状态。

这么做的好处很明显:

  1. 价格、税、配送、优惠、付款都能沿用销售链路;
  2. 购物车不是独立平行世界,而是销售订单的前置阶段;
  3. 弃单、恢复、重算、支付校验都更容易串起来。

二、加入购物车时,系统先想的是“合并行”而不是“新建行”

_cart_add() 的第一件大事,不是 create,而是:

  • 先找有没有可复用的现有行。

源码会先走 _cart_find_product_line(),匹配条件不只是 product,还包括:

  • UoM;
  • linked_line_id
  • 自定义属性值;
  • no_variant 属性值;
  • combo 场景下的额外边界。

也就是说,Odoo 不会简单认为“同产品就合并”。

它真正想判断的是:

这次加购,是否还是同一份可合并配置。

这对电商很重要,因为一旦产品存在:

  • 变体;
  • no_variant 属性;
  • 可选组合;
  • 套餐/组合商品;

“是不是同一行”就不能只看 product_id 了。


三、为什么数量校验放在更新前,而不是支付前才做

无论是新增还是改数量,源码都会先走 _verify_updated_quantity()

这背后的思路很清楚:

  • 不要让明显非法的数量先落到购物车里,最后再在支付页统一报错。

好处是:

  1. 用户反馈更即时;
  2. 购物车状态更可信;
  3. 后续推荐、配送、库存提示也能基于更真实的 cart。

而且 _cart_update_line_quantity() 里还有一个很容易忽略的细节:

  • 如果某条 line 因为多标签页并发、参数错误或已被删除而找不到,系统不会悄悄吞掉,而是直接提示刷新页面。

这说明 Odoo 默认承认:

  • 网站购物车会发生并发修改;
  • 多标签页是正常用户行为;
  • 所以反馈必须偏保守,而不是假装一切顺利。

四、为什么更新购物车后还要做一次全局校验

很多人看到 _cart_add()_cart_update_line_quantity() 已经做过数量检查,就以为流程结束了。

其实后面还有 _verify_cart_after_update()

这个方法做的是“全局整理”,典型包括:

  • 如果整车只剩服务,移除配送行;
  • 如果已经选了 carrier,就重新计算运费;
  • 把 session 里的 website_sale_cart_quantity 刷新成最新值。

这说明 Odoo 把购物车更新分成两层:

  1. 行级校验:你这次更新是否合法;
  2. 车级校验:这次更新对整张订单造成了什么连锁变化。

很多定制问题就出在这里:

  • 只改了 line,没触发 order 级整理;
  • 结果配送费、顶部购物车数字、整车状态全部失真。

五、匿名购物车到底是什么

源码里的 _is_anonymous_cart() 很直白:

  • 如果订单 partner 还是网站 public user 对应的 partner,就视为匿名 cart。

这说明匿名购物车的本质不是“没 session”,而是:

  • 订单还没真正绑定到具体客户身份上。

这点特别重要,因为很多人以为匿名与登录只是前台状态。

但从订单模型视角看,真正关键的是:

  • partner 是否还是公共用户;
  • 地址是否已经补齐;
  • 是否已经进入可恢复、可追踪、可继续营销的客户链路。

也正因为如此,弃单邮件会非常关心:

  • 有没有真实 email;
  • 后续有没有成功下过单;
  • 支付过程是否出现 error;
  • 购物车里是不是全免费商品。

六、为什么 abandoned cart 不是“超时就发邮件”这么简单

_filter_can_send_abandoned_cart_mail() 的条件比很多人想得严。

系统会过滤掉这些情况:

  • 客户没有邮箱;
  • 支付交易已经报错;
  • 购物车全是零价行;
  • 这个客户在弃单之后已经完成了新的 sale order。

_cart_recovery_email_send() 还会先确保 portal token 存在,再把恢复链接直接指向:

  • /shop/cart?id=<order_id>&access_token=<token>

这说明 Odoo 对恢复邮件的理解不是“营销自动化发一封提醒”,而是:

给客户一个能安全、直接、低摩擦回到原购物车的入口。

所以弃单恢复链路其实同时包含:

  • 订单有效性;
  • 访问安全性;
  • 客户状态判断;
  • 营销时机判断。

七、最容易误解的 5 件事

1. 以为购物车只是前台 session 数据

不对。核心状态落在 sale.order

2. 以为加入购物车一定新建一行

不对。系统会先判断是否应该合并到现有行。

3. 以为数量问题只在支付页才校验

不对。加购和改数量时就会先验证。

4. 以为匿名购物车只是“未登录”

不对。关键是订单是否仍绑定 public partner。

5. 以为弃单邮件只看“多久没付款”

不对。源码还会校验邮箱、后续成交、支付错误、零价订单等条件。


八、实战定制最该注意什么

1. 不要绕开 _cart_add() / _cart_update_line_quantity() 直接乱写订单行

你会丢掉合并逻辑、数量校验和整车整理。

2. 改购物车数量规则时,先想 combo、optional products、多标签页并发

电商不是单一 SKU 世界。

3. 做弃单恢复时,不要只盯邮件模板

真正决定“该不该发、能不能恢复”的核心都在模型层。


一句话记忆

Odoo 电商购物车不是“报价单之前的临时容器”,而是“从第一次加购起就已经以 sale.order 运行的前置销售订单生命周期”。

DISCUSSION

评论区

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