Java整合PayPal支付结账和支出

梁丘成和
2023-12-01

转载请表明出处 https://blog.csdn.net/Amor_Leo/article/details/117959323 谢谢

准备

一、环境准备

  1. 注册paypal账号

  2. 注册paypal开发者账号

  3. 创建两个测试用户

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

代码

PayPal官方GitHub

pom

     <properties>
        <paypal-rest.version>1.14.0</paypal-rest.version>
        <paypal-checkout.version>1.0.4</paypal-checkout.version>
        <paypal-out.version>1.1.0</paypal-out.version>
        <paypal-core.version>1.7.2</paypal-core.version>
    </properties>
    
		<!-- PayPal-->
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>rest-api-sdk</artifactId>
            <version>${paypal-rest.version}</version>
        </dependency>
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>checkout-sdk</artifactId>
            <version>${paypal-checkout.version}</version>
        </dependency>
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>payouts-sdk</artifactId>
            <version>${paypal-out.version}</version>
        </dependency>
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>paypal-core</artifactId>
            <version>${paypal-core.version}</version>
        </dependency>

yml

paypal:
  client:
    id: xxxx
    secret: xxxx
    mode: sandbox
#    mode: live

配置

import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import java.util.Iterator;

/**
 * @author LHL
 * @since 2021/6/3
 */
@Slf4j
@Component
public class PayPalClient {

    public PayPalHttpClient client(String mode, String clientId, String clientSecret) {
        log.info("mode={}, clientId={}, clientSecret={}", mode, clientId, clientSecret);
        PayPalEnvironment environment = mode.equals("live") ? new PayPalEnvironment.Live(clientId, clientSecret) : new PayPalEnvironment.Sandbox(clientId, clientSecret);
        return new PayPalHttpClient(environment);
    }

    /**
     * @param jo
     * @param pre
     * @return
     */
    public String prettyPrint(JSONObject jo, String pre) {
        Iterator<?> keys = jo.keys();
        StringBuilder pretty = new StringBuilder();
        while (keys.hasNext()) {
            String key = (String) keys.next();
            pretty.append(String.format("%s%s: ", pre, StringUtils.capitalize(key)));
            if (jo.get(key) instanceof JSONObject) {
                pretty.append(prettyPrint(jo.getJSONObject(key), pre + "\t"));
            } else if (jo.get(key) instanceof JSONArray) {
                int sno = 1;
                for (Object jsonObject : jo.getJSONArray(key)) {
                    pretty.append(String.format("\n%s\t%d:\n", pre, sno++));
                    pretty.append(prettyPrint((JSONObject) jsonObject, pre + "\t\t"));
                }
            } else {
                pretty.append(String.format("%s\n", jo.getString(key)));
            }
        }
        return pretty.toString();
    }

}

支付结账

官网

import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.SerializeException;
import com.paypal.http.serializer.Json;
import com.paypal.orders.AmountBreakdown;
import com.paypal.orders.AmountWithBreakdown;
import com.paypal.orders.ApplicationContext;
import com.paypal.orders.Capture;
import com.paypal.orders.LinkDescription;
import com.paypal.orders.Money;
import com.paypal.orders.Order;
import com.paypal.orders.OrderRequest;
import com.paypal.orders.OrdersCreateRequest;
import com.paypal.orders.OrdersGetRequest;
import com.paypal.orders.PurchaseUnitRequest;
import com.paypal.orders.Refund;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* 创建订单
* @author LHL
**/
@Slf4j
@Component
public class CreateOrder extends PayPalClient {

	@Value("${paypal.client.id}")
	private String clientId;

	@Value("${paypal.client.secret}")
	private String clientSecret;

	/**
	 * sandbox 沙箱/ live 正式环境
	 **/
	@Value("${paypal.client.mode}")
	private String mode;

	public static final String CAPTURE = "CAPTURE";

	/**
	 * 该标签将覆盖PayPal网站上PayPal帐户中的公司名称
	 */
	public static final String BRANDNAME = "Supernote";
	/**
	 * LOGIN。当客户单击PayPal Checkout时,客户将被重定向到页面以登录PayPal并批准付款。
	 * BILLING。当客户单击PayPal Checkout时,客户将被重定向到一个页面,以输入信用卡或借记卡以及完成购买所需的其他相关账单信息
	 * NO_PREFERENCE。当客户单击“ PayPal Checkout”时,将根据其先前的交互方式将其重定向到页面以登录PayPal并批准付款,或重定向至页面以输入信用卡或借记卡以及完成购买所需的其他相关账单信息使用PayPal。
	 * 默认值:NO_PREFERENCE
	 */
	public static final String LANDINGPAGE = "NO_PREFERENCE";
	/**
	 * CONTINUE。将客户重定向到PayPal付款页面后,将出现“ 继续”按钮。当结帐流程启动时最终金额未知时,请使用此选项,并且您想将客户重定向到商家页面而不处理付款。
	 * PAY_NOW。将客户重定向到PayPal付款页面后,出现“ 立即付款”按钮。当启动结帐时知道最终金额并且您要在客户单击“ 立即付款”时立即处理付款时,请使用此选项。
	 */
	public static final String USERACTION = "PAY_NOW";

	/**
	 * 当前货币币种简称, 默认为人名币的币种 EUR CNY USD
	 */
	public static final String CURRENT_CY = "USD";

	/**
	 * GET_FROM_FILE。使用贝宝网站上客户提供的送货地址。
	 * NO_SHIPPING。从PayPal网站编辑送货地址。推荐用于数字商品
	 * SET_PROVIDED_ADDRESS。使用商家提供的地址。客户无法在PayPal网站上更改此地址
	 */
	public static final String SHIPPINGPREFERENCE = "NO_SHIPPING";

