接入Paypal 实现跨境支付 - springboot实战电商项目mall4j

原创
03/31 15:57
阅读数 70

Paypal 实现跨境支付

springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

摘要 Paypal支付对接(V2)

1、商家注册Paypal账号

1.注册前准备

在这里插入图片描述

2.注册完成后验证

在这里插入图片描述

二、登录开发者中心

使用上一步注册好的账号,直接登录开发者中心. https://developer.paypal.com 在这里插入图片描述

1.点击右上角的按钮 “Dashboard”

在这里插入图片描述

点击完成后显示 在这里插入图片描述

2.在沙箱中,创建两个测试账号
1.在左边的导航栏中点击 Sandbox 下的 Accounts

在这里插入图片描述

2.进入Acccouts界面后,可以看到系统有两个已经生成好的测试账号,但是我们不要用系统给的测试账号,很卡的,自己创建两个

在这里插入图片描述

3.点击右上角的“Create Account”,创建测试用户
创建一个北美的Personal账户

在这里插入图片描述

创建一个China 的商家账号

在这里插入图片描述

商家和个人,创建,直接点击创建就自动创建完成了,默认商家/个人账号有 5000美元
再创建两个账号,分别是北美的商家、个人账号

在这里插入图片描述

4.登录创建好的沙箱账户
沙箱登录地址: https://www.sandbox.paypal.com 
3.创建应用,先在沙盒中创建应用

还是在开发者中心. https://developer.paypal.com 在 My Apps & Credentials ----> Sandbox ------> Create App

在这里插入图片描述 在这里插入图片描述

App Name : 应用的名称

App Type: // 商家 - 以商家(卖家)身份接受付款。 Merchant – Accept payments as a merchant (seller)

// 平台--以平台(市场、众筹或电商平台)的形式将款项转移给卖家 Platform – Move payments to sellers as a platform (marketplace, crowdfunding, or e-commerce platform)

Sandbox Business Account: 选择一个创建的沙盒商家账户

4.clientId 和 clientSecret 的获取

点击创建 在这里插入图片描述

Client ID 就是 clientId , Secret 就是 clientSecret 5.应用详情页下方创建WEBHOOKS

然后点击 Add Webhook

在这里插入图片描述 在这里插入图片描述

点击确定即可

三、Java支付对接

支付方式:

paypal提供了多种支付方式,如标准支付和快速支付,其中标准支付誉为最佳实践。

标准支付主要特点是只需要集成paypal按钮,所有的付款流程由paypal控制,接入方不需要关心支付细节。当用户完成支付后,paypal会通过同步PDT或者异步IPN机制来通知接入方,这种方式比较轻量级,对接难度最小,而且对借入方的入侵较小。

快速支付相对复杂,支付过程由接入方控制,通过调用3个接口来实现。从接入方网页跳转到paypal支付页面前,第一个接口触发,用于向paypal申请支付token。接着用户进入paypal支付页面,并进行支付授权,授权接口中会提交上一步获取的支付token,这个过程由paypal控制,并且没有进行实际支付,接着接入方调用第二个接口,用于获取用户的授权信息,包括支付金额,支付产品等信息,这些基础信息核对无误后,调用第三个接口,进行实际付费扣款,扣款成功后,paypal同样会进行同步PDT和异步IPN通知,这种方式很灵活,控制力强,但编码复杂度较高,入侵性较大。从实际情况考虑,我们选择了采标标准支付方式接入paypal支付。

通知方式:

paypal支付的IPN和PDT两种通知方式,IPN异步通知,可能会有时延,但可靠性高,当接入方主机不可达时,有重试机制保证IPN通知尽量抵达接入方服务器。接入方收到IPN通知后,需要对其确认。确认方法为,把接收到的IPN通知原封不动的作为请求体,调用IPN确认接口。PDT通知是是实时的,但可靠性不高,因为只会通知一次,没有重试机制,一旦接入方出现主机不可达,这样的消息将会被丢失。官方推荐,IPN通知和PDT通知最好混合使用,以满足时效性和可靠性的保证。我们采用了IPN和PDT两种通知机制。

Demo 网址:https://demo.paypal.com/c2/demo/home

V1 版本已过期,我们使用V2对接支付

v1文档:https://developer.paypal.com/docs/api/payments/v1/ V2文档: https://developer.paypal.com/docs/api/payments/v2/

四、直接上代码

系统使用创建的默认美元货币支付

