net5 接入 paypal支付 v2
paypal支付流程
1、创建订单(Checkout) => 获取订单中返回的rel=approve的连接,这是需要用户授权的账单 => 用户授权后paypal回调CHECKOUT.ORDER.APPROVED事件,账单授权后服务端发起扣款请求(Capture)=> paypal回调CAPTURE.ORDER.COMPLETE事件 => 完成
具体可参考以下代码 paypal sdk地址 https://github.com/paypal/Checkout-NET-SDK
using Castle.Core.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Payment.Paypal.CaptureOrder; using PayPalCheckoutSdk.Core; using PayPalCheckoutSdk.Orders; using PayPalCheckoutSdk.Payments; using PayPalHttp; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Util; namespace Payment.Paypal { /// <summary> /// paypal支付帮助类 /// </summary> public class PaypalHelper { private static IConfiguration _configuration; private ILogger<PaypalHelper> _logger; private IHttpContextAccessor _httpContextAccessor; private static string _clientId; private static string _clientSecret; public PaypalHelper(ILogger<PaypalHelper> logger, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) { _configuration = configuration; _clientId = configuration["Payments:Paypal:ClientId"]; _clientSecret = configuration["Payments:Paypal:ClientSecret"]; _logger = logger; _httpContextAccessor = httpContextAccessor; } //paypal client private static HttpClient GetClient() { var isSandboxEnvironment = _configuration["Payments:Paypal:IsSandboxEnvironment"]; if (isSandboxEnvironment.IsNullOrEmpty()) throw new Exception("paypal环境未配置,请配置环境"); PayPalEnvironment environment; if (isSandboxEnvironment.ParseToBool()) environment = new SandboxEnvironment(_clientId, _clientSecret); else environment = new LiveEnvironment(_clientId, _clientSecret); return new PayPalHttpClient(environment); } /// <summary> /// 1.创建订单(https://developer.paypal.com/docs/api/orders/v2/#orders_create) /// </summary> public async Task<string> CreateOrderAsync(string tradeNo, decimal amount, string returnUrl) { var rate = _configuration["Rates:CNY:USD"].ParseToDecimal(); PayPalHttp.HttpResponse response; var order = new OrderRequest() { CheckoutPaymentIntent = "CAPTURE", PurchaseUnits = new List<PurchaseUnitRequest>() { new PurchaseUnitRequest() { ReferenceId = tradeNo, AmountWithBreakdown = new AmountWithBreakdown { Value = Math.Round(amount * rate, 2).ToString(), CurrencyCode = "USD" } } }, ApplicationContext = new ApplicationContext { ReturnUrl = returnUrl } }; // Call API with your client and get a response for your call var request = new OrdersCreateRequest(); request.Prefer("return=representation"); request.RequestBody(order); response = await GetClient().Execute(request); if (response.StatusCode == HttpStatusCode.Created) { var result = response.Result<Order>(); return result.Links.FirstOrDefault(p => p.Rel == "approve").Href; } _logger.LogError($"创建paypal订单失败:{JsonConvert.SerializeObject(response.Result<Order>())}"); return string.Empty; } /// <summary> /// 2.捕获订单(https://developer.paypal.com/docs/api/orders/v2/#orders_capture) /// </summary> public async Task<bool> CaptureOrderAsync(string orderId) { if (!string.IsNullOrEmpty(orderId)) { var request = new OrdersCaptureRequest(orderId); request.Prefer("return=representation"); request.RequestBody(new OrderActionRequest()); var response = await GetClient().Execute(request); if (response.StatusCode == HttpStatusCode.Created) { var result = response.Result<Order>(); return true; } _logger.LogError($"捕获paypal订单失败:{JsonConvert.SerializeObject(response.Result<Order>())}"); } _logger.LogError($"捕获paypal订单失败:根据tradeNo查询orderId无数据 tradeNo:{orderId} orderId:{orderId}"); return false; } /// <summary> /// 3.退款(https://developer.paypal.com/docs/api/payments/v2/#refunds) /// </summary> public async Task RefundOrderAsync(string captureId) { var request = new CapturesRefundRequest(captureId); request.Prefer("return=representation"); var response = await GetClient().Execute(request); if (response.StatusCode == HttpStatusCode.Created) { var result = response.Result<PayPalCheckoutSdk.Payments.Refund>(); } } /// <summary> /// 支付回调验签,此方法参考paypal sdk netframework版本的实现 https://github.com/paypal/sdk-core-dotnet /// </summary> public async Task<bool> VerifySig(string requestBody = "") { var _webHookId = _configuration["Payments:Paypal:WebHookId"]; var transmissionId = _httpContextAccessor.HttpContext.Request.Headers["Paypal-Transmission-Id"][0]; var transmissionTime = _httpContextAccessor.HttpContext.Request.Headers["Paypal-Transmission-Time"][0]; var algo = _httpContextAccessor.HttpContext.Request.Headers["Paypal-Auth-Algo"][0].Replace("withRSA", ""); var certUrl = _httpContextAccessor.HttpContext.Request.Headers["Paypal-Cert-Url"][0]; var sig = _httpContextAccessor.HttpContext.Request.Headers["Paypal-Transmission-Sig"][0]; if (string.IsNullOrEmpty(requestBody)) { using var reader = new StreamReader(_httpContextAccessor.HttpContext.Request.Body); requestBody = await reader.ReadToEndAsync(); } var cert = CertificateManager.GetCertificateFromUrl(certUrl); var crc32 = Crc32.ComputeChecksum(requestBody); var str = $"{transmissionId}|{transmissionTime}|{_webHookId}|{crc32}"; var bytes = Encoding.UTF8.GetBytes(str); return cert.GetRSAPublicKey().VerifyData(bytes, Convert.FromBase64String(sig), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } /// <summary> /// 回调事件处理 /// </summary> /// <param name="caFunc">账单授权完成处理逻辑</param> /// <param name="ccFunc">支付完成处理逻辑</param> /// <returns></returns> public async Task<bool> CallbackEventHandle(Func<CheckoutApproved, bool> caFunc, Func<CaptureCompleted, bool> ccFunc) { using var reader = new StreamReader(_httpContextAccessor.HttpContext.Request.Body); var requestBody = await reader.ReadToEndAsync(); //验签 var valid = await VerifySig(requestBody); if (valid) { var eventType = JsonConvert.DeserializeObject<BaseEventType>(requestBody); switch (eventType.event_type) { //账单已授权 case "CHECKOUT.ORDER.APPROVED": var checkoutApproved = JsonConvert.DeserializeObject<CheckoutApproved>(requestBody); return caFunc.Invoke(checkoutApproved); //支付已完成 case "PAYMENT.CAPTURE.COMPLETED": var captureCompleted = JsonConvert.DeserializeObject<CaptureCompleted>(requestBody); return ccFunc.Invoke(captureCompleted); } } _logger.LogError($"验签失败"); return false; } } }
参考paypal sdk netframework的实现 https://github.com/paypal/sdk-core-dotnet
using System; using System.Collections.Concurrent; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text; namespace Payment.Paypal { public static class CertificateManager { private static ConcurrentDictionary<string, X509Certificate2> certificates; /// <summary> /// 从url获取证书 /// </summary> public static X509Certificate2 GetCertificateFromUrl(string certUrl) { if (certificates == null) certificates = new ConcurrentDictionary<string, X509Certificate2>(); if (!certificates.ContainsKey(certUrl)) { var certString = string.Empty; using (var client = new WebClient()) { certString = client.DownloadString(certUrl); } string[] array = certString.Split(new string[2] { "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----" }, StringSplitOptions.RemoveEmptyEntries); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text2 = array2[i].Trim(); if (!string.IsNullOrEmpty(text2)) { var x509Certificate = new X509Certificate2(Encoding.UTF8.GetBytes(text2)); if (x509Certificate.Verify()) { certificates.TryAdd(certUrl, x509Certificate); } } } } return certificates[certUrl]; } } }
参考paypal sdk netframework的实现 https://github.com/paypal/sdk-core-dotnet
using System.Text; namespace Payment.Paypal { public class Crc32 { private static uint[] table = new uint[256] { 0u, 1996959894u, 3993919788u, 2567524794u, 124634137u, 1886057615u, 3915621685u, 2657392035u, 249268274u, 2044508324u, 3772115230u, 2547177864u, 162941995u, 2125561021u, 3887607047u, 2428444049u, 498536548u, 1789927666u, 4089016648u, 2227061214u, 450548861u, 1843258603u, 4107580753u, 2211677639u, 325883990u, 1684777152u, 4251122042u, 2321926636u, 335633487u, 1661365465u, 4195302755u, 2366115317u, 997073096u, 1281953886u, 3579855332u, 2724688242u, 1006888145u, 1258607687u, 3524101629u, 2768942443u, 901097722u, 1119000684u, 3686517206u, 2898065728u, 853044451u, 1172266101u, 3705015759u, 2882616665u, 651767980u, 1373503546u, 3369554304u, 3218104598u, 565507253u, 1454621731u, 3485111705u, 3099436303u, 671266974u, 1594198024u, 3322730930u, 2970347812u, 795835527u, 1483230225u, 3244367275u, 3060149565u, 1994146192u, 31158534u, 2563907772u, 4023717930u, 1907459465u, 112637215u, 2680153253u, 3904427059u, 2013776290u, 251722036u, 2517215374u, 3775830040u, 2137656763u, 141376813u, 2439277719u, 3865271297u, 1802195444u, 476864866u, 2238001368u, 4066508878u, 1812370925u, 453092731u, 2181625025u, 4111451223u, 1706088902u, 314042704u, 2344532202u, 4240017532u, 1658658271u, 366619977u, 2362670323u, 4224994405u, 1303535960u, 984961486u, 2747007092u, 3569037538u, 1256170817u, 1037604311u, 2765210733u, 3554079995u, 1131014506u, 879679996u, 2909243462u, 3663771856u, 1141124467u, 855842277u, 2852801631u, 3708648649u, 1342533948u, 654459306u, 3188396048u, 3373015174u, 1466479909u, 544179635u, 3110523913u, 3462522015u, 1591671054u, 702138776u, 2966460450u, 3352799412u, 1504918807u, 783551873u, 3082640443u, 3233442989u, 3988292384u, 2596254646u, 62317068u, 1957810842u, 3939845945u, 2647816111u, 81470997u, 1943803523u, 3814918930u, 2489596804u, 225274430u, 2053790376u, 3826175755u, 2466906013u, 167816743u, 2097651377u, 4027552580u, 2265490386u, 503444072u, 1762050814u, 4150417245u, 2154129355u, 426522225u, 1852507879u, 4275313526u, 2312317920u, 282753626u, 1742555852u, 4189708143u, 2394877945u, 397917763u, 1622183637u, 3604390888u, 2714866558u, 953729732u, 1340076626u, 3518719985u, 2797360999u, 1068828381u, 1219638859u, 3624741850u, 2936675148u, 906185462u, 1090812512u, 3747672003u, 2825379669u, 829329135u, 1181335161u, 3412177804u, 3160834842u, 628085408u, 1382605366u, 3423369109u, 3138078467u, 570562233u, 1426400815u, 3317316542u, 2998733608u, 733239954u, 1555261956u, 3268935591u, 3050360625u, 752459403u, 1541320221u, 2607071920u, 3965973030u, 1969922972u, 40735498u, 2617837225u, 3943577151u, 1913087877u, 83908371u, 2512341634u, 3803740692u, 2075208622u, 213261112u, 2463272603u, 3855990285u, 2094854071u, 198958881u, 2262029012u, 4057260610u, 1759359992u, 534414190u, 2176718541u, 4139329115u, 1873836001u, 414664567u, 2282248934u, 4279200368u, 1711684554u, 285281116u, 2405801727u, 4167216745u, 1634467795u, 376229701u, 2685067896u, 3608007406u, 1308918612u, 956543938u, 2808555105u, 3495958263u, 1231636301u, 1047427035u, 2932959818u, 3654703836u, 1088359270u, 936918000u, 2847714899u, 3736837829u, 1202900863u, 817233897u, 3183342108u, 3401237130u, 1404277552u, 615818150u, 3134207493u, 3453421203u, 1423857449u, 601450431u, 3009837614u, 3294710456u, 1567103746u, 711928724u, 3020668471u, 3272380065u, 1510334235u, 755167117u }; public static uint ComputeChecksum(string text) { uint num = uint.MaxValue; byte[] bytes = Encoding.UTF8.GetBytes(text); for (int i = 0; i < bytes.Length; i++) { num = (table[(num ^ bytes[i]) & 0xFF] ^ (num >> 8)); } return ~num; } } }

浙公网安备 33010602011771号