Kentico 授权支付时如何保持 OrderIsPaid 为 false
结论
在 Kentico 的标准结账流程中,PaymentResult.PaymentIsCompleted 通常用于判断支付流程是否成功完成。如果它为 false,系统会按支付失败处理,并将订单切换到付款方式中配置的失败状态。
因此,标准流程无法直接表示以下中间状态:
- 信用卡授权成功;
- 尚未实际扣款;
OrderIsPaid == false;- 订单状态为“Payment authorized”。
可以采用以下处理方式:
- 授权阶段将
PaymentIsCompleted保持为false,避免OrderIsPaid被标记为已支付。 - 在支付结果中保存“授权成功”状态和交易编号。
- 允许 Kentico 暂时将订单设为失败状态。
- 监听订单更新事件。确认授权成功后,将订单状态改为“Payment authorized”。
- 使用
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,最好将它们分开保存,不要把所有支付阶段压缩到一个状态字段中。
备注:内容仅供参考。
