PAYATHON 2026

如何在 Xamarin 跨平台应用中集成 payment gateway?

支付老李

结论

Xamarin 跨平台应用可以接入 payment gateway,但银行卡信息、商户密钥和支付签名不应交给 XAML 页面处理。

XAML 负责展示支付入口和支付状态。实际支付一般按以下流程完成:

  1. 应用向自己的后端请求创建订单和支付会话。
  2. 后端调用 payment gateway 的服务端 API。
  3. 应用通过系统浏览器或支付服务商提供的原生 SDK 打开支付页面。
  4. 支付完成后,通过 Universal Links、Android App Links 或自定义 URL Scheme 返回应用。
  5. 后端根据 webhook 或支付网关的查询结果确认支付状态。

如果使用托管支付页面,建议优先通过系统浏览器打开。只有支付服务商明确支持嵌入式 WebView,才考虑在 XAML 页面中使用 WebView。

推荐的支付流程

Xamarin App
    │
    │ 创建订单
    ▼
应用后端
    │
    │ 使用商户密钥调用支付网关
    ▼
Payment Gateway
    │
    │ 返回 checkout URL / session ID
    ▼
Xamarin App 打开托管支付页
    │
    │ 用户完成支付
    ▼
Payment Gateway 通知应用后端(webhook)
    │
    │ 后端验证签名并更新订单
    ▼
Xamarin App 查询并显示最终结果

在这套设计中,客户端只保存订单编号、支付会话标识和托管页面 URL,不保存 gateway secret,也不自行判断支付是否成功。

从 XAML 页面发起支付

XAML 页面可以直接展示支付按钮:

<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="MyApp.Views.CheckoutPage">

    <StackLayout Padding="24" Spacing="16">
        <Label Text="订单金额:¥99.00"
               FontSize="20" />

        <Button Text="立即支付"
                Clicked="OnPayClicked" />

        <ActivityIndicator
            x:Name="PaymentIndicator"
            IsVisible="False"
            IsRunning="False" />

        <Label x:Name="PaymentStatusLabel" />
    </StackLayout>
</ContentPage>

用户点击按钮后,应用先向后端申请支付会话,再打开后端返回的托管支付地址:

using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;

public partial class CheckoutPage : ContentPage
{
    private readonly HttpClient _httpClient = new HttpClient
    {
        BaseAddress = new Uri("https://api.example.com/")
    };

    public CheckoutPage()
    {
        InitializeComponent();
    }

    private async void OnPayClicked(object sender, EventArgs e)
    {
        try
        {
            SetLoading(true);

            var request = new
            {
                orderId = "ORDER-12345"
            };

            using var content = new StringContent(
                JsonSerializer.Serialize(request),
                Encoding.UTF8,
                "application/json");

            using var response = await _httpClient.PostAsync(
                "payments/checkout-sessions",
                content);

            response.EnsureSuccessStatusCode();

            var json = await response.Content.ReadAsStringAsync();
            var session = JsonSerializer.Deserialize<CheckoutSession>(json);

            if (session == null ||
                !Uri.TryCreate(session.CheckoutUrl, UriKind.Absolute, out var checkoutUri))
            {
                throw new InvalidOperationException("后端未返回有效的支付地址。");
            }

            await Browser.OpenAsync(
                checkoutUri,
                BrowserLaunchMode.SystemPreferred);

            PaymentStatusLabel.Text = "请在支付页面完成付款";
        }
        catch (Exception)
        {
            PaymentStatusLabel.Text = "暂时无法发起支付,请稍后重试";
        }
        finally
        {
            SetLoading(false);
        }
    }

    private void SetLoading(bool isLoading)
    {
        PaymentIndicator.IsVisible = isLoading;
        PaymentIndicator.IsRunning = isLoading;
    }

    private sealed class CheckoutSession
    {
        public string CheckoutUrl { get; set; }
    }
}

示例中的接口地址和返回模型仅用于说明结构,实际使用时要根据后端和 payment gateway API 进行调整。生产环境还应在请求中附带登录凭据,订单金额也不能硬编码在客户端。

后端负责创建支付会话

后端应从自己的订单数据中读取金额,不能直接相信客户端提交的金额。下面是一个简化的 ASP.NET Core 示例:

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

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

    [HttpPost("checkout-sessions")]
    public async Task<IActionResult> CreateCheckoutSession(
        [FromBody] CreateCheckoutRequest request)
    {
        var order = await _orders.GetPendingOrderAsync(
            request.OrderId,
            User);

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

        var session = await _gateway.CreateCheckoutSessionAsync(
            orderId: order.Id,
            amount: order.Amount,
            currency: order.Currency,
            successUrl: "https://example.com/payment/return",
            cancelUrl: "https://example.com/payment/cancel");

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

public sealed class CreateCheckoutRequest
{
    public string OrderId { get; set; }
}

IPaymentGateway 的实现方式取决于所选的支付服务商。API secret、签名私钥和 webhook secret 必须保存在后端或安全的密钥管理系统中,不能打包到 Xamarin 应用里。

处理支付完成后的返回

支付网关通常会将浏览器重定向到一个 HTTPS 地址。可以把该地址配置为 Android App Link 或 iOS Universal Link,由系统重新打开应用。

应用收到链接后,可以提取订单标识,再向后端查询支付状态:

protected override void OnAppLinkRequestReceived(Uri uri)
{
    base.OnAppLinkRequestReceived(uri);

    if (uri.Host != "payment-return")
    {
        return;
    }

    var orderId = GetQueryParameter(uri, "orderId");

    if (!string.IsNullOrWhiteSpace(orderId))
    {
        MessagingCenter.Send(
            this,
            "PaymentReturned",
            orderId);
    }
}

private static string GetQueryParameter(Uri uri, string name)
{
    var query = uri.Query.TrimStart('?').Split('&');

    foreach (var part in query)
    {
        var pair = part.Split(new[] { '=' }, 2);

        if (pair.Length == 2 &&
            string.Equals(
                Uri.UnescapeDataString(pair[0]),
                name,
                StringComparison.OrdinalIgnoreCase))
        {
            return Uri.UnescapeDataString(pair[1]);
        }
    }

    return null;
}

页面收到返回事件后,再向后端查询:

protected override void OnAppearing()
{
    base.OnAppearing();

    MessagingCenter.Subscribe<App, string>(
        this,
        "PaymentReturned",
        async (_, orderId) => await RefreshPaymentStatusAsync(orderId));
}

protected override void OnDisappearing()
{
    MessagingCenter.Unsubscribe<App, string>(
        this,
        "PaymentReturned");

    base.OnDisappearing();
}

private async Task RefreshPaymentStatusAsync(string orderId)
{
    var response = await _httpClient.GetAsync(
        $"payments/orders/{Uri.EscapeDataString(orderId)}/status");

    response.EnsureSuccessStatusCode();

    var json = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<PaymentStatusResult>(json);

    PaymentStatusLabel.Text = result?.Status switch
    {
        "paid" => "支付成功",
        "failed" => "支付失败",
        _ => "支付结果确认中"
    };
}

private sealed class PaymentStatusResult
{
    public string Status { get; set; }
}

Android App Links、iOS Universal Links 以及 Xamarin 中的链接分发配置,会因 Xamarin.Forms 版本和项目结构而异。需要根据目标平台配置相应的 intent filter、associated domains 和域名验证文件。

为什么不能只依赖跳转结果

支付页面返回 success,只能说明浏览器完成了跳转,不能证明资金已经到账。用户可能伪造回调 URL,支付也可能还在异步处理中。

后端至少应通过以下一种方式确认最终结果:

  • 验证 payment gateway 发来的 webhook 签名。
  • 使用后端凭据调用 gateway API,查询交易状态。
  • 核对订单号、金额、币种、商户账户和交易标识。
  • 对 webhook 进行幂等处理,避免重复通知导致重复发货。

客户端收到返回链接后,最多只能显示“正在确认支付”,不能直接把订单状态改为已支付。

是否可以使用 XAML WebView

技术上可以这样做:

<WebView
    x:Name="PaymentWebView"
    Navigating="OnPaymentNavigating" />
PaymentWebView.Source = checkoutUrl;

private async void OnPaymentNavigating(
    object sender,
    WebNavigatingEventArgs e)
{
    if (e.Url.StartsWith(
        "https://example.com/payment/return",
        StringComparison.OrdinalIgnoreCase))
    {
        e.Cancel = true;
        await RefreshPaymentStatusAsync("ORDER-12345");
    }
}

不过,WebView 未必适合真实的支付环境。部分 gateway、银行验证页、OAuth 流程或 3-D Secure 页面会限制嵌入式浏览器,也可能依赖新窗口、外部银行应用、Cookie 或跨域跳转。使用前必须确认服务商文档明确支持 Xamarin WebView 或一般的 embedded WebView。

不要向 WebView 注入 JavaScript 来获取卡号、CVV、密码或验证码,也不能绕过证书校验。如果服务商要求使用原生体验,应接入其 Android/iOS SDK,并通过 Xamarin binding project 或平台服务抽象进行统一调用。

还需要注意的事项

  • 全程使用 HTTPS,并由后端验证支付服务商的证书和 webhook 签名。
  • 商户 secret、私钥和 webhook secret 只能保存在服务端。
  • 金额、币种和商品信息应以服务端的订单记录为准。
  • 创建支付会话、处理回调和接收 webhook 时都要设计幂等机制。
  • 不要在日志、异常信息或分析平台中记录银行卡信息、token、验证码或完整回调参数。
  • 在沙箱环境中测试支付成功、失败、取消、超时、重复回调、网络中断和应用被系统终止等情况。
  • 如果销售的是应用内数字内容或数字功能,需要先核对 Apple App Store 和 Google Play 当前的应用内购买政策。这类交易不一定允许直接使用第三方 payment gateway。
  • Xamarin 已经属于较旧的技术栈。仍在维护的项目可以按上述架构接入支付;新项目或准备升级的项目可以评估迁移到 .NET MAUI,但完成支付接入并不以迁移为前提。

备注:内容仅供参考。