C#集成PayPal:支付成功后如何等待接收支付响应
非ASP.NET场景下PayPal支付结果接收实现方案
你当前使用的PayPal Checkout v2 .NET SDK本身不依赖ASP.NET框架,不需要照搬Web框架专属示例,支付结果通过「同步主动查询+异步Webhook兜底」两个机制配合接收即可,具体实现如下:
同步流程:用户支付完成后即时获取结果
- 你代码中
ApplicationContext配置的ReturnUrl是用户在PayPal端完成支付授权后,浏览器自动跳转的地址。该地址不需要是ASP.NET控制器路由,只要是你项目可处理HTTP请求的入口即可:不管是Discord机器人配套的简易HTTP监听端点、桌面程序内嵌的HTTP服务、控制台程序开放的公网接口都能适配。 - 用户跳回
ReturnUrl时,PayPal会在URL查询参数中携带token字段,对应你创建订单时生成的Order ID。绝对不要信任前端传递的「支付成功」标记,拿到Order ID后立刻在服务端调用你已经实现的CaptureOrder(OrderId)方法,主动向PayPal发起扣款请求:当返回结果的Status字段为COMPLETED时,才代表支付实际成功,可以给用户发放对应权益。 - 你测试代码中用
Console.Read()等待用户确认后再调用Capture的逻辑,本质就是模拟同步流程的触发时机,正式环境把人工触发替换成ReturnUrl入口的自动触发即可。 - 注意把占位的
https://www.example.com替换成你自己项目实际的跳转、取消地址。
异步兜底:Webhook通知防止丢单
仅靠同步跳转有概率丢单:比如用户支付完成后直接关闭浏览器、网络中断导致跳转失败,你将无法收到支付结果,必须配置Webhook做兜底:
- Webhook同样不依赖ASP.NET:只要你有一个公网可访问的HTTP POST接收端点,在PayPal开发者后台配置该端点,订阅
PAYMENT.CAPTURE.COMPLETED(扣款成功)、PAYMENT.CAPTURE.DENIED(扣款失败)、PAYMENT.CAPTURE.REFUNDED(退款完成)几个核心事件即可。 - 收到Webhook推送的POST请求后,先通过SDK内置的签名校验方法确认请求确实来自PayPal,再解析请求中携带的Order ID、Capture ID,和你本地存储的业务订单做匹配,更新订单状态。该机制和同步流程独立,哪怕用户没跳回你的地址,只要PayPal完成扣款就会给你发通知。
现有代码优化点
- 不要在异步方法中用
.Result阻塞获取返回值,会触发死锁,全链路使用await调用异步方法即可,比如测试方法里的CreateOrder(true).Result替换为await CreateOrder(true)。 PayPalClient.environment()方法中读取环境变量的键名为空,记得填入对应沙箱/正式环境的Client ID、Client Secret,正式环境将SandboxEnvironment替换为LiveEnvironment实例。- 创建订单拿到Order ID后,要和你自身系统的业务订单做绑定持久化存储,不要仅存在内存中,避免服务重启后订单关联丢失。
你当前的核心业务逻辑代码(创建订单、扣款、退款)已经可以直接复用,不需要针对非ASP.NET场景做特殊改造,只要补全上面的结果接收逻辑即可。
现有实现代码参考:
using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Runtime.Serialization.Json; using System.Text; using System.Threading.Tasks; using PayPalCheckoutSdk.Core; using PayPalCheckoutSdk.Orders; using PayPalCheckoutSdk.Payments; using PayPalHttp; namespace Discord.Modules { public class PaypalPayment { private static OrderRequest BuildRequestBody() { OrderRequest orderRequest = new OrderRequest() { CheckoutPaymentIntent = "CAPTURE", ApplicationContext = new ApplicationContext { BrandName = "EXAMPLE INC", LandingPage = "BILLING", CancelUrl = "https://www.example.com", ReturnUrl = "https://www.example.com", UserAction = "CONTINUE", ShippingPreference = "SET_PROVIDED_ADDRESS" }, PurchaseUnits = new List<PurchaseUnitRequest> { new PurchaseUnitRequest{ ReferenceId = "PUHF", Description = "Sporting Goods", CustomId = "CUST-HighFashions", SoftDescriptor = "HighFashions", AmountWithBreakdown = new AmountWithBreakdown { CurrencyCode = "USD", Value = "220.00", AmountBreakdown = new AmountBreakdown { ItemTotal = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "180.00" }, Shipping = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "20.00" }, Handling = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "10.00" }, TaxTotal = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "20.00" }, ShippingDiscount = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "10.00" } } }, Items = new List<Item> { new Item { Name = "T-shirt", Description = "Green XL", Sku = "sku01", UnitAmount = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "90.00" }, Tax = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "10.00" }, Quantity = "1", Category = "PHYSICAL_GOODS" }, new Item { Name = "Shoes", Description = "Running, Size 10.5", Sku = "sku02", UnitAmount = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "45.00" }, Tax = new PayPalCheckoutSdk.Orders.Money { CurrencyCode = "USD", Value = "5.00" }, Quantity = "2", Category = "PHYSICAL_GOODS" } }, ShippingDetail = new ShippingDetail { Name = new Name { FullName = "John Doe" }, AddressPortable = new AddressPortable { AddressLine1 = "123 Townsend St", AddressLine2 = "Floor 6", AdminArea2 = "San Francisco", AdminArea1 = "CA", PostalCode = "94107", CountryCode = "US" } } } } }; return orderRequest; } public async static Task<HttpResponse> CreateOrder(bool debug = false) { var request = new OrdersCreateRequest(); request.Headers.Add("prefer", "return=representation"); request.RequestBody(BuildRequestBody()); var client = PayPalClient.client(); var response = await client.Execute(request); if (debug) { var result = response.Result<Order>(); Console.WriteLine("Status: {0}", result.Status); Console.WriteLine("Order Id: {0}", result.Id); Console.WriteLine("Intent: {0}", result.CheckoutPaymentIntent); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Orders.LinkDescription link in result.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } AmountWithBreakdown amount = result.PurchaseUnits[0].AmountWithBreakdown; Console.WriteLine("Total Amount: {0} {1}", amount.CurrencyCode, amount.Value); Console.WriteLine("Response JSON: \n {0}", PayPalClient.ObjectToJSONString(result)); } return response; } public async static Task<HttpResponse> CaptureOrder(string OrderId, bool debug = false) { var request = new OrdersCaptureRequest(OrderId); request.Prefer("return=representation"); request.RequestBody(new OrderActionRequest()); var client = PayPalClient.client(); var response = await client.Execute(request); if (debug) { var result = response.Result<Order>(); Console.WriteLine("Status: {0}", result.Status); Console.WriteLine("Order Id: {0}", result.Id); Console.WriteLine("Intent: {0}", result.CheckoutPaymentIntent); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Orders.LinkDescription link in result.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } Console.WriteLine("Capture Ids: "); foreach (PurchaseUnit purchaseUnit in result.PurchaseUnits) { foreach (PayPalCheckoutSdk.Orders.Capture capture in purchaseUnit.Payments.Captures) { Console.WriteLine("\t {0}", capture.Id); } } AmountWithBreakdown amount = result.PurchaseUnits[0].AmountWithBreakdown; Console.WriteLine("Buyer:"); Console.WriteLine("\tEmail Address: {0}\n\tName: {1} {2}\n", result.Payer.Email, result.Payer.Name.GivenName, result.Payer.Name.Surname); Console.WriteLine("Response JSON:\n{0}", PayPalClient.ObjectToJSONString(result)); } return response; } public static async Task asdasd() { Console.WriteLine("Running Capture Intent Flow.."); var createOrderResponse = CreateOrder(true).Result; var createOrderResult = createOrderResponse.Result<Order>(); Console.WriteLine("Status: {0}", createOrderResult.Status); Console.WriteLine("Order Id: {0}", createOrderResult.Id); Console.WriteLine("Intent: {0}", createOrderResult.CheckoutPaymentIntent); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Orders.LinkDescription link in createOrderResult.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } AmountWithBreakdown amount = createOrderResult.PurchaseUnits[0].AmountWithBreakdown; Console.WriteLine("Total Amount: {0} {1}", amount.CurrencyCode, amount.Value); Console.WriteLine("Copy approve link and paste it in browser. Login with buyer account and follow the instructions.\nOnce approved hit enter...\n"); Console.Read(); Console.WriteLine("Capturing the payment..."); var captureOrderResponse = CaptureOrder(createOrderResult.Id, true).Result; var captureOrderResult = captureOrderResponse.Result<Order>(); var captureId = ""; Console.WriteLine("Status: {0}", captureOrderResult.Status); Console.WriteLine("Order Id: {0}", captureOrderResult.Id); Console.WriteLine("Intent: {0}", captureOrderResult.CheckoutPaymentIntent); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Orders.LinkDescription link in captureOrderResult.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } foreach (PurchaseUnit purchaseUnit in captureOrderResult.PurchaseUnits) { foreach (PayPalCheckoutSdk.Orders.Capture capture in purchaseUnit.Payments.Captures) { captureId = capture.Id; } } AmountWithBreakdown captureAmount = captureOrderResult.PurchaseUnits[0].AmountWithBreakdown; Console.WriteLine("Buyer:"); Console.WriteLine("\tEmail Address: {0}\n\tName: {1} {2}", captureOrderResult.Payer.Email, captureOrderResult.Payer.Name.GivenName, captureOrderResult.Payer.Name.Surname); Console.WriteLine("Refunding the Order...."); var refundOrderResponse = CapturesRefund(captureId, true).Result; var refundOrderResult = refundOrderResponse.Result<PayPalCheckoutSdk.Payments.Refund>(); Console.WriteLine("Status: {0}", refundOrderResult.Status); Console.WriteLine("Refund Id: {0}", refundOrderResult.Id); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Payments.LinkDescription link in refundOrderResult.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } } public async static Task<HttpResponse> CapturesRefund(string CaptureId, bool debug = false) { var request = new CapturesRefundRequest(CaptureId); request.Prefer("return=representation"); RefundRequest refundRequest = new RefundRequest() { Amount = new PayPalCheckoutSdk.Payments.Money { Value = "20.00", CurrencyCode = "USD" } }; request.RequestBody(refundRequest); var response = await PayPalClient.client().Execute(request); if (debug) { var result = response.Result<PayPalCheckoutSdk.Payments.Refund>(); Console.WriteLine("Status: {0}", result.Status); Console.WriteLine("Refund Id: {0}", result.Id); Console.WriteLine("Links:"); foreach (PayPalCheckoutSdk.Payments.LinkDescription link in result.Links) { Console.WriteLine("\t{0}: {1}\tCall Type: {2}", link.Rel, link.Href, link.Method); } Console.WriteLine("Response JSON: \n {0}", PayPalClient.ObjectToJSONString(result)); } return response; } } public class PayPalClient { /** Setting up PayPal environment with credentials with sandbox cerdentails. For Live, this should be LiveEnvironment Instance. */ public static PayPalEnvironment environment() { return new SandboxEnvironment( System.Environment.GetEnvironmentVariable("") != null ? System.Environment.GetEnvironmentVariable("") : "", System.Environment.GetEnvironmentVariable("") != null ? System.Environment.GetEnvironmentVariable("") : ""); } /** Returns PayPalHttpClient instance which can be used to invoke PayPal API's. */ public static HttpClient client() { return new PayPalHttpClient(environment()); } public static HttpClient client(string refreshToken) { return new PayPalHttpClient(environment(), refreshToken); } /** This method can be used to Serialize Object to JSON string. */ public static String ObjectToJSONString(Object serializableObject) { MemoryStream memoryStream = new MemoryStream(); var writer = JsonReaderWriterFactory.CreateJsonWriter( memoryStream, Encoding.UTF8, true, true, " "); DataContractJsonSerializer ser = new DataContractJsonSerializer(serializableObject.GetType(), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); ser.WriteObject(writer, serializableObject); memoryStream.Position = 0; StreamReader sr = new StreamReader(memoryStream); return sr.ReadToEnd(); } } }
内容的提问来源于stack exchange,提问作者FanLife
相关产品推荐
相关产品推荐

