PAYATHON 2026

如何在 ASP.NET 计费项目中实现 payment gateway?

支付小周

明确结论

支付网关不是可以”访问所有银行账户”的统一入口。

在 ASP.NET 计费项目中,通常只需接入一家支付服务商,就能受理该服务商支持的银行卡、电子钱包或本地支付方式,不必逐家对接发卡银行。具体能覆盖哪些用户,取决于服务商支持的国家和地区、卡组织、币种、结算方式,以及商户资质。

如果业务需要读取账户余额、查询流水或直接操作用户的银行账户,则需要使用开放银行(Open Banking)或账户信息服务。这些能力不属于普通支付网关,通常需要用户授权,并受当地金融监管要求约束。

支付网关在系统中的作用

典型的支付流程如下:

  1. ASP.NET 服务端创建订单,计算最终应付金额。
  2. 服务端调用支付服务商 API,创建支付会话或支付意图。
  3. 前端跳转到服务商托管的收银台,或使用服务商提供的前端组件采集支付信息。
  4. 用户通过银行卡、电子钱包等方式付款。
  5. 支付服务商通过 Webhook 将支付结果通知服务端。
  6. 服务端验证通知的真实性,然后更新订单状态。
  7. 服务商按照结算周期,将资金划入商户绑定的结算账户。

这套流程是由支付服务商代商户收款,应用不会直接登录或访问付款人的银行账户。

一家支付网关是否足够

如果业务只面向一个市场,且支付服务商已经支持所需的银行卡、币种和付款方式,接入一家通常就够了。

遇到以下情况时,可能需要接入多家服务商:

  • 业务覆盖多个国家或地区;
  • 需要支持某些本地支付方式;
  • 单一服务商不支持目标币种或商户所在地区;
  • 需要在某家服务商发生故障时切换支付通道;
  • 需要根据手续费、成功率或结算周期选择通道;
  • 各服务商对订阅扣款、分账、退款等功能的支持不同。

选择服务商前,需要确认:

  • 商户所在国家或地区能否开户;
  • 支持哪些银行卡组织和付款方式;
  • 是否支持一次性付款、订阅和自动续费;
  • 是否支持目标币种及跨境结算;
  • 手续费、退款费和拒付处理规则;
  • 是否提供 ASP.NET 或 .NET SDK;
  • Webhook 使用什么签名验证方式;
  • 是否提供测试环境;
  • 是否需要进行 PCI DSS 合规评估;
  • 资金最终可以结算到哪些银行账户。

推荐的项目结构

不要把某家服务商的调用逻辑分散在 Controller 中。可以先定义统一的支付接口,再为各服务商编写具体实现。以后增加第二个支付通道时,订单业务不需要重写。

public sealed record CreatePaymentRequest(
    string OrderId,
    long AmountMinor,
    string Currency,
    string SuccessUrl,
    string CancelUrl);

public sealed record CreatePaymentResult(
    string ProviderPaymentId,
    string CheckoutUrl);

public interface IPaymentGateway
{
    Task<CreatePaymentResult> CreatePaymentAsync(
        CreatePaymentRequest request,
        CancellationToken cancellationToken = default);

    Task RefundAsync(
        string providerPaymentId,
        long? amountMinor,
        CancellationToken cancellationToken = default);
}

AmountMinor 表示最小货币单位。例如,人民币 1000 通常表示 10.00 CNY。不要使用 double 保存金额。内部可以用整数表示最小货币单位,也可以根据服务商的要求使用 decimal。

创建支付订单

Controller 负责接收请求和组织流程。订单金额必须根据数据库中的商品、套餐或账单记录重新计算,不能直接采用浏览器提交的金额。

using Microsoft.AspNetCore.Mvc;

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

    public PaymentsController(
        IPaymentGateway gateway,
        IOrderService orders)
    {
        _gateway = gateway;
        _orders = orders;
    }

    [HttpPost("{orderId}/checkout")]
    public async Task<ActionResult<CreatePaymentResult>> CreateCheckout(
        string orderId,
        CancellationToken cancellationToken)
    {
        var order = await _orders.GetPendingOrderAsync(
            orderId,
            cancellationToken);

        if (order is null)
        {
            return NotFound();
        }

        if (order.IsPaid)
        {
            return Conflict("Order has already been paid.");
        }

        var request = new CreatePaymentRequest(
            OrderId: order.Id,
            AmountMinor: order.AmountMinor,
            Currency: order.Currency,
            SuccessUrl: $"https://example.com/payment/success?orderId={order.Id}",
            CancelUrl: $"https://example.com/payment/cancel?orderId={order.Id}");

        var result = await _gateway.CreatePaymentAsync(
            request,
            cancellationToken);

        await _orders.SaveProviderPaymentIdAsync(
            order.Id,
            result.ProviderPaymentId,
            cancellationToken);

        return Ok(result);
    }
}

IPaymentGateway 的具体实现要按照所选服务商的官方 SDK 编写。不同服务商采用的认证方式、请求字段和返回结构并不相同,不能假设它们使用一套通用的 API 参数。

使用依赖注入注册支付服务