	/**
	 * 生成订单主体信息
	 * @author LHL
	 */
	private OrderRequest buildOrderRequestBody(String description, String serialNumber, String price, String cancelUrl, String successUrl) {
		OrderRequest orderRequest = new OrderRequest();
		orderRequest.checkoutPaymentIntent(CAPTURE);
		ApplicationContext applicationContext = new ApplicationContext()
				//可不要
				//.brandName(BRANDNAME)
				//.landingPage(LANDINGPAGE)
				//.cancelUrl(cancelUrl)
				//.returnUrl(successUrl)
				.userAction(USERACTION)
				.shippingPreference(SHIPPINGPREFERENCE);
		orderRequest.applicationContext(applicationContext);
		List<PurchaseUnitRequest> purchaseUnitRequests = new ArrayList<PurchaseUnitRequest>();
		PurchaseUnitRequest purchaseUnitRequest = new PurchaseUnitRequest()
				//支付描述
				.description(description)
				//(唯一)订单号
				.customId(serialNumber)
				.invoiceId(serialNumber)
				.amountWithBreakdown(new AmountWithBreakdown()
						.currencyCode(CURRENT_CY)
						//支付金额 value = itemTotal + shipping + handling + taxTotal + shippingDiscount; "220.00"
						.value(price)
						//可不要
						//.amountBreakdown(new AmountBreakdown()
						//		// itemTotal = Item[Supernote A6](value × quantity) + Item[帆布封套](value × quantity)
						//		.itemTotal(new Money().currencyCode(CURRENT_CY).value(price))
						//		.shipping(new Money().currencyCode(CURRENT_CY).value("0.00"))
						//		.handling(new Money().currencyCode(CURRENT_CY).value("0.00"))
						//		.taxTotal(new Money().currencyCode(CURRENT_CY).value("0.00"))
						//		.shippingDiscount(new Money().currencyCode(CURRENT_CY).value("0.00")))
								)
				;
		//可不要
		//.items(new ArrayList<Item>() {
		//    {
		//        add(new Item().name("Supernote A6").description("丝滑般流畅的书写体验")
		//                .unitAmount(new Money()
		//                        .currencyCode(CURRENT_CY)
		//                        .value("200.00"))
		//                .quantity("1"));
		//        add(new Item().name("帆布封套").description("黑色帆布保护封套")
		//                .unitAmount(new Money()
		//                        .currencyCode(CURRENT_CY)
		//                        .value("20.00"))
		//                .quantity("1"));
		//    }
		//})
		//.shippingDetail(new ShippingDetail()
		//        .name(new Name().fullName("RATTA"))
		//        .addressPortable(new AddressPortable()
		//                .addressLine1("梅陇镇")
		//                .addressLine2("集心路168号")
		//                .adminArea2("闵行区")
		//                .adminArea1("上海市")
		//                .postalCode("20000")
		//                .countryCode("CN")));
		purchaseUnitRequests.add(purchaseUnitRequest);
		orderRequest.purchaseUnits(purchaseUnitRequests);
		return orderRequest;
	}

	/**
	 * 创建订单的方法,返回orderId 给移动端; 再由移动端支付 付费后 ipn监听支付成功处理逻辑; 如果ipn监听失败 移动端主动调用服务端捕获captureOrder再处理逻辑
	 * @author LHL
	 */
	public String createOrder(String description, String serialNumber, String price, String cancelUrl, String successUrl) {
		OrdersCreateRequest request = new OrdersCreateRequest();
		request.header("prefer","return=representation");
		request.requestBody(buildOrderRequestBody(description, serialNumber, price, cancelUrl, successUrl));
		HttpResponse<Order> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (IOException e1) {
			try {
				log.error("第1次调用paypal订单创建失败: {}", e1.getMessage());
				response = client(mode, clientId, clientSecret).execute(request);
			} catch (Exception e) {
				try {
					log.error("第2次调用paypal订单创建失败: {}", e.getMessage());
					response = client(mode, clientId, clientSecret).execute(request);
				} catch (Exception e2) {
					log.error("第3次调用paypal订单创建失败,失败原因:{}", e2.getMessage());
				}
			}
		}
		//String approve = cancelUrl;
		String orderId = null;
		if (response.statusCode() == 201) {
			log.info("Status Code = {}, Status = {}, OrderID = {}, Intent = {}", response.statusCode(), response.result().status(), response.result().id(), response.result().checkoutPaymentIntent());
			orderId = response.result().id();
			//for (LinkDescription link : response.result().links()) {
			//log.info("Links-{}: {}    \tCall Type: {}", link.rel(), link.href(), link.method());
			//	if(link.rel().equals("approve")) {
			//		approve = link.href();
			//	}
			//}
			// 打印 需要删除
			//String totalAmount = response.result().purchaseUnits().get(0).amountWithBreakdown().currencyCode() + ":" + response.result().purchaseUnits().get(0).amountWithBreakdown().value();
			//log.info("Total Amount: {}", totalAmount);
			String json= null;
			try {
				json = new JSONObject(new Json().serialize(response.result())).toString(4);
			} catch (SerializeException e) {
				log.error("json serialize error: {}", e.getMessage());
			}
			log.info("createOrder response body: {}", json);
			return orderId;
		}
		//return approve;
		return orderId;
	}

