• 前言
  • 前期准备
    • 注册paypal账号
    • 登录开发者平台
    • 注册两个沙箱环境账号
    • 创建应用
  • 代码
    • demo地址Gitee
    • PaymentController
    • PaypalService
    • URLUtils
    • PaypalConfig
    • PaypalPaymentIntent
    • PaypalPaymentMethod
    • application.properties
    • pom.xml
  • 支付流程
    • 创建支付订单Payment
    • 成功回调(执行支付 + PDT通知)
    • 取消回调
    • IPN异步通知
  • 概念梳理
    • 账户类型
    • 资费信息
      • 付款
      • 收款【针对企业用户,可申请商家优惠】
      • 货币兑换手续费
      • 提现
      • 退款
      • 退单
    • 付款类型
      • sale(直接付款)
      • authorize(授权付款)
      • order(订单付款)
    • 收款方式
      • credit_card(信用卡收款)
      • paypal(余额收款)
    • 订单对象
      • 三级关联
      • 订单状态

前言

注:可直接使用demo测试,然后再反过来看博客理清思路
经过n次debug和无数的查询资料,终于摸清除了PayPal支付,请听我一一道来
PayPal用于跨国收付款,可以绑定银行卡、信用卡,并且有多种收付款方式,用来实现不同的需求。下面请按照我的章节读完本篇博客,可以帮助您了解到资费信息,收付款方式,成功回调,取消回调,PDT消息通知,IPN消息通知。当然如果你现在还不了解这些概念不必着急,当您仔细看完本片博客,并看完demo代码,相信将paypal支付集成到您的系统已不是难事

前期准备

注册paypal账号

进入该网址注册paypal账号,选择企业账户,并进入邮箱注册

登录开发者平台

进入该网址注册开发者账号,点击右上角的“Log into Dashboard”使用上一步注册的paypal账号登录

注册两个沙箱环境账号

paypal账户内默认提供了两个沙箱账号,但是不要使用这两个账号,自己创建两个(默认账号存在性能问题)

1、创建买家账户:点击Sandbox => Accounts => Create Aaccount => 国家选择China,类型选择Personal
2、创建商家账户:点击Sandbox => Accounts => Create Aaccount => 国家选择China,类型选择Personal
3、创建成功后可以修改账户余额;点击刚创建的账户,记录 沙箱账户的邮箱地址 + 密码
4、使用沙箱账户密码登入沙箱平台,可以查看付款情况、账户余额等信息,还可以设置IPN通知的回调地址(后面会具体阐述IPN通知)

创建应用

应用:使用paypal账户登录开发者平台(注意不是沙箱账户),创建应用后会提供一个clientId和secret,开发的时候调用sdk接口时需要传入clientId和secret作验证

注:应用分为沙箱环境应用和正式环境应用,这里我们创建沙箱环境应用sandbox app,正式上线则替换为正式环境应用live app

1、使用paypal账户登录开发者平台
2、点击DASHBOARD => My Apps and Credentials => 选择Sand Box => Create app => 设置app name => 设置Sandbox Business Account选择刚刚创建的 商家类型的沙箱账户。创建成功
3、点击刚刚创建成功的应用,记录ClientID和secret密钥(开发时会用到)

代码

本项目使用的springboot构建,源码可查看下方gitee,内部也有.md笔记,如果觉得不错可以献上您的star

demo地址Gitee

git地址

PaymentController

