如何通过 Hybris Payment API 记录外部支付并处理订单?
结论
Hybris(现 SAP Commerce)可以接收外部支付网关返回的支付结果,并让订单继续进入履约流程。外部支付成功后,通常不需要由 Hybris 再次发起扣款,而应完成以下操作:
- 校验支付网关通知;
- 为订单创建或更新
PaymentTransactionModel; - 创建对应的
PaymentTransactionEntryModel,记录授权或扣款结果; - 按项目的订单流程更新
paymentStatus; - 触发或继续订单业务流程。
只执行下面这行代码通常不够:
order.setPaymentStatus(PaymentStatus.PAID);
标准或定制的订单流程可能还会检查支付交易、交易条目、金额、状态和交易类型。仅修改订单字段,可能出现订单显示“已付款”,实际却仍停留在支付校验节点的情况。
推荐的处理方式
实际收款由外部支付网关负责,SAP Commerce 负责保存支付事实和外部交易标识。常见流程如下:
外部支付网关完成付款
↓
支付网关回调 SAP Commerce
↓
验证签名、金额、币种和订单号
↓
创建或更新 PaymentTransactionModel
↓
写入 AUTHORIZATION 或 CAPTURE 交易条目
↓
更新订单支付状态
↓
启动或唤醒订单流程
↓
库存、分仓、发货
具体记录哪种交易类型,取决于外部网关已经完成的操作:
- 如果只完成了资金授权,记录
PaymentTransactionType.AUTHORIZATION。 - 如果资金已经实际扣取,通常记录
PaymentTransactionType.CAPTURE。 - 如果项目流程要求先授权再扣款,可以导入两条交易记录,但两条记录都必须真实对应外部网关的操作,不能为了让订单通过流程而伪造支付阶段。
核心模型
这套处理主要涉及以下模型和枚举:
PaymentTransactionModelPaymentTransactionEntryModelPaymentTransactionType.AUTHORIZATIONPaymentTransactionType.CAPTURETransactionStatus.ACCEPTEDPaymentStatus.PAID
PaymentTransactionModel 表示订单的一笔支付交易,PaymentTransactionEntryModel 则表示这笔交易中的某次具体操作,例如授权、扣款、退款或取消。
支付记录中建议保存:
- SAP Commerce 订单号;
- 外部支付订单号;
- 外部授权号或扣款流水号;
- 金额和币种;
- 支付提供商;
- 交易类型;
- 交易状态;
- 外部支付完成时间;
- 原始回调的摘要或可审计引用。
银行卡号、CVV 以及未经脱敏的敏感支付数据,不应保存到普通模型字段或日志中。
示例实现
下面的代码用于说明建模方式。不同 SAP Commerce 版本和项目扩展的字段、包名或必填属性可能不同,应以当前版本的 generated model、Javadoc 和 items.xml 为准。
import de.hybris.platform.core.enums.PaymentStatus;
import de.hybris.platform.core.model.c2l.CurrencyModel;
import de.hybris.platform.core.model.order.OrderModel;
import de.hybris.platform.payment.dto.TransactionStatus;
import de.hybris.platform.payment.enums.PaymentTransactionType;
import de.hybris.platform.payment.model.PaymentTransactionEntryModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.tx.Transaction;
import de.hybris.platform.servicelayer.tx.TransactionBody;
import java.math.BigDecimal;
import java.util.Date;
public class ExternalPaymentRecorder
{
private ModelService modelService;
public void recordCapturedPayment(
final OrderModel order,
final String provider,
final String externalTransactionId,
final BigDecimal amount,
final CurrencyModel currency)
{
Transaction.current().execute(new TransactionBody()
{
@Override
public Object execute()
{
PaymentTransactionModel transaction =
modelService.create(PaymentTransactionModel.class);
transaction.setCode(externalTransactionId);
transaction.setOrder(order);
transaction.setPaymentProvider(provider);
transaction.setPlannedAmount(amount);
transaction.setCurrency(currency);
modelService.save(transaction);
PaymentTransactionEntryModel entry =
modelService.create(PaymentTransactionEntryModel.class);
entry.setCode(externalTransactionId + "-CAPTURE");
entry.setPaymentTransaction(transaction);
entry.setType(PaymentTransactionType.CAPTURE);
entry.setTransactionStatus(TransactionStatus.ACCEPTED.name());
entry.setTransactionStatusDetails("External gateway capture");
entry.setAmount(amount);
entry.setCurrency(currency);
entry.setTime(new Date());
entry.setRequestId(externalTransactionId);
modelService.save(entry);
order.setPaymentStatus(PaymentStatus.PAID);
modelService.save(order);
return null;
}
});
}
public void setModelService(final ModelService modelService)
{
this.modelService = modelService;
}
}
这只是一个最小化示例。生产实现还需要处理幂等控制、订单锁定、金额校验和异常情况。
幂等处理
支付网关通常会重复发送回调。如果每次收到回调都新建一个 CAPTURE 条目,系统可能产生重复支付记录,甚至重复触发发货。
可以把外部交易号作为幂等键,并在写入前查询现有记录:
PaymentTransactionEntryModel existing =
findByProviderAndExternalTransactionId(
provider,
externalTransactionId);
if (existing != null)
{
if (TransactionStatus.ACCEPTED.name()
.equals(existing.getTransactionStatus()))
{
return;
}
// 根据业务规则处理状态变化,不能无条件再创建一条 CAPTURE。
}
不过,仅靠“先查询、后创建”仍可能发生并发竞争。更稳妥的处理包括:
- 为外部交易号设置唯一性约束,或建立专用映射模型;
- 在事务中锁定订单或支付映射记录;
- 收到重复通知时返回成功响应,但不重复写入交易;
- 禁止同一个外部交易号关联多个订单。
金额与币种校验
写入支付记录前,至少要完成以下校验:
if (amount.compareTo(order.getTotalPrice()) != 0)
{
throw new IllegalStateException("External payment amount does not match order total");
}
if (!currency.equals(order.getCurrency()))
{
throw new IllegalStateException("External payment currency does not match order currency");
}
实际项目还需考虑:
- 运费、税费和折扣;
- 舍入规则;
- 部分支付;
- 多次扣款;
- 礼品卡与外部支付组合;
- 订单修改后的补款或退款。
如果系统允许部分支付,就不能在第一笔款项到账后直接设置 PaymentStatus.PAID。应累计所有成功的 CAPTURE 条目,等已收金额达到订单应付金额后再推进流程。
与订单流程衔接
订单能否进入配送流程,取决于项目使用的 Business Process 定义和 Action 实现。需要检查订单流程中与支付有关的节点,包括:
- 支付授权 Action;
- 授权结果检查 Action;
- 扣款 Action;
- 欺诈检查;
- 等待支付通知的 Wait 节点;
- 库存分配和发货前置条件。
一般有两种实现方式。
方案一:让现有流程识别外部支付记录
导入订单和支付交易后,启动标准或现有订单流程。支付检查 Action 发现已有状态为 ACCEPTED 的 AUTHORIZATION 或 CAPTURE 条目,就可以直接进入下一个节点。
这种方式改动较少,但需要先确认现有 Action 的具体检查逻辑。有些项目只检查授权,有些检查扣款,还有一些会调用指定的 PaymentService 或支付适配器。
方案二:为外部支付订单增加专用流程分支
可以在订单上增加支付来源或支付模式标识,例如:
paymentMode = EXTERNAL_GATEWAY
订单流程再根据该标识选择分支:
外部支付订单 → 校验已导入的 CAPTURE → 履约
内部支付订单 → 调用支付服务授权/扣款 → 履约
这样可以清楚地区分外部支付和内部支付,也能避免标准支付 Action 意外向外部网关再次发起请求。
支付节点不应全部删除。流程中至少要保留一个校验节点,用于确认交易成功、金额一致,并且没有发生撤销。
是否需要调用 Payment API
PaymentService 和支付适配器主要用于让 SAP Commerce 发起授权、扣款、取消或退款。如果支付已经在外部完成,通常不必再次调用 authorize() 或 capture()。
可以实现一个职责明确的“外部支付导入服务”,负责:
- 验证可信的网关通知;
- 保存支付交易和交易条目;
- 更新订单支付状态;
- 触发业务流程事件;
- 记录审计信息。
不要直接向外部系统开放通用模型保存接口,也不能允许调用方随意提交订单号和 PAID 状态。
导入订单时的顺序
如果订单来自 ERP、OMS 或其他平台,建议按以下顺序处理:
- 创建并保存订单;
- 校验外部支付数据;
- 创建支付交易和交易条目;
- 更新订单支付状态;
- 提交订单或启动订单流程。
如果订单流程已经启动,并停在等待支付节点,应在支付交易保存成功后发送该流程正在等待的事件。具体事件名称由项目的流程 XML 决定,不能假定所有 SAP Commerce 项目使用相同的事件名。
退款和撤销
如果外部支付由外部网关管理,退款也应遵循相同的责任边界:
- 外部系统执行退款;
- SAP Commerce 接收退款结果;
- 写入对应的
REFUND_FOLLOW_ON、REFUND_STANDALONE、CANCEL或项目实际使用的交易类型; - 更新订单、退货或售后流程。
当前版本和项目扩展决定了可以使用哪些 PaymentTransactionType,选择前应查看本地枚举定义。退款结果不能覆盖原有的 CAPTURE 条目,应新增交易条目,保留完整的支付流水。
文档查找方向
可以在对应 SAP Commerce 版本的 SAP Help Portal、Commerce JavaDoc 和本地扩展源码中搜索:
PaymentService
PaymentTransactionModel
PaymentTransactionEntryModel
PaymentTransactionType
TransactionStatus
PaymentStatus
OrderProcess
AuthorizeOrderPayment
CapturePayment
项目中还要检查:
*-process.xml
items.xml
PaymentService implementation
payment provider adapter
order process actions
SAP Commerce 的支付处理通常会受 Accelerator、Spartacus 后端定制、支付扩展和项目流程定义影响,所以不存在一个适用于所有项目、只需“设置字段即可发货”的通用接口。
注意事项
- 只有通过签名验证的支付网关回调才能改变支付状态。
- 必须核对订单号、金额、币种、商户号和支付结果。
- 外部交易号必须具备幂等性。
- 支付记录和订单状态应在同一事务中更新。
- 不能仅凭浏览器跳转到“支付成功页”就确认付款。
- 不能为了让订单通过流程而伪造
AUTHORIZATION或CAPTURE。 - 启动履约前,应检查退款、撤销、风控和订单取消状态。
- 如果现有订单流程仍会调用支付适配器,必须增加外部支付分支,否则可能造成重复扣款。
SAP Commerce 可以不参与实际收款,但支付数据建模和流程校验仍然需要保留。较稳妥的做法是让 SAP Commerce 记录支付结果并协调履约,通过 PaymentTransactionModel 和 PaymentTransactionEntryModel 保存外部支付事实,再由经过调整的订单流程继续配送。
备注:内容仅供参考。