java对接Paypal支付
公司搭建海外购物网站,支付选择了paypal支付,一下内容是自己开发对接paypal对接的内容。
非常感谢:https://blog.csdn.net/change_on/article/details/73881791 这篇博客的启发。
前言:
paypal是全球性质的支付工具,国内普遍使用支付宝,微信,银联等,但是要跨国付款就得使用paypal了。paypal是收费的,每一笔都需要收取一定的手续费,收取的是公司的收款方的钱,跟消费者没关系,消费者该付多少就是多少。
支付方式:
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两种通知机制。
账号:
要对接paypal首先就要使用paypal给开发者提供的测试环境,沙盒。首先我们就要去这个测试环境注册一个开发者账号,具体怎么注册可以参考 点击打开链接 里面有详细介绍,这里就不啰嗦了。有了开发者账号后,我们就要模拟正式的购物环境,创建一个卖家business账号,一个买家personal账号,创建也参考链接。
代码:
代码我也是参考 点击打开链接 这个博客的,只是根据自己的业务修改了一下。我的环境也是springboot环境
@Controller @RequestMapping("/paypal") public class PaymentController { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private PaypalService paypalService; // 回调地址 private static Integer orderId = null; private static String SUCCESS_URL = "https://www.baidu.com"; private static String FAIL_URL = "http://www.163.com"; private static String CANCEL_URL = "https://www.autohome.com.cn/shenzhen/?pvareaid=1001299"; @RequestMapping(method = RequestMethod.GET) public String index(){ return "index"; } /** * 订单支付 * @param * @return */ @RequestMapping(method = RequestMethod.POST, value = "/payment") public String payment(HttpServletRequest request, @RequestBody PaymentEx paymentEx){ if (null == paymentEx || null == paymentEx.getOrderId() || null == paymentEx.getOrderAddressEx() || null == paymentEx.getOrderItemExList()) { return "redirect:" + FAIL_URL; } orderId = paymentEx.getOrderId(); SUCCESS_URL = paymentEx.getSuccessUrl(); FAIL_URL = paymentEx.getFailUrl(); CANCEL_URL = paymentEx.getCancelUrl(); // 调用本地 successPay方法 String successUrl = URLUtils.getBaseURl(request) + "/" + "paypal/successPay"; // 调用本地 cancel方法 String cancelUrl = URLUtils.getBaseURl(request) + "/" + "paypal/cancelPay"; try { Payment payment = paypalService.createPayment(paymentEx, successUrl,cancelUrl); for(Links links : payment.getLinks()){ if(links.getRel().equals("approval_url")){ // 客户付款登陆地址 return "redirect:" + links.getHref(); } } } catch (PayPalRESTException e) { log.error(e.getMessage()); return "redirect:" + FAIL_URL; } return "redirect:" + FAIL_URL; } /** * 取消支付 * @return */ @RequestMapping(method = RequestMethod.GET, value = "/cancelPay") public String cancelPay(){ return "redirect:" + CANCEL_URL; } /** * 支付成功 * @param paymentId * @param payerId * @return */ @RequestMapping(method = RequestMethod.GET, value = "/successPay") public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId){ try { /** * 支付 */ Payment payment = paypalService.executePayment(paymentId, payerId); /** * 保存支付信息 */ ShopOrderPayment orderPayment = new ShopOrderPayment(); orderPayment.setOrderId(orderId); // 订单总价 String total = payment.getTransactions().get(0).getAmount().getTotal(); orderPayment.setAmountPaid(new BigDecimal(total)); // 交易号 String transationId = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId(); orderPayment.setTransactionId(transationId); paypalService.savePayment(orderPayment); /** * 支付成功重定向页面 */ if(payment.getState().equals("approved")){ return "redirect:" + SUCCESS_URL; } } catch (PayPalRESTException e) { log.error(e.getMessage()); return "redirect:" + FAIL_URL; } return "redirect:" + FAIL_URL; } }
代码说明:
前端调用我的payment方法,传过来的参数包括 订单的基本信息,商品信息,收货地址信息,结构如下:
里面我要前端传了三个url,成功后回调的url,支付失败的url,以及取消paypal支付返回的url。还有orderId因为我们的业务是支付成功后需要把支付信息保存到自己的支付表中。
支付成功后就调用我的successPay方法返回一个payment信息,里面就有支付交易号等信息。
@Service public class PaypalService { @Autowired private ShopOrderMapper shopOrderMapper; @Autowired private ShopOrderPaymentMapper shopOrderPaymentMapper; @Autowired private PaypalConfig paypalConfig; @Autowired private ShopOrderPaymentHistoryService shopOrderPaymentHistoryService; private static final String CURRENCY = "USD"; private static final String COMPLETED_STATUS = "completed"; /** * 创建支付 * @param cancelUrl * @param successUrl * @return * @throws PayPalRESTException */ @Transactional(rollbackFor = Exception.class) public Payment createPayment(PaymentEx paymentEx, String successUrl, String cancelUrl) throws PayPalRESTException{ Transaction transaction = new Transaction(); transaction.setDescription(paymentEx.getDescription()); // 将我们的订单ID保存到支付信息中,用于后面支付回传 if (null != paymentEx.getOrderId()) { transaction.setCustom(paymentEx.getOrderId().toString()); } /** * 订单价格 */ Amount amount = new Amount(); amount.setCurrency(CURRENCY); // 支付的总价,paypal会校验 total = subTotal + tax + ... amount.setTotal(paymentEx.getOrderTotal()); // 设置各种费用 Details details = new Details(); // 商品总价 details.setSubtotal(paymentEx.getSubTotal()); // 税费 details.setTax(paymentEx.getTax()); amount.setDetails(details); transaction.setAmount(amount); ItemList itemList = new ItemList(); /** * 收货地址 */ ShopOrderAddressEx orderAddress = paymentEx.getOrderAddressEx(); ShippingAddress shippingAddress = new ShippingAddress(); if (StringUtil.isNotEmpty(orderAddress.getFirstName()) && StringUtil.isNotEmpty(orderAddress.getLastName())) { shippingAddress.setRecipientName(orderAddress.getFirstName() + "." + orderAddress.getLastName()); } shippingAddress.setCountryCode(orderAddress.getCountry()); shippingAddress.setState(orderAddress.getRegion()); shippingAddress.setCity(orderAddress.getCity()); shippingAddress.setLine1(orderAddress.getAddress1()); shippingAddress.setPhone(orderAddress.getTelephone()); shippingAddress.setPostalCode(orderAddress.getPostcode()); itemList.setShippingAddress(shippingAddress); itemList.setShippingPhoneNumber(orderAddress.getTelephone()); /** * 商品明细 */ List<ShopOrderItemEx> orderItemList = paymentEx.getOrderItemExList(); List<Item> items = new ArrayList<>(); for (ShopOrderItemEx orderItemEx : orderItemList) { Item item = new Item(); item.setSku(orderItemEx.getSku()); item.setName(orderItemEx.getShortName()); item.setPrice(orderItemEx.getPrice().toString()); item.setQuantity(orderItemEx.getQtyOrdered().toString()); item.setCurrency(Constants.CURRENCY_CODE); items.add(item); } itemList.setItems(items); transaction.setItemList(itemList); List<Transaction> transactions = new ArrayList<>(); transactions.add(transaction); /** * 支付信息 */ Payer payer = new Payer(); payer.setPaymentMethod(PaypalPaymentMethod.paypal.toString()); Payment payment = new Payment(); payment.setIntent(PaypalPaymentIntent.sale.toString()); payment.setPayer(payer); payment.setTransactions(transactions); /** * 回调地址 */ RedirectUrls redirectUrls = new RedirectUrls(); redirectUrls.setReturnUrl(successUrl); redirectUrls.setCancelUrl(cancelUrl); payment.setRedirectUrls(redirectUrls); // 创建paypal API对象 APIContext apiContext = new APIContext(paypalConfig.getClientId(), paypalConfig.getClientSecret(), paypalConfig.getMode()); return payment.create(apiContext); } /** * 执行支付 * @param paymentId * @param payerId * @return * @throws PayPalRESTException */ public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException { // 创建paypal API对象 APIContext apiContext = new APIContext(paypalConfig.getClientId(), paypalConfig.getClientSecret(), paypalConfig.getMode()); Payment payment = new Payment(); payment.setId(paymentId); PaymentExecution paymentExecute = new PaymentExecution(); paymentExecute.setPayerId(payerId); return payment.execute(apiContext, paymentExecute); } /** * 保存订单支付信息 * @param shopOrderPayment * @return */ @Transactional(rollbackFor = Exception.class) public ShopOrderPayment savePayment(ShopOrderPayment shopOrderPayment) { if (null == shopOrderPayment) { return null; } /** * 保存支付数据 */ shopOrderPayment.setPaymentMethod(1); shopOrderPayment.setPaymentDate(new Date()); shopOrderPayment.setCreateDate(new Date()); shopOrderPayment.setUpdateDate(new Date()); shopOrderPaymentMapper.insert(shopOrderPayment); /** * 保存支付历史记录数据 */ ShopOrderPaymentHistory paymentHistory = new ShopOrderPaymentHistory(); BeanUtils.copyProperties(shopOrderPayment, paymentHistory); shopOrderPaymentHistoryService.savePaymentHistory(paymentHistory); /** * 修改订单状态 */ if (null != shopOrderPayment.getPaymentStatus() && COMPLETED_STATUS.equalsIgnoreCase(shopOrderPayment.getPaymentStatus())) { // 如果支付成功后返回的状态是 completed状态直接更新订单状态为待收货确认30 this.updateOrderStatus(shopOrderPayment.getOrderId(), Constants.OrderStatus.STATUS_PRE_REC); } else { // 如果支付成功后返回的不是completed状态,则更新订单状态为待发货20 this.updateOrderStatus(shopOrderPayment.getOrderId(), Constants.OrderStatus.STATUS_PRE_SEND); } return shopOrderPayment; } /** * 支付成功后更新订单状态和记录订单状态历史记录 * @param orderId * @return */ @Transactional(rollbackFor = Exception.class) public ShopOrder updateOrderStatus(Integer orderId, Integer orderStatus) { if (null == orderId) { return null; } /** * 1.支付成功后更新订单状态 */ ShopOrder order = new ShopOrder(); order.setId(orderId); order.setOrderStatus(orderStatus); order.setUpdateDate(new Date()); shopOrderMapper.updateByPrimaryKeySelective(order); /** * 2.记录订单状态日志 */ ShopOrderStatusHistory orderStatusHistory = new ShopOrderStatusHistory(); orderStatusHistory.setOrderId(orderId); orderStatusHistory.setOrderStatusId(orderStatus); orderStatusHistory.setRemark("paypal return status"); orderStatusHistory.setCreateDate(new Date()); orderStatusHistory.setUpdateDate(new Date()); return order; } }
上面是支付的接口内容,支付里面设置了商品,收货地址信息,这个会在支付页面提现出来。
这个就是标准支付的登录页面,出现这个页面就说明成功调用了paypal了,接下来的操作全是paypal的跟我们没多大关系了。登录之后就会出现下面这个付款页面。
注意:这里有个收货地址是我们传给paypal的,paypal在展示出来,这里是不支持修改地址的。上面购物车图标里面有我们添加的商品详细,里面还有我们设置的税费。
最后,我们点继续按钮就完成了支付,如果你设置了成功之后的地址,那么paypal就会请求那个地址到我们自己的页面上去了。
总之,paypal支付集成最大的困难不是开发本身,而是弄清楚paypal支付方式,IPN和PDT通知机制,以及沙盒账号的创建,并如何把沙盒账号与测试账号进行关联。搞清楚这些,开发就很简单了。