如何在 eCommerce 结账流程中显示具体错误?
明确结论
可以显示比「无法处理订单」更具体的提示,前提是支付网关或收单机构返回了可用的错误码。
网站通常无法自行判断姓名是否与信用卡信息一致,也不能通过数据库确认 CVV 是否正确。这些信息需要由支付服务商验证,并以结构化响应返回,例如:
- CVV 校验失败
- 账单地址校验失败
- 卡号格式无效
- 卡片已过期
- 余额不足
- 发卡行拒绝交易
- 支付服务暂时不可用
开发时不要把支付网关的原始错误信息直接展示给用户。后端应根据错误码,将其转换成安全、易懂且便于用户采取行动的提示,并把完整响应记录在受保护的日志中。
为什么目前只能显示通用错误
现有结账逻辑可能只判断支付是否成功:
if (!paymentResult.Success)
{
ModelState.AddModelError(
string.Empty,
"无法处理订单。请检查账单和信用卡信息,然后重试。"
);
}
这段代码没有区分具体的失败类型。要显示更明确的原因,需要继续读取支付接口返回的字段,例如:
Status
ErrorCode
DeclineCode
CvvResult
AvsResult
TransactionId
字段名称由支付网关及其 SDK 决定,不能假设所有服务商都使用同一种格式。
发卡行有时只会返回含义模糊的 declined 或 do_not_honor。遇到这类响应时,商户无法得知具体原因,也不应自行推测。
推荐的实现方式
1. 确认支付服务商返回的信息
先检查当前支付接口的响应对象和官方文档,确认以下内容:
- 是否提供结构化错误码或拒绝码
- 是否提供 CVV 校验结果
- 是否提供 AVS(Address Verification Service)结果
- 哪些错误可以安全地展示给持卡人
- 如何在测试环境中模拟不同的失败场景
如果当前集成只能返回成功或失败,可能需要升级 SDK、调整 API 请求,或修改支付服务的封装层,以保留完整的结构化响应。
2. 在支付层定义统一结果
MVC Controller 不应直接依赖某个支付网关返回的错误文本。可以先定义一套网站内部统一使用的结果类型:
public enum PaymentFailureReason
{
None,
InvalidCardNumber,
ExpiredCard,
InvalidCvv,
AddressMismatch,
InsufficientFunds,
IssuerDeclined,
GatewayUnavailable,
Unknown
}
public sealed class PaymentResult
{
public bool Success { get; init; }
public PaymentFailureReason FailureReason { get; init; }
public string? ProviderCode { get; init; }
public string? TransactionId { get; init; }
}
再由支付服务把网关响应转换成内部类型:
public PaymentResult MapProviderResponse(ProviderResponse response)
{
if (response.IsSuccessful)
{
return new PaymentResult
{
Success = true,
FailureReason = PaymentFailureReason.None,
TransactionId = response.TransactionId
};
}
var reason = response.DeclineCode switch
{
"invalid_number" => PaymentFailureReason.InvalidCardNumber,
"expired_card" => PaymentFailureReason.ExpiredCard,
"incorrect_cvc" => PaymentFailureReason.InvalidCvv,
"incorrect_address" => PaymentFailureReason.AddressMismatch,
"insufficient_funds" => PaymentFailureReason.InsufficientFunds,
"issuer_declined" => PaymentFailureReason.IssuerDeclined,
"processing_error" => PaymentFailureReason.GatewayUnavailable,
_ => PaymentFailureReason.Unknown
};
return new PaymentResult
{
Success = false,
FailureReason = reason,
ProviderCode = response.DeclineCode,
TransactionId = response.TransactionId
};
}
上面的错误码仅用于演示。实际实施时,必须替换成当前支付服务商定义的代码。
3. 将内部原因转换成用户提示
用户提示应简洁,并说明接下来可以怎么做:
private static string GetCustomerMessage(PaymentFailureReason reason)
{
return reason switch
{
PaymentFailureReason.InvalidCardNumber =>
"卡号似乎不正确,请检查后重新输入。",
PaymentFailureReason.ExpiredCard =>
"这张卡已过期,请使用其他银行卡。",
PaymentFailureReason.InvalidCvv =>
"安全码(CVV)不正确,请检查卡片背面或正面的安全码。",
PaymentFailureReason.AddressMismatch =>
"账单地址未通过验证,请确认其与发卡行登记的信息一致。",
PaymentFailureReason.InsufficientFunds =>
"交易未获批准,请更换付款方式或联系发卡行。",
PaymentFailureReason.IssuerDeclined =>
"发卡行未批准本次交易,请联系发卡行或使用其他付款方式。",
PaymentFailureReason.GatewayUnavailable =>
"支付服务暂时不可用,请稍后重试。",
_ =>
"暂时无法处理订单,请检查付款信息或使用其他付款方式。"
};
}
部分支付服务商可能不允许商户直接显示「余额不足」,或者返回的信息不足以支持这一判断。这时应改用「交易未获批准」等中性表述。
4. 在 MVC Controller 中显示提示
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Checkout(CheckoutViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
PaymentResult result = await _paymentService.ChargeAsync(model);
if (!result.Success)
{
string message = GetCustomerMessage(result.FailureReason);
ModelState.AddModelError(nameof(model.Payment), message);
_logger.LogWarning(
"Payment failed. Reason={Reason}, ProviderCode={ProviderCode}, TransactionId={TransactionId}",
result.FailureReason,
result.ProviderCode,
result.TransactionId
);
ClearSensitivePaymentFields(model);
return View(model);
}
return RedirectToAction("Confirmation", new { id = result.TransactionId });
}
视图可以通过验证组件显示错误:
@Html.ValidationMessageFor(
model => model.Payment,
null,
new { @class = "validation-error" }
)
如果项目使用 ASP.NET Core MVC,通常可以写成:
<span asp-validation-for="Payment" class="text-danger"></span>
前端校验与支付校验要分开
部分错误可以在提交前发现,例如:
- 卡号长度不正确或未通过 Luhn 校验
- 有效期格式错误
- 必填字段为空
- 邮政编码格式错误
前端和服务器端模型验证可以立即提示这些问题:
public sealed class CheckoutViewModel
{
[Required]
public string BillingName { get; set; } = string.Empty;
[Required]
public string BillingAddress { get; set; } = string.Empty;
[Required]
[RegularExpression(@"^\d{3,4}$", ErrorMessage = "请输入 3 位或 4 位安全码。")]
public string Cvv { get; set; } = string.Empty;
}
格式正确并不代表信息真实有效。CVV、地址和交易授权的最终结果,仍要以支付网关或发卡行的响应为准。
「姓名不匹配」通常无法准确提示
「输入的姓名与信用卡信息不匹配」并不一定是能够实现的提示。
大多数银行卡授权流程不会向商户提供可靠的持卡人姓名匹配结果。支付网关更常返回以下信息:
- CVV/CVC 校验结果
- 数字街道地址匹配结果
- 邮政编码匹配结果
- 发卡行授权结果
因此,只有支付服务商明确返回姓名校验结果时,网站才能显示相关提示。不能根据一次普通的拒付响应,自行将原因判断为「姓名不匹配」。
安全与合规注意事项
不要直接显示原始网关消息
原始响应可能包含内部代码、风控规则或其他不适合公开的信息。应通过错误码白名单进行映射。无法识别的错误统一显示安全的通用提示。
不要记录敏感卡片数据
日志、SQL 数据库和异常信息中不应保存:
- CVV/CVC
- 完整卡号
- 磁道数据
- PIN
- 未脱敏的支付请求或响应
CVV 尤其不能存储。如果日志需要识别银行卡,应使用支付服务商提供的 payment method ID、token,或经过合规处理的卡号后四位。
优先使用支付服务商的托管控件或令牌化方案
理想情况下,卡号和 CVV 不应直接经过网站服务器,而应由支付服务商提供的托管字段或前端 SDK 收集,并转换成 token。这样可以降低敏感数据泄露的风险,但实际 PCI DSS 范围仍需根据系统架构和支付服务商的要求确定。
避免错误提示泄露过多信息
过于具体的错误提示可能被用于批量测试盗取的卡片信息。应同时采取以下措施:
- 限制结账和支付尝试的频率
- 对连续失败进行风控拦截
- 使用 CAPTCHA 或设备风险识别
- 监控同一账户、IP、设备或银行卡的异常尝试
- 不显示「卡号存在,但只有 CVV 错误」等可能帮助攻击者逐项验证的信息
实施工作量取决于现有支付集成
如果当前支付 SDK 已经返回结构化错误码,改造通常涉及三处:
- 在支付服务层解析错误码。
- 建立内部失败原因与用户提示之间的映射。
- 在结账页面显示字段级或页面级错误,并补充安全日志和测试。
如果支付接口只返回布尔值,需要先修改接口封装,保留支付服务商返回的拒绝码、CVV/AVS 结果和交易标识。如果服务商本身不提供更详细的结果,网站就无法自行判断具体错误,只能继续使用通用提示,或考虑更换支持结构化失败响应的支付接口。
备注:内容仅供参考。