	/**
	 * 查询订单详情
	 *
	 * @param orderId 订单id,CreateOrder 生成
	 * @author LHL
	 * */
	public Map<String, Object> testOrdersGetRequest(String orderId) {
		Map<String, Object> map = new HashMap<>(5);
		OrdersGetRequest request = new OrdersGetRequest(orderId);
		HttpResponse<Order> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (Exception e) {
			try {
				log.error("调用paypal订单查询失败,链接异常1: {}", e.getMessage());
				response = client(mode, clientId, clientSecret).execute(request);
			} catch (Exception e2) {
				try {
					log.error("调用paypal订单查询失败,链接异常2: {}", e2.getMessage());
					response = client(mode, clientId, clientSecret).execute(request);
				} catch (Exception e3) {
					log.error("调用paypal订单查询失败,链接异常3", e3.getMessage());
					map.put("flag", false);
					map.put("msg", "调用paypal订单查询失败");
					return map;
				}
			}
		}
		//打印 需要删除
		log.info("Status Code: {}" , response.statusCode());
		log.info("Status: {}" , response.result().status());
		log.info("Order id: {}" , response.result().id());
		if(response.result().purchaseUnits().get(0).payments() != null) {
			List<Capture> captures = response.result().purchaseUnits().get(0).payments().captures();
			if(captures != null) {
				Capture capture = captures.get(0);
				//for (Capture capture : captures) {
					log.info("\t订单编号= " + capture.invoiceId() + "\tCapture Id 支付id= " + capture.id() + "\tCapture status= " + capture.status() + "\tCapture amount= " + capture.amount().currencyCode() + ":" + capture.amount().value());
				//}
				map.put("invoice", capture.invoiceId());
				map.put("payment_status", capture.status());
				map.put("flag", true);
				map.put("msg", "支付查询成功");
				return map;
			}
			//List<Refund> refunds = response.result().purchaseUnits().get(0).payments().refunds();
			//if(refunds != null) {
			//	for (Refund refund : refunds) {
			//		System.out.println("\t售后编号= " + refund.invoiceId() + "\tRefund Id= " + refund.id() + "\tRefund status= " + refund.status() + "\tRefund amount= " + refund.amount().currencyCode() + ":" + refund.amount().value());
			//	}
			//}

		}
		//System.out.println("Links: ");
		//for (LinkDescription link : response.result().links()) {
		//	System.out.println("\t" + link.rel() + ": " + link.href() + "\tCall Type: " + link.method());
		//}
		//
		//System.out.println("Full response body:");
		//String json = null;
		//try {
		//	json = new JSONObject(new Json().serialize(response.result())).toString(4);
		//} catch (SerializeException e) {
		//	log.error("json serialize error: {}", e.getMessage());
		//}
		//System.out.println(json);
		map.put("flag", false);
		map.put("msg", "支付查询失败");
		return map;
	}

}
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.SerializeException;
import com.paypal.http.serializer.Json;
import com.paypal.orders.Capture;
import com.paypal.orders.LinkDescription;
import com.paypal.orders.Order;
import com.paypal.orders.OrderRequest;
import com.paypal.orders.OrdersCaptureRequest;
import com.paypal.orders.Payer;
import com.paypal.orders.PurchaseUnit;
import com.paypal.payments.CapturesGetRequest;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;

/**
* 捕获订单
* @author LHL
**/
@Slf4j
@Component
public class CaptureOrder extends PayPalClient {


	@Value("${paypal.client.id}")
	private String clientId;

	@Value("${paypal.client.secret}")
	private String clientSecret;

	/**
	 * sandbox 沙箱/ live 正式环境
	 **/
	@Value("${paypal.client.mode}")
	private String mode;

	/**
	 * 用户授权支付成功,进行扣款操作
	 * 用户通过CreateOrder生成orderid  移动端跳转paypal支付成功后,只是授权,并没有将用户的钱打入我们的paypal账户,我们需要通过 CaptureOrder接口,将钱打入我的PayPal账户
	 * @author LHL
	 */
	public HttpResponse<Order> captureOrder(String orderId) throws IOException {
		OrdersCaptureRequest request = new OrdersCaptureRequest(orderId);
		request.requestBody(new OrderRequest());
		HttpResponse<Order> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (IOException e1) {
			try {
				log.error("第1次调用paypal扣款失败");
				response = client(mode, clientId, clientSecret).execute(request);
			} catch (Exception e) {
				try {
					log.error("第2次调用paypal扣款失败");
					response = client(mode, clientId, clientSecret).execute(request);
				} catch (Exception e2) {
					log.error("第3次调用paypal扣款失败,失败原因 {}", e2.getMessage() );
				}
			}
		}
		// 打印 需要删掉
		log.info("Status Code = {}, Status = {}, OrderID = {}", response.statusCode(), response.result().status(), response.result().id());
		for (LinkDescription link : response.result().links()) {
			log.info("Links-{}: {}    \tCall Type: {}", link.rel(), link.href(), link.method());
		}
		for (PurchaseUnit purchaseUnit : response.result().purchaseUnits()) {
			for (Capture capture : purchaseUnit.payments().captures()) {
				log.info("Capture id: {}", capture.id());
				log.info("status: {}", capture.status());
				log.info("invoice_id: {}", capture.invoiceId());
				if("COMPLETED".equals(capture.status())) {
					//进行数据库操作,修改订单状态为已支付成功,尽快发货(配合回调和CapturesGet查询确定成功)
					log.info("支付成功,状态为=COMPLETED");
				}
				if("PENDING".equals(capture.status())) {
					log.info("status_details: {}", capture.captureStatusDetails().reason());
					String reason = "PENDING";
					if(capture.captureStatusDetails() != null && capture.captureStatusDetails().reason() != null) {
						reason = capture.captureStatusDetails().reason();
					}
					//进行数据库操作,修改订单状态为已支付成功,但触发了人工审核,请审核通过后再发货(配合回调和CapturesGet查询确定成功)
					log.info("支付成功,状态为=PENDING : {}", reason);
				}
			}
		}
		Payer buyer = response.result().payer();
		log.info("Buyer Email Address: {}", buyer.email());
		log.info("Buyer Name: {} {}", buyer.name().givenName(), buyer.name().surname());
		String json = new JSONObject(new Json().serialize(response.result())).toString(4);
		log.info("captureOrder response body: {}", json);
		return response;
	}


