在这记录并分享一下最近学习的PayPal接口使用,PayPal提供了非常多的API接口,我这里只是实现了订单的支付以及退款功能(Sandbox模式下的实现)
具体API可以看PayPal官方接口文档:https://developer.paypal.com/api/rest/
一、注册PayPal账号
这个过程这里就不过多介绍了
二、PayPal开发者中心准备(可选)
1. 在开发者中心登录账号,之后在 Testing Tools =》Sandbox Accounts 中创建一个商家账号和一个用户账号(默认会各有一个商家账号和用户账号,不新建直接用这个也可以)
两种类型各创建一个
点击进入用户页面可以看到用户的邮箱、密码(这个用于后续支付登录使用的);以及可以在这里修改当前账户的账户余额
2.然后在 Apps & Credentials 中新建自己的项目(默认会有一个 Default Application,不新建直接用这个也可以)
输入自己的项目名称,选择 Merchant类型,再选择一个商家账号,就创建成功了;
点击进入项目页面,在 API credentials 中的 Client ID 和 Secret key 1 在后续代码实现中会使用用到
三、前端代码实现
前端是使用的模板引擎实现的HTML,注意${}是模板引擎语言
在支付的前端HTML中直接引用JavaScript SDK的js,并在增加一个div用来展示支付按钮(CLIENT_ID是PayPal开发者中心项目页面中对应的 Client ID )
<script src="https://www.paypal.com/sdk/js?client-id=${CLIENT_ID}&components=buttons"></script>
增加PayPal按钮的初始化代码,(页面中需要增加一个显示PayPal支付按钮的div,并且设置有id为paypal-button-container,也可以按自己的想法换成别的值)添加完打开页面就会有PayPal的按钮显示了
paypal.Buttons({
style: {
layout: 'vertical',
color: 'gold',
shape: 'pill',
label: 'paypal'
}
}).render('#paypal-button-container')
需要完成支付,我们还需要一些别的代码,createOrder()、onApprove(data)一定要重写的方法,不然无法完成支付
paypal.Buttons({
style: {
layout: 'vertical',
color: 'gold',
shape: 'pill',
label: 'paypal'
},
// 创建订单
createOrder() {
return fetch("/自己的后端创建订单接口", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
// 传入订单信息,我这传入的是SKU、数量、收货信息等等
})
})
.then((response) => response.json())
.then((order) => {
return order.id;
});
},
// 确认支付
onApprove(data) {
return fetch("/自己的后端确认支付接口", {
method: "POST",
body: data.orderID
})
.then((response) => response.json())
.then((details) => {
console.log("付款成功");
});
},
// 取消支付,按自己的业务决定是否实现,我这里是和PayPal接口无关的业务处理
onCancel(data) {
$.ajax({
type: 'POST',
url: '/自己后端的取消支付接口',
data: {
orderID: data.orderID
},
success: function (result) {
if (result.status !== 1) {
alert(result.message);
}
}
});
},
// 支付报错
onError(err) {
alert(err);
}
}).render('#paypal-button-container')
createOrder():
点击PayPal的支付按钮就会调用这个函数,根据接口返回信息会打开一个PayPal的支付弹框页面,使用第二步创建的用户账号登录,然后支付就好了(接口返回的order.id是PayPal生成的订单ID)
onApprove(data):
弹框页面中支付完成了,就会调用这个函数;这不很重要,如果不确认支付,那只会扣除用户账号中的金额,并不会增加商家账号中的金额(函数入口的data.orderID就是PayPal的订单ID)
onCancel(data):
弹框页面没有支付关闭了页面,就会调用到这个函数,按自己的业务决定是否实现,我这里是和PayPal接口无关的本地业务处理(没有业务逻辑需要实现,可以不重写该函数)
四、后端代码实现
1.鉴权接口
所有PayPal接口请求前都需要先获取PayPal的访问令牌才能调用别的接口,其中CLIENT_ID是PayPal开发者中心项目页面中对应的 Client ID;CLIENT_SECRET是PayPal开发者中心项目页面中对应的 Secret key 1
import org.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
public String getAccessToken() throws Exception {
String url = "https://api-m.sandbox.paypal.com/v1/oauth2/token";
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET);
headers.set("Content-Type", "application/x-www-form-urlencoded");
String requestBody = "grant_type=client_credentials";
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject jsonBody = new JSONObject(Objects.requireNonNull(response.getBody()));
return jsonBody.getString("access_token");
}
2.创建订单
创建订单接口需要先调用鉴权接口获取请求token,再根据自己的业务情况创建具体的请求对象,进行请求(这里最好是把PayPal的订单号和本地订单号的关联关系保存起来)
接口返回的body内容就是需要返回给前端createOrder()调用的信息了
这里的Order是我本地的订单对象
import org.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
public String createOrderToPayPal(Order order, BillDetail billDetail) throws Exception {
String url = "https://api-m.sandbox.paypal.com/v2/checkout/orders";
// 鉴权接口获取的token值
String accessToken = this.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer " + accessToken);
// 根据本地订单信息和收货信息生成请求数据(按各自项目业务来)
String requestBody = createOrder.buildRequestBody(order, billDetail).toString();
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
String post = response.getBody();
JSONObject jsonBody = new JSONObject(Objects.requireNonNull(post));
// PayPal订单号
String paypalOrderNo = jsonBody.getString("id");
// todo 保存本地订单和PayPal订单号关系,以便后续操作
// 返回给前端createOrder()函数的信息
return post;
}
我这里是按我所需设置创建订单的请求数据,大家可以按官方文档定制自己业务所需要的值(官方文档地址:https://developer.paypal.com/docs/api/orders/v2/#orders_create)
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* CreateOrder
*
* @author Peterxie
*/
@Component
public class CreateOrder {
/**
* 收款方邮箱地址
*/
@Value("${paypal.email-address}")
private String payeeEmailAddress;
/**
* 收款方账号ID
*/
@Value("${paypal.merchant-id}")
private String payeeMerchantId;
public JSONObject buildRequestBody(Order order) {
Map<String, Object> requestBodyMap = new HashMap<>();
requestBodyMap.put("purchase_units", this.buildPurchaseUnits(order));
// 立即捕获付款
requestBodyMap.put("intent", "CAPTURE");
requestBodyMap.put("payment_source", this.buildPaymentSource());
return new JSONObject(requestBodyMap);
}
private List<Map<String, Object>> buildPurchaseUnits(Order order) {
Map<String, Object> purchaseUnitMap = new HashMap<>();
// 本地订单号
purchaseUnitMap.put("reference_id", order.getOrderCode());
// 支付金额
purchaseUnitMap.put("amount", this.buildAmount(order));
purchaseUnitMap.put("payee", this.buildPayee());
return Lists.newArrayList(purchaseUnitMap);
}
private Map<String, Object> buildAmount(Order order) {
Map<String, Object> amountMap = new HashMap<>();
// 货币类型
amountMap.put("currency_code", "USD");
// 支付金额
amountMap.put("value", order.getAmount().setScale(2, RoundingMode.HALF_UP).toString());
return amountMap;
}
private Map<String, Object> buildPayee() {
Map<String, Object> payeeMap = new HashMap<>();
payeeMap.put("email_address", payeeEmailAddress);
payeeMap.put("merchant_id", payeeMerchantId);
return payeeMap;
}
private Map<String, Object> buildPaymentSource() {
Map<String, Object> paymentSourceMap = new HashMap<>();
paymentSourceMap.put("paypal", this.buildPayPal());
return paymentSourceMap;
}
private Map<String, Object> buildPayPal() {
Map<String, Object> payPalMap = new HashMap<>();
payPalMap.put("experience_context", this.buildExperienceContext());
return payPalMap;
}
private Map<String, Object> buildExperienceContext() {
Map<String, Object> experienceContextMap = new HashMap<>();
// 公司名称,自定义
experienceContextMap.put("brand_name", "XZ-Shopping");
// 不在PayPal页面中设置收货地址信息,只付款
experienceContextMap.put("shipping_preference", "NO_SHIPPING");
experienceContextMap.put("landing_page", "LOGIN");
experienceContextMap.put("user_action", "PAY_NOW");
experienceContextMap.put("payment_method_preference", "IMMEDIATE_PAYMENT_REQUIRED");
experienceContextMap.put("locale", "en-US");
return experienceContextMap;
}
}
3.确认支付
同样在请求前需要先调用鉴权接口获取请求token;请求返回成功时,在返回数据中会有一个当前订单对应的capture_id需要保存起来,在后续的退款中会需要用上
接口返回的body内容就是需要返回给前端onApprove(data)调用的信息了
方法入参orderID为PayPal的订单号
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
public String capture(String orderID) throws Exception {
String url = "https://www.sandbox.paypal.com/v2/checkout/orders/" + orderID + "/capture";
// 鉴权接口获取的token值
String accessToken = this.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer " + accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject jsonBody = new JSONObject(Objects.requireNonNull(response.getBody()));
if (!jsonBody.getString("status").equals("COMPLETED")) {
// 请求失败结果处理
} else {
// 获取capture_id
String captureId = ((JSONObject) ((JSONArray) ((JSONObject) ((JSONObject) ((JSONArray) jsonBody.get("purchase_units")).get(0)).get("payments")).get("captures")).get(0)).get("id").toString();
// todo 保存capture_id
}
return response.getBody();
}
4.退款
同样在请求前需要先调用鉴权接口获取请求token;该接口只获取PayPal退款处理结果,退款页面根据自己的项目设计处理
这里的Order是我本地的订单对象,order.getCaptureId()是确认支付时保存的capture_id,order.getRefundAmount()是订单退款金额
import org.json.JSONObject;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
public boolean refund(Order order) throws Exception {
String url = "https://api-m.sandbox.paypal.com/v2/payments/captures/" + order.getCaptureId() + "/refund";
// 鉴权接口获取的token值
String accessToken = this.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer " + accessToken);
headers.set("Prefer", "return=representation");
// 根据退款金额生成请求数据
String requestBody = RefundOrder.buildRequestBody(order.getRefundAmount()).toString();
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
JSONObject jsonBody = new JSONObject(Objects.requireNonNull(response.getBody()));
if (!jsonBody.getString("status").equals("COMPLETED")) {
// 请求失败结果处理
}
return true;
}
我这里是按我所需设置订单退款的请求数据,大家可以按官方文档定制自己业务所需要的值(官方文档地址:https://developer.paypal.com/docs/api/payments/v2/#captures_refund)
import org.json.JSONObject;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* RefundOrder
*
* @author Peterxie
*/
public class RefundOrder {
public static JSONObject buildRequestBody(BigDecimal refundAmount) {
Map<String, Object> requestBodyMap = new HashMap<>();
requestBodyMap.put("amount", buildAmount(refundAmount));
// 退款理由
requestBodyMap.put("note_to_payer", "客户要求退款");
return new JSONObject(requestBodyMap);
}
private static Map<String, Object> buildAmount(BigDecimal refundAmount) {
Map<String, Object> amountMap = new HashMap<>();
// 货币类型
amountMap.put("currency_code", "USD");
// 退款金额
amountMap.put("value", refundAmount);
return amountMap;
}
}
五、总结
我这就是最简单的一种PayPal接口的应用,只是单纯得实现了单个订单的付款、退款;但其实在PayPal的REST APIs还有非常多的接口,比如SKU接口、发票接口等等可以去学习;
我这也是从各处参考,加上自己的理解得出来的结果,如果有哪些地方有问题的,希望可以帮我指出来,谢谢!