非敏感选项可以保存在配置文件中,API 密钥则应通过环境变量或密钥管理服务保存。

builder.Services.Configure<PaymentOptions>(
    builder.Configuration.GetSection("Payment"));

builder.Services.AddHttpClient<IPaymentGateway, ExamplePaymentGateway>();

配置模型示例:

public sealed class PaymentOptions
{
    public string ApiBaseUrl { get; init; } = string.Empty;
    public string ApiKey { get; init; } = string.Empty;
    public string WebhookSecret { get; init; } = string.Empty;
}

不要将生产环境密钥提交到 Git 仓库,也不要在日志中记录私钥、完整支付请求或银行卡敏感数据。

必须通过 Webhook 确认支付结果

支付成功页面只能用于改善用户体验,不能作为记账依据。用户可能会关闭页面、伪造跳转参数,也可能在付款成功后未能返回网站。

服务端应以通过签名验证的 Webhook 为准:

[ApiController]
[Route("api/payment-webhooks")]
public sealed class PaymentWebhooksController : ControllerBase
{
    private readonly IWebhookVerifier _verifier;
    private readonly IOrderService _orders;

    public PaymentWebhooksController(
        IWebhookVerifier verifier,
        IOrderService orders)
    {
        _verifier = verifier;
        _orders = orders;
    }

    [HttpPost]
    public async Task<IActionResult> Handle(
        CancellationToken cancellationToken)
    {
        using var reader = new StreamReader(Request.Body);
        var payload = await reader.ReadToEndAsync(cancellationToken);

        var signature = Request.Headers["X-Payment-Signature"].ToString();

        PaymentWebhookEvent paymentEvent;

        try
        {
            paymentEvent = _verifier.VerifyAndParse(payload, signature);
        }
        catch
        {
            return Unauthorized();
        }

        if (paymentEvent.Type == "payment.succeeded")
        {
            await _orders.MarkAsPaidAsync(
                paymentEvent.OrderId,
                paymentEvent.ProviderPaymentId,
                paymentEvent.EventId,
                cancellationToken);
        }

        return Ok();
    }
}

这里的 X-Payment-Signature、事件名称和签名算法只是结构示例,实际名称应以所选服务商的官方文档为准。

Webhook 处理还要保证幂等。同一个事件可能会被重复发送,因此需要保存唯一的 EventId。如果事件已经处理过,直接返回成功,避免重复入账、发货或开通订阅。

安全与合规注意事项

不要自行处理银行卡数据

优先使用服务商托管的收银台或官方前端组件,让银行卡号直接提交给支付服务商。不要让完整卡号、CVV 等敏感数据经过自己的 ASP.NET 服务器,否则会明显扩大 PCI DSS 合规范围,并增加数据泄露风险。

服务端校验金额和币种

收到 Webhook 后,需要核对:

  • 订单编号是否存在;
  • 支付服务商订单号是否匹配;
  • 金额是否与本地账单一致;
  • 币种是否一致;
  • 当前订单状态是否允许变更;
  • 支付状态是否确实成功。

不能仅凭 "payment.succeeded" 就把任意订单标记为已支付。

使用 HTTPS 和密钥管理

生产环境必须使用 HTTPS。API 密钥和 Webhook Secret 应保存在受控配置中,例如环境变量或云平台的密钥管理服务,并根据服务商的支持情况定期轮换。

设计完整的订单状态

计费系统不能只有”已支付”和”未支付”两种状态。通常还要考虑:

Pending
Processing
Paid
Failed
Cancelled
PartiallyRefunded
Refunded
Disputed

具体采用哪些状态,应根据一次性付款、订阅、退款和拒付等业务场景决定。

测试异常流程

除了正常付款,还要测试:

  • 用户取消支付;
  • 支付失败或仍在处理中;
  • Webhook 重复、乱序或延迟;
  • 创建支付请求超时;
  • 支付已经成功,但本地更新失败;
  • 全额退款与部分退款;
  • 订阅续费失败;
  • 拒付和争议处理。

如果真正需要访问银行账户

如果应用需要在用户授权后读取余额、获取交易记录或从银行账户发起转账,应选择支持 Open Banking、账户聚合或银行转账发起的服务商。

这类流程通常包括:

  1. 将用户重定向到银行或授权机构;
  2. 获取用户的明确授权;
  3. 使用短期授权码交换访问令牌;
  4. 在授权范围内读取账户信息或发起付款;
  5. 处理令牌过期、授权撤销和数据保护要求。

这类服务与银行卡收单不是同一种产品,也没有一个接口可以天然覆盖全球所有银行账户。实际覆盖范围取决于服务商接入了哪些银行、在哪些司法辖区运营,以及持有哪些监管许可。

实施建议

普通的 ASP.NET 计费项目可以先选择一家覆盖目标市场的成熟支付服务商,用托管收银台完成第一版,同时把支付能力封装在 IPaymentGateway 接口后面。

上线前需要完成 Webhook 验签、幂等处理、服务端金额校验、退款流程和异常补偿。只有现有服务商无法满足地区、付款方式或可靠性要求时,再接入第二家支付通道。

备注:内容仅供参考。