	/**
	 * 查询扣款详情
	 * @param captureId 扣款id, captureOrder 生成
	 * @author LHL
	 * */
	public Map<String, Object> testCapturesGetRequest(String captureId) {
		Map<String, Object> map = new HashMap<>(5);
		CapturesGetRequest request = new CapturesGetRequest(captureId);
		HttpResponse<com.paypal.payments.Capture> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (IOException e) {
			log.error("client execute error: {}", e.getMessage());
			map.put("flag", false);
			map.put("msg", "调用paypal付款查询失败");
			return map;
		}
		// 打印  到时候删掉
		System.out.println("Status Code: " + response.statusCode());
		System.out.println("Status: " + response.result().status());
		System.out.println("Capture ids: " + response.result().id());
		//System.out.println("Links: ");
		//for (com.paypal.payments.LinkDescription link : response.result().links()) {
		//	System.out.println("\t" + link.rel() + ": " + link.href() + "\tCall Type: " + link.method());
		//}
		//System.out.println("Full response body:");
		try {
			System.out.println(new JSONObject(new Json().serialize(response.result())).toString(4));
		} catch (SerializeException e) {
			log.error("json serialize error: {}", e.getMessage());
		}
		//log.info("Capture ids: " + response.result().id());

		map.put("invoice", response.result().invoiceId());
		map.put("payment_status", response.result().status());
		map.put("flag", true);
		map.put("msg", "付款查询成功");
		return map;
	}
} 
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.SerializeException;
import com.paypal.http.serializer.Json;
import com.paypal.orders.OrdersGetRequest;
import com.paypal.payments.CapturesRefundRequest;
import com.paypal.payments.LinkDescription;
import com.paypal.payments.Money;
import com.paypal.payments.Refund;
import com.paypal.payments.RefundRequest;
import com.paypal.payments.RefundsGetRequest;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;

/**
* 退回
* @author LHL
**/
@Slf4j
@Component
public class RefundOrder extends PayPalClient {

	@Value("${paypal.client.id}")
	private String clientId;

	@Value("${paypal.client.secret}")
	private String clientSecret;

	/**
	 * sandbox 沙箱/ live 正式环境
	 **/
	@Value("${paypal.client.mode}")
	private String mode;

	/**
	 * 当前货币币种简称, 默认为人名币的币种 EUR CNY USD
	 */
	public static final String CURRENT_CY = "USD";

	/**
	 * @author LHL
	 * 创建退款请求体
	 */
	public com.paypal.payments.RefundRequest buildRefundRequestBody(String description, String serialNumber, String price) {
		RefundRequest refundRequest = new RefundRequest();
		Money money = new Money();
		money.currencyCode(CURRENT_CY);
		money.value(price);
		refundRequest.amount(money);
		refundRequest.invoiceId(serialNumber);
		refundRequest.noteToPayer(description);
		return refundRequest;
	}

	/**
	 * 申请退款
	 * @param orderId  订单id,CreateOrder 生成
	 * @author LHL
	 */
	public HttpResponse<com.paypal.payments.Refund> refundOrder(String orderId, String description, String serialNumber, String price) {
		OrdersGetRequest ordersGetRequest = new OrdersGetRequest(orderId);
		HttpResponse<com.paypal.orders.Order> ordersGetResponse = null;
		try {
			ordersGetResponse = client(mode, clientId, clientSecret).execute(ordersGetRequest);
		} catch (Exception e) {
			try {
				log.error("第1次调用paypal订单查询失败");
				ordersGetResponse = client(mode, clientId, clientSecret).execute(ordersGetRequest);
			} catch (Exception e2) {
				try {
					log.error("第2次调用paypal订单查询失败");
					ordersGetResponse = client(mode, clientId, clientSecret).execute(ordersGetRequest);
				} catch (Exception e3) {
					log.error("第3次调用paypal订单查询失败,失败原因:{}", e3.getMessage());
				}
			}
		}
		String captureId = ordersGetResponse.result().purchaseUnits().get(0).payments().captures().get(0).id();
		CapturesRefundRequest request = new CapturesRefundRequest(captureId);
		request.prefer("return=representation");
		request.requestBody(buildRefundRequestBody(description, serialNumber, price));
		HttpResponse<Refund> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (IOException e) {
			try {
				log.error("第1次调用paypal退款申请失败");
				response = client(mode, clientId, clientSecret).execute(request);
			} catch (Exception e1) {
				try {
					log.error("第2次调用paypal退款申请失败");
					response = client(mode, clientId, clientSecret).execute(request);
				} catch (Exception e2) {
					log.error("第3次调用paypal退款申请失败,失败原因 {}", e2.getMessage());
				}
			}
		}
		//打印 需要删除
		log.info("Status Code = {}, Status = {}, RefundID = {}", response.statusCode(), response.result().status(), response.result().id());
		if ("COMPLETED".equals(response.result().status())) {
			//进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功)
			log.info("退款成功");
		}
		for (LinkDescription link : response.result().links()) {
			log.info("Links-{}: {}    \tCall Type: {}", link.rel(), link.href(), link.method());
		}
		String json = null;
		try {
			json = new JSONObject(new Json().serialize(response.result())).toString(4);
		} catch (SerializeException e) {
			log.error("json serialize error: {}", e.getMessage());
		}
		log.info("refundOrder response body: {}", json);
		return response;
	}
	/**
	 * 查询退款详情
	 *
	 * @param refundId 退款id RefundOrder生成
	 * @author LHL
	 * */
	public Boolean testRefundsGetRequest(String refundId) {
		RefundsGetRequest request = new RefundsGetRequest(refundId);
		HttpResponse<Refund> response = null;
		try {
			response = client(mode, clientId, clientSecret).execute(request);
		} catch (IOException e) {
			log.error("client execute error: {}", e.getMessage());
			return false;
		}
		//打印 需要删除
		System.out.println("Status Code: " + response.statusCode());
		System.out.println("Status: " + response.result().status());
		System.out.println("Refund Id: " + response.result().id());
		System.out.println("Links: ");
		for (LinkDescription link : response.result().links()) {
			System.out.println("\t" + link.rel() + ": " + link.href() + "\tCall Type: " + link.method());
		}
		System.out.println("Full response body:");
		try {
			System.out.println(new JSONObject(new Json().serialize(response.result())).toString(4));
		} catch (SerializeException e) {
			log.error("json serialize error: {}", e.getMessage());
		}
		return true;
	}
}