@Controller
@RequestMapping("/")
public class PaymentController {@Autowiredprivate APIContext apiContext;@Autowiredprivate PaypalConfig paypalConfig;@Autowiredprivate PaypalService paypalService;private ConcurrentHashMap<String, String> orderPaymMap = new ConcurrentHashMap();// orderId与paymentId的对应关系private Logger log = LoggerFactory.getLogger(getClass());/*** 首页*/@RequestMapping(method = RequestMethod.GET)public String index() {return "index";}/*** 创建订单请求* 创建成功返回payment对象,保存本地OrderId和paymentId对应关系*/@RequestMapping(method = RequestMethod.POST, value = "pay")public String pay(HttpServletRequest request) {log.info("=========================================================================================");String orderId = "2020110200001";// 本地系统IdString cancelUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.CANCEL_URL + "?orderId=" + orderId;// http://localhost:8080/pay/cancelString successUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.SUCCESS_URL;try {//调用交易方法Payment payment = paypalService.createPayment(orderId,300.00,"USD",PaypalPaymentMethod.paypal,PaypalPaymentIntent.authorize,"这是一笔300美元的交易",cancelUrl,successUrl);for (Links links : payment.getLinks()) {if (links.getRel().equals("approval_url")) {// 客户付款登陆地址【判断币种CNY无法交易】String paymentId = payment.getId();orderPaymMap.put(orderId, paymentId);// 保存本地OrderId和paymentId对应关系log.info("创建支付订单返回paymentId : " + paymentId);log.info("支付订单状态state : " + payment.getState());log.info("支付订单创建时间create_time : " + payment.getCreateTime());log.info("=========================================================================================");return "redirect:" + links.getHref();}}} catch (PayPalRESTException e) {log.error(e.getMessage());// 支付失败【使用CNY】}log.info("=========================================================================================");return "redirect:/";}/*** 失败回调*   触发回调情景:*     1、用户在支付页面点击取消支付*     2、用户支付成功后点击网页回退,再点击返回商家按钮触发*   判断是否用户主动取消付款逻辑:*     1、设置回调地址的时候拼接本地订单ID:?orderId=XXXX*     2、然后根据orderId查询paymentId,继而调用sdk查询订单支付状态*      * http://localhost:8080/pay/cancel?orderId=2020110200001&token=EC-70674489LL9806126*/@RequestMapping(method = RequestMethod.GET, value = PaypalConfig.CANCEL_URL)public String cancelPay(@RequestParam("token") String token, @RequestParam("orderId") String orderId) {try {String paymentId = orderPaymMap.get(orderId);Payment payment = Payment.get(apiContext, paymentId);String state = payment.getState();log.info("交易取消回调:支付订单状态:{} ", state);if (state.equals("approved")) {// 已支付return "success";}} catch (PayPalRESTException e) {e.printStackTrace();}return "cancel";}/*** 成功回调 + 支付 + PDT同步通知*    买家确认付款,执行支付并直接返回通知*/@RequestMapping(method = RequestMethod.GET, value = PaypalConfig.SUCCESS_URL)public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId, @RequestParam("token") String token) {log.info("=========================================================================================");try {/*** 执行支付*/Payment payment = paypalService.executePayment(paymentId, payerId);if (payment.getState().equals("approved")) {String transactionId= "";     // 交易ID,transactionIdString transactionState = "";  // 交易订单状态String transactionTime = "";   // 交易时间String custom = ""; // 本地OrderIdif (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getId();transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getCreateTime();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getId();transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getCreateTime();custom = payment.getTransactions().get(0).getCustom();}log.info("PDT通知:交易成功回调");log.info("付款人账户:" + payment.getPayer().getPayerInfo().getEmail());log.info("支付订单Id {}", paymentId);log.info("支付订单状态state : " + payment.getState());log.info("交易订单Id:{}", transactionId);log.info("交易订单状态state : " + transactionState);log.info("交易订单支付时间:" + transactionTime);log.info("本地系统OrderId:{}", custom);log.info("=========================================================================================");return "success";}} catch (PayPalRESTException e) {// 如果同步通知返回异常,可根据paymentId 来查询刷新订单状态// 同时IPN异步通知也可以更新订单状态log.error(e.getMessage());}return "redirect:/";}/*** IPN异步通知*   触发情景:*      1、买家支付成功*      2、卖家确认收取授权或订单款项*      3、卖家发放退款*/@RequestMapping(method = RequestMethod.POST, value = "/notificationIPN")public void receivePaypalStatus(HttpServletRequest request, HttpServletResponse response) throws Exception {log.info("=========================================================================================");log.info("IPN通知:交易成功异步回调");PrintWriter out = response.getWriter();try {Enumeration<String> en = request.getParameterNames();/*** 修改订单状态*      保存失败则不验签,继续接受paypal异步回调直至保存成功【或者用MQ】*/String paymentStatus = request.getParameter("payment_status").toUpperCase();  // 交易状态String paymentDate = request.getParameter("payment_date");      // 交易时间String custom = request.getParameter("custom");                 // 本地系统订单IDString auth_id = request.getParameter("auth_id");               // transactionIdString txnId = request.getParameter("txn_id");                  // 当前回调数据id【具体逻辑查看 .md文档】String parentTxnId = request.getParameter("parent_txn_id");     // 父idString receiverEmail = request.getParameter("receiver_email");  // 收款人emailString receiverId = request.getParameter("receiver_id");        // 收款人idString payerEmail = request.getParameter("payer_email");        // 付款人emailString payerId = request.getParameter("payer_id");              // 付款人idString mcGross = request.getParameter("mc_gross");              // 交易金额String item_name = request.getParameter("item_name");log.info("paymentStatus = " + paymentStatus);log.info("txnId = " + txnId);log.info("parentTxnId = " + parentTxnId);log.info("authId(transactionId)= " + auth_id);log.info("custom(orderId)= " + custom);log.info("item_name= " + item_name);/*** 验证*   作用:*     订单状态修改成功,告诉paypal停止回调*   实现:*     在原参数的基础上加cmd=_notify-validate,然后对https://www.sandbox.paypal.com/cgi-bin/webscr发起POST验证请求*/String str = "cmd=_notify-validate";while (en.hasMoreElements()) {String paramName = en.nextElement();String paramValue = request.getParameter(paramName);//此处的编码一定要和自己的网站编码一致,不然会出现乱码,paypal回复的通知为"INVALID"str = str + "&" + paramName + "=" + URLEncoder.encode(paramValue, "utf-8");}log.info("paypal传递过来的交易信息:" + str);// 建议在此将接受到的信息 str 记录到日志文件中以确认是否收到 IPN 信息URL url = new URL(paypalConfig.getWebscr());HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST");connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//设置 HTTP 的头信息PrintWriter pw = new PrintWriter(connection.getOutputStream());pw.println(str);pw.close();/*** 回复*    接受PayPal对验证的回复信息*/BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));String resp = in.readLine();in.close();resp = StringUtils.isEmpty(resp) ? "0" : resp;log.info("resp = " + resp);/*** 验证返回状态*/if (PaypalConfig.PAYMENT_IPN_VERIFIED.equalsIgnoreCase(resp)) {/*** 修改订单状态*      根据订单状态paymentStatus确定当前回调的类型*/switch (paymentStatus) {case PaypalConfig.PAYMENT_STATUS_PENDING:// 商家待领取状态break;case PaypalConfig.PAYMENT_STATUS_VOIDED:// 商家作废(30天以内,且必须是授权付款类型 或 订单付款类型),款项原路返回买家break;case PaypalConfig.PAYMENT_STATUS_COMPLETED:// 商家领取String captureId = txnId;   // 实际领取对象ID【授权付款 和 订单付款需要商家领取】break;case PaypalConfig.PAYMENT_STATUS_REFUNDED:// 商家退款,需扣除费用String refundId = txnId;String captureId2 = parentTxnId;break;}} else if (PaypalConfig.PAYMENT_IPN_INVALID.equalsIgnoreCase(resp)) {// 非法信息,可以将此记录到您的日志文件中以备调查log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");} else {// 处理其他错误log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");}} catch (Exception e) {log.error("IPN通知发生IO异常" + e.getMessage());log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");e.printStackTrace();}out.flush();out.close();log.info("=========================================================================================");}/*** 查看已付款账单的状态*/@RequestMapping(method = RequestMethod.GET, value = "test")@ResponseBodypublic String selectTransactionState(@RequestParam("paymentId") String paymentId) {log.info("=========================================================================================");String state = "未产生支付信息";String custom = "";try {Payment payment = Payment.get(apiContext, paymentId);if (payment.getTransactions().size() > 0 && payment.getTransactions().get(0).getRelatedResources().size() > 0) {if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {// 交易订单state = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {// 授权订单state = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {// 授权订单state = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();custom = payment.getTransactions().get(0).getCustom();}}} catch (PayPalRESTException e) {e.printStackTrace();}log.info("订单状态:{} ", state);log.info(custom);log.info("=========================================================================================");return state;}
}

