PAYATHON 2026

Kentico 授权支付时如何保持 OrderIsPaid 为 false

支付阿杰

结论

在 Kentico 的标准结账流程中,PaymentResult.PaymentIsCompleted 通常用于判断支付流程是否成功完成。如果它为 false,系统会按支付失败处理,并将订单切换到付款方式中配置的失败状态。

因此,标准流程无法直接表示以下中间状态:

  • 信用卡授权成功;
  • 尚未实际扣款;
  • OrderIsPaid == false;
  • 订单状态为“Payment authorized”。

可以采用以下处理方式:

  1. 授权阶段将 PaymentIsCompleted 保持为 false,避免 OrderIsPaid 被标记为已支付。
  2. 在支付结果中保存“授权成功”状态和交易编号。
  3. 允许 Kentico 暂时将订单设为失败状态。
  4. 监听订单更新事件。确认授权成功后,将订单状态改为“Payment authorized”。
  5. 使用 PaymentTransactionID 实现幂等处理,避免重复处理同一次授权。

为什么会出现付款失败状态

PaymentIsCompleted 和 OrderInfo.OrderIsPaid 是两个不同的属性,但在 Kentico 的标准支付流程中关系密切。

PaymentIsCompleted 表示支付网关流程是否已经完成。标准流程通常按以下逻辑处理:

  • PaymentIsCompleted == true:支付成功,进入付款成功状态,并可能将订单标记为已支付。
  • PaymentIsCompleted == false:支付未完成,进入付款失败状态。

信用卡“授权成功但尚未捕获资金”属于单独的业务状态,而标准流程主要区分“支付完成”和“支付失败”。如果把 PaymentIsCompleted 设为 true,虽然不会进入失败状态,却可能误将订单视为已经支付。如果保持为 false,系统又会触发失败状态。

解决办法是在订单保存后,根据网关返回的具体结果修正订单状态。

配置订单状态

建议至少准备以下两个状态:

  • PaymentFailed:授权失败。
  • PaymentAuthorized:授权成功,等待后续扣款。

付款方式中的“付款失败时的订单状态”应设置为 PaymentFailed。

是否将 PaymentOptionSucceededOrderStatusID 直接配置为 PaymentAuthorized,取决于该付款方式是否只用于预授权。如果同一种付款方式还会处理直接扣款,最好单独查找“Payment authorized”状态,不要复用通用的付款成功状态。

在支付处理中记录授权结果

调用支付网关完成授权后,需要保存网关返回的交易编号和授权结果,但不能因为授权成功就将 PaymentIsCompleted 设为 true。

示意代码如下:

protected override void ProcessPayment()
{
    AuthorizationResult authorization = AuthorizePayment();

    base.PaymentResult.PaymentTransactionID = authorization.TransactionID;
    base.PaymentResult.PaymentStatusValue =
        authorization.Succeeded ? "completed" : "failed";

    // 授权不等于扣款,因此保持 false。
    base.PaymentResult.PaymentIsCompleted = false;
}

这里的 AuthorizationResult 和 AuthorizePayment() 仅用于示意,实际类型和调用方式由所使用的支付网关决定。

PaymentStatusValue 也必须以当前支付集成实际写入的值为准。下面的事件处理代码使用 "completed",前提是支付模块确实会写入这个值。未核实网关返回值之前,不要直接套用。

在订单更新后修正状态

可以订阅订单对象的更新后事件。当 Kentico 因 PaymentIsCompleted == false 将订单切换到失败状态后,检查实际的授权结果。如果确认授权成功,就将订单改为授权状态,同时保持 OrderIsPaid == false。

下面的代码使用付款方式的成功状态作为授权状态:

void UpdateAfter(object sender, ObjectEventArgs e)
{
    try
    {
        if (!e.Object.TypeInfo.ObjectClassName.Equals(
                "ecommerce.order",
                StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        OrderInfo order = e.Object as OrderInfo;

        if (order == null)
        {
            throw new ArgumentNullException(
                nameof(order),
                "Order object of ecommerce.order event is null.");
        }

        if (order.OrderPaymentResult == null)
        {
            return;
        }

        string transactionId =
            order.OrderPaymentResult.PaymentTransactionID;

        if (String.IsNullOrWhiteSpace(transactionId))
        {
            return;
        }

        string processedTransactionId = null;

        try
        {
            processedTransactionId =
                order.OrderCustomData.GetValue(
                    "SuccessfulPaymentTransactionID"
                ) as string;
        }
        catch
        {
            // 旧订单可能不存在该自定义数据项。
        }

        // 同一笔交易已经修正过,不再重复更新。
        if (String.Equals(
                processedTransactionId,
                transactionId,
                StringComparison.Ordinal))
        {
            return;
        }

        PaymentOptionInfo paymentOption =
            PaymentOptionInfoProvider.GetPaymentOptionInfo(
                order.OrderPaymentOptionID);

        if (paymentOption == null)
        {
            throw new InvalidOperationException(
                String.Format(
                    "Payment option for OrderPaymentOptionID '{0}' is null.",
                    order.OrderPaymentOptionID));
        }

        bool isTemporaryFailureStatus =
            order.OrderStatusID ==
            paymentOption.PaymentOptionFailedOrderStatusID;

        bool authorizationSucceeded =
            String.Equals(
                order.OrderPaymentResult.PaymentStatusValue,
                "completed",
                StringComparison.OrdinalIgnoreCase);

        if (!isTemporaryFailureStatus || !authorizationSucceeded)
        {
            return;
        }

        order.OrderStatusID =
            paymentOption.PaymentOptionSucceededOrderStatusID;

        order.OrderIsPaid = false;

        order.OrderCustomData["SuccessfulPaymentTransactionID"] =
            transactionId;

        order.Update();
    }
    catch (Exception ex)
    {
        EventLogProvider.LogException(
            "EcommerceOrderInfoModule",
            "UpdateAfter",
            ex,
            0,
            null,
            null);
    }
}

order.Update() 会再次触发订单更新事件,所以交易编号检查不可省略。事件处理器第二次运行时,SuccessfulPaymentTransactionID 已经与当前的 PaymentTransactionID 相同,此时会立即返回,不再递归更新。

如果授权状态不是付款成功状态

如果 PaymentOptionSucceededOrderStatusID 对应“Paid”或“Payment completed”,就不能将它用于预授权。这时应查询专用的 PaymentAuthorized 状态,再赋值给 OrderStatusID:

OrderStatusInfo authorizedStatus =
    OrderStatusInfoProvider.GetOrderStatusInfo(
        "PaymentAuthorized",
        order.OrderSiteID);

if (authorizedStatus == null)
{
    throw new InvalidOperationException(
        "Order status 'PaymentAuthorized' was not found.");
}

order.OrderStatusID = authorizedStatus.StatusID;
order.OrderIsPaid = false;

不同 Kentico 版本提供的订单状态查询 API 重载可能不同,需要按照项目实际引用的版本调整查询方式。

注意事项

  • “授权成功”不等于“付款完成”。只有成功捕获资金后,才应设置 OrderIsPaid = true。
  • 不能仅凭订单当前处于失败状态,就将其改为授权状态。还必须验证支付状态、交易编号或网关返回的授权标志。
  • PaymentStatusValue 由支付集成写入,需要确认实际值是 "completed"、"complete"、"authorized",还是网关定义的其他值。
  • 应保存 PaymentTransactionID,否则订单后续的普通更新可能反复覆盖状态。
  • 如果捕获资金失败,不应继续保留“Payment authorized”状态,而应按照业务规则切换到捕获失败或需要人工处理的状态。
  • 如果系统存在异步回调,还需要处理重复回调、回调顺序变化和同一订单重新授权等情况。每次新的授权都应使用新的 PaymentTransactionID。
  • 如果网关分别提供 authorization ID、capture ID 和退款交易 ID,最好将它们分开保存,不要把所有支付阶段压缩到一个状态字段中。

备注:内容仅供参考。