支出

官网

import com.paypal.http.Encoder;
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.HttpException;
import com.paypal.http.serializer.Json;
import com.paypal.payouts.Error;
import com.paypal.payouts.PayoutItemResponse;
import com.paypal.payouts.PayoutsItemCancelRequest;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
@Component
public class CancelPayoutItem extends PayPalClient {

    private static final Encoder encoder = new Encoder();

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    /**
     * sandbox 沙箱/ live 正式环境
     **/
    @Value("${paypal.client.mode}")
    private String mode;

    /**
     * Cancels an UNCLAIMED Payout item (POST - /v1/payments/payouts-item/{<item-id>}/cancel)
     * An item can be cancelled if it is in UNCLAIMED status and the Payouts batch is in SUCCESS status.
     * Upon cancelling an item the status changes to RETURNED and the funds are given back to the sender
     * 取消未声明的支付项目(POST - /v1/payments/payouts-item/{<item-id>}/cancel)
     * 如果项目处于 UNCLAIMED 状态并且付款批次处于 SUCCESS 状态,则可以取消该项目。
     * 取消项目后,状态更改为 RETURNED,资金将退还给发件人
     *
     * @author LHL
     * @param itemId the Id of a Payout item 支付项目的 ID
     * @return the details of the Payouts Item 支付项目的详细信息
     */
    public HttpResponse<PayoutItemResponse> cancelPayoutItem(String itemId) {
        PayoutsItemCancelRequest request = new PayoutsItemCancelRequest(itemId);
        try {
            HttpResponse<PayoutItemResponse> response = client(mode, clientId, clientSecret).execute(request);
            log.info("Success Response Body: {}", new JSONObject(new Json().serialize(response.result())).toString(4));
            return response;
        } catch (HttpException e) {
            //Server side API failure
            try {
                log.error("Error Response Body: {}", new JSONObject(new Json().serialize(encoder.deserializeResponse(new ByteArrayInputStream(e.getMessage().getBytes(StandardCharsets.UTF_8)), Error.class, e.headers()))).toString(4));
            } catch (IOException ioException) {
                log.error("encoder deserializeResponse error: {}", ioException.getMessage());
            }
        } catch (IOException e) {
            //Client side failure
            log.error("Client side failure: {}", e.getMessage());
        }
        return null;
    }
}
import com.paypal.http.Encoder;
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.HttpException;
import com.paypal.http.serializer.Json;
import com.paypal.payouts.Error;
import com.paypal.payouts.PayoutItemResponse;
import com.paypal.payouts.PayoutsItemGetRequest;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
@Slf4j
public class GetPayoutItem extends PayPalClient {

    private static final Encoder encoder = new Encoder();

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    /**
     * sandbox 沙箱/ live 正式环境
     **/
    @Value("${paypal.client.mode}")
    private String mode;

    /**
     * Retrieves a Payouts item details. Payouts item retrieval api(GET - /v1/payments/payouts-items/<item-id>)
     * 检索 Payouts 项目详细信息。 付款项检索 api(GET - /v1/payments/payouts-items/<item-id>)
     *
     * @param itemId the Id of a Payout item 支付项目的 ID
     * @return the details of the Payouts Item 支付项目的详细信息
     */
    public HttpResponse<PayoutItemResponse> getPayoutItem(String itemId) {
        PayoutsItemGetRequest request = new PayoutsItemGetRequest(itemId);
        try {
            HttpResponse<PayoutItemResponse> response = client(mode, clientId, clientSecret).execute(request);
           log.info("Success Response Body: {}", new JSONObject(new Json().serialize(response.result())).toString(4));
            return response;
        } catch (HttpException e) {
            try {
                //Server side API failure
                log.error("Error Response Body: {}", new JSONObject(new Json().serialize(encoder.deserializeResponse(new ByteArrayInputStream(e.getMessage().getBytes(StandardCharsets.UTF_8)), Error.class, e.headers()))).toString(4));
            } catch (IOException ioException) {
                log.error("encoder deserializeResponse error: {}", ioException.getMessage());
            }
        } catch (IOException e) {
            //Client side failure
            log.error("Client side failure: {}", e.getMessage());
        }
        return null;
    }
}
import com.paypal.http.Encoder;
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.HttpException;
import com.paypal.http.serializer.Json;
import com.paypal.payouts.CreatePayoutRequest;
import com.paypal.payouts.CreatePayoutResponse;
import com.paypal.payouts.Currency;
import com.paypal.payouts.Error;
import com.paypal.payouts.PayoutItem;
import com.paypal.payouts.PayoutsPostRequest;
import com.paypal.payouts.SenderBatchHeader;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Component
@Slf4j
public class CreatePayoutsBatch extends PayPalClient {

    private static final Encoder encoder = new Encoder();

    /**
     * 当前货币币种简称, 默认为人名币的币种 EUR CNY USD
     */
    public static final String CURRENT_CY = "USD";

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    /**
     * sandbox 沙箱/ live 正式环境
     **/
    @Value("${paypal.client.mode}")
    private String mode;

