Paypal 实现跨境支付
springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)
摘要 Paypal支付对接(V2)
1、商家注册Paypal账号
1.注册前准备
2.注册完成后验证
二、登录开发者中心
使用上一步注册好的账号,直接登录开发者中心. https://developer.paypal.com
1.点击右上角的按钮 “Dashboard”
点击完成后显示
2.在沙箱中,创建两个测试账号
1.在左边的导航栏中点击 Sandbox 下的 Accounts
2.进入Acccouts界面后,可以看到系统有两个已经生成好的测试账号,但是我们不要用系统给的测试账号,很卡的,自己创建两个
3.点击右上角的“Create Account”,创建测试用户
创建一个北美的Personal账户
创建一个China 的商家账号
商家和个人,创建,直接点击创建就自动创建完成了,默认商家/个人账号有 5000美元
再创建两个账号,分别是北美的商家、个人账号
4.登录创建好的沙箱账户
沙箱登录地址: https://www.sandbox.paypal.com
3.创建应用,先在沙盒中创建应用
还是在开发者中心. https://developer.paypal.com
在 My Apps & Credentials ----> Sandbox ------> Create App
App Name : 应用的名称
App Type:
// 商家 - 以商家(卖家)身份接受付款。
Merchant – Accept payments as a merchant (seller)// 平台--以平台(市场、众筹或电商平台)的形式将款项转移给卖家
Platform – Move payments to sellers as a platform (marketplace, crowdfunding, or e-commerce platform)Sandbox Business Account: 选择一个创建的沙盒商家账户
4.clientId 和 clientSecret 的获取
点击创建
Client ID 就是 clientId , Secret 就是 clientSecret
5.应用详情页下方创建WEBHOOKS
然后点击 Add Webhook
点击确定即可
三、Java支付对接
支付方式:
paypal提供了多种支付方式,如标准支付和快速支付,其中标准支付誉为最佳实践。
标准支付主要特点是只需要集成paypal按钮,所有的付款流程由paypal控制,接入方不需要关心支付细节。当用户完成支付后,paypal会通过同步PDT或者异步IPN机制来通知接入方,这种方式比较轻量级,对接难度最小,而且对借入方的入侵较小。
快速支付相对复杂,支付过程由接入方控制,通过调用3个接口来实现。从接入方网页跳转到paypal支付页面前,第一个接口触发,用于向paypal申请支付token。接着用户进入paypal支付页面,并进行支付授权,授权接口中会提交上一步获取的支付token,这个过程由paypal控制,并且没有进行实际支付,接着接入方调用第二个接口,用于获取用户的授权信息,包括支付金额,支付产品等信息,这些基础信息核对无误后,调用第三个接口,进行实际付费扣款,扣款成功后,paypal同样会进行同步PDT和异步IPN通知,这种方式很灵活,控制力强,但编码复杂度较高,入侵性较大。从实际情况考虑,我们选择了采标标准支付方式接入paypal支付。
通知方式:
paypal支付的IPN和PDT两种通知方式,IPN异步通知,可能会有时延,但可靠性高,当接入方主机不可达时,有重试机制保证IPN通知尽量抵达接入方服务器。接入方收到IPN通知后,需要对其确认。确认方法为,把接收到的IPN通知原封不动的作为请求体,调用IPN确认接口。PDT通知是是实时的,但可靠性不高,因为只会通知一次,没有重试机制,一旦接入方出现主机不可达,这样的消息将会被丢失。官方推荐,IPN通知和PDT通知最好混合使用,以满足时效性和可靠性的保证。我们采用了IPN和PDT两种通知机制。
Demo 网址:https://demo.paypal.com/c2/demo/home
V1 版本已过期,我们使用V2对接支付
v1文档:https://developer.paypal.com/docs/api/payments/v1/
V2文档: https://developer.paypal.com/docs/api/payments/v2/
四、直接上代码
系统使用创建的默认美元货币支付
不多说我们直接上代码
maven参数引入
<!--paypal-->
<dependency>
<groupId>com.paypal.sdk</groupId>
<artifactId>rest-api-sdk</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.paypal.sdk</groupId>
<artifactId>checkout-sdk</artifactId>
<version>1.0.2</version>
</dependency>
PayPalConfig.class
package com.xxxx.config;
import cn.hutool.core.util.StrUtil;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.OAuthTokenCredential;
import com.paypal.base.rest.PayPalRESTException;
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import com.paypal.http.HttpResponse;
import com.paypal.orders.*;
import com.paypal.payments.CapturesGetRequest;
import com.paypal.payments.CapturesRefundRequest;
import com.paypal.payments.RefundRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.*;
/**
* 通过微信配置获取微信的支付信息,登陆信息等
* @author cl
*/
@Slf4j
@Component
@AllArgsConstructor
public class PayPalConfig {
private static final String clientId = "xxxx";
private static final String clientSecret = "xxxx";
// sandbox 沙箱/ live 正式环境
private static final String mode = "sandbox";
private static final String PP_SUCCESS = "success";
/**
* approved 获准
*/
public static final String APPROVED = "approved";
/**
* sandbox 沙箱or live生产
*/
public static final String PAYPAY_MODE = "live";
/**
* PayPal 取消支付回调地址
*/
public static final String PAYPAL_CANCEL_URL = "/result/pay/cancel";
/**
* PayPal 取消支付回调地址
*/
public static final String PAYPAL_SUCCESS_URL = "/result/pay/success";
/**
* 当前货币币种简称, 默认为人名币的币种 EUR CNY USD
*/
public static final String CURRENTCY = "USD";
/**
* approval_url 验证url
*/
public static final String APPROVAL_URL = "approval_url";
public static final String CAPTURE = "CAPTURE";
public static final String BRANDNAME = "Supernote";
public static final String LANDINGPAGE = "NO_PREFERENCE";
public static final String USERACTION = "PAY_NOW";
public static final String SHIPPINGPREFERENCE = "SET_PROVIDED_ADDRESS";
public static final String COMPLETED = "COMPLETED";
public Map<String, String> paypalSdkConfig(){
Map<String, String> sdkConfig = new HashMap<>(16);
sdkConfig.put("mode", mode);
return sdkConfig;
}
public OAuthTokenCredential authTokenCredential(){
return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
}
public PayPalHttpClient client() {
PayPal payPal = shopConfig.getPayPal();
String mode = payPal.getMode();
String clientId = payPal.getClientId();
String clientSecret = payPal.getClientSecret();
PayPalEnvironment environment = StrUtil.equals("live",mode) ?
new PayPalEnvironment.Live(clientId, clientSecret) :
new PayPalEnvironment.Sandbox(clientId, clientSecret);
return new PayPalHttpClient(environment);
}
/**
* 生成paypal支付订单
*/
public OrdersCreateRequest createPayPalOrder(PayInfoDto payInfo) {
OrdersCreateRequest request = new OrdersCreateRequest();
request.header("Content-Type","application/json");
OrderRequest orderRequest = new OrderRequest();
orderRequest.checkoutPaymentIntent(Constant.CAPTURE);
com.paypal.orders.ApplicationContext payPalApplicationContext = new com.paypal.orders.ApplicationContext()
.brandName("Shopping")
.landingPage(NO_PREFERENCE)
.cancelUrl(PAYPAL_CANCEL_URL)
.returnUrl(PAYPAL_SUCCESS_URL)
.userAction("PAY_NOW")
;
orderRequest.applicationContext(payPalApplicationContext);
List<PurchaseUnitRequest> purchaseUnitRequests = new ArrayList<>();
PurchaseUnitRequest purchaseUnitRequest = new PurchaseUnitRequest()
.description("Shopping")
// 生成的系统生成的订单交易号
.customId(payInfo.getPayNo())
.invoiceId(payInfo.getPayNo())
.amountWithBreakdown(new AmountWithBreakdown()
.currencyCode(Constant.CURRENTCY)
// value = itemTotal + shipping + handling + taxTotal + shippingDiscount
.value(payInfo.getPayAmount().toString())
);
purchaseUnitRequests.add(purchaseUnitRequest);
orderRequest.purchaseUnits(purchaseUnitRequests);
request.requestBody(orderRequest);
return request;
}
/**
* 创建订单
*/
public String getExcetuteHref (OrdersCreateRequest request) {
HttpResponse<Order> response = null;
try {
response = client().execute(request);
} catch (IOException e) {
log.error("调用paypal订单创建失败,失败原因:{}", e.getMessage());
throw new YamiShopBindException("paypal请求支付失败");
}
String href = "";
if (response.statusCode() == 201) {
for (LinkDescription link : response.result().links()) {
if(link.rel().equals("approve")) {
href = link.href();
}
}
}
return href;
}
/**
* 用户授权支付成功,进行扣款操作
* 用户通过CreateOrder生成 approveUrl 跳转paypal支付成功后,只是授权,并没有将用户的钱打入我们的paypal账户,
* 我们需要通过 CaptureOrder接口,将钱打入我的PayPal账户
*/
public PayInfoBo captureOrder(String token){
PayInfoBo payInfoBo = new PayInfoBo();
OrdersCaptureRequest request = new OrdersCaptureRequest(token);
request.requestBody(new OrderRequest());
HttpResponse<Order> response = null;
try {
response = client().execute(request);
} catch (IOException e1) {
log.error("调用paypal扣款失败,失败原因 {}", e1.getMessage() );
}
payInfoBo.setBizOrderNo(response.result().id());
for (PurchaseUnit purchaseUnit : response.result().purchaseUnits()) {
for (Capture capture : purchaseUnit.payments().captures()) {
log.info("Capture id: {}", capture.id());
log.info("status: {}", capture.status());
log.info("invoice_id: {}", capture.invoiceId());
//paypal交易号
payInfoBo.setBizPayNo(capture.id());
//商户订单号,之前生成的带用户ID的订单号
payInfoBo.setPayNo(capture.invoiceId());
payInfoBo.setIsPaySuccess(false);
if("COMPLETED".equals(capture.status())) {
//支付成功,状态为=COMPLETED
payInfoBo.setIsPaySuccess(true);
payInfoBo.setSuccessString(PP_SUCCESS);
}
if("PENDING".equals(capture.status())) {
log.info("status_details: {}", capture.captureStatusDetails().reason());
String reason = "PENDING";
if(capture.captureStatusDetails() != null && capture.captureStatusDetails().reason() != null) {
reason = capture.captureStatusDetails().reason();
}
// 支付成功,状态为=PENDING
log.info("支付成功,状态为=PENDING : {}", reason);
payInfoBo.setIsPaySuccess(true);
}
}
}
return payInfoBo;
}
/**
* 支付后查询扣款信息
*/
public Boolean getCapture(String captureId) {
// 扣款查询
CapturesGetRequest restRequest = new CapturesGetRequest(captureId);
HttpResponse<com.paypal.payments.Capture> response = null;
try {
response = client().execute(restRequest);
} catch (IOException e) {
log.info("查询支付扣款失败:{}",e.getMessage());
return false;
}
log.info("Capture ids: " + response.result().id());
return true;
}
Controller中代码
/**
* 支付接口
*/
@PostMapping("/pay")
@SneakyThrows
public String pay(HttpServletResponse httpResponse,@Valid @RequestBody PayParam payParamPayInfo payInfo) {
payInfo.setApiNoticeUrl(notifyUrl);
OrdersCreateRequest request = payPalConfig.createPayPalOrder(payInfo);
String href = payPalConfig.getExcetuteHref(request);
return href;
}
支付后通知
PayNotice.class
/**
* V2:
* 客户登陆付款后paypal返回路径参数示例
* https://xxxx/p//result/pay/success?token=9DT13847SW4445928&PayerID=B2YKY8DYERF46
*/
@RequestMapping("/result/pay/success")
public String successPayPal(@RequestParam("token") String token, @RequestParam("PayerID") String PayerID){
// 执行扣款
PayInfoBo payInfoBo = payPalConfig.captureOrder(token);
// 查询扣款信息, 需要用到第三方交易号 29911292F3566070S
payPalConfig.getCapture(payInfoBo.getBizPayNo());
// 扣款成功后将订单改成已支付状态,执行支付成功业务
return ResponseEntity.ok(payInfoBo.toString());
}
/**
* 取消接口
*/
@RequestMapping("/result/pay/cancel")
public String successPayPal(@RequestParam("token") String token, @RequestParam("PayerID") String PayerID){
// 取消订单业务
return ResponseEntity.ok("cancel");
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。