在骚扰了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() { @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { // TODO Auto-generated method stub } @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { // TODO Auto-generated method stub } @Override public X509Certificate[] getAcceptedIssuers() { // TODO Auto-generated method stub return 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; } }