先说结论
Odoo 的 Payment 模块并不是“支付网关回个 success,订单就结束了”。
从 /home/ubuntu/odoo-temp/addons/payment/models/payment_transaction.py、payment_provider.py 和 controllers/post_processing.py 可以看出,官方真正搭的是一套平台状态机:
- 前面由 provider 决定可用能力
- 中间用
payment.transaction保存追踪与状态 - 后面通过
/payment/status和轮询后处理完成业务落地
所以最短结论是:
在 Odoo 里,支付结果不是一个瞬时布尔值,而是一条会继续演化的事务状态链。
为什么 payment.transaction 有这么多状态
源码里的状态至少包括:
draftpendingauthorizeddonecancelerror
很多团队会本能地觉得:
- 成功 / 失败不就够了?
但支付世界不是这么工作的。
authorized 为什么单独存在
因为有些支付提供商支持先授权、后捕获。
也就是说:
- 钱还没最终入账
- 但持卡人的可支付能力已被冻结或确认
如果系统把它粗暴当成“done”,后面就会在发货、撤销、部分捕获这些环节把业务语义搞乱。
pending 为什么也不能省
因为真实支付链路经常有这些情况:
- 用户跳出到第三方支付页
- 银行 / 风控还在处理中
- 网关先给了“受理中”
- 后台 webhook 和前台回跳并不同步
这时候系统必须承认:
- 交易存在
- 但结论还没最终确定
所以 pending 不是拖延,它是现实。
为什么 transaction 要复制 partner 信息
create() 里会把 partner 的:
- 名称
- 语言
- 邮箱
- 地址
- 国家 / 省州
- 电话
复制到 transaction 上。
这不是重复存储的坏味道,而是支付追踪的典型做法。
因为支付不是单纯的当前实时对象关系,它还承担审计责任。
如果以后 partner 主数据被改了,平台仍然需要知道:
- 这笔交易当时是以什么客户信息送去支付提供商的
换句话说,transaction 记录的是:
发起支付那一刻的业务快照,而不只是今天还能从 partner 上读到什么。
provider 和 transaction 的职责为什么要拆开
payment.provider 负责的是能力与可用性边界,例如:
- 是否处于
disabled / test / enabled - 是否允许 tokenization
- 是否支持 manual capture
- 是否支持 refund
- 是否支持 express checkout
- 对国家、币种、金额有没有限制
而 payment.transaction 负责的是单笔交易追踪,例如:
- 本次金额与币种
- 当前状态
- 操作类型
operation - 提供商参考号
- 是否已 post-process
- 是否要 token 化
- 最终落地页面
landing_route
这个拆分很重要。
因为 provider 是“平台能力配置”,transaction 是“单笔业务事实”。
如果把两者混在一起,最后会出现两种灾难:
- 平台配置变动污染历史交易理解
- 历史交易状态又反过来影响平台能力判断
operation 为什么比很多人想得更关键
源码里的 operation 包括:
online_redirectonline_directonline_tokenvalidationofflinerefund
这说明 Odoo 不把所有支付都视为同一种动作。
例如:
- “在线跳转支付一次”
- “用保存的 token 复用扣款”
- “只是验证支付方式,不真正扣钱”
- “这是退款,不是新收款”
这些动作如果都混成“支付”,后续的账务、风控、客服解释都会开始混乱。
所以 operation 本质上是平台对业务语义的显式建模。
/payment/status 为什么不是装饰页面,而是后处理枢纽
post_processing.py 里的注释写得很直白:
- 所有支付流都应该在某个时点经过
/payment/status - 用户在这里查看交易状态
- 同时系统会触发 post-processing
这说明支付完成不是“回调到了,就万事大吉”。
它往往还差最后一段:
- 事务是否落库
- token 是否生成
- 订单 / 单据是否跳去最终落地页
- 页面向用户展示什么状态
所以 /payment/status 更像一个“支付尾声协调器”。
它一边给用户看进度,一边替平台补完最后那段易抖动的状态收敛。
为什么要轮询 poll_status()
poll_status() 会:
- 取出当前 session 监控的 transaction
- 如果还没
is_post_processed - 就调用
_post_process() - 若数据库提交出错,回滚并让前端重试
这件事非常关键,因为现实支付链路经常会遇到:
- provider 已经说成功
- 但 Odoo 这边还没完全完成业务后处理
- 或提交过程中出现瞬时数据库问题
如果平台没有一个“可重试的后处理收敛层”,最后用户只会看到一种很糟糕的产品体验:
- 钱扣了
- 页面却像没成功
- 订单也没落下来
所以轮询不是多余,而是把不稳定世界和稳定业务状态隔开的一层缓冲。
is_post_processed 为什么这么重要
这字段看起来只是一个布尔值,但它实际回答的是:
- 这笔交易的支付结果,是否已经被 Odoo 内部业务逻辑完整承接
注意,这和“网关是不是成功”不是一回事。
真正的业务世界里,你要同时满足:
- 外部支付有结论
- 内部状态也收敛
is_post_processed 正是在描述第二件事。
所以一笔交易即便 provider 已经成功,也可能还没完全变成“平台层意义上的完成”。
为什么 capture_manually、refund、tokenization 都是平台能力而不是临时按钮
payment.provider 会计算:
support_manual_capturesupport_refundsupport_tokenizationsupport_express_checkout
这说明在 Odoo 眼里,这些都不是页面随便显示一下的功能点,而是:
该 provider 在平台契约层是否具备某种能力。
一旦被建模成能力,平台就可以稳定地问:
- 当前 provider 是否允许我把交易推进到
authorized - 当前 provider 是否允许我后续发起部分退款
- 当前 provider 是否允许我生成支付 token 给下次复用
这就是为什么支付平台一定要做能力建模,而不是只堆集成开关。
新手最容易误解的 5 件事
1. 误以为支付就只有成功和失败
现实链路里至少还有 pending、authorized、cancel、error,这些都是真实业务状态。
2. 误以为 provider 回调成功就等于订单落地完成
没有后处理收敛,这件事根本没结束。
3. 误以为 tokenization 是“顺手存一下卡”
它其实影响后续复用扣款、风险边界和客户体验,是平台能力之一。
4. 误以为授权和捕获是同一件事
对支持 manual capture 的 provider,这两步是不同业务承诺。
5. 误以为 /payment/status 只是一个用户提示页
它实际上承担着状态监控和后处理触发的枢纽角色。
实战排错顺序
如果你碰到“支付成功但订单没正确落地”这类问题,建议按这个顺序查:
- 先看 transaction 当前状态是
pending / authorized / done / error哪一种 - 确认 provider 是否支持当前你想要的动作,例如 manual capture 或 refund
- 检查 session 里监控的 transaction 是否正确挂到了
/payment/status - 看
is_post_processed是否一直没有完成 - 如果 post-process 出错,再看是不是数据库提交异常导致前端一直在重试
这样查,比只看“网关回调日志”有效得多。
一句话记忆
Odoo Payment 的本质不是“收款成功 / 失败”,而是用 provider 能力、transaction 状态机和可重试后处理,把外部支付世界收敛成内部可追踪的业务事实。
DISCUSSION
评论区