先说结论
很多人把网站支付页理解成这样:
- 地址填完;
- 到支付页;
- 选支付方式;
- 下单。
但 Odoo 的 website_sale 并不相信“前一步通过了,后一步就一定没问题”。
它在支付页前后会反复检查几件事:
- 购物车本身是否仍有效;
- 地址是不是完整;
- 当前地址下是否还有可用配送方式;
- 购物车改动后,优惠、运费、总价是否需要重算;
- 如果应付金额已经是 0,是否可以跳过真实支付直接确认订单;
- 用户从支付渠道跳回时,系统还能不能正确找回这张单。
所以 Odoo 的支付页,本质上不是“展示 payment provider 列表”,而是:
付款前最后一道商业一致性闸门。
一、为什么进入 /shop/payment 前还要再跑一次地址校验
shop_payment() 里第一件事不是取支付方式,而是:
_check_cart_and_addresses(order_sudo)
这很有代表性。
因为在真实网站环境里,地址虽然看似已经填完,但并不代表此刻仍然有效。比如:
- 用户在别的标签页改过购物车;
- 配送商品发生变化;
- 地址其实只填了一半;
- 某个地址刚被切换成了另一个子联系人;
- 之前可配送,现在这个国家或州下已经无合适配送商。
Odoo 的态度是:
- 只要还没真正付款,订单就仍然可能变化。
所以支付页不是“承接前一步结果”,而是再做一次确认。
二、为什么支付页打开时还会 _recompute_cart()
通过地址检查后,shop_payment() 还会执行:
order_sudo._recompute_cart()
这个动作特别关键。
因为网站购物车在用户看来像静态页面,但对 Odoo 来说,它仍是一张会受多种规则影响的动态销售单:
- 优惠活动可能刚失效;
- 自动奖励可能刚满足或不再满足;
- 运费可能因地址或金额变化而变化;
- 税额可能因配送地址更新而变化;
- 某些 reward line 可能需要重新整理。
也就是说,支付页真正要面对的金额,不该依赖“上一步显示过什么”,而该依赖:
- 此刻重算后的订单事实。
这也是为什么做支付前定制时,不能只改模板,不看 _recompute_cart()。
三、为什么“没有可用配送方式”会直接阻断支付
_get_shop_payment_errors() 里有个非常实际的检查:
- 如果订单有可配送商品,但当前地址下
_get_delivery_methods()为空,就直接报错,禁止进入正常支付流程。
这说明在 Odoo 看来,支付资格不是只看 payment provider 是否可用,还要看订单是否具备可履约性。
这是非常合理的。
因为如果一个地址根本没有配送方式,你让用户先付款,后面再人工解释“其实寄不到”,体验和风险都很差。
所以 Odoo 选择把这个问题前置到支付页。
这也是电商定制里一个很容易忽略的边界:
- 支付不只是金融动作,还是履约承诺动作。
四、为什么零金额订单不一定要走支付渠道
在 /shop/payment/validate 里,Odoo 对两种场景分开处理:
场景一:有应付金额
- 应该存在最后一笔 portal transaction;
- 否则就回到商店路径,避免出现“没支付却当成成功”。
场景二:应付金额为 0
- 如果没有 transaction,且订单尚未 sale;
- 系统会先
_check_cart_is_ready_to_be_paid(); - 然后直接
_validate_order()。
这点很值得注意。
它说明 Odoo 并不把“支付页”机械等同于“必须发起支付交易”。
如果订单经过优惠、礼品卡、余额或其他机制后总额为零,真正需要的是:
- 确认订单具备成立条件;
- 然后直接确认。
这是一种很符合商业语义的设计。
五、为什么 sale_last_order_id 这么关键
源码在 checkout 和 express checkout 流程里都会把:
request.session['sale_last_order_id']
记下来。
这看似普通,实际上是网站支付回跳流程的关键保险丝。
因为真实世界里很容易出现:
- 用户跳到第三方支付页;
- 支付完又没按标准回跳;
- 或浏览器会话里的当前 cart 标识已经被清掉;
- 用户直接刷新确认页。
如果系统没有一个“最近正在处理的订单”指针,就很难把支付结果和正确订单重新对上。
所以 sale_last_order_id 的职责,本质上是:
- 给支付后的页面恢复和确认流程兜底。
六、为什么支付页故意把 submit button 放到支付表单外面
_get_shop_payment_values() 里有个细节:
display_submit_button被设成False;- 然后在网站模板外部重新放一个提交按钮。
这反映出 Odoo 网站支付页并不想完全照搬 portal 支付组件,而是保留 checkout 页面自己的布局和步骤感。
本质上这是两层体系的结合:
- 底层复用 payment portal 的交易能力;
- 上层保留 website_sale 自己的 checkout UI 节奏。
这对定制很重要。
因为你修改支付页时,往往不是只改 payment provider 列表,而是在改“电商结账页中的支付阶段”。
七、支付成功后为什么还要 sale_reset()
在 shop_payment_validate() 里,订单确认或校验通过后,会执行:
request.website.sale_reset()
它的意义很直接:
- 这张购物车不再应被当前网站会话当成“仍可继续编辑的 cart”。
否则会出现很多经典问题:
- 用户支付后刷新页面还在同一 cart 里;
- 又继续改数量;
- 或造成确认页和当前会话订单状态不一致。
所以支付后的 session 清理,不是小细节,而是 checkout 完整性的最后一步。
八、做网站支付定制时最容易踩的坑
1)只关心支付渠道,不关心履约条件
最后就会出现“能付钱但不能发货”的订单。
2)在支付页前跳过重算
优惠、运费、税、总价都有可能过期或变动,跳过重算等于接受脏数据下单。
3)忽略零金额订单
很多项目只测正常付款,却没测折扣后 0 元订单,最终会卡在“没有 transaction 也不确认”。
4)破坏 sale_last_order_id 或支付后 reset 流程
支付回跳、确认页和购物车清理很容易因此全部乱套。
最后一句
Odoo 电商支付页真正负责的,不只是展示支付方式,而是把:
地址完整性、配送可行性、购物车重算、交易存在性、零金额确认与会话清理
串成一次付款前后的最终一致性检查。
把这一层看懂之后,你才会明白:Odoo 的网站支付页其实更像一扇闸门,而不是一个简单的支付方式选择器。
DISCUSSION
评论区