PaypalService

/*** 支付service类*/
@Service
public class PaypalService {// 注入凭证信息bean@Autowiredprivate APIContext apiContext;/*** 创建支付订单* @param total  交易金额* @param currency 货币类型* @param method  付款类型* @param intent  收款方式* @param description  交易描述* @param cancelUrl  取消后回调地址* @param successUrl  成功后回调地址*/public Payment  createPayment(String orderId,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.setCustom(orderId);// 交易集合-可以添加多个交易对象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);// 设置反馈urlRedirectUrls redirectUrls = new RedirectUrls();redirectUrls.setCancelUrl(cancelUrl);redirectUrls.setReturnUrl(successUrl);// 加入反馈对象payment.setRedirectUrls(redirectUrls);// 加入认证并创建交易return payment.create(apiContext);}/*** 执行支付*   获取支付订单,和买家ID,执行支付*/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(apiContext, paymentExecute);}
}

URLUtils

/*** 获取请求url的util*/
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();}
}

PaypalConfig

/*** 配置类,注入PayPal需要的认证信息*/
@Configuration
@Data
public class PaypalConfig {@Value("${paypal.client.app}")private String clientId;@Value("${paypal.client.secret}")private String clientSecret;@Value("${paypal.mode}")private String mode;@Value("${paypal.webscr}")private String webscr;/*** 创建支付回调地址参数*/public static final String SUCCESS_URL = "pay/success";  // 成功回调地址PDTpublic static final String CANCEL_URL = "pay/cancel";    // 取消回调地址PDT/*** IPN异步验证返回*/public static final String PAYMENT_IPN_VERIFIED = "VERIFIED";public static final String PAYMENT_IPN_COMPLETED_STATUS = "COMPLETED_STATUS";public static final String PAYMENT_IPN_REFUNDED_STATUS = "REFUNDED_STATUS";public static final String PAYMENT_IPN_INVALID = "INVALID";/*** IPN异步通知返回通知消息类型*/public static final String PAYMENT_STATUS_PENDING = "PENDING";public static final String PAYMENT_STATUS_VOIDED = "VOIDED ";public static final String PAYMENT_STATUS_COMPLETED = "COMPLETED ";public static final String PAYMENT_STATUS_REFUNDED = "REFUNDED ";/*** APP的认证信息* clientId、Secret,开发者账号创建APP时提供*/@Beanpublic APIContext apiContext() throws PayPalRESTException {APIContext apiContext = new APIContext(clientId, clientSecret, mode);return apiContext;}
}

