PayPal接入过程

Paypal 支付接入

序:参考了现有许多博客,大多数的开发者最终选择RestFulAPi进行Paypal的接入,于是笔者在多方面比较后结合自身业务的实际情况终采用这种方式接入。

认识Paypal

PayPal是倍受全球亿万用户追捧的国际贸易支付工具,即时支付,即时到账,全中文操作界面,能通过中国的本地银行轻松提现,解决外贸收款难题,助您成功开展海外业务,决胜全球。注册PayPal后就可立即开始接受信用卡付款。作为在线付款服务商,PayPal是您向全世界近2.54亿的用户敞开大门的最快捷的方式。 最大的好处是,注册完全免费!集国际流行的信用卡,借记卡,电子支票等支付方式于一身。帮助买卖双方解决各种交易过程中的支付难题。PayPal是名副其实的全球化支付平台,服务范围超过200个市场,支持的币种超过100个。在跨国交易中,将近70%的在线跨境买家更喜欢用PayPal支付海外购物款项。

简而言之,就是应用于国际支付场景下的第三方支持多货币交易的支付工具。

几个概念

  • Paypal Checkout
  • RestFul API
  • IPN
  1. Paypal Checkout是贝宝公司在2018年后推出的第二代(V2)快速接入支持方式,通过数行简单的HTML代码嵌入到商户的WEB支付场景页面携带相关参数,通过JS等方式的调用以比较好的体验完成支付。

  2. RestFul API 是由贝宝公司在2012年从传统SOA接口到当下主流RestFul风格接口的第一个版本(V1),开发方式由商家自主开发页面组成表单然后提交到自己的服务器再向Paypal发起支付下单,用户在Paypal完成支付后继而跳转回商户的业务中来。整个流程可控行和定制型比较强,出来的时间比较久技术偏成熟。

  3. IPN 全称为 Instant Payment Notification 即使付款通知,也是Paypal最简单的网站标准付款方式,通过在WEB页面组成的表单直接进行调用官方接口下单不需要请求自己的后台。完成支付后,Paypal会异步循环去通知商户提前设置好的通知地址,通知地址接收到通知后对通知中的业务参数进行甄别判断此订单支付状态再进行具体业务的处理。

Paypal RestFul 接入流程

在集成paypal支付接口之前,首先要有一系列的准备,开发者账号啊、sdk、测试环境等等先要有,然后再码代码。这里顺便提下paypal webhook的特点,所谓webhook可以翻译成“钩子”。也可以理解成支付宝、微信等国内第三方支付异步通知,它的机制是三天内重复发25次。收到HTTP200响应码就会认为用户的业务处理成功,如果是其他响应码则认为不成功。我在业务中错误响应返回的是500,当然其他的也可以。没有接受到成功响应码paypal的webhook系统则进入重发机制。

具体集成的步骤如下:

  1. 环境准备

    • 注册paypal账号

    • 注册paypal开发者账号

    • 创建两个测试用户

    • 创建应用,生成用于测试的clientID 和 密钥

  2. 代码集成

    • Spring 环境

    • pom引进paypal-sdk的jar包

    • 码代码

    • 测试

    • 后言

现在开始

  1. 注册paypal账号
  1. 创建账户
  • “创建商家用户”,根据要求填写信息,一分钟的事,注册完得去邮箱激活
  1. 登录到paypal开发者
  1. 创建自己的应用
  1. 配置自己的webHookID和通知地址,通知地址必须是https的。这里推荐使用ngrok内网穿透(稳定免费,同时支持http和https)进行调试开发。
  1. 创建一个自己用来测试支付的用户,收款账户就使用系统默认创建的就可以了

代码编写

  1. 工程中引入Paypal的rest-api-sdk
  • 在maven官网可以搜索到,当然也可以在Paypal官网中找到。
<dependency>
    <groupId>com.paypal.sdk</groupId>
    <artifactId>rest-api-sdk</artifactId>
    <version>1.14.0</version>
</dependency>
  1. 创建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}");
}

  1. 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;
    }
    
}
  1. 创建测试相关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>
  1. 编写业务实际调用层
/**
 * 贝宝支付
 */
@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;
    }
    
}

  1. 在调用客户端完成支付流程
  • 打开支付测试首页 /paypal/index.do

  • 跳转到paypal支付页

  • 支付中

  • 完成支付


  • 流程结束

  • 后言

总的来说,paypal的接入要比支付宝微信更简单一些。侧重点在于理解几种不一样的支付接入以及它们的应用场景,最后找到最适合自己业务的接入方式。

此文章参考于 最详细的 paypal 支付接口开发--Java版

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容