不多说我们直接上代码

maven参数引入

 <!--paypal-->
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>rest-api-sdk</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>checkout-sdk</artifactId>
            <version>1.0.2</version>
        </dependency>

PayPalConfig.class

package com.xxxx.config;

import cn.hutool.core.util.StrUtil;
import com.paypal.base.rest.APIContext;
import com.paypal.base.rest.OAuthTokenCredential;
import com.paypal.base.rest.PayPalRESTException;
import com.paypal.core.PayPalEnvironment;
import com.paypal.core.PayPalHttpClient;
import com.paypal.http.HttpResponse;
import com.paypal.orders.*;
import com.paypal.payments.CapturesGetRequest;
import com.paypal.payments.CapturesRefundRequest;
import com.paypal.payments.RefundRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.*;

/**
 * 通过微信配置获取微信的支付信息,登陆信息等
 * @author cl
 */
@Slf4j
@Component
@AllArgsConstructor
public class PayPalConfig {
	
	
    private static final String clientId = "xxxx";
    private static final String clientSecret = "xxxx";
    // sandbox 沙箱/ live 正式环境
    private static final String mode = "sandbox";
	
    private static final String PP_SUCCESS = "success";
	/**
	 * approved 获准
	 */
	public static final String APPROVED = "approved";
	/**
	 * sandbox 沙箱or live生产
	 */
	public static final String PAYPAY_MODE = "live";
	/**
	 * PayPal 取消支付回调地址
	 */
	public static final String PAYPAL_CANCEL_URL = "/result/pay/cancel";
	/**
	 * PayPal 取消支付回调地址
	 */
	public static final String PAYPAL_SUCCESS_URL = "/result/pay/success";

	/**
	 * 当前货币币种简称, 默认为人名币的币种 EUR CNY USD
	 */
	public static final String CURRENTCY = "USD";
	/**
	 * approval_url 验证url
	 */
	public static final String APPROVAL_URL = "approval_url";

	public static final String CAPTURE = "CAPTURE";
	public static final String BRANDNAME = "Supernote";
	public static final String LANDINGPAGE = "NO_PREFERENCE";
	public static final String USERACTION = "PAY_NOW";
	public static final String SHIPPINGPREFERENCE = "SET_PROVIDED_ADDRESS";
	public static final String COMPLETED = "COMPLETED";    

    public Map<String, String> paypalSdkConfig(){
        Map<String, String> sdkConfig = new HashMap<>(16);
        sdkConfig.put("mode", mode);
        return sdkConfig;
    }

    public OAuthTokenCredential authTokenCredential(){
        return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
    }

    public PayPalHttpClient client() {
        PayPal payPal = shopConfig.getPayPal();
        String mode = payPal.getMode();
        String clientId = payPal.getClientId();
        String clientSecret = payPal.getClientSecret();
        PayPalEnvironment environment = StrUtil.equals("live",mode) ?
                new PayPalEnvironment.Live(clientId, clientSecret) :
                new PayPalEnvironment.Sandbox(clientId, clientSecret);
        return new PayPalHttpClient(environment);
    }

    /**
     * 生成paypal支付订单
     */
    public OrdersCreateRequest createPayPalOrder(PayInfoDto payInfo) {
        OrdersCreateRequest request = new OrdersCreateRequest();
        request.header("Content-Type","application/json");
        OrderRequest orderRequest = new OrderRequest();
        orderRequest.checkoutPaymentIntent(Constant.CAPTURE);
        com.paypal.orders.ApplicationContext payPalApplicationContext = new com.paypal.orders.ApplicationContext()
                .brandName("Shopping")
                .landingPage(NO_PREFERENCE)
                .cancelUrl(PAYPAL_CANCEL_URL)
                .returnUrl(PAYPAL_SUCCESS_URL)
                .userAction("PAY_NOW")
                ;
        orderRequest.applicationContext(payPalApplicationContext);
        List<PurchaseUnitRequest> purchaseUnitRequests = new ArrayList<>();
        PurchaseUnitRequest purchaseUnitRequest = new PurchaseUnitRequest()
                .description("Shopping")
                // 生成的系统生成的订单交易号
                .customId(payInfo.getPayNo())
                .invoiceId(payInfo.getPayNo())
                .amountWithBreakdown(new AmountWithBreakdown()
                        .currencyCode(Constant.CURRENTCY)
                        // value = itemTotal + shipping + handling + taxTotal + shippingDiscount
                        .value(payInfo.getPayAmount().toString())
                );
        purchaseUnitRequests.add(purchaseUnitRequest);
        orderRequest.purchaseUnits(purchaseUnitRequests);
        request.requestBody(orderRequest);
        return request;
    }

