PAYATHON 2026

MVC 电商网站如何接入第三方 Payment provider

支付小周

结论

可以。你需要接入支持 Hosted Checkout(托管收银台) 或 Hosted Payment Page(托管支付页) 的 Payment provider。

一般流程如下:

  1. 用户在网站创建订单,选择银行卡等付款方式。
  2. MVC 后端调用支付服务商 API,创建支付会话。
  3. 支付服务商返回安全的支付页面 URL。
  4. 后端将用户重定向到该 URL。
  5. 用户在支付服务商的页面填写银行卡等敏感信息。
  6. 支付服务商通过 Webhook 将最终支付结果通知服务器。
  7. 服务器验证通知,随后更新订单状态。

Stripe Checkout、Adyen Hosted Checkout、Braintree、Checkout.com,以及银行或本地收单机构提供的托管支付页,都能实现这套流程。具体可以使用哪些服务,取决于网站注册国家、结算币种和业务类型。

为什么应使用托管支付页

除非团队有能力承担完整的 PCI DSS 合规、安全审计和密钥管理工作,否则不要自行设计表单来接收银行卡号、CVV 或有效期。

使用托管支付页有以下好处:

  • 银行卡敏感信息直接提交给 Payment provider。
  • 你的服务器不必保存或处理完整卡号、CVV。
  • 3-D Secure、身份验证和风控流程通常由服务商处理。
  • 支付页面使用 HTTPS,并由服务商持续维护。
  • PCI DSS 合规范围通常更小。

不过,使用托管页面并不等于完全免除合规责任。上线前仍需按照服务商要求完成商户审核、网站信息配置和相应的 PCI 自评。

MVC 中的接入流程

1. 创建本地订单

用户点击付款前,先在数据库中创建订单,此时不要将订单标记为已付款:

Pending -> PaymentPending -> Paid
                          -> PaymentFailed
                          -> Cancelled

订单金额必须由服务器根据商品、优惠和运费重新计算,不能直接采用浏览器传来的金额。

2. 创建支付会话

后端向 Payment provider 提交以下信息:

  • 商户订单号
  • 金额和币种
  • 商品摘要
  • 支付成功返回地址
  • 取消返回地址
  • 用于关联订单的 metadata
  • Webhook 通知地址,具体配置方式由服务商决定

服务商通常会返回会话 ID 和支付页面 URL。

下面用 ASP.NET MVC 风格的通用接口说明代码结构。这不是任何服务商的实际 SDK,正式接入时需要替换为所选服务商的官方 SDK:

public sealed class CreateCheckoutRequest
{
    public string OrderNumber { get; init; } = string.Empty;
    public long AmountInMinorUnit { get; init; }
    public string Currency { get; init; } = string.Empty;
    public string SuccessUrl { get; init; } = string.Empty;
    public string CancelUrl { get; init; } = string.Empty;
}

public sealed class CheckoutSession
{
    public string SessionId { get; init; } = string.Empty;
    public string CheckoutUrl { get; init; } = string.Empty;
}

public interface IPaymentProvider
{
    Task<CheckoutSession> CreateCheckoutSessionAsync(
        CreateCheckoutRequest request,
        CancellationToken cancellationToken);
}

金额建议用货币最小单位的整数表示。例如,对于支持两位小数的币种,19.99 通常写成 1999。不同币种的小数位规则并不相同,必须以支付服务商的币种文档为准,不能全部统一乘以 100。

3. 重定向用户

Controller 从数据库读取订单,再使用服务器中保存的金额创建支付会话:

public sealed class PaymentController : Controller
{
    private readonly IOrderRepository _orders;
    private readonly IPaymentProvider _paymentProvider;