PaypalPaymentIntent

下面展示一些 内联代码片

/*** 付款类型*  sale:直接付款*  authorize:授权付款*  order:订单付款*/
public enum PaypalPaymentIntent {sale,authorize,order
}

PaypalPaymentMethod

/*** 收款方式*  credit_card:信用卡收款*  paypal:余额收款*/
public enum PaypalPaymentMethod {credit_card,paypal
}

application.properties

server.port=8080
spring.thymeleaf.cache=false
paypal.mode=sandbox
paypal.client.app=请填写你的应用
paypal.client.secret=请填写你的密钥
paypal.webscr=https://www.sandbox.paypal.com/cgi-bin/webscr

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.5.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.wan</groupId><artifactId>paypal</artifactId><version>0.0.1-SNAPSHOT</version><name>paypal</name><description>paypal</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- paypal 开发时需要的jar包 --><dependency><groupId>com.paypal.sdk</groupId><artifactId>rest-api-sdk</artifactId><version>1.14.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

支付流程

创建支付订单Payment

参数:本地系统订单ID orderId,金额,货币类型,收款方式,付款方式,描述信息,取消回调,成功回调

如果创建成功,会重定向到PayPal付款页面

注:取消回调接口需要拼接参数 本地订单Id orderId,作用会在取消回调小节中详细阐述。创建订单成功后保存到map中,orderId作为key,paymentId作为value。因为paymentId可以查询订单消息

    /*** 创建订单请求* 创建成功返回payment对象,保存本地OrderId和paymentId对应关系*/@RequestMapping(method = RequestMethod.POST, value = "pay")public String pay(HttpServletRequest request) {log.info("=========================================================================================");String orderId = "2020110200001";// 本地系统IdString cancelUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.CANCEL_URL + "?orderId=" + orderId;// http://localhost:8080/pay/cancelString successUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.SUCCESS_URL;try {//调用交易方法Payment payment = paypalService.createPayment(orderId,300.00,"USD",PaypalPaymentMethod.paypal,PaypalPaymentIntent.authorize,"这是一笔300美元的交易",cancelUrl,successUrl);for (Links links : payment.getLinks()) {if (links.getRel().equals("approval_url")) {// 客户付款登陆地址【判断币种CNY无法交易】String paymentId = payment.getId();orderPaymMap.put(orderId, paymentId);// 保存本地OrderId和paymentId对应关系log.info("创建支付订单返回paymentId : " + paymentId);log.info("支付订单状态state : " + payment.getState());log.info("支付订单创建时间create_time : " + payment.getCreateTime());log.info("=========================================================================================");return "redirect:" + links.getHref();}}} catch (PayPalRESTException e) {log.error(e.getMessage());// 支付失败【使用CNY】}log.info("=========================================================================================");return "redirect:/";}

成功回调(执行支付 + PDT通知)

订单创建成功,会重定向到PayPal付款页面,买家登录PayPal账号并点击付款,执行成功回调,paypalService.executePayment(paymentId, payerId)执行支付。

PDT通知:同步通知,执行支付后会同步返回支付结果对象Payment对象,内部引用了交易对象Sale或Authorization或Order(根据付款类型决定)。根据交易对象状态修改本地订单状态,本地订单ID通过custom属性获得,但是PDT同步通知可能因为网络波动导致消息丢失,所以引出了IPN异步通知,在后面小节会详细阐述

    /*** 成功回调 + 支付 + PDT同步通知*    买家确认付款,执行支付并直接返回通知*/@RequestMapping(method = RequestMethod.GET, value = PaypalConfig.SUCCESS_URL)public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId, @RequestParam("token") String token) {log.info("=========================================================================================");try {/*** 执行支付*/Payment payment = paypalService.executePayment(paymentId, payerId);if (payment.getState().equals("approved")) {String id = "";     // 交易ID,transactionIdString state = "";  // 交易订单状态String time = "";   // 交易时间String custom = ""; // 本地OrderIdif (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {id = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getId();state = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();time = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getCreateTime();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {id = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();state = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();time = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime();custom = payment.getTransactions().get(0).getCustom();} else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {id = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getId();state = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();time = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getCreateTime();custom = payment.getTransactions().get(0).getCustom();}log.info("PDT通知:交易成功回调");log.info("付款人账户:" + payment.getPayer().getPayerInfo().getEmail());log.info("支付订单Id {}", paymentId);log.info("支付订单状态state : " + payment.getState());log.info("交易订单Id:{}", id);log.info("交易订单状态state : " + state);log.info("交易订单支付时间:" + time);log.info("本地系统OrderId:{}", custom);log.info("=========================================================================================");return "success";}} catch (PayPalRESTException e) {// 如果同步通知返回异常,可根据paymentId 来查询刷新订单状态// 同时IPN异步通知也可以更新订单状态log.error(e.getMessage());}return "redirect:/";}

取消回调

触发条件:
1、付款页面买家登录账号,点击"取消并回到XXX",买家主动取消(Payment对象状态仍然是created创建状态,所以如果网页回退仍然可以对当前支付订单进行付款)
2、用户付款成功后,在浏览器上点击回退会触发取消回调(因为回退网页会导致重复付款,所以PayPal设计该部分逻辑【幂等性问题】)为取消回调

    /*** 失败回调*   触发回调情景:*     1、用户在支付页面点击取消支付*     2、用户支付成功后点击网页回退,再点击返回商家按钮触发*   判断是否用户主动取消付款逻辑:*     1、设置回调地址的时候拼接本地订单ID:?orderId=XXXX*     2、然后根据orderId查询paymentId,继而调用sdk查询订单支付状态*      * http://localhost:8080/pay/cancel?orderId=2020110200001&token=EC-70674489LL9806126*/@RequestMapping(method = RequestMethod.GET, value = PaypalConfig.CANCEL_URL)public String cancelPay(@RequestParam("token") String token, @RequestParam("orderId") String orderId) {try {String paymentId = orderPaymMap.get(orderId);Payment payment = Payment.get(apiContext, paymentId);String state = payment.getState();log.info("交易取消回调:支付订单状态:{} ", state);if (state.equals("approved")) {// 已支付return "success";}} catch (PayPalRESTException e) {e.printStackTrace();}return "cancel";}

IPN异步通知

IPN异步通知会在特定情况下被触发,用于弥补PDT同步通知消息丢失的情况(网络波动导致),由Paypal主动推送。其中IPN功能需要手动开启

开启方式:登录企业收款账户,Account Setting ==》 网站付款 ==》 即时付款通知 ==》 更新,设置IPN回调接口(外网可访问,测试期间可使用内网穿透)

触发情景:
1、买家支付成功
2、卖家作废授权单或订购单
3、卖家领取授权单或订购单
4、卖家发放退款

IPN接口代码逻辑:
paypal每隔一段时间会调用一次IPN通知,直至我们回复paypal后一次IPN异步调用才会结束,否则paypal会多次调用IPN通知已保证客户端可以接收到订单状态改变的消息。

IPN异步通知部分参数解释:
payment_status:触发IPN通知的当前订单状态
txn_id:触发本次IPN异步通知的id(可能是saleId、authorizationId、orderId、captureId、refundId,根据触发情景+付款类型决定)
parent_txn_id:当前订单父关联ID
auth_id:始终为transactionId(可能是saleId、authorizationId、orderId)

以下只针对授权付款方式的所有回调返回参数作一个总结:

回调情景 payment_status txn_id parent_txn_id auth_id
authorize支付成功回调 Pending 待领取 authorizationId authorizationId
商户作废授权单回调 Voided 作废 authorizationId authorizationId
商户领取授权单回调 Completed 已领取 captureId authorizationId authorizationId
商户退款回调 Refunded 退款 refundId captureId authorizationId
    /*** IPN异步通知*   触发情景:*      1、买家支付成功*      2、卖家确认收取授权或订单款项*      3、卖家发放退款*/@RequestMapping(method = RequestMethod.POST, value = "/notificationIPN")public void receivePaypalStatus(HttpServletRequest request, HttpServletResponse response) throws Exception {log.info("=========================================================================================");log.info("IPN通知:交易成功异步回调");PrintWriter out = response.getWriter();try {Enumeration<String> en = request.getParameterNames();/*** 修改订单状态*      保存失败则不验签,继续接受paypal异步回调直至保存成功【或者用MQ】*/String paymentStatus = request.getParameter("payment_status").toUpperCase();  // 交易状态String paymentDate = request.getParameter("payment_date");      // 交易时间String custom = request.getParameter("custom");                 // 本地系统订单IDString auth_id = request.getParameter("auth_id");               // transactionIdString txnId = request.getParameter("txn_id");                  // 当前回调数据id【具体逻辑查看 .md文档】String parentTxnId = request.getParameter("parent_txn_id");     // 父idString receiverEmail = request.getParameter("receiver_email");  // 收款人emailString receiverId = request.getParameter("receiver_id");        // 收款人idString payerEmail = request.getParameter("payer_email");        // 付款人emailString payerId = request.getParameter("payer_id");              // 付款人idString mcGross = request.getParameter("mc_gross");              // 交易金额String item_name = request.getParameter("item_name");log.info("paymentStatus = " + paymentStatus);log.info("txnId = " + txnId);log.info("parentTxnId = " + parentTxnId);log.info("authId(transactionId)= " + auth_id);log.info("custom(orderId)= " + custom);log.info("item_name= " + item_name);/*** 验证*   作用:*     订单状态修改成功,告诉paypal停止回调*   实现:*     在原参数的基础上加cmd=_notify-validate,然后对https://www.sandbox.paypal.com/cgi-bin/webscr发起POST验证请求*/String str = "cmd=_notify-validate";while (en.hasMoreElements()) {String paramName = en.nextElement();String paramValue = request.getParameter(paramName);//此处的编码一定要和自己的网站编码一致,不然会出现乱码,paypal回复的通知为"INVALID"str = str + "&" + paramName + "=" + URLEncoder.encode(paramValue, "utf-8");}log.info("paypal传递过来的交易信息:" + str);// 建议在此将接受到的信息 str 记录到日志文件中以确认是否收到 IPN 信息URL url = new URL(paypalConfig.getWebscr());HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("POST");connection.setDoOutput(true);connection.setDoInput(true);connection.setUseCaches(false);connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//设置 HTTP 的头信息PrintWriter pw = new PrintWriter(connection.getOutputStream());pw.println(str);pw.close();/*** 回复*    接受PayPal对验证的回复信息*/BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));String resp = in.readLine();in.close();resp = StringUtils.isEmpty(resp) ? "0" : resp;log.info("resp = " + resp);/*** 验证返回状态*/if (PaypalConfig.PAYMENT_IPN_VERIFIED.equalsIgnoreCase(resp)) {/*** 修改订单状态*      根据订单状态paymentStatus确定当前回调的类型*/switch (paymentStatus) {case PaypalConfig.PAYMENT_STATUS_PENDING:// 商家待领取状态break;case PaypalConfig.PAYMENT_STATUS_VOIDED:// 商家作废(30天以内,且必须是授权付款类型 或 订单付款类型),款项原路返回买家break;case PaypalConfig.PAYMENT_STATUS_COMPLETED:// 商家领取String captureId = txnId;   // 实际领取对象ID【授权付款 和 订单付款需要商家领取】break;case PaypalConfig.PAYMENT_STATUS_REFUNDED:// 商家退款,需扣除费用String refundId = txnId;String captureId2 = parentTxnId;break;}} else if (PaypalConfig.PAYMENT_IPN_INVALID.equalsIgnoreCase(resp)) {// 非法信息,可以将此记录到您的日志文件中以备调查log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");} else {// 处理其他错误log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");}} catch (Exception e) {log.error("IPN通知发生IO异常" + e.getMessage());log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());out.println("confirmError");e.printStackTrace();}out.flush();out.close();log.info("=========================================================================================");}

概念梳理

账户类型

个人账户:不支持跨国收款,并且每月有限额。收款免费
企业账户:收款收取费用

资费信息

付款

免费,用户付款不收取费用

收款【针对企业用户,可申请商家优惠】

账户类型 费用
美国账户 2.9% + $ 0.30(固定费用)
符合条件的慈善机构 2.2% + $ 0.30(固定费用)
小额商品 6% + $ 0.50(固定费用)
国际账户(可申请商家优惠) 4.4% + 根据收款货币类型计算的固定费用

国际账户申请商家优惠费用调整:

月销售额(美元) 费率
3000及以下 4.4%+ $ 0.30(固定费用)
3000-10000 3.9%+ $ 0.30(固定费用)
10000-100000 3.7%+ $ 0.30(固定费用)
100000以上 3.4%+ $ 0.30(固定费用)

商家优惠申请方式:登录你的paypal账户——用户信息——财务信息——更多财务设置(商家费用)——立即申请
货币类型与对应固定费用

货币 费用
澳大利亚元: 0.30澳元
巴西雷亚尔: 0.60巴西雷亚尔
加拿大元: 0.30加元
捷克克朗: 10.00 CZK
丹麦克朗: 2.60 DKK
欧元: 0.35欧元
港元: 2.35港币
匈牙利福林: 90福林
以色列谢克尔: 1.20 ILS
日圆: 40日圆
马来西亚林吉特: 2.00马币
墨西哥比索: 4.00 MXN
新西兰元: 0.45纽币
挪威克朗: 2.80挪威克朗
菲律宾比索: 15.00 PHP
波兰兹罗提: 1.35 PLN
俄罗斯卢布: 10卢布
新加坡元: 0.50新元
瑞典克朗: 3.25 SEK
瑞士法郎: 0.55瑞士法郎
台湾新台币: 10.00 TWD
泰铢: 11.00泰铢
英镑: 0.20英镑
美元: 0.30美元

货币兑换手续费

参照转换费率表 2.5%~4.0%,链接: 汇率转换表

案例:A帐户向B帐户付100美金,但A帐户余额里只有欧元,若A帐户付欧元给B帐户,B帐户同意并接收了,则B帐户在提现美金的时候,需要把欧元转换成美金,这时则需要承担货币兑换手续费。

提现

之前在网上有看到可直接提现人民币到中国的银行的方法已经不支持【公司合作结束】,如有误请及时指出

提现方式 费用 优势
电汇至中国的银行账户(收款USD) $35 / 笔 金额越大越划算
提现到香港银行(收款HKD) 1000港币及以上,免费;1000港币以下,每笔3.50港币 适合有香港银行账户,可以自行进行转账和人民币兑换的商户
通过支票提现(收款USD) $5 / 笔 5-7周,试用不急用且可兑换人民币用户

退款

这里提到的退款是商家接收付款之后,再由商家发起退款产生的资费问题。
1、商家收款时会产生收款手续费;
2、收款的180天内商家可发起退款(可仅退部分款项或全部退款),按照退款比率退换收款时扣取的手续费,但是固定费用例如$ 0.3 不会退回,可以看做退款的损失是$ 0.3。
3、收款180天之后商家不可发起退款,此时如果需要退款需要采用汇款的方式完成退款,此时用户接受汇款形式的退款会征收用户收款手续费。

请看以下案例:
A账户给B账户汇款 $ 100,B账户收款支付收款手续费 $ 4.4 + $ 0.3,实际收款$ 95.3。180天内B账户发起全额退款,A账户收到款项$ 100【A账户不需要支付收款手续费】,手续费$ 4.4退换给B,但是$ 0.3固定费用不退

链接: 如何发放退款

退单

退单是由买家发起的:
买家向信用卡公司提出退单时,PayPal会向卖家收取一笔费用。如果交易受卖家保障规则保护,PayPal将支付退单金额并免除退单费。且卖家收款费用不退还

链接: 具体条约文档
链接: 退单费用
链接: 卖家如何处理退单?
链接: 调解中心

付款类型

sale(直接付款)

款项直接打到商户PayPal账户余额,商家直接接受货款并支付手续费。注:① 如果当前账户是开户以来第一次接受付款,商户需要确认货款,如果30天未接收货款则自动退回买家账户。② 如果当前账户接收的货币类型,商户账户余额中并未持有,则需要商户确认货款,如果30天未接收货款则自动退回买家账户

authorize(授权付款)

买家给商户授权一笔款项,商户账户需要在30天内领取(商户账号可以修改授权款项),若未确认领取则款项自动退还买家。商户可以在30天内作废次交易,则款项原路退还给买家

order(订单付款)

买家给商户支付一笔订单,商户账户需要在30天内领取(商户账号不可以修改订单款项),若未确认领取则款项自动退还买家。商户可以在30天内作废次交易,则款项原路退还给买家

收款方式

credit_card(信用卡收款)

使用信用卡收款

paypal(余额收款)

买家打款,商家使用paypal账户余额收款

订单对象

三级关联

这一部分需要代码debug测试时访问selectTransactionState接口传入paymentId查看支付订单payment对象。(查看代码controller中的selectTransactionState接口)
支付订单payment对象关联交易订单对象,其中交易订单分为Sale、Authorization、order三类,其中Authorization授权订单对象关联Capture商户确认授权单对象(保存了商户实际收款金额),其中交易订单Sale、order和确认授权对象Capture还关联了退款订单Refund对象(保存了商户实际退款金额)

对象类型 作用 通用ID
Payment 支付订单,包含付款金额、paymentId
Sale 直接付款订单,包含付款、saleId 字段设计可以共用transactionId,用付款类型区分
Authorization 授权付款订单,包含付款金额、authorizationId 字段设计可以共用transactionId,用付款类型区分
order 订购付款订单,包含付款金额、orderId 字段设计可以共用transactionId,用付款类型区分
Capture 商家确认授权对象,包含商家实际授权的金额、captureId
Refund 商家退款对象,包含商家实际退款的金额、refundId

订单状态

1、Payment对象:支付订单对象

state 状态
created 创建
approved 已支付(如果商家发起退款不会修改payment状态)
failed 订单取消

2、Sale对象:直接付款对象

state 状态
completed 款项到达商户账户余额
refunded 商家发起全部退款
partially_refunded 商家发起部分退款

3、Authorization:授权付款对象

state 状态
authorized 等待商家领取款项
captured 商家领取全部款项
partially_captured 商家领取部分款项
voided 商家作废此次交易,款项原路退还买家

4、Order:订单付款对象

state 状态
PENDING 等待商家领取款项
VOIDED 商家作废此次交易,款项原路退还买家

5、Capture:商家授权对象

state 状态
completed 款项到达商户账户余额
refunded 商家发起全部退款
partially_refunded 商家发起部分退款

6、Refund:退款信息对象

state 状态
completed 款项到达买家账户(余额或者信用卡)

java对接PayPal支付(v1)相关推荐

  1. java对接PayPal支付-自动续费功能

    一. java对接PayPal支付(v2). 讲了PayPal v2:checkout-sdk 的对接过程 二. java对接PayPal支付 (添加物流跟踪信息). 讲了PayPal添加物流信息 的 ...

  2. java对接PayPal支付(v2)

    java对接PayPal支付 我们公司最近开通了网上支付功能,国内选择对接支付宝和微信,国外选择对接paypal, 今天我先将paypal对接方式记录下来,后面会记录微信和支付宝(本人比较懒,微信和支 ...

  3. java 对接 paypal支付

    码字不易,开源更不易,点赞收藏关注,多多支持 开源地址  paypal-demo: java 对接 paypal 的案例,下载项目,注册paypal账号,拿到秘钥,即可使用 效果图 准备环境 1.注册 ...

  4. java对接PayPal支付(ipn中文乱码解决)

    IPN验证有中文的时候会出现乱码,是由于encoding设置导致的,请通过以下步骤将encoding设置改为UTF-8应该就能解决: 1)登录您的PayPal账号后打开这个链接进入设置页面: http ...

  5. JAVA对接支付宝支付(超详细,一看就懂)

    Java对接支付宝支付 更多内容 冷文博客: 传送门 引入 为什么要发这篇帖子呢?原因很简单,就是因为在一个稍稍正规一点的应用上都会有支付这个环节,我们日常的在线支付如今包括支付宝,微信钱包,QQ钱包 ...

  6. Java集成PayPal支付

    Java集成PayPal支付 1.申请账号 浏览器中输入:https://www.paypal.com,点击 "注册" 选择 "企业账号" ,信息可以随意填写 ...

  7. java对接微信支付收不到支付通知问题(亲身实践)

    问题描述: 用java对接微信支付时,统一下单接口正常.但是用户扫码付款成功后,设置用于回调的notify_url对应的接口并没有收到请求(这个url测试过,是正常的且外网能访问的). 由于官方文档没 ...

  8. paypal html5 支付,uniapp 对接 paypal支付 (h5,app端)

    由于工作需要,需要对接国外的PayPal支付,前端框架用的又是UNIAPP,众所周知UNIAPP国内的生态环境还可以,但是到了国外嘛  嘿嘿  懂得都懂. uniapp app对接Paypal支付 作 ...

  9. java对接支付宝支付

    java对接支付宝支付演示 现在有不少的项目都需要对接支付,这里主要是进行讲解对接支付宝H5支付 废话不多说 上代码 引入支付宝官方的sdk <!-- https://mvnrepository ...

  10. Android java对接建行支付SDK

    Android java 对接建行支付 准备工作 创建常量类 编写支付工具类 遇到的坑 用到的工具类 准备工作 在对接建行支付之前,需要准备好这几个东西:商户代码.商户柜台代码.分行代码.公钥 创建常 ...

最新文章

  1. PHP----------php封装的一些简单实用的方法汇总
  2. POJ 2728 Desert King [最优比率生成树]
  3. Android之自定义瀑布流式的标签列表
  4. js中Window跟window的区别
  5. Eclipse旧版本Luna SR2(版本4.4.2)下载地址
  6. java的startswith_java startsWith和endsWith的用法 | 学步园
  7. AOP - PostSharp 2.0
  8. 反思读别人代码的思路
  9. 【JAVASCRIPT】无刷新评论
  10. react项目如何按需加载antdDesign组件
  11. vue --- 使用animate.css实现动画
  12. qq降龙电脑版_分享 | 如何利用QQ群上课 简单操作步骤
  13. php preg_replace html,php – 忽略preg_replace中的html标签
  14. recyclerview放不同的布局_RecyclerView系列之(2):为RecyclerView添加分隔线
  15. Revealing图片展示效果(jQuery)
  16. 千载新论:只能指望员工做完工作,要做好依靠主管
  17. RADIUS协议指南
  18. html 点击按钮刷新验证码,HTML点击刷新验证码
  19. 时钟周期 指令周期 MIPS CPI
  20. Qt开发经验小技巧176-180

热门文章

  1. php怎么上传文档,php
  2. JAVA开源协同过滤算法,推荐算法:协同过滤算法的介绍
  3. jmp怎么做合并的箱线图_基于JMP 15的箱线图(Box Plot)的着色
  4. 极易上手搭建自己日志采集服务器分析日志(winlogbeat+Elasticsearch+Kibana)
  5. php运行日志在哪里看,thinkphp错误日志在哪
  6. 如何在CAD(CASS)中加载卫星影像
  7. win10传真服务器位置,win10系统电脑发传真的操作方法
  8. syslog (cactiez)
  9. c语言二级安卓软件,C语言二级考试题库安卓下载-C语言二级考试题库APK下载 - Iefans...
  10. 设计c语言程序,输出形状为直角三角形的九九乘法表,c语言题库(全国c语言二级考试题库)...