    /**
     * 创建订单
     */
    public String getExcetuteHref (OrdersCreateRequest request) {
        HttpResponse<Order> response = null;
        try {
            response = client().execute(request);
        } catch (IOException e) {
            log.error("调用paypal订单创建失败,失败原因:{}", e.getMessage());
            throw new YamiShopBindException("paypal请求支付失败");
        }
        String href = "";
        if (response.statusCode() == 201) {
            for (LinkDescription link : response.result().links()) {
                if(link.rel().equals("approve")) {
                    href = link.href();
                }
            }
        }
        return href;
    }


    /**
     * 用户授权支付成功,进行扣款操作
     * 用户通过CreateOrder生成 approveUrl 跳转paypal支付成功后,只是授权,并没有将用户的钱打入我们的paypal账户,
     * 我们需要通过 CaptureOrder接口,将钱打入我的PayPal账户
     */
    public PayInfoBo captureOrder(String token){
        PayInfoBo payInfoBo = new PayInfoBo();
        OrdersCaptureRequest request = new OrdersCaptureRequest(token);
        request.requestBody(new OrderRequest());
        HttpResponse<Order> response = null;
        try {
            response = client().execute(request);
        } catch (IOException e1) {
            log.error("调用paypal扣款失败,失败原因 {}", e1.getMessage() );
        }
        payInfoBo.setBizOrderNo(response.result().id());
        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());
                //paypal交易号
                payInfoBo.setBizPayNo(capture.id());
                //商户订单号,之前生成的带用户ID的订单号
                payInfoBo.setPayNo(capture.invoiceId());
                payInfoBo.setIsPaySuccess(false);
                if("COMPLETED".equals(capture.status())) {
                    //支付成功,状态为=COMPLETED
                    payInfoBo.setIsPaySuccess(true);
                    payInfoBo.setSuccessString(PP_SUCCESS);
                }
                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();
                    }
                    // 支付成功,状态为=PENDING
                    log.info("支付成功,状态为=PENDING : {}", reason);
                    payInfoBo.setIsPaySuccess(true);
                }
            }
        }
        return payInfoBo;
    }



    /**
     * 支付后查询扣款信息
     */
    public Boolean getCapture(String captureId) {
        // 扣款查询
        CapturesGetRequest restRequest = new CapturesGetRequest(captureId);
        HttpResponse<com.paypal.payments.Capture> response = null;
        try {
            response = client().execute(restRequest);
        } catch (IOException e) {
            log.info("查询支付扣款失败:{}",e.getMessage());
            return false;
        }
        log.info("Capture ids: " + response.result().id());
        return true;
    }


Controller中代码

/**
     * 支付接口
     */
    @PostMapping("/pay")
    @SneakyThrows
    public String pay(HttpServletResponse httpResponse,@Valid @RequestBody PayParam payParamPayInfo payInfo) {
    	 payInfo.setApiNoticeUrl(notifyUrl);
            OrdersCreateRequest request = payPalConfig.createPayPalOrder(payInfo);
            String href = payPalConfig.getExcetuteHref(request);
 			return href;
    }
    

支付后通知

PayNotice.class

 /**
     * V2:
     * 客户登陆付款后paypal返回路径参数示例
     * https://xxxx/p//result/pay/success?token=9DT13847SW4445928&PayerID=B2YKY8DYERF46
     */
    @RequestMapping("/result/pay/success")
    public String successPayPal(@RequestParam("token") String token, @RequestParam("PayerID") String PayerID){
        // 执行扣款
        PayInfoBo payInfoBo = payPalConfig.captureOrder(token);
        // 查询扣款信息, 需要用到第三方交易号 29911292F3566070S
        payPalConfig.getCapture(payInfoBo.getBizPayNo());
        // 扣款成功后将订单改成已支付状态,执行支付成功业务
        return ResponseEntity.ok(payInfoBo.toString());
    }
	/**
	* 取消接口
	*/
    @RequestMapping("/result/pay/cancel")
    public String successPayPal(@RequestParam("token") String token, @RequestParam("PayerID") String PayerID){
      	// 取消订单业务
        return ResponseEntity.ok("cancel");
    }







springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部