Paypal 支付接入
序:参考了现有许多博客,大多数的开发者最终选择RestFulAPi进行Paypal的接入,于是笔者在多方面比较后结合自身业务的实际情况终采用这种方式接入。
认识Paypal
PayPal是倍受全球亿万用户追捧的国际贸易支付工具,即时支付,即时到账,全中文操作界面,能通过中国的本地银行轻松提现,解决外贸收款难题,助您成功开展海外业务,决胜全球。注册PayPal后就可立即开始接受信用卡付款。作为在线付款服务商,PayPal是您向全世界近2.54亿的用户敞开大门的最快捷的方式。 最大的好处是,注册完全免费!集国际流行的信用卡,借记卡,电子支票等支付方式于一身。帮助买卖双方解决各种交易过程中的支付难题。PayPal是名副其实的全球化支付平台,服务范围超过200个市场,支持的币种超过100个。在跨国交易中,将近70%的在线跨境买家更喜欢用PayPal支付海外购物款项。
简而言之,就是应用于国际支付场景下的第三方支持多货币交易的支付工具。
几个概念
- Paypal Checkout
- RestFul API
- IPN
Paypal Checkout是贝宝公司在2018年后推出的第二代(V2)快速接入支持方式,通过数行简单的HTML代码嵌入到商户的WEB支付场景页面携带相关参数,通过JS等方式的调用以比较好的体验完成支付。
RestFul API 是由贝宝公司在2012年从传统SOA接口到当下主流RestFul风格接口的第一个版本(V1),开发方式由商家自主开发页面组成表单然后提交到自己的服务器再向Paypal发起支付下单,用户在Paypal完成支付后继而跳转回商户的业务中来。整个流程可控行和定制型比较强,出来的时间比较久技术偏成熟。
IPN 全称为 Instant Payment Notification 即使付款通知,也是Paypal最简单的网站标准付款方式,通过在WEB页面组成的表单直接进行调用官方接口下单不需要请求自己的后台。完成支付后,Paypal会异步循环去通知商户提前设置好的通知地址,通知地址接收到通知后对通知中的业务参数进行甄别判断此订单支付状态再进行具体业务的处理。
Paypal RestFul 接入流程
在集成paypal支付接口之前,首先要有一系列的准备,开发者账号啊、sdk、测试环境等等先要有,然后再码代码。这里顺便提下paypal webhook的特点,所谓webhook可以翻译成“钩子”。也可以理解成支付宝、微信等国内第三方支付异步通知,它的机制是三天内重复发25次。收到HTTP200响应码就会认为用户的业务处理成功,如果是其他响应码则认为不成功。我在业务中错误响应返回的是500,当然其他的也可以。没有接受到成功响应码paypal的webhook系统则进入重发机制。
具体集成的步骤如下:
-
环境准备
注册paypal账号
注册paypal开发者账号
创建两个测试用户
创建应用,生成用于测试的clientID 和 密钥
-
代码集成
Spring 环境
pom引进paypal-sdk的jar包
码代码
测试
后言
现在开始
- 注册paypal账号
- 在浏览器输入“https://www.paypal.com” 跳转到如下界面,点击右上角的注册
- 创建账户
- “创建商家用户”,根据要求填写信息,一分钟的事,注册完得去邮箱激活
- 登录到paypal开发者
- 在浏览器输入“https://developer.paypal.com”,点击右上角的“Log into Dashboard”,用上一步创建好的账号登录
- 创建自己的应用
- 配置自己的webHookID和通知地址,通知地址必须是https的。这里推荐使用ngrok内网穿透(稳定免费,同时支持http和https)进行调试开发。
- 创建一个自己用来测试支付的用户,收款账户就使用系统默认创建的就可以了
代码编写
- 工程中引入Paypal的rest-api-sdk
- 在maven官网可以搜索到,当然也可以在Paypal官网中找到。
<dependency>
<groupId>com.paypal.sdk</groupId>
<artifactId>rest-api-sdk</artifactId>
<version>1.14.0</version>
</dependency>
- 创建Paypal配置相关文件
- 枚举支付目的
public enum PaypalPaymentIntent {
sale, authorize, order
}
- 枚举支付方式
public enum PaypalPaymentMethod {
credit_card, paypal
}
- 工具类
/**
* 获取url
*/
public class URLUtils {
public static String getBaseURl(HttpServletRequest request) {
String scheme = request.getScheme();
String serverName = request.getServerName();
int serverPort = request.getServerPort();
String contextPath = request.getContextPath();
StringBuffer url = new StringBuffer();
url.append(scheme).append("://").append(serverName);
if ((serverPort != 80) && (serverPort != 443)) {
url.append(":").append(serverPort);
}
url.append(contextPath);
if(url.toString().endsWith("/")){
url.append("/");
}
return url.toString();
}
}
- 核心配置
public class PaypalConfig {
/**
* 正式模式
*/
public static final String PAYPAl_MODE_PRO = "live";
/**
* 开发模式
*/
public static final String PAYPAl_MODE_DEV = "sandbox";
private String clientId = "{你的应用编号}";
private String clientSecret = "{你的秘钥}";
/**
* sandbox 沙盒 live 生产
*/
private String mode = PAYPAl_MODE_DEV;
private APIContext apiContext = new APIContext(clientId, clientSecret, mode);
public APIContext getApiContext(){
return apiContext;
}
/**
* WEBHOOK_ID
*/
public static final String WEBHOOK_ID= ConfigProperties.getInstance().getValue("{webhookId}");
}
- Service构建
- 接口
/**
* paypal支付
*/
public interface PaypalService {
/**
* 创建支付
* @return Payment
*/
Payment createPayment(String extraParam,String orderNo,Double total,
String currency,
PaypalPaymentMethod method,
PaypalPaymentIntent intent,
String description,
String cancelUrl,
String successUrl)throws PayPalRESTException;
/**
* 执行支付
* @return Payment
*/
Payment executePayment(String paymentId, String payerId) throws PayPalRESTException;
/**
* webhook数据验证
* @return
*/
Boolean webhookValidate(String body,HttpServletRequest request) throws PayPalRESTException, NoSuchAlgorithmException, InvalidKeyException, SignatureException;
}
- 实现
@Service
public class PaypalServiceImpl implements PaypalService {
@Override
public Payment createPayment(String extraParam,String orderNo,Double total, String currency, PaypalPaymentMethod method, PaypalPaymentIntent intent,
String description, String cancelUrl, String successUrl) throws PayPalRESTException {
Amount amount = new Amount();
amount.setCurrency(currency);
amount.setTotal(String.format("%.2f", total));
Transaction transaction = new Transaction();
transaction.setDescription(description);
transaction.setAmount(amount);
transaction.setInvoiceNumber(orderNo);
transaction.setCustom(extraParam);
List<Transaction> transactions = new ArrayList<>();
transactions.add(transaction);
Payer payer = new Payer();
payer.setPaymentMethod(method.toString());
Payment payment = new Payment();
payment.setIntent(intent.toString());
payment.setPayer(payer);
payment.setTransactions(transactions);
RedirectUrls redirectUrls = new RedirectUrls();
redirectUrls.setCancelUrl(cancelUrl);
redirectUrls.setReturnUrl(successUrl);
payment.setRedirectUrls(redirectUrls);
return payment.create(new PaypalConfig().getApiContext());
}
@Override
public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException {
Payment payment = new Payment();
payment.setId(paymentId);
PaymentExecution paymentExecute = new PaymentExecution();
paymentExecute.setPayerId(payerId);
return payment.execute(new PaypalConfig().getApiContext(), paymentExecute);
}
@Override
public Boolean webhookValidate(String body,HttpServletRequest request) throws PayPalRESTException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
APIContext apiContext = new PaypalConfig().getApiContext();
apiContext.addConfiguration(Constants.PAYPAL_WEBHOOK_ID, PaypalConfig.WEBHOOK_ID);
Boolean result = Event.validateReceivedEvent(apiContext,getHeadersInfo(request), body);
return result;
}
/**
* 引用参考github上paypal-restful-api-example
/*
private static Map<String, String> getHeadersInfo(HttpServletRequest request) {
Map<String, String> map = new HashMap<String, String>();
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
- 创建测试相关jsp页面或模板引擎页面
- 此处使用jsp演示
- cancel.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Canceled by user</h1>
</body>
</html>
- index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<head>
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
<form method="post" action="pay.do">
<input hidden name="money" value="500"/>
<button type="submit"><img src="/img/paypal.jpg" style="border: none" width="200px;" height="auto;"/></button>
</form>
</body>
</html>
- success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Payment Success</h1>
</body>
</html>
- 编写业务实际调用层
/**
* 贝宝支付
*/
@RequestMapping("/paypal")
@Controller
public class PaypalController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(PaypalController.class);
public static final String PAYPAL_SUCCESS_URL = "paypal/success.do";
public static final String PAYPAL_CANCEL_URL = "paypal/cancel.do";
@Autowired
private PaypalService paypalService;
@RequestMapping(method = RequestMethod.GET,value = "index.do")
public String index(){
return "paypal/index";
}
/**
* paypal 下单接口
* @param money
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping(method = RequestMethod.POST, value = "pay.do")
public void pay(Double money,HttpServletRequest request, HttpServletResponse response) throws Exception {
String cancelUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_CANCEL_URL;
String successUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_SUCCESS_URL;
//前置业务判断 此处省略
// 生成支付订单号
String orderid = "你的业务规则";
//执行业务 此处省略
//拓展参数
JSONObject extraParam = new JSONObject();
extraParam.put("key1",value1);
extraParam.put("key2",value2);
try {
//创建支付
Payment payment = paypalService.createPayment(extraParam.toJSONString(),orderid,
money,
"USD",
PaypalPaymentMethod.paypal,
PaypalPaymentIntent.sale,
"service fee",
cancelUrl,
successUrl);
for(Links links : payment.getLinks()){
if(links.getRel().equals("approval_url")){
response.sendRedirect(links.getHref());
}
}
} catch (PayPalRESTException e) {
log.error(e.getMessage());
}
return;
}
/**
* 订单撤销
* @return
*/
@RequestMapping(method = RequestMethod.GET, value = "cancel")
public String cancelPay(){
return "paypal/cancel";
}
/**
* paypal 异步执行支付接口 返回支付结果
* @param paymentId
* @param payerId
* @return
*/
@RequestMapping(method = RequestMethod.GET, value = "success")
public void successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId,HttpServletRequest request,HttpServletResponse response) throws Exception{
try {
Payment payment = paypalService.executePayment(paymentId, payerId);
if(payment.getState().equals("approved")){
if (paySuccess(payment, request)) {
request.getRequestDispatcher("/jsp/paypal/success.jsp").forward(request, response);
return;
} else {
request.getRequestDispatcher("/jsp/paypal/cancel.jsp").forward(request, response);
}
}
} catch (PayPalRESTException e) {
log.error(e.getMessage());
}
return;
}
/**
* paypal webhook 异步回调
* @param request
* @param response
* @throws Exception
*/
@RequestMapping("/notify.do")
@ResponseBody
public void notifyUrl(HttpServletRequest request,HttpServletResponse response) throws Exception {
String body = getBody(request);
JSONObject json = JSONObject.parseObject(body);
log.info("贝宝通知>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"+json.toJSONString());
// 获取支POST过来反馈信息
boolean isValid = paypalService.webhookValidate(body,request);
if(isValid) {
log.info("webhook数据验证通过,event_type="+json.getString("event_type"));
Payment payment = new Payment();
JSONObject resource = json.getJSONObject("resource");
List<Transaction> transactionList = new ArrayList<>();
Transaction transaction = new Transaction();
transaction.setInvoiceNumber(resource.getString("invoice_number"));
transaction.setCustom(resource.getString("custom"));
List<RelatedResources> relatedResourcesList = new ArrayList<>();
RelatedResources relatedResources = new RelatedResources();
//paypal订单唯一识别号
Sale sale = new Sale();
sale.setId(resource.getString("id"));
//支付状态
sale.setState(resource.getString("state"));
relatedResources.setSale(sale);
relatedResourcesList.add(relatedResources);
transaction.setRelatedResources(relatedResourcesList);
transactionList.add(transaction);
payment.setTransactions(transactionList);
if(sale.getState().equalsIgnoreCase("completed")) {
if (paySuccess(payment, request)) {
response.setStatus(200);
return;
}else{
log.warn("此支付订单更新失败,订单ID=" + sale.getState() + ",参数信息:" + BeanUtil.objectToJson(payment));
//这里返回500或者其他HTTP错误码,即可重发
response.setStatus(500);
return;
}
}
}
}
/**
* 支付成功的处理逻辑
*
* @param payment
* 贝宝的参数信息
* @return
* @throws ServletException
* @throws IOException
*/
public boolean paySuccess(Payment payment, HttpServletRequest request) {
// 交易状态
String status = "";
for(Links links : payment.getLinks()){
if(links.getRel().equals("approval_url")){
status=links.getRel();
break;
}
status=links.getRel();
}
// 商户订单号
String out_trade_no = payment.getTransactions().get(0).getInvoiceNumber();
// paypal交易号
String trade_no = payment.getId();
// 附加json字段,取出下单时存放的业务信息
String extra_common_param = payment.getTransactions().get(0).getCustom();
JSONObject extraParam = JSONObject.parseObject(extra_common_param);
//执行业务 此处省略
return true;
}
/**
* 引用参考github上paypal-restful-api-example
*/
private static String getBody(HttpServletRequest request) throws IOException {
String body;
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
body = stringBuilder.toString();
return body;
}
}
- 在调用客户端完成支付流程
打开支付测试首页 /paypal/index.do
跳转到paypal支付页
支付中
完成支付
- 流程结束
- 后言
总的来说,paypal的接入要比支付宝微信更简单一些。侧重点在于理解几种不一样的支付接入以及它们的应用场景,最后找到最适合自己业务的接入方式。
此文章参考于 最详细的 paypal 支付接口开发--Java版