PAYATHON 2026

如何在 .Net MVC Web App 中集成 Spreedly

支付老李

结论

Spreedly 文档里的 curl 是 HTTP 请求的命令行示例,并不表示需要在 MVC 页面中运行 curl。在 .NET 中,只要通过 HttpClient 发送相同的 URL、HTTP 方法、认证头和 JSON 请求体,就能完成同样的调用。

通常可以这样集成:

  1. 浏览器通过 Spreedly Express、Hosted Fields 或当前官方提供的客户端组件采集支付信息。
  2. Spreedly 把支付信息转换为 payment_method_token。
  3. 浏览器将 token 提交给 ASP.NET MVC 后端。
  4. 后端使用 HttpClient 和 Spreedly 凭据调用交易 API。

银行卡号不应经过自己的 MVC Controller,Spreedly Access Secret 也不能放在 JavaScript、HTML 或前端配置中。使用 MVC 还是 Web Forms,对 API 调用方式没有本质影响。

如何理解文档中的 curl

假设文档提供了下面这样的请求:

curl https://core.spreedly.com/v1/gateways/GATEWAY_TOKEN/purchase.json \
  -u "ENVIRONMENT_KEY:ACCESS_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction": {
      "payment_method_token": "PAYMENT_METHOD_TOKEN",
      "amount": 1999,
      "currency_code": "USD"
    }
  }'

各部分与 .NET 代码的对应关系如下:

curl 参数含义.NET 对应方式
URLAPI 地址HttpRequestMessage.RequestUri
-uHTTP Basic AuthenticationAuthorization 请求头
-H请求头request.Headers 或 request.Content.Headers
-d请求体JsonContent.Create(...)
未显式写出的 POST使用 -d 时通常发送 POSTHttpMethod.Post

转换时不必考虑如何“执行 curl”,只需要还原它所描述的 HTTP 请求。

使用 HttpClient 调用 Spreedly

下面的示例适用于现代 ASP.NET Core MVC。API 地址和字段应以 Spreedly 当前文档中对应操作的说明为准。

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;

public sealed class SpreedlyClient
{
    private readonly HttpClient _httpClient;
    private readonly string _environmentKey;
    private readonly string _accessSecret;

    public SpreedlyClient(
        HttpClient httpClient,
        IConfiguration configuration)
    {
        _httpClient = httpClient;
        _environmentKey =
            configuration["Spreedly:EnvironmentKey"]
            ?? throw new InvalidOperationException(
                "Missing Spreedly environment key.");

        _accessSecret =
            configuration["Spreedly:AccessSecret"]
            ?? throw new InvalidOperationException(
                "Missing Spreedly access secret.");
    }

    public async Task<string> PurchaseAsync(
        string gatewayToken,
        string paymentMethodToken,
        int amount,
        string currencyCode,
        CancellationToken cancellationToken = default)
    {
        var url =
            $"v1/gateways/{Uri.EscapeDataString(gatewayToken)}/purchase.json";

        var payload = new
        {
            transaction = new
            {
                payment_method_token = paymentMethodToken,
                amount,
                currency_code = currencyCode
            }
        };

        using var request = new HttpRequestMessage(HttpMethod.Post, url)
        {
            Content = JsonContent.Create(payload)
        };

        var credentials = Convert.ToBase64String(
            Encoding.UTF8.GetBytes(
                $"{_environmentKey}:{_accessSecret}"));

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Basic", credentials);

        using var response = await _httpClient.SendAsync(
            request,
            cancellationToken);

        var responseBody = await response.Content.ReadAsStringAsync(
            cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            throw new SpreedlyException(
                response.StatusCode,
                responseBody);
        }

        return responseBody;
    }
}

异常类型可以简单定义为:

using System.Net;

public sealed class SpreedlyException : Exception
{
    public HttpStatusCode StatusCode { get; }
    public string ResponseBody { get; }

    public SpreedlyException(
        HttpStatusCode statusCode,
        string responseBody)
        : base($"Spreedly returned HTTP {(int)statusCode}.")
    {
        StatusCode = statusCode;
        ResponseBody = responseBody;
    }
}

注册 HttpClient:

builder.Services.AddHttpClient<SpreedlyClient>(client =>
{
    client.BaseAddress = new Uri("https://core.spreedly.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
});

配置文件中只保留非敏感的空值占位符:

{
  "Spreedly": {
    "EnvironmentKey": "",
    "AccessSecret": ""
  }
}

实际密钥应通过环境变量、Secret Manager 或 Key Vault 等方式注入,不能提交到源代码仓库。

在 MVC Controller 中使用

Controller 应接收 Spreedly 生成的 token,而不是原始银行卡号:

using Microsoft.AspNetCore.Mvc;

public sealed record PaymentRequest(
    string PaymentMethodToken,
    int Amount,
    string CurrencyCode);

[ApiController]
[Route("payments")]
public sealed class PaymentsController : ControllerBase
{
    private readonly SpreedlyClient _spreedly;

    public PaymentsController(SpreedlyClient spreedly)
    {
        _spreedly = spreedly;
    }

    [HttpPost]
    public async Task<IActionResult> Create(
        [FromBody] PaymentRequest input,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(input.PaymentMethodToken))
            return BadRequest("Missing payment method token.");

        if (input.Amount <= 0)
            return BadRequest("Amount must be greater than zero.");

        const string gatewayToken = "YOUR_GATEWAY_TOKEN";

        try
        {
            var result = await _spreedly.PurchaseAsync(
                gatewayToken,
                input.PaymentMethodToken,
                input.Amount,
                input.CurrencyCode,
                cancellationToken);

            return Content(result, "application/json");
        }
        catch (SpreedlyException ex)
        {
            // 生产环境不要直接向客户端返回可能包含敏感细节的完整响应。
            return StatusCode(
                StatusCodes.Status502BadGateway,
                new { error = "Payment provider rejected the request." });
        }
    }
}

这里直接返回 JSON 字符串,只是为了让请求流程更清楚。实际项目通常会把 Spreedly 响应反序列化为 DTO,检查交易状态和错误字段,再返回自己的业务响应。

如果使用旧版 ASP.NET MVC

.NET Framework 项目同样可以使用 HttpClient,只是没有 JsonContent.Create,因此需要先序列化 JSON:

using Newtonsoft.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;

var payload = new
{
    transaction = new
    {
        payment_method_token = paymentMethodToken,
        amount = 1999,
        currency_code = "USD"
    }
};

var json = JsonConvert.SerializeObject(payload);

using var request = new HttpRequestMessage(
    HttpMethod.Post,
    "https://core.spreedly.com/v1/gateways/GATEWAY_TOKEN/purchase.json");

request.Content = new StringContent(
    json,
    Encoding.UTF8,
    "application/json");

var credentials = Convert.ToBase64String(
    Encoding.UTF8.GetBytes(
        environmentKey + ":" + accessSecret));

request.Headers.Authorization =
    new AuthenticationHeaderValue("Basic", credentials);

var response = await httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();

Web Forms 也可以使用同一套服务层代码,无须为了兼容 Spreedly 改用其他调用方式。

支付表单应如何处理

支付表单不应把卡号直接提交到自己的服务器。常见流程是:

支付页面
   ↓
Spreedly 客户端组件安全采集支付信息
   ↓
获得 payment_method_token
   ↓
提交 token 和订单标识到 MVC 后端
   ↓
后端重新读取并校验订单金额
   ↓
后端调用 Spreedly API

服务端不能直接信任浏览器提交的金额。客户端可以提交商品或订单标识,但后端应根据数据库中的订单重新计算金额,以免用户通过修改请求内容篡改金额。

订阅需要额外设计

Spreedly 主要提供支付方式 tokenization 和支付网关编排。订阅能否直接通过 Spreedly API 管理,取决于所用产品和账户能力,不能从一次性支付示例中推断。

如果周期扣款由应用自行管理,通常需要:

  1. 首次支付时按照官方要求保留支付方式。
  2. 在数据库中保存 Spreedly token,而不是卡号。
  3. 保存订阅状态、周期、下次扣款时间和业务订单。
  4. 由后台任务在到期时使用 token 发起交易。
  5. 根据交易结果更新订阅,并处理失败重试和用户通知。

retain_on_success 等字段是否适用于具体接口,以及它们的准确含义,都应以当前接口文档为准。

需要特别注意的事项

  • amount 通常使用最小货币单位。例如,1999 可能表示 USD 19.99。零小数货币等情况需要按照 Spreedly 及所接网关的规则处理。
  • 不要混用测试环境和生产环境的凭据、网关 token 或支付方式 token。
  • 不要记录卡号、CVV、Access Secret 或完整的敏感响应。
  • 不要在每次请求时创建一个长期未管理的 HttpClient。ASP.NET Core 应优先使用 IHttpClientFactory。
  • 不能只检查 HTTP 200。响应体中还可能包含交易结果、网关错误和校验错误,因此必须解析业务状态。
  • 请求超时不代表交易一定失败,也不应立即再次扣款。应先查询交易状态,避免重复支付。
  • 保存业务订单号时,也要保存 Spreedly 返回的 transaction token,以便对账和排查问题。
  • 3-D Secure、SCA、异步交易、回调和重定向流程,需要根据所用网关及 Spreedly 当前文档分别实现。

可以把每个 curl 示例拆成“方法、URL、认证、请求头、请求体”五部分,再逐项映射到 HttpRequestMessage。MVC 和 Web Forms 只是页面框架,不会改变 Spreedly API 的调用方式。

备注:内容仅供参考。