paypal本身有sdk,不過這裏選擇使用braintree服務進行對接,paypal本身也比較推薦這種方式。
準備工作
paypal賬號
braintree賬號(包括正式賬號和沙盒賬號)
申請流程這裏不做說明了。
配置過程
- 登陸paypal開發後臺,點擊右上角的Dashboard,左邊菜單欄中找到Sandbox–>Account,在這裏可以創建測試賬號,創建賬號時注意選擇類型personal,賬號密碼注意修改,密碼修改後看不到。
- 左邊菜單欄中找到Dashboard–>My Apps & Credentials,可以看到Sandbox和Live兩個選項,分別時沙盒和正式環境的app配置。兩邊配置的步驟是一樣的,選擇Create App,填寫相關內容完成創建。點擊剛剛創建的app可以看到Sandbox account、Client ID、Secret,等下需要用到。往下滑,可以看到Add Webhook按鈕,點擊創建,這裏需要準備webhook的接收地址,注意必須是https地址。
- 使用braintree沙盒賬號登陸braintree沙盒後臺,點擊右上角齒輪,選擇Processing進入,下方Payment Methods中找到paypal並開啓,點擊Options,配置PayPal Email,PayPal Client Id,PayPal Client Secret。相關參數由第2步獲取。(正式環境配置過程類似,需要使用正式賬號登陸barintree正式環境後臺)
- braintree後臺首頁點擊右上角齒輪,選擇Fraud Management進入,可以設置相關信用卡支付安全校驗,相關配置說明可以查看braintree,部分配置需要謹慎,可能會導致用戶信用卡無法使用。
- braintree後臺首頁點擊右上角齒輪,選擇API,可以看到Keys、Webhooks、Security,其中Keys中的API Keys下可以點擊Private Key的VIEW拿到Merchant ID、Public Key、Private Key,是請求braintree api時所必須的東西。Webhooks中需要配置對應的webhook地址,接受消息通知,配置以後可以使用Check URL進行地址校驗。Security可以開啓IP和主機名限制。
- 額外需要注意的一點, braintree後臺首頁點擊右上角齒輪,選擇Business,在Merchant Accounts中可以看到Merchant Account,如果有多個,使用braintree時需要進行指定,另外注意每個Merchant Account對應的Currency,即貨幣,如果使用時,用到了不對應的貨幣,將無法成功完成支付流程。
- 配置Plan(如果有訂閱訂單,會用到),braintree後臺首頁頂部選擇Subscriptions,跳轉後選擇Plans,點擊New Plan,進行Plan創建,填寫相關內容,其中Plan ID是調用api時所需參數,如果需要添加試用期,選中Trial Period,請求api時需要開啓試用。
開發流程
- 引入braintree的jar包(maven項目),開發參考braintree開發文檔
<dependency>
<groupId>com.braintreepayments.gateway</groupId>
<artifactId>braintree-java</artifactId>
<version>2.87.0</version>
</dependency>
- 獲取braintree的token,交給客戶端,由客戶端braintreeSDK換取paymentMethodNonce,如果使用了braintree的訂閱服務,還需要客戶端獲取用戶賬戶的firstName和lastName,服務端代碼如下:
public String getBraintreeToken() {
try {
BraintreeGateway gateway = new BraintreeGateway(
Environment.SANDBOX,
PayConfig.BraintreeMerchantId,
PayConfig.BraintreePublicKey,
PayConfig.BraintreePrivateKey
);
return gateway.clientToken().generate();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
- 客戶端獲取到相關參數後,服務端通過api,進行訂單創建、結算。
(1). 普通訂單:
public void braintreeTransaction(String nonce) {
try {
BraintreeGateway gateway = new BraintreeGateway(
Environment.SANDBOX,
PayConfig.BraintreeMerchantId,
PayConfig.BraintreePublicKey,
PayConfig.BraintreePrivateKey
);
TransactionRequest request = new TransactionRequest()
.amount(new BigDecimal(100))
//客戶端根據服務器返回的accesstoken獲取的paymentMethodNonce
.paymentMethodNonce(nonce)
//映射到PayPal的發票號碼
.orderId(yourServerOrderId)
.descriptor()
//描述符顯示在客戶CC報表中。22個字符馬克斯
// .name("Descriptor displayed in customer CC statements. 22 char max")
.done()
.shippingAddress()
//對應商家賬號的firstName
.firstName(PayConfig.BraintreeFirstName)
//對應商家賬號的lastName
.lastName(PayConfig.BraintreeLastName)
//公司名
// .company("Braintree")
// .streetAddress("1 E 1st St")
// .extendedAddress("Suite 403")
// .locality("Bartlett")
// .region("IL")
// .postalCode("60103")
// .countryCodeAlpha2("US")
.done()
.options()
//是否結算。如果不結算,用戶的金額不會被扣除,需要手工去後臺確認收款
.submitForSettlement(true)
.paypal()
//PayPal自定義字段
.customField("PayPal custom field")
//PayPal電子郵件說明
.description("")
.done()
//If you want to create a new payment method in the Vault upon a successful transaction, use the this
.storeInVaultOnSuccess(true)
.done();
Result<Transaction> saleResult = gateway.transaction().sale(request);
if (saleResult.isSuccess()) {
Transaction transaction = saleResult.getTarget();
transactionId = transaction.getId();
System.out.println("Success ID: " + transaction.getId());
System.out.println("transaction ====== "+JSON.toJSON(transaction));
} else {
//支付失敗的情況
logger.error("Message: {}",saleResult.getMessage());
logger.error("Error: {}",saleResult.getErrors().toString());
logger.error("Error-JSON: {}",JSON.toJSON(saleResult.getErrors()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
(2). 訂閱訂單,需要注意braintree本身允許用戶重複訂閱,相關邏輯需要自行實現:
public void braintreeSubscription(String nonce,String firstName,String lastName) {
try {
BraintreeGateway gateway = new BraintreeGateway(
Environment.SANDBOX,
PayConfig.BraintreeMerchantId,
PayConfig.BraintreePublicKey,
PayConfig.BraintreePrivateKey
);
CustomerRequest customerRequest = new CustomerRequest()
.firstName(firstName)
.lastName(lastName)
.paymentMethodNonce(nonce);
Result<Customer> customerResult = gateway.customer().create(customerRequest);
//顧客信息不必每次獲取,可以將顧客id或者token緩存起來下次使用,但需要注意用戶本次使用的paypal賬戶是否與服務器緩存的一致,以防扣錯賬戶。
Customer customer = customerResult.getTarget();
if (customer==null){ logger.error("獲取用戶買家信息失敗");
return;
}
SubscriptionRequest request = new SubscriptionRequest()
.paymentMethodToken(customer.getPaymentMethods().get(0).getToken())
.planId(yourPlanId)
//啓用試用期,啓用以後,不能設置開始時間,會衝突
// .trialPeriod(isFreeFirst)
.options()
//設置立即開始,如果啓用了試用期,就不能設置本項
.startImmediately(true)
.paypal()
.description("")
.done()
.done();
Result<Subscription> subscriptionResult = gateway.subscription().create(request);
try {
System.out.println(JSON.toJSON(subscriptionResult));
} catch (Exception e) {
e.printStackTrace();
}
if (subscriptionResult.isSuccess()) {
orderSuccess = true;
Subscription subscription = subscriptionResult.getTarget();
List<Transaction> transactionList = subscription.getTransactions();
String subscriptionId = subscription.getId();
} else {
//支付失敗的情況
logger.error("Message: {}",subscriptionResult.getMessage());
logger.error("Error: {}",subscriptionResult.getErrors().toString());
logger.error("Error-JSON: {}",JSON.toJSON(subscriptionResult.getErrors()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 普通transaction會通過paypal的webhook進行通知。subscription會分別通過paypal和braintree進行webhook通知。
(1). paypal的webhook可以參考WebhooksManagementAPI文檔
public String paypalNotify(@RequestBody(required = false) byte[] body, HttpServletRequest request, HttpServletResponse response) {
PrintWriter out = null;
JSONObject rsJson = new JSONObject();
try {
String bodyStr = null;
JSONObject paramJson = null;
try {
bodyStr = new String(body, "utf-8");
paramJson = JSONObject.parseObject(bodyStr);
} catch (Exception e) {
e.printStackTrace();
}
/**
* PAYMENT.SALE.COMPLETED
*/
if (paramJson!=null){
String eventType = paramJson.getString("event_type");
if ("PAYMENT.SALE.COMPLETED".equalsIgnoreCase(eventType)){
//支付完成處理邏輯
JSONObject resourceJson = paramJson.getJSONObject("resource");
String orderNo = resourceJson.getString("invoice_number");
String paymentId = resourceJson.getString("parent_payment");
}
}
out = response.getWriter();
rsJson.put("status", "200");
} catch (Exception e) {
e.printStackTrace();
rsJson.put("status", "500");
}
out.println(rsJson.toString());
out.flush();
out.close();
return null;
}
(2). braintree的Webhook回調處理,配置webhook時可以使用Check URL進行測試,braintree的webhook回調攜帶參數爲bt_signature、bt_payload,可以參考braintreeWebhook文檔:
public String braintreeNotify(HttpServletRequest request, HttpServletResponse response)
PrintWriter out = null;
JSONObject rsJson = new JSONObject();
try {
String signature = null;
String payload = null;
Object btSignatureObj = request.getParameter("bt_signature");
Object btPayloadObj = request.getParameter("bt_payload");
if (btSignatureObj!=null){
signature = btSignatureObj.toString();
}
if (btPayloadObj!=null){
payload = btPayloadObj.toString();
}
CErrorData cErrorData = null;
if (!CStr.isEmpty(signature)&&!CStr.isEmpty(payload)){
BraintreeGateway gateway = new BraintreeGateway(
Environment.SANDBOX,
PayConfig.BraintreeMerchantId,
PayConfig.BraintreePublicKey,
PayConfig.BraintreePrivateKey
);
WebhookNotification webhookNotification = gateway.webhookNotification().parse(signature,payload);
if ("CHECK".equals(webhookNotification.getKind().name())){
//測試
return;
}
Subscription subscription = webhookNotification.getSubscription();
if (subscription==null){
//沒有訂閱信息
return;
}
//如果是試用訂閱,則不包含transactions
List<Transaction> transactionList = subscription.getTransactions();
if ("SUBSCRIPTION_WENT_ACTIVE".equals(webhookNotification.getKind().name())) {
//創建訂閱的第一個授權交易,或者成功的交易將訂閱從“ 過期”狀態轉移到“ 活動”狀態。具有試用期的訂閱從試用期進入第一個計費週期後不會觸發此通知。
}else if ("SUBSCRIPTION_CHARGED_SUCCESSFULLY".equals(webhookNotification.getKind().name())){
//訂閱成功進入下一個計費週期,即續訂成功
}
}
out = response.getWriter();
rsJson.put("status", "200");
} catch (Exception e) {
rsJson.put("status", "500");
e.printStackTrace();
}
out.println(rsJson.toString());
out.flush();
out.close();
return null;
}