    /**
     * Creates a Payouts batch request(POST - /v1/payments/payouts) with 5 payout items
     * 创建一个包含 5 个支付项目的支付批处理请求 (POST - /v1/payments/payouts)
     * A maximum of 15000 payout items are supported in a single batch request
     * 单个批量请求最多支持 15000 个支付项目
     *
     * @return Response for the create payouts call 响应创建支付调用
     */
    public HttpResponse<CreatePayoutResponse> createPayout(String amount, String paypalId,  String description, String senderBatchId, String senderItemId) {
        PayoutsPostRequest request = buildRequestBody(amount, paypalId,  description, senderBatchId, senderItemId);
        HttpResponse<CreatePayoutResponse> response = null;
        try {
            response = client(mode, clientId, clientSecret).execute(request);
            log.info("Success Response Body: {}", new JSONObject(new Json().serialize(response.result())).toString(4));
        } catch (IOException e) {
            log.error("Error Response Body: {}", e.getMessage());
        }
        return response;
    }

    /**
     * Creates a Payouts batch request(POST - /v1/payments/payouts) with 5 payout items
     * All the items are created with invalid amount value to simulate a validation failure
     * 创建一个包含 5 个支付项目的支付批处理请求 (POST - /v1/payments/payouts)
     * 所有项目都使用无效的金额值创建以模拟验证失败
     *
     * @return Response for the create payouts call 对创建付款调用的响应
     */
    public void createPayoutFailure(String amount, String paypalId,  String description, String senderBatchId, String senderItemId) {
        PayoutsPostRequest request = buildRequestBody(amount, paypalId, description, senderBatchId, senderItemId);
        try {
            client(mode, clientId, clientSecret).execute(request);
        } catch (HttpException e) {
            //Server side API failure
            try {
                log.error("Error Response Body: {}", new JSONObject(new Json().serialize(encoder.deserializeResponse(new ByteArrayInputStream(e.getMessage().getBytes(StandardCharsets.UTF_8)), Error.class, e.headers()))).toString(4));
            } catch (IOException ioException) {
                log.error("encoder deserializeResponse error: {}", ioException.getMessage());
            }
        } catch (IOException e) {
            //Client side failure
            log.error("Client side failure: {}", e.getMessage());
        }
    }

    /**
     * Builds a Payouts batch create request body with 5 payout items to receivers email
     * 构建一个 Payouts 批量创建请求正文,其中包含 5 个付款项目到接收者电子邮件
     *
     * @param amount  金额
     * @param paypalId  收款人 paypal id
     * @param description 描述
     * @return Request payload for Payouts create(POST)  用于支付创建(POST)请求的请求负载
     */
    private PayoutsPostRequest buildRequestBody(String amount, String paypalId,  String description, String senderBatchId, String senderItemId) {
        List<PayoutItem> items = IntStream
                .range(1, 2)
                .mapToObj(index -> new PayoutItem()
                        //发件人项目 ID
                        .senderItemId(senderItemId)
                        //可以在每个付款项目的电子邮件通知中发送的自定义注释
                        .note(description)
                        .receiver(paypalId)
                        //所有个人支付项目的价值。
                        .amount(new Currency()
                                .currency(CURRENT_CY)
                                //仅使用十进制格式。不支持本地化货币格式
                                .value(amount)))
                .collect(Collectors.toList());

        CreatePayoutRequest payoutBatch = new CreatePayoutRequest()
                .senderBatchHeader(new SenderBatchHeader()
                        //发件人批次 ID
                        .senderBatchId(senderBatchId)
                        //发送给所有收款人的所有付款项目的电子邮件通知中的通用注释。
                        .emailMessage(description)
                        //收件人作为付款通知收到的电子邮件的主题。
                        .emailSubject("xxAPP提现PAYPAL")
                        .note(description)
                        //收件人标识符。使用收件人的电子邮件地址、电话号码或他们加密的 PayPal 帐号。
                        //PAYPAL_ID EMAIL PHONE
                        .recipientType("PAYPAL_ID"))
                .items(items);

        return new PayoutsPostRequest()
                .requestBody(payoutBatch);
    }
}
import com.paypal.http.Encoder;
import com.paypal.http.HttpResponse;
import com.paypal.http.exceptions.HttpException;
import com.paypal.http.serializer.Json;
import com.paypal.payouts.Error;
import com.paypal.payouts.PayoutBatch;
import com.paypal.payouts.PayoutsGetRequest;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
@Slf4j
public class GetPayoutBatch extends PayPalClient {

    private static final Encoder encoder = new Encoder();

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    /**
     * sandbox 沙箱/ live 正式环境
     **/
    @Value("${paypal.client.mode}")
    private String mode;

    /**
     * Retrieves a Payouts batch details. Payouts batch retrieval api(GET - /v1/payments/payouts/<batch-id>) is a Paginated API
     * that supports retrieving a maximum of 1000 items per page. Batches having items more than that have to paginate
     * to get complete details. Check 'links' in the response for prev and next page URI's.
     * Add 'totalRequired' parameter to know the total number of pages available
     * 检索付款批次详细信息。 付款批量检索 api(GET - /v1/payments/payouts/<batch-id>) 是一个分页 API
     * 支持每页最多检索 1000 个项目。 具有更多项目的批次必须分页
     * 以获取完整的详细信息。 检查响应中的“链接”以获取上一页和下一页 URI。
     * 添加“totalRequired”参数以了解可用页面的总数
     *
     * @param batchId the Id of a Payouts batch 支付批次的 ID
     * @return the details of the Payouts Batch 付款批次的详细信息
     */
    public HttpResponse<PayoutBatch> getPayoutBatch(String batchId) {
        PayoutsGetRequest request = new PayoutsGetRequest(batchId)
                //Optional parameters, maximum of 1000 items are retrieved by default. 可选参数,默认最多检索 1000 项。
                .page(1)
                .pageSize(10)
                .totalRequired(true);
        try {
            HttpResponse<PayoutBatch> response = client(mode, clientId, clientSecret).execute(request);
            log.info("Success Response Body: {}", new JSONObject(new Json().serialize(response.result())).toString(4));
            return response;
        } catch (HttpException e) {
            //Server side API failure
            try {
                log.error("Error Response Body: {}", new JSONObject(new Json().serialize(encoder.deserializeResponse(new ByteArrayInputStream(e.getMessage().getBytes(StandardCharsets.UTF_8)), Error.class, e.headers()))).toString(4));
            } catch (IOException ioException) {
                log.error("encoder deserializeResponse error: {}", ioException.getMessage());
            }
        } catch (IOException e) {
            //Client side failure
            log.error("Client side failure: {}", e.getMessage());
        }
        return null;
    }
}

