你的位置:首页 > Java教程

[Java教程]PayPal支付接入



在骚扰了PayPal的技术支持好几天之后终于成功对接了PayPal支付,非常感谢PayPal的技术支持人员,没有她估计一周都搞不定。记录一下这个过程。

接到这个任务联系了PayPal的技术之后,第一件事就是向她要了一些文档。PayPal提供了一个demo商店https://demo.paypal.com/c2/demo/navigation?merchant=bigbox&page=shoppingCart&locale.x=zh_XC&token=EC-8F899625FB177521V

,首先我在上面体验了一把PayPal支付的整个流程,登录sandbox账号然后支付就行了,可以看下面的截图感受一下,

 

 

sandbox账号是你注册了企业账号之后PayPal送给你的两个测试用账号,你可以在sandbox环境中测试你的代码,当然你也可以自己注册sandbox账号,当商务部给我一个企业账号密码时,我登录https://developer.paypal.com/developer/accounts?event=linkAccountAssociated

可以看到里面的sandbox账户,我选择新建一个自己的sandbox商家账户,与live环境同样且必须要做的是申请签名:https://www.sandbox.paypal.com/webapps/customerprofile/summary.view

(切记!!!sandbox环境用live环境的签名会报"security header is not valid",这是sandbox的签名,同样的,live环境只需要域名中去掉

sandbox即可找到),如下图

PayPal的API有提供两种调用方式,NVP和SOAP,我选择了前者。支持方式是IPN,一般都是选择IPN,因为我们开发基本上都要根据支付平台的结果处理一下自己的业务,在看了IPN这个文档https://www.paypal-biz.com/product/pdf/PayPal_IPN&PDT_Guide_V1.0.pdf之后,大致了解了和PayPal的交互流程,文档中的notify_url是PayPal在你调用DoEC后回调你的链接,PayPal会在请求你链接时带上一些订单的参数,详请点击

https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/

做了这些准备工作之后我们可以开始写代码了。

此处应当插入个时序图的-_-!!

调用API都会用到的公共参数是:"USER=""PWD=""SIGNATURE=""VERSION=",后面不再赘述

下订单调用PayPal的SetExpressCheckout方法,可以参考这个已经过验证的示例(密码签名记得用自己申请的哦_):

附上你们也许用得着的代码:

Map<String, String> nvpMap = PayPalUtil.setExpressCheckout(params);if (nvpMap != null && nvpMap.get("ACK") != null) {String strAck = nvpMap.get("ACK").toUpperCase();if (strAck.equals("SUCCESS")|| strAck.equals("SUCCESSWITHWARNING")) {String checkoutUrl = PayPalUtil.checkoutUrl;String token = checkoutUrl, nvpMap.get("TOKEN");//下单成功之后可以保存此token以做后续调用API之用//do something} else {String ErrorCode = nvpMap.get("L_ERRORCODE0");String ErrorShortMsg = nvpMap.get("L_SHORTMESSAGE0");String ErrorLongMsg = nvpMap.get("L_LONGMESSAGE0");String ErrorSeverityCode = nvpMap.get("L_SEVERITYCODE0");String errorString = "SetExpressCheckout API call failed. "+ "Detailed Error Message: " + ErrorLongMsg+ "Short Error Message: " + ErrorShortMsg+ "Error Code: " + ErrorCode+ "Error Severity Code: " + ErrorSeverityCode;log.error(errorString);}}

 

具体的请求参数以及响应说明在这里:https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/

下单成功拿到与此订单相关的token之后,用户点击continue付款之后PayPal会请求你在setExpressCheckout中传给PayPal的RETURNURL,PayPal带来的请求参数中只有两个参数,

一个token,一个payerID这两个参数都是在调用最后一个DoExpressCheckout API的时候要传的,PayPal的请求可能如下示例:

http://XXX.com/paypal?token=EC-0B0244963D237432J&PayerID=93JGVG4CSVCN4 ,PayerID为买家的account ID.因此,你必须提供一个处理类来处理PayPal的请求,接到PayPal请求之后,接下来你要做的是调用API GetExpressCheckoutDetails

需要带上两个参数:METHOD=GetExpressCheckoutDetails&TOKEN=DJFJSLDFJS ,这个API并不是必须调用,建议调用来获取订单信息和订单状态,比如金额等做风控校验。响应码详情请参考:
https://developer.paypal.com/docs/classic/api/merchant/GetExpressCheckoutDetails_API_Operation_NVP/

来到这里,离成功只差一步了,那就是在上面确认了订单无误之后调用API DoExpressCheckoutPayment告诉PayPal这个交易没错,你可以扣钱了~~~最后你可以再重定向到自己的商户页面。以下参数为必须:
parasMap.put("METHOD", "DoExpressCheckoutPayment");
parasMap.put("TOKEN", token);
parasMap.put("PAYERID", payerId);
parasMap.put("PAYMENTREQUEST_0_PAYMENTACTION", "Sale");//sale是立即到账,order是预授权,一般都是sale
parasMap.put("PAYMENTREQUEST_0_AMT", amount);
parasMap.put("PAYMENTREQUEST_0_NOTIFYURL", notify_url);//这个notify_url是成功调用DoExpressCheckoutPayment后PayPal最后请求你的处理器,你可以做一些风险控制。里面同样会传很多的订单数据给你。


DoExpressCheckoutPayment的请求与相应:
https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/

PayPal在最后请求你的notify_url中所带来的参数如下:
https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/


另外的两个用于查询订单的API你也许也会用得到:

TransactionSearch API Operation (NVP) : https://developer.paypal.com/docs/classic/api/merchant/TransactionSearch_API_Operation_NVP/

GetTransactionDetails API Operation (NVP) : https://developer.paypal.com/docs/classic/api/merchant/GetTransactionDetails_API_Operation_NVP/

然而有一个问题,所需的参数txn_id是PayPal在notify_url请求时才带来的,这可是最后一个交互了,万一丢了这个订单岂不是再也查不到了?别急,看本文最后的说明。

PayPal错误码参考:
https://developer.paypal.com/docs/classic/api/errorcodes/


有几点最后但却非常重要的需要说明:

1.用心的同学估计已经发现了,以上所述与IPN的文档少了一步,那就是IPN文档"1.3 通知确认- 给PayPal的https回拨",这步在我咨询了PayPal的技术支持后我直接省略了,正如文档中所说,假如你满足如下条件,建议保留此步:

您的网站是放在共享服务器上的;您未在您的 Web 服务器上启用 SSL;

2.若是你丢失了PayPal的notify_url请求,你可以根据TransactionSearch这个API去查某个订单详情,有一个INVNUM的参数,如果你在SetExpressCheckOut时传过去了,PayPal会帮你保存在订单中,你可以用此参数获得某一个订单,因此这个参数你在SetEC

的时候便可以传给PayPal,值一般取自己这边的订单号。这样即使你最后收不到notify_url你后续也可以根据你自己的订单号去获取订单。这里也有一个问题需要切记,这个API中的STARTDATE必须要是GMT时间,转换代码如下可以参考:

// Must be a valid date, in UTC/GMT format; for example,// 2013-08-24T05:38:48Z.// 将北京时间转为GMT时间,此处直接减一天好了,虽然只是相差8个小时SimpleDateFormat pSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");DateFormat fSdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");fSdf.setTimeZone(TimeZone.getTimeZone("GMT"));try {Calendar temp = Calendar.getInstance();temp.setTime(pSdf.parse(startdate));temp.add(Calendar.DAY_OF_YEAR, -1);startdate = fSdf.format(temp.getTime());} catch (ParseException e1) {e1.printStackTrace();log.error("时间转换失败,startdate为:" + startdate);return null;}

 

3.若在你部署了本地环境之后,测试时PayPal无法请求到你的局域网,你可以安装localtunnel,它可以帮助你
http://stackoverflow.com/questions/11469636/paypal-sandbox-test-tool-ipn-simulator-in-localhost
https://localtunnel.me/

4.若是你的代码无法请求到PayPal的API,那很有可能你的通信协议不是TLSv1.2,PayPal会在2016年六月底全面启用TLSv1.2,这是一份声明:https://github.com/paypal/TLS-update

解决办法有两个:

一,配置你的服务器支持TLSv1.2,jdk1.7是有的,但并未显示支持,你需要配置jdk使之显示支持TLSv1.2;你可以参考
http://stackoverflow.com/questions/34963083/paypal-sandbox-api-javax-net-ssl-sslhandshakeexception-received-fatal-alert-h
http://stackoverflow.com/questions/9749339/does-tomcat-supports-tls-v1-2


二,升级至jdk8,默认支持,直接可用。

5.若是你的代码报"peer not authenticated"这个错误,说明你的服务器证书不受信任。

解决办法:增加如下方法,

HttpClient httpclient = new DefaultHttpClient();httpclient = wrapClient(httpclient);/*** 获取可信任https链接,以避免不受信任证书出现peer not authenticated异常** @param base* @return*/public static HttpClient wrapClient(HttpClient base) {try {SSLContext ctx = SSLContext.getInstance("TLS");X509TrustManager tm = new X509TrustManager() {@Overridepublic void checkClientTrusted(X509Certificate[] arg0,String arg1) throws CertificateException {// TODO Auto-generated method stub}@Overridepublic void checkServerTrusted(X509Certificate[] arg0,String arg1) throws CertificateException {// TODO Auto-generated method stub}@Overridepublic X509Certificate[] getAcceptedIssuers() {// TODO Auto-generated method stubreturn null;}};ctx.init(null, new TrustManager[] { tm }, null);SSLSocketFactory ssf = new SSLSocketFactory(ctx);ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);ClientConnectionManager ccm = base.getConnectionManager();SchemeRegistry sr = ccm.getSchemeRegistry();sr.register(new Scheme("https", ssf, 443));return new DefaultHttpClient(ccm, base.getParams());} catch (Exception ex) {ex.printStackTrace();return null;}}