    public PaymentController(
        IOrderRepository orders,
        IPaymentProvider paymentProvider)
    {
        _orders = orders;
        _paymentProvider = paymentProvider;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Checkout(
        long orderId,
        CancellationToken cancellationToken)
    {
        var order = await _orders.FindAsync(orderId, cancellationToken);

        if (order == null || order.Status != OrderStatus.Pending)
        {
            return HttpNotFound();
        }

        // 金额来自服务器中的订单,而不是请求参数。
        var session = await _paymentProvider.CreateCheckoutSessionAsync(
            new CreateCheckoutRequest
            {
                OrderNumber = order.OrderNumber,
                AmountInMinorUnit = order.AmountInMinorUnit,
                Currency = order.Currency,
                SuccessUrl = Url.Action(
                    "Return",
                    "Payment",
                    new { orderId = order.Id },
                    Request.Url.Scheme),
                CancelUrl = Url.Action(
                    "Cancelled",
                    "Payment",
                    new { orderId = order.Id },
                    Request.Url.Scheme)
            },
            cancellationToken);

        order.PaymentSessionId = session.SessionId;
        order.Status = OrderStatus.PaymentPending;
        await _orders.SaveAsync(order, cancellationToken);

        return Redirect(session.CheckoutUrl);
    }
}

生产环境只能重定向到服务商返回的 HTTPS 地址,或配置中明确允许的 HTTPS 地址。不要接受客户端提交的任意跳转 URL,以免产生开放重定向漏洞。

使用 Webhook 确认付款

用户被重定向回 SuccessUrl,只能说明浏览器返回了网站,不能证明支付已经成功。用户可能伪造请求,也可能在付款成功后直接关闭页面。

订单是否已付款,应以通过签名验证的 Webhook 为准,也可以由服务器主动向服务商查询支付状态:

[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> Webhook(
    CancellationToken cancellationToken)
{
    string payload;

    using (var reader = new StreamReader(Request.InputStream))
    {
        payload = await reader.ReadToEndAsync();
    }

    var signature = Request.Headers["Payment-Signature"];

    PaymentEvent paymentEvent;

    try
    {
        paymentEvent = PaymentWebhookVerifier.Verify(
            payload,
            signature,
            WebhookSecret);
    }
    catch (PaymentSignatureException)
    {
        return new HttpStatusCodeResult(400);
    }

    if (paymentEvent.Type == "payment.succeeded")
    {
        var order = await _orders.FindByPaymentSessionAsync(
            paymentEvent.SessionId,
            cancellationToken);

        if (order == null)
        {
            return HttpNotFound();
        }

        // Webhook 可能被重复发送,因此更新必须具备幂等性。
        if (order.Status != OrderStatus.Paid)
        {
            if (order.AmountInMinorUnit != paymentEvent.AmountInMinorUnit ||
                !string.Equals(
                    order.Currency,
                    paymentEvent.Currency,
                    StringComparison.OrdinalIgnoreCase))
            {
                return new HttpStatusCodeResult(400);
            }

            order.Status = OrderStatus.Paid;
            order.PaidAtUtc = paymentEvent.PaidAtUtc;
            await _orders.SaveAsync(order, cancellationToken);
        }
    }

    return new HttpStatusCodeResult(200);
}

这里的 PaymentWebhookVerifier、事件名称和请求头都只是示意名称。实际实现必须使用所选 Payment provider 官方 SDK 提供的签名验证功能,不能自行猜测签名算法或字段名称。

返回页面应该怎样处理

返回页面只用于显示订单当前的状态:

[HttpGet]
public async Task<ActionResult> Return(
    long orderId,
    CancellationToken cancellationToken)
{
    var order = await _orders.FindAsync(orderId, cancellationToken);

    if (order == null)
    {
        return HttpNotFound();
    }

    return View(order);
}

如果 Webhook 还没有到达,可以显示“付款结果确认中”,再由前端轮询订单状态。不要因为 URL 中带有 success=true、订单号或支付会话 ID 就直接发货。

选择 Payment provider 时要确认什么

接入前至少要检查这些事项:

  • 是否支持商户所在的国家或地区。
  • 是否支持所需币种和结算银行账户。
  • 是否提供 Hosted Checkout,而不只是底层银行卡 API。
  • 是否支持 3-D Secure、退款、部分退款和拒付处理。
  • 是否提供沙箱环境、官方服务器端 SDK 和 Webhook 签名验证。
  • 交易费、退款费、货币转换费和结算周期。
  • 支付页面能否配置品牌、语言和移动端样式。
  • 对网站隐私政策、退款政策、配送说明和商户审核有哪些要求。

如果业务主要面向某个国家,还应优先了解当地银行或持牌支付机构,因为国际服务商未必能为该地区的商户开户。

上线注意事项

  • API secret 和 Webhook secret 只能保存在服务器端配置或密钥管理系统中。
  • 测试环境和 production 环境必须使用不同的密钥及 Webhook 地址。
  • 所有支付相关页面和回调地址都必须启用 HTTPS。
  • 创建支付会话时使用幂等键,避免重复提交产生多笔付款。
  • 分别保存服务商支付 ID、事件 ID 和本地订单号,方便对账。
  • Webhook 处理必须允许重复通知,同时防止同一订单重复发货。
  • 验证 Webhook 签名后,还要核对金额、币种、商户账户和订单关联关系。
  • 不要记录完整请求中的银行卡敏感信息、密钥或签名。
  • 为超时、付款失败、用户取消、退款和拒付设置明确的订单状态。
  • 使用服务商提供的测试卡和沙箱,测试支付成功、支付失败、重复通知和延迟通知等情况。

在 MVC 项目中增加第三种支付方式,无需自行搭建银行卡付款页面。选择支持 Hosted Checkout 的 Payment provider,由 MVC 后端创建支付会话并重定向用户,最后通过验证后的 Webhook 确认订单即可。

备注:内容仅供参考。