创建订单和支出

移动端的请看官网,我也不是很清楚,只能说下过程.
结账: 移动端调用/createOrder接口,创建订单,获取订单id,然后移动端去跳转网页paypal登录支付(自定义按钮,不要使用官方的登录按钮),最后paypal服务IPN自动监听到调用你自己设置的IPN接口(类似支付宝支付设置回调接口);如果IPN的接口没有运行(我这边是ios是好的,安卓的监听不到),那只能先捕获接口,再处理回调逻辑(ipn里的逻辑).


    @Resource
    private CreateOrder createOrder;

    @Resource
    private CaptureOrder captureOrder;

    @Resource
    private CreatePayoutsBatch createPayoutsBatch;

    @Resource
    private GetPayoutBatch getPayoutBatch;

    @Resource
    private GetPayoutItem getPayoutItem;

    @Resource
    private CancelPayoutItem cancelPayoutItem;

    /**
     * paypal 创建订单
     *
     * @param orderParam 信息
     * @author LHL
     * */
    @RequestMapping(value="/createOrder",method=RequestMethod.POST)
    public String payment(@RequestBody Map<String,Object> orderParam) {
    	// 描述
        String description = String.valueOf(orderParam.get("tradeDescribe"));
        // 订单号
        String serialNumber = String.valueOf(orderParam.get("serialNumber"));
        // 金额
        String price = new DecimalFormat("0.##").format(orderParam.get("price"));
        //String cancelUrl = String.valueOf(orderParam.get("cancelUrl"));
        //String successUrl = String.valueOf(orderParam.get("successUrl"));

        // String orderId = createOrder.createOrder(description, serialNumber, price, cancelUrl, successUrl);
        String orderId = createOrder.createOrder(description, serialNumber, price, null, null);
        //return "redirect:" + url;
        return orderId;
    }
    
    /**
     * 创建订单成功 执行支付捕获
     * @author LHL
     */
    @RequestMapping(value = "/success", method = RequestMethod.GET)
    public Map<String, Object> success(@RequestParam("orderId") String orderId) {
        Map<String, Object> map = new HashMap<>(5);
        //可以根据新增的付款记录规避重复扣费 参数新增orderId 捕获订单 进行支付
        HttpResponse<Order> response = null;
        try {
            //环境判定sanbox 或 live
            response = captureOrder.captureOrder(orderId);
        } catch (IOException e) {
            log.error("调用paypal扣款失败,失败原因 {}", e.getMessage());
        }
        assert response != null;
        for (PurchaseUnit purchaseUnit : response.result().purchaseUnits()) {
            for (Capture capture : purchaseUnit.payments().captures()) {
                map.put("invoice", capture.invoiceId());
                //支付id
                map.put("paymentId", capture.id());
                //付款人id
                map.put("payerId", response.result().payer().payerId());
                if ("COMPLETED".equals(capture.status())) {
                    Map<String, Object> map1 = captureOrder.testCapturesGetRequest(capture.id());
                    if ((Boolean) map1.get("flag")) {
                        //支付成功 支付状态
                        map.put("payment_status", capture.status());
                        map.put("flag", true);
                        return map;
                    }
                }
            }
        }
        map.put("payment_status", "Denied");
        map.put("flag", false);
        return map;
    }

    /**
     * 支出
     * @author LHL
     */
    @RequestMapping(value = "/payout", method = RequestMethod.POST)
    public Map<String, Object> payout(@RequestBody Map<String,Object> orderParam) {
        Map<String, Object> map = new HashMap<>(10);
        //订单号
        String serialNumber = String.valueOf(orderParam.get("serialNumber"));
        //金额
        String amount = String.valueOf(orderParam.get("amount"));
        //描述
        String description = String.valueOf(orderParam.get("tradeDescribe"));
        //用户paypal的 paypal_id
        String account = String.valueOf(orderParam.get("account"));
        String senderItemId = serialNumber + StringUtil.getRandomCode(4);
        //Create Payout 创建支付
        log.info("Creating Payout");
        String subMsg = "";
        HttpResponse<CreatePayoutResponse> createResponse = createPayoutsBatch.createPayout(amount, account, description, serialNumber, senderItemId);
        if (null != createResponse && createResponse.statusCode() == 201) {
            String batchId = createResponse.result().batchHeader().payoutBatchId();
            //Retrieve Payout Batch 检索支付批次 已成功创建带有 ID 的付款批次
            log.info("Successfully created Payout batch with id: {}", batchId);
            HttpResponse<PayoutBatch> getResponse = getPayoutBatch.getPayoutBatch(batchId);
            if (null != getResponse && getResponse.statusCode() == 200) {
                //Retrieve Payout Item 检索支付项目 已成功检索带 ID 的付款批次
                log.info("Successfully retrieved Payout batch with id: {}", batchId);
                String itemId = getResponse.result().items().get(0).payoutItemId();
                HttpResponse<PayoutItemResponse> getItemResponse = getPayoutItem.getPayoutItem(itemId);
                if (null != getItemResponse && getItemResponse.statusCode() == 200) {
                    System.out.println("Successfully retrieved payout item with id: {}" + itemId);
                    //Check if Payout Batch status is SUCCESS(indicates all Payout items are processed) 检查 Payout Batch 状态是否为 SUCCESS(表示所有 Payout 项目已处理)
                    getResponse = new GetPayoutBatch().getPayoutBatch(batchId);
                    subMsg = "Payout Batch status is " + getResponse.result().batchHeader().batchStatus();
                    if (getResponse.result().batchHeader().batchStatus().equals("SUCCESS")) {
                        map.put("result", true);
                        map.put("order_id", getResponse.result().batchHeader().payoutBatchId());
                        return map;
                    }
                } else {
                    subMsg = "Failed to retrieve Payout item with id";
                    //无法检索带 ID 的付款项目
                    log.error("Failed to retrieve Payout item with id: {}", itemId);
                }
            } else {
                subMsg = "Failed to retrieve Payout batch with id";
                //无法检索带 ID 的付款批次
                log.error("Failed to retrieve Payout batch with id: {}", batchId);
            }
        }
        map.put("result", false);
        map.put("sub_msg", subMsg);
        return map;
    }

