做全球性的支付,选用paypal!为什么选择paypal? 因为paypal是目前全球最大的在线支付工具,就像国内的支付宝一样,是一个基于买卖双方的第三方平台。买家只需知道你的paypal账号,即可在线直接把钱汇入你的账户,即时到账,简单方便快捷。
查看进入支付页面的历史记录:
在集成paypal支付接口之前,首先要有一系列的准备,开发者账号啊、sdk、测试环境等等先要有,然后再码代码。集成的步骤如下:
PayPal开发者:https://developer.paypal.com/dashboard/accounts
sandbox测试账号登录的测试网址:https://www.sandbox.paypal.com
一、环境准备
注册paypal账号
注册paypal开发者账号
创建两个测试用户
创建应用,生成用于测试的clientID 和 密钥
二、代码集成
springboot环境
pom引进paypal-sdk的jar包
码代码
测试
目录
注册paypal账号
注册paypal开发者账号
创建应用,生成用于测试的clientID 和 密钥
springboot环境搭建
pom引进paypal-sdk的jar包
码代码
(1)Application.java
(2)PaypalConfig.java
(3)PaypalPaymentIntent.java
(4)PaypalPaymentMethod.java
(5)PaymentController.java
PayPalRESTExcption:
(6)PaypalService.java
(7)URLUtils.java
我的Restful风格controller:
Payment数据内容:
(8)cancel.html
(9)index.html
(10)success.html
(11)application.properties
测试
报错
现在开始
(1)在浏览器输入“安全海淘国际支付平台_安全收款外贸平台-PayPal CN” 跳转到如下界面,点击右上角的注册
(2)选择,”创建商家用户”,根据要求填写信息,一分钟的事,注册完得去邮箱激活
(1)在浏览器输入“https://developer.paypal.com”,点击右上角的“Log into Dashboard”,用上一步创建好的账号登录
(1)登录成功后,在左边的导航栏中点击 Sandbox 下的 Accounts
(2)进入Acccouts界面后,可以看到系统有两个已经生成好的测试账号,但是我们不要用系统给的测试账号,很卡的,自己创建两个
(3)点击右上角的“Create Account”,创建测试用户
<1> 先创建一个“ PERSONAL”类型的用户,国家一定要选“China”,账户余额自己填写
<2> 接着创建一个“BUSINESS”类型的用户,国家一定要选“China”,账户余额自己填写
<3>创建好之后可以点击测试账号下的”Profile“,可以查看信息,如果没加载出来,刷新
<4>用测试账号登录测试网站查看,注意!这跟paypal官网不同!不是同一个地址,在浏览器输入:安全海淘国际支付平台_安全收款外贸平台-PayPal CN 在这里登陆测试账户
(1)点击左边导航栏Dashboard下的My Apps & Credentials,创建一个Live账号,下图是我已经创建好的
(2)然后再到下边创建App
这是我创建好的“Test”App
(3)点击刚刚创建好的App“Test”,注意看到”ClientID“ 和”Secret“(Secret如果没显示,点击下面的show就会看到,点击后show变为hide)
(1)新建几个包,和目录,项目结构如下
com.paypal.sdk
rest-api-sdk
1.4.2
com.paypal.sdk
rest-api-sdk
1.4.2
com.paypal.sdk
checkout-sdk
1.0.2
package com.masasdani.paypal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.masasdani.paypal.config;
import java.util.HashMap;import java.util.Map;
import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
import com.paypal.base.rest.APIContext;import com.paypal.base.rest.OAuthTokenCredential;import com.paypal.base.rest.PayPalRESTException;
@Configurationpublic class PaypalConfig {
@Value("${paypal.client.app}")
private String clientId;
@Value("${paypal.client.secret}")
private String clientSecret;
@Value("${paypal.mode}")
private String mode;
@Bean
public Map paypalSdkConfig(){
Map sdkConfig = new HashMap<>();
sdkConfig.put("mode", mode);
return sdkConfig;
}
@Bean
public OAuthTokenCredential authTokenCredential(){
return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
}
@Bean
public APIContext apiContext() throws PayPalRESTException{
APIContext apiContext = new APIContext(authTokenCredential().getAccessToken());
apiContext.setConfigurationMap(paypalSdkConfig());
return apiContext;
}
}
在 PayPal API 中,PaypalPaymentIntent 表示支付的意图或目的。
以下是 PayPal Payment Intent 的四种可能取值:
package com.masasdani.paypal.config;
public enum PaypalPaymentIntent {
sale, authorize, order
}
在 PayPal API 中,PaypalPaymentMethod 表示支付所使用的支付方式。
以下是 PayPal Payment Method 的一些可能取值:
package com.masasdani.paypal.config;
public enum PaypalPaymentMethod {
credit_card, paypal
}
package com.masasdani.paypal.controller;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.masasdani.paypal.config.PaypalPaymentIntent;
import com.masasdani.paypal.config.PaypalPaymentMethod;
import com.masasdani.paypal.service.PaypalService;
import com.masasdani.paypal.util.URLUtils;
import com.paypal.api.payments.Links;
import com.paypal.api.payments.Payment;
import com.paypal.base.rest.PayPalRESTException;
@Controller
@RequestMapping("/")
public class PaymentController {
public static final String PAYPAL_SUCCESS_URL = "pay/success";
public static final String PAYPAL_CANCEL_URL = "pay/cancel";
private Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private PaypalService paypalService;
@RequestMapping(method = RequestMethod.GET)
public String index(){
return "index";
}
@RequestMapping(method = RequestMethod.POST, value = "pay")
public String pay(HttpServletRequest request){ // RestFul风格里也能传入HttpServletRequest
String cancelUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_CANCEL_URL;
String successUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_SUCCESS_URL;
try {
Payment payment = paypalService.createPayment(
500.00,
"USD",
PaypalPaymentMethod.paypal,
PaypalPaymentIntent.sale,
"payment description",
cancelUrl,
successUrl);
for(Links links : payment.getLinks()){
if(links.getRel().equals("approval_url")){
// 输出跳转到paypal支付页面的网站,eg:https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-8HL7026676482401S
logger.info("links is {}", links.getHref());
return "redirect:" + links.getHref();
}
}
} catch (PayPalRESTException e) {
// PayPalRESTException是PayPal的REST API客户端中的一个异常类,它用于处理与PayPal REST API相关的异常情况
// 当使用 PayPal 的 REST API 进行付款处理、交易查询、退款请求等操作时,如果发生错误或异常情况,PayPalRestException 将被抛出,以便开发者可以捕获并处理这些异常
log.error(e.getMessage());
}
return "redirect:/";
}
@RequestMapping(method = RequestMethod.GET, value = PAYPAL_CANCEL_URL)
public String cancelPay(){
return "cancel";
}
@RequestMapping(method = RequestMethod.GET, value = PAYPAL_SUCCESS_URL)
public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId){ // 一定是PayerID,PayPal通常使用"PayerID"(ID和P都大小写)作为参数名称
try {
Payment payment = paypalService.executePayment(paymentId, payerId);
if(payment.getState().equals("approved")){
log.info("支付成功Payment:" + payment);
return "success";
}
} catch (PayPalRESTException e) {
log.error(e.getMessage());
}
return "redirect:/";
}
}
PayPalRESTException
类中包含以下常见的属性:
getMessage()
: 返回异常的详细错误消息。getResponseCode()
: 返回 HTTP 响应的状态码。getDetails()
: 返回一个 ErrorDetails
对象,其中包含有关异常的更多详细信息。getInformationLink()
: 返回一个 URL,提供有关异常的更多信息和解决方案的链接。PayPalRESTException
是 PayPal REST API SDK 中的一个异常类,用于处理与 PayPal REST API 请求和响应相关的错误和异常情况。这个异常类通常用于开发者在使用 PayPal REST API 时捕获和处理错误,包括但不限于如下作用:
认证错误:当提供的 PayPal 客户端ID和秘密无效或过期时,可能会引发异常。
请求错误:如果请求的数据不符合 PayPal REST API 的要求,如无效的金额、货币或请求格式,也可能引发异常。
支付处理错误:在创建支付、执行支付或查询支付状态时,可能会出现与支付相关的问题,例如支付已取消或已拒绝等情况。
通信问题:如果与 PayPal 服务器之间的通信中断或出现问题,也可以引发异常。
认证错误:
无效的客户端ID或客户端秘密:如果你提供的 PayPal 客户端ID或客户端秘密无效,可能会抛出认证错误。
PayPalRESTException: Authentication failed with the provided credentials.
请求错误:
PayPalRESTException: Request failed with status code 400 and message: Currency is not supported.
无效的金额:如果请求中的金额不合法,也可能引发请求错误。
PayPalRESTException: Request failed with status code 400 and message: Invalid total amount.
支付处理错误:
PayPalRESTException: Payment has been canceled by the user.
PayPalRESTException: Payment has been declined by PayPal.
网络问题:
网络连接问题:当发生网络连接问题时,可能会引发网络异常。
PayPalRESTException: Network error: Connection timed out.
服务器通信错误:如果 PayPal 服务器无法响应请求,也可能引发异常。
PayPalRESTException: Network error: Unable to communicate with PayPal servers.
注意:以下是两种不同的处理异常的方式!
1. 使用 try-catch 块:
· 作用:try-catch
块用于捕获和处理可能发生的异常,以便在异常发生时采取适当的措施,如记录错误、向用户提供错误消息或执行其他操作。它允许你在同一方法内部处理异常,避免异常的传播到上层调用层次。
· 使用场景:使用 try-catch
块来处理已知的、可以预测的异常,以确保程序能够正常继续执行,而不会中断。适用于在方法内部处理异常并采取特定的操作。
- 对于try { } catch (Exception e) { } 的 catch 中的处理方式有两种:e.printStackTrace() 与 log.error(e.getMessage()):
- e.printStackTrace()
是异常对象 e
的一个方法,它用于将异常的堆栈跟踪信息打印到标准错误流(通常是控制台)。
这种方式通常用于调试目的,以便开发者可以看到详细的异常信息,包括异常发生的位置和方法调用链。这会输出异常的完整堆栈跟踪,包括方法名、类名、行号等信息。
- log.error()
是一种记录日志的方式,通常使用日志框架(如Log4j、Logback、SLF4J等)提供的方法来记录异常信息。
e.getMessage()
只获取异常的消息部分,通常包含异常的简要说明。这种方式更适用于生产环境,以便将异常信息记录到日志文件中,以便后续的故障排除和监控。
Logger log = LoggerFactory.getLogger(getClass());
或者:
Logger log = LoggerFactory.getLogger(YourClass.class);
2. 使用 throw 关键字抛出异常:
· 作用:throw
关键字用于主动抛出异常,将异常传递到方法的调用者,以便在上层调用层次中进行处理。它用于表示方法无法处理异常,需要由调用者来处理。(用于将异常传播到调用方的方式,而不是用于捕获和处理异常)
· 使用场景:使用 throw
来通知调用者方法内部发生了异常,并将异常传递给调用者,由调用者负责处理异常。适用于方法内部无法处理的异常,或者需要将异常传递给上层调用层次的情况。
PayPalRESTException
提供了错误的详细信息,包括错误消息、错误代码、响应状态等,开发者可以使用这些信息来识别问题的性质和原因,并采取适当的措施来处理异常情况。
PayPalRESTException的使用:
import com.paypal.api.payments.*;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.PayPalRESTException;
public class PayPalExample {
public static void main(String[] args) {
// 设置 PayPal REST API 的认证信息
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
APIContext apiContext = new APIContext(clientId, clientSecret, "sandbox");
// 创建一个 PayPal 支付对象
Payment payment = new Payment();
// 设置支付相关信息,例如总金额、货币等
Amount amount = new Amount();
amount.setCurrency("USD");
amount.setTotal("100.00");
payment.setAmount(amount);
// 设置支付的执行链接
RedirectUrls redirectUrls = new RedirectUrls();
redirectUrls.setReturnUrl("http://example.com/return");
redirectUrls.setCancelUrl("http://example.com/cancel");
payment.setRedirectUrls(redirectUrls);
// 设置支付方式为PayPal
payment.setIntent("sale");
payment.setPayer(new Payer("paypal"));
try {
Payment createdPayment = payment.create(apiContext);
// 创建支付请求,并处理响应
} catch (PayPalRESTException e) {
// 处理异常情况
System.err.println(e.getDetails());
}
}
}
如何根据异常的消息不同返回不同的文字给 ResponseEntity :
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import com.paypal.base.rest.PayPalRESTException;
public ResponseEntity processPayPalPayment() {
try {
// 执行 PayPal 支付操作
// ...
// 如果一切正常,返回成功响应
return ResponseEntity.status(HttpStatus.OK).body("支付成功");
} catch (PayPalRESTException e) {
String errorMessage = e.getMessage();
if (errorMessage.contains("Authentication failed with the provided credentials")) {
// 无效的客户端ID或客户端秘密
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("认证失败");
} else if (errorMessage.contains("Currency is not supported")) {
// 无效的货币
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("无效的货币");
} else if (errorMessage.contains("Invalid total amount")) {
// 无效的金额
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("无效的金额");
} else if (errorMessage.contains("Payment has been canceled by the user")) {
// 支付已取消
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("支付已取消");
} else if (errorMessage.contains("Payment has been declined by PayPal")) {
// 支付已拒绝
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("支付已拒绝");
} else if (errorMessage.contains("Network error: Connection timed out")) {
// 网络连接问题
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("网络连接超时");
} else {
// 其他异常情况
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("支付失败,请联系客服");
}
}
}
HttpStatus 是 Spring Framework 提供了一组常用的 HTTP 状态码,以便在构建 RESTful API 或 Web应用程序时使用。这些常用的状态码位于 org.springframework.http.HttpStatus
枚举中,包括 200 OK、201 CREATED、400 BAD_REQUEST、401 UNAUTHORIZED、404 NOT_FOUND、500 INTERNAL_SERVER_ERROR 等等。你可以直接使用这些常量来设置响应的状态码,而无需自行定义。也可以直接:
return new CommonResult(e.getResponsecode(), e.getMessage(), null);
package com.masasdani.paypal.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.masasdani.paypal.config.PaypalPaymentIntent;
import com.masasdani.paypal.config.PaypalPaymentMethod;
import com.paypal.api.payments.Amount;
import com.paypal.api.payments.Payer;
import com.paypal.api.payments.Payment; // 并不是自己写的实体类Payment
import com.paypal.api.payments.PaymentExecution;
import com.paypal.api.payments.RedirectUrls;
import com.paypal.api.payments.Transaction;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.PayPalRESTException;
@Service
public class PaypalService {
@Autowired
private APIContext apiContext;
// 创建支付
public Payment createPayment(
Double total,
String currency,
PaypalPaymentMethod method,
PaypalPaymentIntent intent,
String description,
String cancelUrl,
String successUrl) throws PayPalRESTException{
// 接受参数包括总金额(total)、货币类型(currency)、支付方法(method)、支付意图(intent)、描述(description)、取消 URL(cancelUrl)和成功 URL(successUrl)。在方法内部,它使用这些参数创建一个支付请求,并返回创建的 Payment 对象
Amount amount = new Amount();
amount.setCurrency(currency);
amount.setTotal(String.format("%.2f", total));
Transaction transaction = new Transaction();
transaction.setDescription(description);
transaction.setAmount(amount);
List 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);
RedirectUrls redirectUrls = new RedirectUrls();
// Paypal取消支付回调链接
redirectUrls.setCancelUrl(cancelUrl);
// Paypal付完款回调链接
redirectUrls.setReturnUrl(successUrl);
// Paypal付完款回调链接:如果要其他数据作为参数传递给成功支付后的回调URL即控制器类中的successPay方法,则对回调URL进行拼接:redirectUrls.setReturnUrl(successUrl + "?param1=" + param1 + "¶m2=" + param2 + "¶m3=" + paaram3);
redirectUrls.setReturnUrl(successUrl + "?userId=" + entity.getUserId() + "&totalFee=" + entity.getTotalFee() + "&payFrom=" + entity.getPayFrom());
payment.setRedirectUrls(redirectUrls);
return payment.create(apiContext);
}
// 执行支付
public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException{
// 接受支付 ID(paymentId)和付款人 ID(payerId)作为参数。在方法内部,它使用这些参数创建一个 PaymentExecution 对象,并使用支付 ID 和付款人 ID 执行支付请求,返回执行支付后的 Payment 对象
Payment payment = new Payment();
payment.setId(paymentId);
PaymentExecution paymentExecute = new PaymentExecution();
paymentExecute.setPayerId(payerId);
return payment.execute(apiContext, paymentExecute);
}
}
package com.masasdani.paypal.util;
import javax.servlet.http.HttpServletRequest;
public class URLUtils {
public static String getBaseURl(HttpServletRequest request) {
String scheme = request.getScheme(); // 获取请求的协议,如 "http" 或 "https"
String serverName = request.getServerName(); // 获取服务器名称
int serverPort = request.getServerPort();// 获取服务器端口
String contextPath = request.getContextPath();// 获取上下文路径
// 建议在此处插入以下代码查看上下文路径是否符合期望,因为有时候可能部署之类导致不对
System.out.println("contextPanth: " + contextPath);
StringBuffer url = new StringBuffer();
url.append(scheme).append("://").append(serverName);
// 判断服务器端口是否为标准的 HTTP(80)或 HTTPS(443)端口,决定是否将端口号添加到 URL 中
// 当浏览器发起 HTTP 请求时,通常会使用默认的端口号,即 HTTP 使用 80 端口,HTTPS 使用 443 端口。这些端口号是默认的,因此在 URL 中不需要显式指定
// 然而,如果应用程序部署在非默认的端口上,例如使用自定义的端口号(例如 8080、3000 等),则需要将该端口号包含在 URL 中,以确保浏览器能够正确地访问应用程序
if ((serverPort != 80) && (serverPort != 443)) {
url.append(":").append(serverPort);
}
url.append(contextPath);
if(url.toString().endsWith("/")){ // URL 是否以斜杠结尾,如果不是,则添加斜杠
url.append("/");
}
return url.toString(); // 获取当前请求的完整 URL
}
}
比如我的controller中的cancelUrl与successUrl就需要改成这样,因为在上面方法这种获取到的context为空,重定向之后就会报404路径不存在,可能是其他配置问题:
先生成预订单,将 url 返回给前端,前端进行跳转,之后即可获得取消/成功回调。
package com.harmony.supreme.modular.paypal.controller;
import com.harmony.supreme.modular.paypal.entity.PaypalOrderParam;
import com.harmony.supreme.modular.paypal.service.PaypalService;
import com.harmony.supreme.modular.paypal.util.URLUtils;
import com.paypal.api.payments.Links;
import com.paypal.api.payments.Payment;
import com.paypal.base.rest.PayPalRESTException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
@RestController
@RequestMapping("/marketing")
public class PaypalController {
public static final String PAYPAL_SUCCESS_URL = "/marketing/paypal/success";
public static final String PAYPAL_CANCEL_URL = "/marketing/paypal/cancel";
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private PaypalService paypalService;
/**
* 生成订单
*/
@PostMapping("/paypal")
public CommonResult paypal(@RequestBody PaypalOrderParam entity, HttpServletRequest request, HttpServletResponse response) {
String cancelUrl = URLUtils.getBaseUrl(request) + '/' +PAYPAL_CANCEL_URL;
logger.info("cancelUrl is {}", cancelUrl); // 日志打印
String successUrl = URLUtils.getBaseUrl(request) + '/' +PAYPAL_SUCCESS_URL;
logger.info("successUrl is {}", successUrl); // 日志打印
try {
//Payment payment = paypalService.createPayment(entity.getTotalFee(), entity.getUserId(), entity.getContext(),
Payment payment = paypalService.createPayment(5.0, "1686590877167329281", "视频",
new Date(), cancelUrl, successUrl);
for (Links links : payment.getLinks()) {
if (links.getRel().equals("approval_url")) {
logger.info("links.getHref() is {}", links.getHref());
logger.info("支付订单返回paymentId:" + payment.getId());
logger.info("支付订单状态state:" + payment.getState());
logger.info("支付订单创建时间:" + payment.getCreateTime());
String href = links.getHref();
return CommonResult.data(href);
}
}
} catch (PayPalRESTException e) {
logger.error(e.getMessage());
return new CommonResult<>(e.getResponsecode(), e.getMessage(), null);
}
return CommonResult.error("错误");
}
/**
* 取消支付
*/
@GetMapping("/paypal/cancel")
public CommonResult cancelPay(){
return CommonResult.data("用户取消支付");
}
/**
* 支付操作
*/
@GetMapping("/paypal/success")
public CommonResult successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId) { // 一定是PayerID,PayPal通常使用"PayerID"(ID和P都大小写)作为参数名称
try {
Payment payment = paypalService.executePayment(paymentId, payerId);
// 支付成功
if(payment.getState().equals("approved")){
// 订单号
String saleId = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();
paypalService.addPay(request, saleId);
logger.info("PDT通知:交易成功回调");
logger.info("付款人账户:"+payment.getPayer().getPayerInfo().getEmail());
logger.info("支付订单Id: {}",paymentId);
logger.info("支付订单状态state:" + payment.getState());
logger.info("交易订单Id: {}",saleId);
logger.info("交易订单状态state:"+payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState());
logger.info("交易订单支付时间:"+payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime());
return CommonResult.data("支付成功");
}
} catch (PayPalRESTException e) {
logger.info(e.getMessage());
return new CommonResult(e.getResponsecode(), e.getMessage(), null);
}
return CommonResult.data("支付失败");
}
}
最后支付成功后返回的Payment数据如下:
支付成功Payment:{
"id": "PAYID-MUY7X3I47036021V43838732",
"intent": "sale",
"payer": {
"payment_method": "paypal",
"status": "VERIFIED",
"payer_info": {
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"payer_id": "PRKXLARWJVWQL",
"country_code": "C2",
"shipping_address": {
"recipient_name": "Doe John",
"line1": "NO 1 Nan Jin Road",
"city": "Shanghai",
"country_code": "C2",
"postal_code": "200000",
"state": "Shanghai"
}
}
},
"cart": "92948520FV7438203",
"transactions": [
{
"transactions": [],
"related_resources": [
{
"sale": {
"id": "9GJ58424N8173751G",
"amount": {
"currency": "USD",
"total": "5.00",
"details": {
"shipping": "0.00",
"subtotal": "5.00",
"handling_fee": "0.00",
"insurance": "0.00",
"shipping_discount": "0.00"
}
},
"payment_mode": "INSTANT_TRANSFER",
"state": "completed",
"protection_eligibility": "ELIGIBLE",
"protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
"transaction_fee": {
"currency": "USD",
"value": "0.47"
},
"parent_payment": "PAYID-MUY7X3I47036021V43838732",
"create_time": "2023-10-20T07:04:41Z",
"update_time": "2023-10-20T07:04:41Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/sale/9GJ58424N8173751G",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/sale/9GJ58424N8173751G/refund",
"rel": "refund",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUY7X3I47036021V43838732",
"rel": "parent_payment",
"method": "GET"
}
]
}
}
],
"amount": {
"currency": "USD",
"total": "5.00",
"details": {
"shipping": "0.00",
"subtotal": "5.00",
"handling_fee": "0.00",
"insurance": "0.00",
"shipping_discount": "0.00"
}
},
"payee": {
"email": "[email protected]",
"merchant_id": "AZ5ZMSER4CFS2"
},
"description": "视频",
"item_list": {
"items": [],
"shipping_address": {
"recipient_name": "Doe John",
"line1": "NO 1 Nan Jin Road",
"city": "Shanghai",
"country_code": "C2",
"postal_code": "200000",
"state": "Shanghai"
}
}
}
],
"failed_transactions": [],
"state": "approved",
"create_time": "2023-10-20T04:02:53Z",
"update_time": "2023-10-20T07:04:41Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUY7X3I47036021V43838732",
"rel": "self",
"method": "GET"
}
]
}
或者:
支付成功Payment:{
"id": "PAYID-MUZC3FI3NX78464UJ4562005",
"intent": "sale",
"payer": {
"payment_method": "paypal",
"status": "VERIFIED",
"payer_info": {
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"payer_id": "PRKXLARWJVWQL",
"country_code": "C2",
"shipping_address": {
"recipient_name": "Doe John",
"line1": "NO 1 Nan Jin Road",
"city": "Shanghai",
"country_code": "C2",
"postal_code": "200000",
"state": "Shanghai"
}
}
},
"cart": "8HL7026676482401S",
"transactions": [
{
"transactions": [],
"related_resources": [
{
"sale": {
"id": "0KC07001909543205",
"amount": {
"currency": "USD",
"total": "5.00",
"details": {
"shipping": "0.00",
"subtotal": "5.00",
"handling_fee": "0.00",
"insurance": "0.00",
"shipping_discount": "0.00"
}
},
"payment_mode": "INSTANT_TRANSFER",
"state": "completed",
"protection_eligibility": "ELIGIBLE",
"protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
"transaction_fee": {
"currency": "USD",
"value": "0.47"
},
"parent_payment": "PAYID-MUZC3FI3NX78464UJ4562005",
"create_time": "2023-10-20T07:35:21Z",
"update_time": "2023-10-20T07:35:21Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/sale/0KC07001909543205",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/sale/0KC07001909543205/refund",
"rel": "refund",
"method": "POST"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUZC3FI3NX78464UJ4562005",
"rel": "parent_payment",
"method": "GET"
}
]
}
}
],
"amount": {
"currency": "USD",
"total": "5.00",
"details": {
"shipping": "0.00",
"subtotal": "5.00",
"handling_fee": "0.00",
"insurance": "0.00",
"shipping_discount": "0.00"
}
},
"payee": {
"email": "[email protected]",
"merchant_id": "AZ5ZMSER4CFS2"
},
"description": "合恕支付充值",
"item_list": {
"items": [],
"shipping_address": {
"recipient_name": "Doe John",
"line1": "NO 1 Nan Jin Road",
"city": "Shanghai",
"country_code": "C2",
"postal_code": "200000",
"state": "Shanghai"
}
}
}
],
"failed_transactions": [],
"state": "approved",
"create_time": "2023-10-20T07:34:44Z",
"update_time": "2023-10-20T07:35:21Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUZC3FI3NX78464UJ4562005",
"rel": "self",
"method": "GET"
}
]
}
在PayPal的Payment对象中,两种状态:支付状态(Payment Status)、交易状态(Transaction Status)。
支付状态(Payment Status):
取值:payment.getId()
交易状态(Transaction Status):
取值:payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId()
(其中:PaypalPaymentIntent有order、sale、authorize三种状态,所以获取时分别将getSale()换为getOrder()、getAuthorize()即可)
交易订单id为:payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId()或修改Sale为其他两种状态。
Insert title here
Canceled by user
Insert title here
Insert title here
Payment Success
paypal.client.app是App的CilentID, paypal.client.secret是Secret
server.port: 8088
spring.thymeleaf.cache=false
paypal.mode=sandbox
paypal.client.app=AeVqmY_pxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKYniPwzfL1jGR
paypal.client.secret=ELibZhExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxUsWOA_-
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.excel.util.StringUtils;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.paypal.api.payments.Payment;
import com.paypal.base.rest.PayPalRESTException;
import com.paypal.http.HttpResponse;
import com.paypal.orders.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class PaypalV2Controller {
@Value("${paypal.client.mode}")
private String mode;
@Value("${paypal.client.app}")
private String clientId;
@Value("${paypal.client.secret}")
private String secret;
@Resource
private PaypalV2Service paypalV2Service;
/**
* 生成订单
*/
@ApiOperationSupport(order = 1)
@ApiOperation("生成订单")
@PostMapping("/pay/paypal")
public CommonResult paypalV2(@RequestBody PaypalOrderParam entity, HttpServletRequest request, HttpServletResponse response) {
// 在哪里部署就写哪个域名xxxxx,因为在服务器上如按PayPal V1中所写的方法结果xxxxx还是localhost,此路径在服务器上将报错
String cancelUrl = "http://xxxxx:xxxx/pay/cancel";
String successUrl = "http://xxxxx:xxxx/pay/success";
if (StringUtils.isBlank(entity.getUserId())) {
entity.setUserId(StpUtil.getLoginIdAsString());
}
try {
String href = paypalV2Service.createPayment(entity, cancelUrl, successUrl);
return CommonResult.data(href);
} catch (PayPalRESTException e) {
return new CommonResult<>(e.getResponsecode(), e.getMessage(), null);
}
}
/**
* 取消支付
*/
@ApiOperationSupport(order = 2)
@ApiOperation("取消支付")
@GetMapping("/pay/cancel")
public String cancelPay(){
return "已取消支付,请返回上一级页面";
}
/**
* 支付成功
*/
@ApiOperationSupport(order = 3)
@ApiOperation("支付成功")
@GetMapping("/pay/success")
public String successPay(@RequestParam("token") String token, @RequestParam("userId") String userId, @RequestParam("beanNum") String beanNum) {
//捕获订单 进行支付
HttpResponse response = null;
OrdersCaptureRequest ordersCaptureRequest = new OrdersCaptureRequest(token);
ordersCaptureRequest.requestBody(new OrderRequest());
PayPalClient payPalClient = new PayPalClient();
try {
//环境判定sandbox 或 live
response = payPalClient.client(mode, clientId, secret).execute(ordersCaptureRequest);
for (PurchaseUnit purchaseUnit : response.result().purchaseUnits()) {
for (Capture capture : purchaseUnit.payments().captures()) {
if ("COMPLETED".equals(capture.status())) {
//支付成功
// 订单号
String saleId = capture.id();
String fee = capture.amount().value();
paypalV2Service.addPay(fee, saleId, userId, beanNum);
return "支付成功,请返回上一级页面";
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return "支付失败,请返回上一级页面";
}
}
public interface PaypalV2Service {
/**
* 创建支付:实体类、取消支付时的重定向 URL、支付成功时的重定向 URL
*/
public String createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) throws PayPalRESTException;
/**
* 支付成功生成订单
*/
void addPay(String fee, String saleId, String userId, String beanNum);
}
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fhs.common.utils.StringUtil;
import com.paypal.core.PayPalHttpClient;
import com.paypal.http.HttpResponse;
import com.paypal.orders.*;
import com.paypal.orders.Order;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
@Service
public class PaypalV2ServiceImpl implements PaypalV2Service {
@Value("${paypal.client.mode}")
private String mode;
@Value("${paypal.client.app}")
private String clientId;
@Value("${paypal.client.secret}")
private String secret;
@Resource
private UserPayService userPayService;
@Resource
private UserOrderService userOrderService;
@Resource
private SysRechargePlanService sysRechargePlanService;
@Override
public String createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) {
PayPalClient payPalClient = new PayPalClient();
// 设置环境沙盒或生产
PayPalHttpClient client = payPalClient.client(mode, clientId, secret);
//回调参数(支付成功success路径所携带的参数)
Map sParaTemp = new HashMap();
BigDecimal bigDecimal = new BigDecimal("100");
String totalMoney = String.valueOf(entity.getTotalFee().divide(bigDecimal));
// 回调的参数可以多设置几个
// 插入用户USERID
if (StringUtil.isEmpty(entity.getUserId())) {
entity.setUserId(StpUtil.getLoginIdAsString());
}
sParaTemp.put("userId", entity.getUserId());
// 根据价钱查找方案
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("TYPE", "BALANCE")
.eq("FEE", Float.valueOf(totalMoney));
SysRechargePlan one = sysRechargePlanService.getOne(wrapper);
// 插入所得金豆数
sParaTemp.put("beanNum", String.valueOf(one.getUnit()));
String url = successUrl + paramsConvertUrl(sParaTemp);
// eg:回调链接:http://localhost:xxxx/pay/success?beanNum=150&userId=1655776615868264450
System.out.println("回调链接:"+url);
// 配置请求参数
OrderRequest orderRequest = new OrderRequest();
orderRequest.checkoutPaymentIntent("CAPTURE");
List purchaseUnits = new ArrayList<>();
purchaseUnits.add(new PurchaseUnitRequest().amountWithBreakdown(new AmountWithBreakdown().currencyCode("USD").value(totalMoney)));
orderRequest.purchaseUnits(purchaseUnits);
orderRequest.applicationContext(new ApplicationContext().returnUrl(url).cancelUrl(cancelUrl));
OrdersCreateRequest request = new OrdersCreateRequest().requestBody(orderRequest);
HttpResponse response;
try {
response = client.execute(request);
Order order = response.result();
String payHref = null;
String status = order.status();
if (status.equals("CREATED")) {
List links = order.links();
for (LinkDescription linkDescription : links) {
if (linkDescription.rel().equals("approve")) {
payHref = linkDescription.href();
}
}
}
return payHref;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private static String paramsConvertUrl(Map params) {
StringBuilder urlParams = new StringBuilder("?");
Set> entries = params.entrySet();
for (Map.Entry entry : params.entrySet()) {
urlParams.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
String urlParamsStr = urlParams.toString();
return urlParamsStr.substring(0, urlParamsStr.length()-1);
}
@Override
public void addPay(String fee, String saleId, String userId, String beanNum) {
UserPay userPay = new UserPay();
userPay.setUserId(userId);
userPay.setOrderNo(saleId);
userPay.setPayFrom("PayPal");
userPay.setPayType("YES");
userPay.setTotalFee(new BigDecimal(beanNum));
userPay.setCreateTime(new Date());
userPay.setSuccessTime(new Date());
userPay.setCreateUser(userId);
userPayService.addPay(userPay);
// 金豆
UserOrder userOrder = new UserOrder();
userOrder.setUserId(userId);
userOrder.setOrderNo(saleId);
userOrder.setPaySource("COST");
userOrder.setPayStatus("INCOME");
userOrder.setType("BALANCE");
userOrder.setUnit(new BigDecimal(beanNum));
userOrder.setCreateUser(userId);
userOrderService.addPaypal(userOrder);
}
}
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
public class PayPalClient {
public PayPalHttpClient client(String mode, String clientId, String clientSecret) {
PayPalEnvironment environment = mode.equals("live") ? new PayPalEnvironment.Live(clientId, clientSecret) : new PayPalEnvironment.Sandbox(clientId, clientSecret);
return new PayPalHttpClient(environment);
}
}
// 将控制器方法中的返回值类型设置为 String ,将 return 中的内容修改为如下:
// 即成功后访问页面,1秒后跳转回上一级(前端)页面
// 支付成功(返回一级后将返回到支付链接:https://www.sandbox.paypal.com/checkoutnow?token=7LK561281D3524115,此时会显示PayPal支付结果
return "支付成功,返回上一级页面";
// 支付失败(返回一级后将返回到支付链接:https://www.sandbox.paypal.com/checkoutnow?token=7LK561281D3524115,此时会显示PayPal支付结果
return "支付失败,返回上一级页面";
// 上一级:即跳转后“支付成功,返回上一级页面”页面/“支付失败,返回上一级页面”页面的上一级
// 上几级就负几,-2、-3...
(1)启动项目
(2)在浏览器输入localhost:8088
(3)点击paypal后,会跳到paypal的登录界面,登录测试账号(PRESONAL)后点击继续即可扣费,扣500$(具体数额可在controller中自定义)
Payment payment = paypalService.createPayment(
500.00,
"USD",
PaypalPaymentMethod.paypal,
PaypalPaymentIntent.sale,
"payment description",
cancelUrl,
successUrl);
(4)到安全海淘国际支付平台_安全收款外贸平台-PayPal CN 登录测试账号看看余额有没有变化
Error code : 400 with response : {"name":"DUPLICATE_REQUEST_ID","message":"The value of PayPal-Request-Id header has already been used","information_link":"https://developer.paypal.com/docs/api/payments/v1/#error-DUPLICATE_REQUEST_ID","debug_id":"a3d876b7ebd44"}
服务器未知异常:response-code: 400 details: name: DUPLICATE_REQUEST_ID message: The value of PayPal-Request-Id header has already been used details: null debug-id: a3d876b7ebd44 information-link: https://developer.paypal.com/docs/api/payments/v1/#error-DUPLICATE_REQUEST_ID, 请求地址:http://localhost:82/marketing/paypal/success
原因:
报错的原因是请求中的 PayPal-Request-Id 标头的值已经被使用过,导致请求被认为是重复的。
PayPal-Request-Id 是一个用于标识 PayPal API 请求的唯一标识符。每次进行 PayPal API 请求时,应该使用一个新的、唯一的 PayPal-Request-Id 值。如果重复使用相同的 PayPal-Request-Id 值进行请求,PayPal 服务器会将其视为重复请求,并返回 DUPLICATE_REQUEST_ID 错误。
解决:
修改PaypalConfig:
@Configuration
public class PaypalConfig {
@Value("${paypal.client.app}")
private String clientId;
@Value("${paypal.client.secret}")
private String clientSecret;
@Value("${paypal.client.mode}")
private String mode;
@Bean
public Map paypalSdkConfig() {
Map sdkConfig = new HashMap<>();
sdkConfig.put("mode", mode);
return sdkConfig;
}
public OAuthTokenCredential authTokenCredential() {
return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
}
public APIContext apiContext() throws PayPalRESTException {
APIContext apiContext = new APIContext(authTokenCredential().getAccessToken());
apiContext.setConfigurationMap(paypalSdkConfig());
return apiContext;
}
}
修改PaypalService,每次每次使用apiContext时生成新的requestId:
@Service
public class PaypalServiceImpl implements PaypalService {
@Resource
private PaypalConfig paypalConfig;
@Resource
private UserPayService userPayService;
@Resource
private UserOrderService userOrderService;
@Override
public Payment createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) throws PayPalRESTException {
Amount amount = new Amount();
amount.setCurrency("USD"); // 美金
// total 是以分为单位,所以得除以100
BigDecimal divisor = new BigDecimal("100");
amount.setTotal(String.format("%.2f", entity.getTotalFee().divide(divisor)));
Transaction transaction = new Transaction();
transaction.setAmount(amount);
transaction.setDescription(StringUtils.isBlank(entity.getContext())?"合恕支付充值":entity.getContext());
List transactionList = new ArrayList<>();
transactionList.add(transaction);
Payer payer = new Payer();
payer.setPaymentMethod("paypal"); // paypal购买
Payment payment = new Payment();
payment.setIntent("sale");// 直接购买
payment.setPayer(payer);
payment.setTransactions(transactionList);
RedirectUrls redirectUrls = new RedirectUrls();
redirectUrls.setCancelUrl(cancelUrl);
redirectUrls.setReturnUrl(successUrl + "?userId="+entity.getUserId()+"&totalFee="+entity.getTotalFee()+"&payFrom="+ entity.getPayFrom());
payment.setRedirectUrls(redirectUrls);
// 每次使用apiContext时生成新的requestId
APIContext apiContext = paypalConfig.apiContext();
return payment.create(apiContext);
}
@Override
public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException {
Payment payment = new Payment();
payment.setId(paymentId);
PaymentExecution paymentExecution = new PaymentExecution();
paymentExecution.setPayerId(payerId);
// 每次使用apiContext时生成新的requestId
APIContext apiContext = paypalConfig.apiContext();
return payment.execute(apiContext, paymentExecution);
}
}