PAYATHON 2026

ASP.NET 购物车应集成哪种支付网关?

支付老李

结论

没有适用于所有项目的”最佳支付网关”。如果 ASP.NET 项目刚开始开发支付功能,应优先考虑具备以下能力的正规支付服务商:

  • 托管收银台或安全支付组件
  • 服务端 SDK 和 Webhook
  • 3-D Secure 等强客户认证流程
  • 退款、对账、争议处理和测试环境
  • 能在目标国家或地区合法使用,并支持所需币种和支付方式

客户主要使用国际银行卡时,可以比较 Stripe、Adyen、Braintree、PayPal 等服务。业务面向中国大陆时,通常还要根据主体资质接入微信支付、支付宝,或者选择能统一接入这些渠道的合规服务商。

新项目比较稳妥的做法是:由支付服务商采集银行卡等敏感信息,ASP.NET 后端只创建支付订单、校验回调并更新业务订单状态。不要在自己的服务器上保存银行卡号、有效期或 CVV。

为什么推荐托管收银台

直接处理银行卡信息会大幅增加安全风险,也会扩大 PCI DSS 的合规范围。托管收银台把敏感数据的录入交给支付服务商,系统接触持卡人数据的机会更少,处理 3-D Secure、钱包支付和后续认证流程也更方便。

选择服务商时,不能只比较手续费,还需要确认:

  • 是否支持公司的注册地和客户所在地
  • 是否支持业务需要的银行卡、钱包和本地支付方式
  • 结算币种、结算周期及退款费用
  • 是否提供沙箱环境、官方 .NET SDK 和完整文档
  • Webhook 是否支持签名验证和事件重试
  • 是否支持订阅、部分退款和争议处理等后续功能
  • 商户账户申请和行业审核是否符合业务类型

具体费率、可用地区和接口参数都可能调整,接入时应以官方文档和商户协议为准。

推荐的支付流程

购物车支付可以按以下流程实现:

  1. 用户在购物车中提交结算请求。
  2. ASP.NET 后端从数据库读取商品价格,重新计算订单金额。
  3. 后端创建本地订单,并将初始状态设为 PendingPayment。
  4. 后端调用支付服务商 API,创建支付会话或支付意图。
  5. 前端跳转到托管收银台,或者通过服务商提供的安全组件完成支付。
  6. 支付服务商使用 Webhook 将支付结果通知后端。
  7. 后端验证 Webhook 签名,再根据事件更新订单状态。
  8. 系统执行发货、扣减库存、发送确认邮件等后续操作。

浏览器跳转到”支付成功”页面,并不能证明订单已经付款。用户可以伪造请求,也可能付款后直接关闭页面。订单的最终状态应以通过签名验证的服务端 Webhook 为准。

ASP.NET Core 集成示例

下面用 Stripe Checkout 的典型流程说明实现结构。这只是一个示例,不代表 Stripe 适合所有地区和业务。安装包名称、API 参数和事件类型应在开发时根据当前官方文档核对。

先安装服务商提供的 .NET SDK:

dotnet add package Stripe.net

将密钥放在配置或密钥管理系统中,不要写进源代码:

{
  "Stripe": {
    "SecretKey": "",
    "WebhookSecret": ""
  }
}

生产环境可以通过环境变量、Azure Key Vault 或其他密钥管理服务注入这些值。

创建支付会话

using Microsoft.AspNetCore.Mvc;
using Stripe.Checkout;

[ApiController]
[Route("api/payments")]
public sealed class PaymentsController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IOrderService _orders;

    public PaymentsController(
        IConfiguration configuration,
        IOrderService orders)
    {
        _configuration = configuration;
        _orders = orders;
    }

    [HttpPost("checkout")]
    public async Task<IActionResult> CreateCheckout(
        [FromBody] CreateCheckoutRequest request)
    {
        // 必须从数据库读取商品和价格,不能信任浏览器提交的金额。
        var order = await _orders.CreatePendingOrderAsync(
            request.ProductIds,
            request.CustomerId);

        Stripe.StripeConfiguration.ApiKey =
            _configuration["Stripe:SecretKey"];

        var options = new SessionCreateOptions
        {
            Mode = "payment",
            SuccessUrl =
                $"https://shop.example.com/payment/success?session_id={{CHECKOUT_SESSION_ID}}",
            CancelUrl =
                $"https://shop.example.com/cart",
            ClientReferenceId = order.Id.ToString(),
            Metadata = new Dictionary<string, string>
            {
                ["order_id"] = order.Id.ToString()
            },
            LineItems = order.Items.Select(item =>
                new SessionLineItemOptions
                {
                    Quantity = item.Quantity,
                    PriceData = new SessionLineItemPriceDataOptions
                    {
                        Currency = order.Currency.ToLowerInvariant(),
                        UnitAmount = item.UnitAmountInMinorUnits,
                        ProductData =
                            new SessionLineItemPriceDataProductDataOptions
                            {
                                Name = item.Name
                            }
                    }
                }).ToList()
        };

        var service = new SessionService();
        var session = await service.CreateAsync(
            options,
            requestOptions: new Stripe.RequestOptions
            {
                IdempotencyKey = $"checkout-order-{order.Id}"
            });

        await _orders.SavePaymentReferenceAsync(order.Id, session.Id);

        return Ok(new { checkoutUrl = session.Url });
    }
}