回调

IPN

官网

	
    private static final String SUCCESS_URL_V2 =  "intlPay/paypal/notify";
	
	/**
     * 创建订单成功回调 v2 ipn
     */
    @RequestMapping(value = SUCCESS_URL_V2, method = RequestMethod.POST)
    public String paypalPayNotifyV2(HttpServletRequest request, HttpServletResponse response) {
        try {
            Map map = RequestToMapUtil.getParameterMap(request);
            log.info(map.toString());
            //商家订单号
            String serialNumber = (String)map.get("invoice");
            //订单状态
            String paymentStatus = (String)map.get("payment_status");
            //流水号
            String paymentId = (String)map.get("txn_id");
            //付款人id
            String payerId  = (String)map.get("payer_id");
            if("Completed".equals(paymentStatus)) {
                log.info("支付成功");
                // 只处理支付成功的订单: 修改交易表状态,支付成功
                // 代码逻辑 ......
                return "success";
          	}
            if("Denied".equals(paymentStatus)) {
                log.info("拒绝支付");
                //代码逻辑 ......
                return "success";
            }
        } catch (Exception e) {
            log.error("paypal success notify error: {}", e.getMessage());
        }
        return "success";
    }

/**
 * 将Request转换成Map
 *
 * @author LHL
 * @since 2021/6/11
 */
public class RequestToMapUtil {

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static Map getParameterMap(HttpServletRequest request) {
        // 参数Map
        Map properties = request.getParameterMap();
        // 返回值Map
        Map returnMap = new HashMap();
        Iterator entries = properties.entrySet().iterator();
        Map.Entry entry;
        String name = "";
        String value = "";
        while (entries.hasNext()) {
            entry = (Map.Entry) entries.next();
            name = (String) entry.getKey();
            Object valueObj = entry.getValue();
            if (null == valueObj) {
                value = "";
            } else if (valueObj instanceof String[]) {
                String[] values = (String[]) valueObj;
                for (int i = 0; i < values.length; i++) {
                    value = values[i] + ",";
                }
                value = value.substring(0, value.length() - 1);
            } else {
                value = valueObj.toString();
            }
            returnMap.put(name, value);
        }
        return returnMap;
    }

    public static Map<String, Object> getPrepayMapInfo(String Str) {
        String notityXml = Str.replaceAll("</?xml>", "");
        Pattern pattern = Pattern.compile("<.*?/.*?>");
        Matcher matcher = pattern.matcher(notityXml);
        Pattern pattern2 = Pattern.compile("!.*]");
        Map<String, Object> mapInfo = new HashMap<>();
        while (matcher.find()) {
            String key = matcher.group().replaceAll(".*/", "");
            key = key.substring(0, key.length() - 1);
            Matcher matcher2 = pattern2.matcher(matcher.group());
            String value = matcher.group().replaceAll("</?.*?>", "");
            if (matcher2.find() && !value.equals("DATA")) {
                value = matcher2.group().replaceAll("!.*\\[", "");
                value = value.substring(0, value.length() - 2);
            }
            mapInfo.put(key, value);
        }
        return mapInfo;
    }
}

APP主动捕获

如果IPN没有监听到就使用这个


	/**
     * 创建订单成功 app回调
     * @author LHL
     */
    @PostMapping(value = "intlPay/paypal/drivingNotify")
    public String paypalPayDrivingNotify(PaypalDto paypalDto) {
        try {
            log.info("payerId: {}", paypalDto.getPayerId());
            log.info("orderId: {}", paypalDto.getOrderId());
            Map<String, Object> map = payFeignClient.success(paypalDto.getOrderId());
            //商家订单号
            String serialNumber = (String)map.get("invoice");
            //订单状态
            String paymentStatus = (String)map.get("payment_status");
            //流水号
            //String paymentId = paypalDto.getPaymentId();
            String paymentId = (String)map.get("paymentId");
            log.info("paymentId: {}", paypalDto.getPaymentId());
            //付款人id
            String payerId  = (String)map.get("payerId");
            //String payerId  = paypalDto.getPayerId();
            log.info("paymentStatus: {}", paymentStatus);
            if("COMPLETED".equals(paymentStatus)) {
            log.info("支付成功");
                //代码逻辑...
                return "success";
            }
            if("Denied".equals(paymentStatus)) {
                log.info("支付失败");
                //代码逻辑...
                return "fail";
            }
        } catch (Exception e) {
            log.error("paypal success notify error: {}", e.getMessage());
        }
        return "fail";
    }

 类似资料: