.NET如何对接PayPal支付

本文介绍了.NET如何对接PayPal支付,包括支付流程、开发前准备、注意事项和常见错误点。重点强调了PayPal支付的独特步骤——主动向PayPal确认收款,以及在对接过程中需要注意的配置、验证和异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

如题,最近在研究如何对接PayPal支付,在经过一段时间的摸索后,终于实现简单的支付操作,在这里分享下我个人的思路,让其他需要了解的人少走弯路,尽快对接上,申请注册PayPal账号这些我这里不多做解析,网上其他博主已经说得很详细(文字和截图都有),我主要说下如何入手,注意点以及易错点。


一、如何入手

在对接PayPal支付之前,相信很多人都已经对接过微信,支付宝支付,我也不例外,所以一开始接触PayPal支付的时候,我就习惯性的将其代入到以前对接过的支付中,觉得流程应该大同小异,简单点来说就是 发起支付请求 -> 用户授权支付 -> 支付回调 -> 业务处理,但是在查看了PayPal对接文档以及参考了其他博主文章后,发现除了上面我说的流程外,PayPal还多了一步,这一步按照文档的说法叫“批准订单”,但是这样听起来貌似不太好理解,换成通俗点来说就是 主动向PayPal确认收款,发请求给PayPal服务器跟他说可以收款了,如果没有这一步,支付并不算完成,卖家同样收不到款,这一点切记,这样我们就可以将PayPal支付流程简单的概括为 发起支付请求 -> 用户授权支付 -> 支付回调 -> 主动向PayPal确认收款 -> 业务处理


二、开发前准备

在正式开始之前,我们需要在PayPal开发者中心获取ClientIdSecret及设置Webhooks并得到对应Webhook ID,我暂且不细说这些东西的作用,等下根据流程来一一详解,如果不知道在哪里获取这些东西,我引入了其他博主的文章链接,可以直接点击查看,这个博主说的十分详细,里面包含文字和截图,相信看了之后就知道如何获取。


三、支付流程

通过上面得知,我们已将整个流程简单概括为发起支付请求 -> 用户授权支付 -> 支付回调 -> 主动向PayPal确认收款 -> 业务处理这几个步骤,接下来,我们将按照这个思路一步一步分析

通用类和通用方法如下:

---通用类---
public class Links
{
	public string href { get; set; }
    public string rel { get; set; }
    public string method { get; set; }
}

public class CheckoutOrdersResult
{
	public string id { get; set; }
    public string status { get; set; }
    public List<Links> links { get; set; }
}

public class WebhookEvent
 {
	public string id { get; set; }
    public string event_version { get; set; }
    public string create_time { get; set; }
    public string resource_type { get; set; }
    public string resource_version { get; set; }
    public string event_type { get; set; }
    public string summary { get; set; }
    public Resource resource { get; set; }
    public List<Links> links { get; set; }
}

public class Resource
{
    public string create_time { get; set; }
    public List<PurchaseUnits> purchase_units { get; set; }
    public List<Links> links { get; set; }
    public string id { get; set; }
    public string intent { get; set; }
    public string status { get; set; }
}

public class PurchaseUnits
{
    public string reference_id { get; set; }        
}
---通用类---

---通用方法
public string Post(string url, string clientId, string secret, Dictionary <string, object> dic, Encoding encoding)
{
    var param = string.Empty;
    foreach(var o in dic)
    {
        if(string.IsNullOrEmpty(param)) 
            param += o.Key + "=" + o.Value;
        else 
            param += "&" + o.Key + "=" + o.Value;
    }
    byte[] byteArray = encoding.GetBytes(param);
    ServicePointManager.ServerCertificateValidationCallback += (s, cert, chain, sslPolicyErrors) => true;
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
    HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
    request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(encoding.GetBytes(clientId + ":" + secret)));
    request.PreAuthenticate = true;
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.ContentLength = byteArray.Length;

    Stream newStream = request.GetRequestStream();
    newStream.Write(byteArray, 0, byteArray.Length);
    newStream.Close();
    using(HttpWebResponse response = (HttpWebResponse) request.GetResponse())
    {
        using(var stream = response.GetResponseStream())
        {
            if(stream != null) 
            {
                using(StreamReader sr = new StreamReader(stream, Encoding.UTF8))
                {
                    return sr.ReadToEnd();
                }
            }
        }
    }
    return string.Empty;
}

public string PostJsonByBearer(string url, string json, Encoding encoding, string accessToken)
{
	try
	{
    	byte[] byteArray = encoding.GetBytes(json);
        ServicePointManager.ServerCertificateValidationCallback += (s, cert, chain, sslPolicyErrors) => true;
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        request.Headers.Add("Authorization", $"Bearer {accessToken}");
        request.PreAuthenticate = true;

        request.Method = "POST";
        request.Headers.Add("Cache-Control", "no-cache");
        request.ContentType = "application/json";
        request.ContentLength = byteArray.Length;

        Stream newStream = request.GetRequestStream();
        newStream.Write(byteArray, 0, byteArray.Length);
        newStream.Close();

        using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
        {
            using (var stream = response.GetResponseStream())
        	{
            	if (stream != null)
                using (StreamReader sr = new StreamReader(stream, Encoding.UTF8))
                {
                	return sr.ReadToEnd();
                }
            }
        }
	}
	catch (WebException e)
    {
    	var eMsg = string.Empty;
        var eResponse = e.Response as HttpWebResponse;
        using (var eStream = eResponse.GetResponseStream())
        {
        	if (eStream != null)
            using (StreamReader sr = new StreamReader(eStream, Encoding.UTF8))
            {
            	eMsg = sr.ReadToEnd();
            }
        }
    }
    return string.Empty;
}

public string GetByBearer(string url, string accessToken)
 {
	ServicePointManager.ServerCertificateValidationCallback += (s, cert, chain, sslPolicyErrors) => true;
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
	HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Headers.Add("Authorization", $"Bearer {accessToken}");
    request.PreAuthenticate = true;

    request.Method = "GET";
    using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
    {
		using (var stream = response.GetResponseStream())
        {
        	if (stream != null)
            using (StreamReader sr = new StreamReader(stream, Encoding.UTF8))
            {
            	return sr.ReadToEnd();
            }
        }
    }
    return string.Empty;
}
---通用方法

1.获取access_token

之所以先提这个,是因为接下来的支付流程都跟access_token紧密相关,这里我们就需要用到上面说的ClientIdSecret查看对应接口文档,参考代码如下,这样我们就获取到凭证access_token,下面用到的accessToken都来源于此。

var ret = Post("域名/v1/oauth2/token/", ClientId, Secret, new Dictionary<string, object> { { "grant_type", "client_credentials" }, System.Text.Encoding.UTF8})

2.发起支付请求

通过第1点我们已经拿到了access_token,在成功创建支付订单返回的内容中拿到rel为“approve”的链接进行跳转查看对应接口文档,参考代码如下

代码如下(示例):

var param = new
{
	intent = "CAPTURE",
	// 总价
	purchase_units = new dynamic[]
	{
		new
        {
        	reference_id = DateTime.Now.ToString("yyyyMMddHHmmssffffff"),
            description = "订单",
            amount = new
            {
	        	currency_code = "HKD",
            	value = "200",
                // 价格明细,如果要在下面加商品明细,则必须在这里加上价格明细,否则提交出错
                // 商品明细中的商品合计金额必须等于价格明细中的金额,按照以下例子模拟计算75*2+50*1=200,否则提交出错
                // 商品明细和价格明细的货币必须一致,否则提交出错
                breakdown = new
                {
                	item_total = new
                    {
                     	currency_code = "HKD",
                        value = "200"
                    }
                }
            },
            // 商品明细
            items = new dynamic[]
            {
	        	new
                {
               	    name = "衣服",
                    unit_amount = new
                    {
                        currency_code = "HKD",
                        value = "75"
                    },
                    quantity = 2 // 数量
                },
                new
                {
                    name = "裤子",
                    unit_amount = new
                    {
                        currency_code = "HKD",
                        value = "50"
                    },
                    quantity = 1 // 数量
                }
            }
        }
    },
	application_context = new
	{
    	return_url = $"{Request.Url.Scheme}://{Request.Url.Authority}/Success.aspx", //支付完成跳转链接
    	cancel_url = $"{Request.Url.Scheme}://{Request.Url.Authority}/Cancel.aspx" //取消支付跳转链接
	}
};
var json = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(param);
var ret = PostJsonByBearer("域名/v2/checkout/orders", json, System.Text.Encoding.UTF8, accessToken);
var checkoutOrdersResult = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<CheckoutOrdersResult>(ret);
if (checkoutOrdersResult != null)
{
	var approveUrl = checkoutOrdersResult.links.FirstOrDefault(x => x.rel.Equals("approve")).href;
    Response.Redirect($"approveUrl:{approveUrl}");
}

3.用户授权支付

通过第2点我们已经拿到rel为“approve”的链接并进行跳转,这时就到用户授权支付的操作(流程类似进入到支付宝网页版本进行输入账号密码登录并确定支付),这一步不需要我们做什么,只需要等待用户操作即可。

4.支付回调

用户支付完成后,就到了支付回调,目前分为WebHooksIPN两种方式,看了网上的资料,大部分比较推荐使用WebHooks,所以我就直接选用WebHooks,详细解释我也不多说,我直接说怎么使用,第一步就是得配置接收通知地址,设置完之后得到 Webhook ID,Webhook ID用于验证Webhook消息,记得存下来;第二步选择对应的事件订阅,至于支付应该订阅哪些事件,主流的选择是CHECKOUT.ORDER.APPROVEDPAYMENT.CAPTURE.COMPLETED,但是根据我实际用到的,感觉订阅CHECKOUT.ORDER.APPROVED事件就够了;第三步回调验证,分为调用验签API和通过算法自行验证两种,我用的是第一种调用验签API,需要用到前面提到的Webhook ID,以此来作为消息验证,查看对应接口文档,参考代码如下

public bool VerifySign(string requestBody, Encoding encoding,string accessToken)
{
	string json = string.Empty;
    string webhook_id = "XXXXXXXXXXXXXXXXX";
    string url = "域名/v1/notifications/verify-webhook-signature";
    string transmission_id = Request.Headers["PAYPAL-TRANSMISSION-ID"];
    string transmission_time = Request.Headers["PAYPAL-TRANSMISSION-TIME"];
    string cert_url = Request.Headers["PAYPAL-CERT-URL"];
    string auth_algo = Request.Headers["PAYPAL-AUTH-ALGO"];
    string transmission_sig = Request.Headers["PAYPAL-TRANSMISSION-SIG"];
    string webhook_event = requestBody;

	json = "{\"transmission_id\":\"" + transmission_id + "\",\"transmission_time\":\"" + transmission_time + "\",\"cert_url\":\"" + cert_url + "\",";
    json += "\"auth_algo\":\"" + auth_algo + "\",\"transmission_sig\":\"" + transmission_sig + "\",\"webhook_id\":\"" + webhook_id + "\",";
    json += "\"webhook_event\":" + webhook_event + "}";

    var jsSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    var ret = PostJsonByBearer(url, json, encoding, accessToken);
    var retDic = jsSerializer.Deserialize<Dictionary<string, string>>(ret);

	if (retDic != null && retDic.ContainsKey("verification_status") && retDic["verification_status"].ToUpper().Equals("SUCCESS"))
    	return true;  //验证成功
    else    
        return false; //验证失败
}

public void PaypalNotify()
{
	string requestBody = string.Empty;
    Encoding encoding = Encoding.UTF8;
    using (StreamReader reader = new StreamReader(Request.InputStream, encoding))
    {
    	requestBody = reader.ReadToEnd();
    }

    var result = VerifySign(requestBody, encoding, accessToken);
    if (result)
    {
    	var data = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<WebhookEvent>(requestBody);
        if (data == null)
        	return;

        switch (data.event_type.ToUpper())
        {
        	case "CHECKOUT.ORDER.APPROVED":
                break;
        	case "PAYMENT.CAPTURE.COMPLETED":
             	break;
            default:
                break;
        }
    }
}

5.主动向PayPal确认收款及业务处理

支付回调验证通过后,我们在上面的"CHECKOUT.ORDER.APPROVED"事件中加入以下代码,先查询下账单的状态查看对应接口文档,当状态为"APPROVED"时,主动向paypal确认收款(批准账单)查看对应接口文档,账单完成后执行业务

var data = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<WebhookEvent>(requestBody);
//查询账单的详情
var ret = GetByBearer($"域名/v2/checkout/orders/{data.resource.id}", accessToken);
var retJson = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<Resource>(ret);
if (retJson.status.ToUpper().Equals("APPROVED"))
{
	//主动向paypal确认收款
    ret = PostJsonByBearer($"域名/v2/checkout/orders/{data.resource.id}/capture", "", encoding, accessToken);
    retJson = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize<Resource>(ret);
    if (retJson != null)
    {
    	if (retJson.status.ToUpper().Equals("COMPLETED"))
        {
            //执行业务
        }
    } 
}

四、注意点

1、域名包含沙盒地址和真实地址,分别对应https://api-m.sandbox.paypal.com和https://api-m.paypal.com;
2、在开发中中心配置WebHooks接收通知地址时要注意,只支持https;
3、需要主动向paypal确认收款已完成支付流程;
4、支付成功后执行业务放在主动向paypal确认收款后而不是PAYMENT.CAPTURE.COMPLETED事件,之所以这么说,是因为相比主动向paypal确认收款,PAYMENT.CAPTURE.COMPLETED事件有一定的延迟,这样就意味着你支付完后,可能在一定时间内,你的订单支付状态依旧是未支付,这也是前面我为啥说只订阅CHECKOUT.ORDER.APPROVED事件就够的原因,当然这个只是我个人的见解;
5、捕获PayPal接口异常时,不要直接try{}catch(Exception e){},这样永远只能得到“远程服务器返回错误: (404) 未找到”,而是要用try{}catch (WebException e){}获取响应体返回的内容(参考上面通用方法PostJsonByBearer);

五、易错点

1、发起支付请求,价格明细和商品明细的对应关系,详情看发起支付请求示例代码备注;
2、支付回调中调用验签API时,由于PayPal提交过来的webhook_event参数本身就是json字符串,在提交验签API的时候注意不要重复去序列化webhook_event参数;
3、不同货币有不同的要求,大部分支持小数,但像日元这种是不支持小数的,请点击货币代码查看,还有一点让我比较疑惑,人民币是不支持的,但是里面又有提供货币代码,好像是因为国内支持的是 贝宝 而不会 PayPal

总结

相比国内的其他支付,PayPal在开发文档和demo等资料上相对贫瘠,且在并不多的资料中还充斥着不少被弃用的v1版本,导致在对接过程中需要浪费不少时间去排雷,这次我以v2版本作为基础进行开发,整理出我在开发过程中遇到的问题以及经验,希望能帮助到有需要的人。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值