public sealed record CreateCheckoutRequest(
    IReadOnlyCollection<long> ProductIds,
    long CustomerId);

金额通常用最小货币单位的整数表示。例如,对某些币种来说,1000 表示 10.00。不同币种的小数位规则并不完全相同,必须按照支付服务商的货币规则处理,不能假定所有币种都有两位小数。

IdempotencyKey 可以降低重复请求创建多个支付会话或造成重复扣款的风险。实际项目还需要限制同一订单重复创建支付。

验证 Webhook 并更新订单

using Microsoft.AspNetCore.Mvc;
using Stripe;

[ApiController]
[Route("api/webhooks")]
public sealed class StripeWebhookController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IOrderService _orders;

    public StripeWebhookController(
        IConfiguration configuration,
        IOrderService orders)
    {
        _configuration = configuration;
        _orders = orders;
    }

    [HttpPost("stripe")]
    public async Task<IActionResult> Handle()
    {
        var json = await new StreamReader(Request.Body)
            .ReadToEndAsync();

        Event stripeEvent;

        try
        {
            stripeEvent = EventUtility.ConstructEvent(
                json,
                Request.Headers["Stripe-Signature"],
                _configuration["Stripe:WebhookSecret"]);
        }
        catch (StripeException)
        {
            return BadRequest();
        }

        // 事件名称及对象结构应以当前 SDK 和官方文档为准。
        if (stripeEvent.Type == "checkout.session.completed" &&
            stripeEvent.Data.Object is Stripe.Checkout.Session session &&
            session.Metadata.TryGetValue("order_id", out var orderIdText) &&
            long.TryParse(orderIdText, out var orderId))
        {
            // 该方法必须具备幂等性,同一事件重复到达时不能重复发货。
            await _orders.MarkPaidAsync(
                orderId,
                session.Id,
                stripeEvent.Id);
        }

        return Ok();
    }
}

支付平台可能多次发送同一个 Webhook,事件也不一定按固定顺序到达。后端需要保存已经处理过的事件 ID,并通过数据库事务或唯一约束,防止重复更新订单、扣减库存或发货。

如果启用了异步支付方式,还需要处理”支付失败”、“支付延迟成功”、“退款”和”争议”等事件,不能只监听一个成功事件。

订单状态应独立设计

不要用一个简单的 IsPaid 字段覆盖所有情况。实际需要的状态可能包括:

PendingPayment
PaymentProcessing
Paid
PaymentFailed
Cancelled
PartiallyRefunded
Refunded
Disputed

本地订单号与支付服务商返回的支付会话 ID、支付意图 ID 或交易 ID 应分别保存。将订单更新为 Paid 前,建议至少核对以下内容:

  • Webhook 签名有效
  • 本地订单存在且尚未支付
  • 商户账户与预期一致
  • 币种与本地订单一致
  • 金额与服务端计算结果一致
  • 支付状态确实满足履约条件

必须注意的安全问题

不信任客户端金额

前端只应提交商品标识、数量和优惠码等信息。单价、折扣、税费、运费及最终金额必须由服务器重新计算,因为隐藏字段、JavaScript 变量和请求正文都可以被修改。

不保存敏感卡片数据

不要记录完整银行卡号、CVV、磁道数据,也不要记录包含这些信息的请求正文。即使数据库已经加密,也不能因此绕过支付行业的合规要求。

验证 Webhook 签名

不能只检查请求中是否包含订单号。必须按照服务商规定的流程,使用原始请求体、签名请求头和 Webhook 密钥完成验签。

使用 HTTPS 和安全密钥管理

所有生产接口都必须使用 HTTPS。测试密钥和生产密钥应分开保存,并限制能够访问密钥的人员和服务。密钥一旦泄露,应立即轮换。

保证幂等性

用户可能重复点击付款按钮,网络请求也可能重试,Webhook 同样可能重复发送。创建支付、处理通知、退款和履约都需要幂等保护。

不在支付回调中执行耗时任务

Webhook 接口应尽快完成验证和状态写入,然后返回成功。发送邮件、生成发票和调用物流等耗时操作可以交给后台队列处理。

日志中避免敏感数据

日志可以记录订单号、事件 ID、金额、币种和处理结果,但不要记录支付密钥、完整请求头或银行卡敏感信息。

不建议自行开发的部分

除非拥有专门的支付安全和合规团队,否则不要自行实现以下功能:

  • 直接采集和保存银行卡信息
  • 自定义银行卡加密或令牌化协议
  • 根据浏览器返回参数判断付款成功
  • 跳过 Webhook 验签
  • 使用浮点数计算金额
  • 在一个数据库事务中长时间等待外部支付 API
  • 收到成功通知后无条件重复发货

如何做最终选择

先列出业务必须支持的国家、币种、支付方式、退款方式和结算周期,再选择两到三家服务商进行沙箱验证。验证内容至少包括:

  • 正常支付
  • 支付失败
  • 用户取消
  • 3-D Secure 验证
  • 重复提交
  • Webhook 重试
  • 金额不一致
  • 全额与部分退款
  • 网络超时
  • 支付成功但浏览器没有返回网站

初次实现支付功能时,可以采用”本地订单 + 托管收银台 + 已验签 Webhook + 幂等订单更新”的架构。与直接处理银行卡数据相比,这种方式通常更安全,也更容易维护。至于选择哪家支付服务商,应结合业务地区、支付方式、合规要求和费用决定。

备注:内容仅供参考。