PAYATHON 2026

如何通过 Hybris Payment API 记录外部支付并处理订单?

支付阿杰

结论

Hybris(现 SAP Commerce)可以接收外部支付网关返回的支付结果,并让订单继续进入履约流程。外部支付成功后,通常不需要由 Hybris 再次发起扣款,而应完成以下操作:

  1. 校验支付网关通知;
  2. 为订单创建或更新 PaymentTransactionModel;
  3. 创建对应的 PaymentTransactionEntryModel,记录授权或扣款结果;
  4. 按项目的订单流程更新 paymentStatus;
  5. 触发或继续订单业务流程。

只执行下面这行代码通常不够:

order.setPaymentStatus(PaymentStatus.PAID);

标准或定制的订单流程可能还会检查支付交易、交易条目、金额、状态和交易类型。仅修改订单字段,可能出现订单显示“已付款”,实际却仍停留在支付校验节点的情况。

推荐的处理方式

实际收款由外部支付网关负责,SAP Commerce 负责保存支付事实和外部交易标识。常见流程如下:

外部支付网关完成付款
        ↓
支付网关回调 SAP Commerce
        ↓
验证签名、金额、币种和订单号
        ↓
创建或更新 PaymentTransactionModel
        ↓
写入 AUTHORIZATION 或 CAPTURE 交易条目
        ↓
更新订单支付状态
        ↓
启动或唤醒订单流程
        ↓
库存、分仓、发货

具体记录哪种交易类型,取决于外部网关已经完成的操作:

  • 如果只完成了资金授权,记录 PaymentTransactionType.AUTHORIZATION。
  • 如果资金已经实际扣取,通常记录 PaymentTransactionType.CAPTURE。
  • 如果项目流程要求先授权再扣款,可以导入两条交易记录,但两条记录都必须真实对应外部网关的操作,不能为了让订单通过流程而伪造支付阶段。

核心模型

这套处理主要涉及以下模型和枚举:

  • PaymentTransactionModel
  • PaymentTransactionEntryModel
  • PaymentTransactionType.AUTHORIZATION
  • PaymentTransactionType.CAPTURE
  • TransactionStatus.ACCEPTED
  • PaymentStatus.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 或其他平台,建议按以下顺序处理:

  1. 创建并保存订单;
  2. 校验外部支付数据;
  3. 创建支付交易和交易条目;
  4. 更新订单支付状态;
  5. 提交订单或启动订单流程。

如果订单流程已经启动,并停在等待支付节点,应在支付交易保存成功后发送该流程正在等待的事件。具体事件名称由项目的流程 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 保存外部支付事实,再由经过调整的订单流程继续配送。

备注:内容仅供参考。