java对接PayPal支付(v1)

前言

注:可直接使用demo测试,然后再反过来看博客理清思路
经过n次debug和无数的查询资料,终于摸清除了PayPal支付,请听我一一道来
PayPal用于跨国收付款,可以绑定银行卡、信用卡,并且有多种收付款方式,用来实现不同的需求。下面请按照我的章节读完本篇博客,可以帮助您了解到资费信息,收付款方式,成功回调,取消回调,PDT消息通知,IPN消息通知。当然如果你现在还不了解这些概念不必着急,当您仔细看完本片博客,并看完demo代码,相信将paypal支付集成到您的系统已不是难事

前期准备

注册paypal账号

进入该网址注册paypal账号,选择企业账户,并进入邮箱注册

登录开发者平台

进入该网址注册开发者账号,点击右上角的“Log into Dashboard”使用上一步注册的paypal账号登录

注册两个沙箱环境账号

paypal账户内默认提供了两个沙箱账号,但是不要使用这两个账号,自己创建两个(默认账号存在性能问题)

1、创建买家账户:点击Sandbox => Accounts => Create Aaccount => 国家选择China,类型选择Personal
2、创建商家账户:点击Sandbox => Accounts => Create Aaccount => 国家选择China,类型选择Personal
3、创建成功后可以修改账户余额;点击刚创建的账户,记录 沙箱账户的邮箱地址 + 密码
4、使用沙箱账户密码登入沙箱平台,可以查看付款情况、账户余额等信息,还可以设置IPN通知的回调地址(后面会具体阐述IPN通知)

创建应用

应用:使用paypal账户登录开发者平台(注意不是沙箱账户),创建应用后会提供一个clientId和secret,开发的时候调用sdk接口时需要传入clientId和secret作验证

注:应用分为沙箱环境应用和正式环境应用,这里我们创建沙箱环境应用sandbox app,正式上线则替换为正式环境应用live app

1、使用paypal账户登录开发者平台
2、点击DASHBOARD => My Apps and Credentials => 选择Sand Box => Create app => 设置app name => 设置Sandbox Business Account选择刚刚创建的 商家类型的沙箱账户。创建成功
3、点击刚刚创建成功的应用,记录ClientID和secret密钥(开发时会用到)

代码

本项目使用的springboot构建,源码可查看下方gitee,内部也有.md笔记,如果觉得不错可以献上您的star

demo地址Gitee

git地址

PaymentController

@Controller
@RequestMapping("/")
public class PaymentController {
    @Autowired
    private APIContext apiContext;
    @Autowired
    private PaypalConfig paypalConfig;
    @Autowired
    private PaypalService paypalService;
    private ConcurrentHashMap<String, String> orderPaymMap = new ConcurrentHashMap();// orderId与paymentId的对应关系
    private Logger log = LoggerFactory.getLogger(getClass());

    /**
     * 首页
     */
    @RequestMapping(method = RequestMethod.GET)
    public String index() {
        return "index";
    }

    /**
     * 创建订单请求
     * 创建成功返回payment对象,保存本地OrderId和paymentId对应关系
     */
    @RequestMapping(method = RequestMethod.POST, value = "pay")
    public String pay(HttpServletRequest request) {
        log.info("=========================================================================================");
        String orderId = "2020110200001";// 本地系统Id
        String cancelUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.CANCEL_URL + "?orderId=" + orderId;// http://localhost:8080/pay/cancel
        String successUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.SUCCESS_URL;
        try {
            //调用交易方法
            Payment payment = paypalService.createPayment(
                    orderId,
                    300.00,
                    "USD",
                    PaypalPaymentMethod.paypal,
                    PaypalPaymentIntent.authorize,
                    "这是一笔300美元的交易",
                    cancelUrl,
                    successUrl);
            for (Links links : payment.getLinks()) {
                if (links.getRel().equals("approval_url")) {
                    // 客户付款登陆地址【判断币种CNY无法交易】
                    String paymentId = payment.getId();
                    orderPaymMap.put(orderId, paymentId);// 保存本地OrderId和paymentId对应关系
                    log.info("创建支付订单返回paymentId : " + paymentId);
                    log.info("支付订单状态state : " + payment.getState());
                    log.info("支付订单创建时间create_time : " + payment.getCreateTime());
                    log.info("=========================================================================================");
                    return "redirect:" + links.getHref();
                }
            }
        } catch (PayPalRESTException e) {
            log.error(e.getMessage());// 支付失败【使用CNY】
        }
        log.info("=========================================================================================");
        return "redirect:/";
    }

    /**
     * 失败回调
     *   触发回调情景:
     *     1、用户在支付页面点击取消支付
     *     2、用户支付成功后点击网页回退,再点击返回商家按钮触发
     *   判断是否用户主动取消付款逻辑:
     *     1、设置回调地址的时候拼接本地订单ID:?orderId=XXXX
     *     2、然后根据orderId查询paymentId,继而调用sdk查询订单支付状态
     *      * http://localhost:8080/pay/cancel?orderId=2020110200001&token=EC-70674489LL9806126
     */
    @RequestMapping(method = RequestMethod.GET, value = PaypalConfig.CANCEL_URL)
    public String cancelPay(@RequestParam("token") String token, @RequestParam("orderId") String orderId) {
        try {
            String paymentId = orderPaymMap.get(orderId);
            Payment payment = Payment.get(apiContext, paymentId);
            String state = payment.getState();
            log.info("交易取消回调:支付订单状态:{} ", state);
            if (state.equals("approved")) {// 已支付
                return "success";
            }
        } catch (PayPalRESTException e) {
            e.printStackTrace();
        }
        return "cancel";
    }

    /**
     * 成功回调 + 支付 + PDT同步通知
     *    买家确认付款,执行支付并直接返回通知
     */
    @RequestMapping(method = RequestMethod.GET, value = PaypalConfig.SUCCESS_URL)
    public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId, @RequestParam("token") String token) {
        log.info("=========================================================================================");
        try {
            /**
             * 执行支付
             */
            Payment payment = paypalService.executePayment(paymentId, payerId);
            if (payment.getState().equals("approved")) {
                String transactionId= "";     // 交易ID,transactionId
                String transactionState = "";  // 交易订单状态
                String transactionTime = "";   // 交易时间
                String custom = ""; // 本地OrderId
                if (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {
                    transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getId();
                    transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();
                    transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {
                    transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();
                    transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();
                    transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {
                    transactionId = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getId();
                    transactionState = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();
                    transactionTime = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                }
                log.info("PDT通知:交易成功回调");
                log.info("付款人账户:" + payment.getPayer().getPayerInfo().getEmail());
                log.info("支付订单Id {}", paymentId);
                log.info("支付订单状态state : " + payment.getState());
                log.info("交易订单Id:{}", transactionId);
                log.info("交易订单状态state : " + transactionState);
                log.info("交易订单支付时间:" + transactionTime);
                log.info("本地系统OrderId:{}", custom);
                log.info("=========================================================================================");
                return "success";
            }
        } catch (PayPalRESTException e) {
            // 如果同步通知返回异常,可根据paymentId 来查询刷新订单状态
            // 同时IPN异步通知也可以更新订单状态
            log.error(e.getMessage());
        }
        return "redirect:/";
    }

    /**
     * IPN异步通知
     *   触发情景:
     *      1、买家支付成功
     *      2、卖家确认收取授权或订单款项
     *      3、卖家发放退款
     */
    @RequestMapping(method = RequestMethod.POST, value = "/notificationIPN")
    public void receivePaypalStatus(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("=========================================================================================");
        log.info("IPN通知:交易成功异步回调");
        PrintWriter out = response.getWriter();
        try {
            Enumeration<String> en = request.getParameterNames();
            /**
             * 修改订单状态
             *      保存失败则不验签,继续接受paypal异步回调直至保存成功【或者用MQ】
             */
            String paymentStatus = request.getParameter("payment_status").toUpperCase();  // 交易状态
            String paymentDate = request.getParameter("payment_date");      // 交易时间
            String custom = request.getParameter("custom");                 // 本地系统订单ID
            String auth_id = request.getParameter("auth_id");               // transactionId
            String txnId = request.getParameter("txn_id");                  // 当前回调数据id【具体逻辑查看 .md文档】
            String parentTxnId = request.getParameter("parent_txn_id");     // 父id
            String receiverEmail = request.getParameter("receiver_email");  // 收款人email
            String receiverId = request.getParameter("receiver_id");        // 收款人id
            String payerEmail = request.getParameter("payer_email");        // 付款人email
            String payerId = request.getParameter("payer_id");              // 付款人id
            String mcGross = request.getParameter("mc_gross");              // 交易金额
            String item_name = request.getParameter("item_name");
            log.info("paymentStatus = " + paymentStatus);
            log.info("txnId = " + txnId);
            log.info("parentTxnId = " + parentTxnId);
            log.info("authId(transactionId)= " + auth_id);
            log.info("custom(orderId)= " + custom);
            log.info("item_name= " + item_name);
            /**
             * 验证
             *   作用:
             *     订单状态修改成功,告诉paypal停止回调
             *   实现:
             *     在原参数的基础上加cmd=_notify-validate,然后对https://www.sandbox.paypal.com/cgi-bin/webscr发起POST验证请求
             */
            String str = "cmd=_notify-validate";
            while (en.hasMoreElements()) {
                String paramName = en.nextElement();
                String paramValue = request.getParameter(paramName);
                //此处的编码一定要和自己的网站编码一致,不然会出现乱码,paypal回复的通知为"INVALID"
                str = str + "&" + paramName + "=" + URLEncoder.encode(paramValue, "utf-8");
            }
            log.info("paypal传递过来的交易信息:" + str);// 建议在此将接受到的信息 str 记录到日志文件中以确认是否收到 IPN 信息
            URL url = new URL(paypalConfig.getWebscr());
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//设置 HTTP 的头信息
            PrintWriter pw = new PrintWriter(connection.getOutputStream());
            pw.println(str);
            pw.close();
            /**
             * 回复
             *    接受PayPal对验证的回复信息
             */
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String resp = in.readLine();
            in.close();
            resp = StringUtils.isEmpty(resp) ? "0" : resp;
            log.info("resp = " + resp);
            /**
             * 验证返回状态
             */
            if (PaypalConfig.PAYMENT_IPN_VERIFIED.equalsIgnoreCase(resp)) {
                /**
                 * 修改订单状态
                 *      根据订单状态paymentStatus确定当前回调的类型
                 */
                switch (paymentStatus) {
                    case PaypalConfig.PAYMENT_STATUS_PENDING:
                        // 商家待领取状态
                        break;
                    case PaypalConfig.PAYMENT_STATUS_VOIDED:
                        // 商家作废(30天以内,且必须是授权付款类型 或 订单付款类型),款项原路返回买家
                        break;
                    case PaypalConfig.PAYMENT_STATUS_COMPLETED:
                        // 商家领取
                        String captureId = txnId;   // 实际领取对象ID【授权付款 和 订单付款需要商家领取】
                        break;
                    case PaypalConfig.PAYMENT_STATUS_REFUNDED:
                        // 商家退款,需扣除费用
                        String refundId = txnId;
                        String captureId2 = parentTxnId;
                        break;
                }
            } else if (PaypalConfig.PAYMENT_IPN_INVALID.equalsIgnoreCase(resp)) {
                // 非法信息,可以将此记录到您的日志文件中以备调查
                log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);
                log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            } else {// 处理其他错误
                log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);
                log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            }
        } catch (Exception e) {
            log.error("IPN通知发生IO异常" + e.getMessage());
            log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
            out.println("confirmError");
            e.printStackTrace();
        }
        out.flush();
        out.close();
        log.info("=========================================================================================");
    }

    /**
     * 查看已付款账单的状态
     */
    @RequestMapping(method = RequestMethod.GET, value = "test")
    @ResponseBody
    public String selectTransactionState(@RequestParam("paymentId") String paymentId) {
        log.info("=========================================================================================");
        String state = "未产生支付信息";
        String custom = "";
        try {
            Payment payment = Payment.get(apiContext, paymentId);
            if (payment.getTransactions().size() > 0 && payment.getTransactions().get(0).getRelatedResources().size() > 0) {
                if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {// 交易订单
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {// 授权订单
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {// 授权订单
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();
                    custom = payment.getTransactions().get(0).getCustom();
                }
            }
        } catch (PayPalRESTException e) {
            e.printStackTrace();
        }
        log.info("订单状态:{} ", state);
        log.info(custom);
        log.info("=========================================================================================");
        return state;
    }
}

PaypalService

/**
 * 支付service类
 */
@Service
public class PaypalService {
    // 注入凭证信息bean
    @Autowired
    private APIContext apiContext;

    /**
     * 创建支付订单
     * @param total  交易金额
     * @param currency 货币类型
     * @param method  付款类型
     * @param intent  收款方式
     * @param description  交易描述
     * @param cancelUrl  取消后回调地址
     * @param successUrl  成功后回调地址
     */
    public Payment  createPayment(
            String orderId,
            Double total,
            String currency,
            PaypalPaymentMethod method,
            PaypalPaymentIntent intent,
            String description,
            String cancelUrl,
            String successUrl) throws PayPalRESTException {
        // 设置金额和单位对象
        Amount amount = new Amount();
        amount.setCurrency(currency);
        amount.setTotal(String.format("%.2f", total));
        // 设置具体的交易对象
        Transaction transaction = new Transaction();
        transaction.setDescription(description);
        transaction.setAmount(amount);
        transaction.setCustom(orderId);
        // 交易集合-可以添加多个交易对象
        List<Transaction> 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);
        // 设置反馈url
        RedirectUrls redirectUrls = new RedirectUrls();
        redirectUrls.setCancelUrl(cancelUrl);
        redirectUrls.setReturnUrl(successUrl);
        // 加入反馈对象
        payment.setRedirectUrls(redirectUrls);
        // 加入认证并创建交易
        return payment.create(apiContext);
    }

    /**
     * 执行支付
     *   获取支付订单,和买家ID,执行支付
     */
    public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException{
        Payment payment = new Payment();
        payment.setId(paymentId);
        PaymentExecution paymentExecute = new PaymentExecution();
        paymentExecute.setPayerId(payerId);
        return payment.execute(apiContext, paymentExecute);
    }
}

URLUtils

/**
 * 获取请求url的util
 */
public class URLUtils {

    public static String getBaseURl(HttpServletRequest request) {
        String scheme = request.getScheme();
        String serverName = request.getServerName();
        int serverPort = request.getServerPort();
        String contextPath = request.getContextPath();
        StringBuffer url =  new StringBuffer();
        url.append(scheme).append("://").append(serverName);
        if ((serverPort != 80) && (serverPort != 443)) {
            url.append(":").append(serverPort);
        }
        url.append(contextPath);
        if(url.toString().endsWith("/")){
            url.append("/");
        }
        return url.toString();
    }
}

PaypalConfig

/**
 * 配置类,注入PayPal需要的认证信息
 */
@Configuration
@Data
public class PaypalConfig {
    @Value("${paypal.client.app}")
    private String clientId;
    @Value("${paypal.client.secret}")
    private String clientSecret;
    @Value("${paypal.mode}")
    private String mode;
    @Value("${paypal.webscr}")
    private String webscr;

    /**
     * 创建支付回调地址参数
     */
    public static final String SUCCESS_URL = "pay/success";  // 成功回调地址PDT
    public static final String CANCEL_URL = "pay/cancel";    // 取消回调地址PDT

    /**
     * IPN异步验证返回
     */
    public static final String PAYMENT_IPN_VERIFIED = "VERIFIED";
    public static final String PAYMENT_IPN_COMPLETED_STATUS = "COMPLETED_STATUS";
    public static final String PAYMENT_IPN_REFUNDED_STATUS = "REFUNDED_STATUS";
    public static final String PAYMENT_IPN_INVALID = "INVALID";

    /**
     * IPN异步通知返回通知消息类型
     */
    public static final String PAYMENT_STATUS_PENDING = "PENDING";
    public static final String PAYMENT_STATUS_VOIDED = "VOIDED ";
    public static final String PAYMENT_STATUS_COMPLETED = "COMPLETED ";
    public static final String PAYMENT_STATUS_REFUNDED = "REFUNDED ";

    /**
     * APP的认证信息
     * clientId、Secret,开发者账号创建APP时提供
     */
    @Bean
    public APIContext apiContext() throws PayPalRESTException {
        APIContext apiContext = new APIContext(clientId, clientSecret, mode);
        return apiContext;
    }
}

PaypalPaymentIntent

下面展示一些 内联代码片

/**
 * 付款类型
 *  sale:直接付款
 *  authorize:授权付款
 *  order:订单付款
 */
public enum PaypalPaymentIntent {
    sale,
    authorize,
    order
}

PaypalPaymentMethod

/**
 * 收款方式
 *  credit_card:信用卡收款
 *  paypal:余额收款
 */
public enum PaypalPaymentMethod {
    credit_card,
    paypal
}

application.properties

server.port=8080
spring.thymeleaf.cache=false
paypal.mode=sandbox
paypal.client.app=请填写你的应用
paypal.client.secret=请填写你的密钥
paypal.webscr=https://www.sandbox.paypal.com/cgi-bin/webscr

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wan</groupId>
    <artifactId>paypal</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>paypal</name>
    <description>paypal</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- paypal 开发时需要的jar包 -->
        <dependency>
            <groupId>com.paypal.sdk</groupId>
            <artifactId>rest-api-sdk</artifactId>
            <version>1.14.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

支付流程

创建支付订单Payment

参数:本地系统订单ID orderId,金额,货币类型,收款方式,付款方式,描述信息,取消回调,成功回调

如果创建成功,会重定向到PayPal付款页面

注:取消回调接口需要拼接参数 本地订单Id orderId,作用会在取消回调小节中详细阐述。创建订单成功后保存到map中,orderId作为key,paymentId作为value。因为paymentId可以查询订单消息

    /**
     * 创建订单请求
     * 创建成功返回payment对象,保存本地OrderId和paymentId对应关系
     */
    @RequestMapping(method = RequestMethod.POST, value = "pay")
    public String pay(HttpServletRequest request) {
        log.info("=========================================================================================");
        String orderId = "2020110200001";// 本地系统Id
        String cancelUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.CANCEL_URL + "?orderId=" + orderId;// http://localhost:8080/pay/cancel
        String successUrl = URLUtils.getBaseURl(request) + "/" + PaypalConfig.SUCCESS_URL;
        try {
            //调用交易方法
            Payment payment = paypalService.createPayment(
                    orderId,
                    300.00,
                    "USD",
                    PaypalPaymentMethod.paypal,
                    PaypalPaymentIntent.authorize,
                    "这是一笔300美元的交易",
                    cancelUrl,
                    successUrl);
            for (Links links : payment.getLinks()) {
                if (links.getRel().equals("approval_url")) {
                    // 客户付款登陆地址【判断币种CNY无法交易】
                    String paymentId = payment.getId();
                    orderPaymMap.put(orderId, paymentId);// 保存本地OrderId和paymentId对应关系
                    log.info("创建支付订单返回paymentId : " + paymentId);
                    log.info("支付订单状态state : " + payment.getState());
                    log.info("支付订单创建时间create_time : " + payment.getCreateTime());
                    log.info("=========================================================================================");
                    return "redirect:" + links.getHref();
                }
            }
        } catch (PayPalRESTException e) {
            log.error(e.getMessage());// 支付失败【使用CNY】
        }
        log.info("=========================================================================================");
        return "redirect:/";
    }

成功回调(执行支付 + PDT通知)

订单创建成功,会重定向到PayPal付款页面,买家登录PayPal账号并点击付款,执行成功回调,paypalService.executePayment(paymentId, payerId)执行支付。

PDT通知:同步通知,执行支付后会同步返回支付结果对象Payment对象,内部引用了交易对象Sale或Authorization或Order(根据付款类型决定)。根据交易对象状态修改本地订单状态,本地订单ID通过custom属性获得,但是PDT同步通知可能因为网络波动导致消息丢失,所以引出了IPN异步通知,在后面小节会详细阐述

    /**
     * 成功回调 + 支付 + PDT同步通知
     *    买家确认付款,执行支付并直接返回通知
     */
    @RequestMapping(method = RequestMethod.GET, value = PaypalConfig.SUCCESS_URL)
    public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId, @RequestParam("token") String token) {
        log.info("=========================================================================================");
        try {
            /**
             * 执行支付
             */
            Payment payment = paypalService.executePayment(paymentId, payerId);
            if (payment.getState().equals("approved")) {
                String id = "";     // 交易ID,transactionId
                String state = "";  // 交易订单状态
                String time = "";   // 交易时间
                String custom = ""; // 本地OrderId
                if (payment.getIntent().equals(PaypalPaymentIntent.authorize.toString())) {
                    id = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getId();
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getState();
                    time = payment.getTransactions().get(0).getRelatedResources().get(0).getAuthorization().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.sale.toString())) {
                    id = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState();
                    time = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                } else if (payment.getIntent().equals(PaypalPaymentIntent.order.toString())) {
                    id = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getId();
                    state = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getState();
                    time = payment.getTransactions().get(0).getRelatedResources().get(0).getOrder().getCreateTime();
                    custom = payment.getTransactions().get(0).getCustom();
                }
                log.info("PDT通知:交易成功回调");
                log.info("付款人账户:" + payment.getPayer().getPayerInfo().getEmail());
                log.info("支付订单Id {}", paymentId);
                log.info("支付订单状态state : " + payment.getState());
                log.info("交易订单Id:{}", id);
                log.info("交易订单状态state : " + state);
                log.info("交易订单支付时间:" + time);
                log.info("本地系统OrderId:{}", custom);
                log.info("=========================================================================================");
                return "success";
            }
        } catch (PayPalRESTException e) {
            // 如果同步通知返回异常,可根据paymentId 来查询刷新订单状态
            // 同时IPN异步通知也可以更新订单状态
            log.error(e.getMessage());
        }
        return "redirect:/";
    }

取消回调

触发条件:
1、付款页面买家登录账号,点击"取消并回到XXX",买家主动取消(Payment对象状态仍然是created创建状态,所以如果网页回退仍然可以对当前支付订单进行付款)
2、用户付款成功后,在浏览器上点击回退会触发取消回调(因为回退网页会导致重复付款,所以PayPal设计该部分逻辑【幂等性问题】)为取消回调

    /**
     * 失败回调
     *   触发回调情景:
     *     1、用户在支付页面点击取消支付
     *     2、用户支付成功后点击网页回退,再点击返回商家按钮触发
     *   判断是否用户主动取消付款逻辑:
     *     1、设置回调地址的时候拼接本地订单ID:?orderId=XXXX
     *     2、然后根据orderId查询paymentId,继而调用sdk查询订单支付状态
     *      * http://localhost:8080/pay/cancel?orderId=2020110200001&token=EC-70674489LL9806126
     */
    @RequestMapping(method = RequestMethod.GET, value = PaypalConfig.CANCEL_URL)
    public String cancelPay(@RequestParam("token") String token, @RequestParam("orderId") String orderId) {
        try {
            String paymentId = orderPaymMap.get(orderId);
            Payment payment = Payment.get(apiContext, paymentId);
            String state = payment.getState();
            log.info("交易取消回调:支付订单状态:{} ", state);
            if (state.equals("approved")) {// 已支付
                return "success";
            }
        } catch (PayPalRESTException e) {
            e.printStackTrace();
        }
        return "cancel";
    }

IPN异步通知

IPN异步通知会在特定情况下被触发,用于弥补PDT同步通知消息丢失的情况(网络波动导致),由Paypal主动推送。其中IPN功能需要手动开启

开启方式:登录企业收款账户,Account Setting ==》 网站付款 ==》 即时付款通知 ==》 更新,设置IPN回调接口(外网可访问,测试期间可使用内网穿透)

触发情景:
1、买家支付成功
2、卖家作废授权单或订购单
3、卖家领取授权单或订购单
4、卖家发放退款

IPN接口代码逻辑:
paypal每隔一段时间会调用一次IPN通知,直至我们回复paypal后一次IPN异步调用才会结束,否则paypal会多次调用IPN通知已保证客户端可以接收到订单状态改变的消息。

IPN异步通知部分参数解释:
payment_status:触发IPN通知的当前订单状态
txn_id:触发本次IPN异步通知的id(可能是saleId、authorizationId、orderId、captureId、refundId,根据触发情景+付款类型决定)
parent_txn_id:当前订单父关联ID
auth_id:始终为transactionId(可能是saleId、authorizationId、orderId)

以下只针对授权付款方式的所有回调返回参数作一个总结:

回调情景payment_statustxn_idparent_txn_idauth_id
authorize支付成功回调Pending 待领取authorizationIdauthorizationId
商户作废授权单回调Voided 作废authorizationIdauthorizationId
商户领取授权单回调Completed 已领取captureIdauthorizationIdauthorizationId
商户退款回调Refunded 退款refundIdcaptureIdauthorizationId
    /**
     * IPN异步通知
     *   触发情景:
     *      1、买家支付成功
     *      2、卖家确认收取授权或订单款项
     *      3、卖家发放退款
     */
    @RequestMapping(method = RequestMethod.POST, value = "/notificationIPN")
    public void receivePaypalStatus(HttpServletRequest request, HttpServletResponse response) throws Exception {
        log.info("=========================================================================================");
        log.info("IPN通知:交易成功异步回调");
        PrintWriter out = response.getWriter();
        try {
            Enumeration<String> en = request.getParameterNames();
            /**
             * 修改订单状态
             *      保存失败则不验签,继续接受paypal异步回调直至保存成功【或者用MQ】
             */
            String paymentStatus = request.getParameter("payment_status").toUpperCase();  // 交易状态
            String paymentDate = request.getParameter("payment_date");      // 交易时间
            String custom = request.getParameter("custom");                 // 本地系统订单ID
            String auth_id = request.getParameter("auth_id");               // transactionId
            String txnId = request.getParameter("txn_id");                  // 当前回调数据id【具体逻辑查看 .md文档】
            String parentTxnId = request.getParameter("parent_txn_id");     // 父id
            String receiverEmail = request.getParameter("receiver_email");  // 收款人email
            String receiverId = request.getParameter("receiver_id");        // 收款人id
            String payerEmail = request.getParameter("payer_email");        // 付款人email
            String payerId = request.getParameter("payer_id");              // 付款人id
            String mcGross = request.getParameter("mc_gross");              // 交易金额
            String item_name = request.getParameter("item_name");
            log.info("paymentStatus = " + paymentStatus);
            log.info("txnId = " + txnId);
            log.info("parentTxnId = " + parentTxnId);
            log.info("authId(transactionId)= " + auth_id);
            log.info("custom(orderId)= " + custom);
            log.info("item_name= " + item_name);
            /**
             * 验证
             *   作用:
             *     订单状态修改成功,告诉paypal停止回调
             *   实现:
             *     在原参数的基础上加cmd=_notify-validate,然后对https://www.sandbox.paypal.com/cgi-bin/webscr发起POST验证请求
             */
            String str = "cmd=_notify-validate";
            while (en.hasMoreElements()) {
                String paramName = en.nextElement();
                String paramValue = request.getParameter(paramName);
                //此处的编码一定要和自己的网站编码一致,不然会出现乱码,paypal回复的通知为"INVALID"
                str = str + "&" + paramName + "=" + URLEncoder.encode(paramValue, "utf-8");
            }
            log.info("paypal传递过来的交易信息:" + str);// 建议在此将接受到的信息 str 记录到日志文件中以确认是否收到 IPN 信息
            URL url = new URL(paypalConfig.getWebscr());
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//设置 HTTP 的头信息
            PrintWriter pw = new PrintWriter(connection.getOutputStream());
            pw.println(str);
            pw.close();
            /**
             * 回复
             *    接受PayPal对验证的回复信息
             */
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String resp = in.readLine();
            in.close();
            resp = StringUtils.isEmpty(resp) ? "0" : resp;
            log.info("resp = " + resp);
            /**
             * 验证返回状态
             */
            if (PaypalConfig.PAYMENT_IPN_VERIFIED.equalsIgnoreCase(resp)) {
                /**
                 * 修改订单状态
                 *      根据订单状态paymentStatus确定当前回调的类型
                 */
                switch (paymentStatus) {
                    case PaypalConfig.PAYMENT_STATUS_PENDING:
                        // 商家待领取状态
                        break;
                    case PaypalConfig.PAYMENT_STATUS_VOIDED:
                        // 商家作废(30天以内,且必须是授权付款类型 或 订单付款类型),款项原路返回买家
                        break;
                    case PaypalConfig.PAYMENT_STATUS_COMPLETED:
                        // 商家领取
                        String captureId = txnId;   // 实际领取对象ID【授权付款 和 订单付款需要商家领取】
                        break;
                    case PaypalConfig.PAYMENT_STATUS_REFUNDED:
                        // 商家退款,需扣除费用
                        String refundId = txnId;
                        String captureId2 = parentTxnId;
                        break;
                }
            } else if (PaypalConfig.PAYMENT_IPN_INVALID.equalsIgnoreCase(resp)) {
                // 非法信息,可以将此记录到您的日志文件中以备调查
                log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);
                log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            } else {// 处理其他错误
                log.error("IPN通知返回状态非法,请联系管理员,请求参数:" + str);
                log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
                out.println("confirmError");
            }
        } catch (Exception e) {
            log.error("IPN通知发生IO异常" + e.getMessage());
            log.error("Class: " + this.getClass().getName() + " method: " + Thread.currentThread().getStackTrace()[1].getMethodName());
            out.println("confirmError");
            e.printStackTrace();
        }
        out.flush();
        out.close();
        log.info("=========================================================================================");
    }

概念梳理

账户类型

个人账户:不支持跨国收款,并且每月有限额。收款免费
企业账户:收款收取费用

资费信息

付款

免费,用户付款不收取费用

收款【针对企业用户,可申请商家优惠】

账户类型费用
美国账户2.9% + $ 0.30(固定费用)
符合条件的慈善机构2.2% + $ 0.30(固定费用)
小额商品6% + $ 0.50(固定费用)
国际账户(可申请商家优惠)4.4% + 根据收款货币类型计算的固定费用

国际账户申请商家优惠费用调整:

月销售额(美元)费率
3000及以下4.4%+ $ 0.30(固定费用)
3000-100003.9%+ $ 0.30(固定费用)
10000-1000003.7%+ $ 0.30(固定费用)
100000以上3.4%+ $ 0.30(固定费用)

商家优惠申请方式:登录你的paypal账户——用户信息——财务信息——更多财务设置(商家费用)——立即申请
货币类型与对应固定费用

货币费用
澳大利亚元:0.30澳元
巴西雷亚尔:0.60巴西雷亚尔
加拿大元:0.30加元
捷克克朗:10.00 CZK
丹麦克朗:2.60 DKK
欧元:0.35欧元
港元:2.35港币
匈牙利福林:90福林
以色列谢克尔:1.20 ILS
日圆:40日圆
马来西亚林吉特:2.00马币
墨西哥比索:4.00 MXN
新西兰元:0.45纽币
挪威克朗:2.80挪威克朗
菲律宾比索:15.00 PHP
波兰兹罗提:1.35 PLN
俄罗斯卢布:10卢布
新加坡元:0.50新元
瑞典克朗:3.25 SEK
瑞士法郎:0.55瑞士法郎
台湾新台币:10.00 TWD
泰铢:11.00泰铢
英镑:0.20英镑
美元:0.30美元

货币兑换手续费

参照转换费率表 2.5%~4.0%,链接: 汇率转换表

案例:A帐户向B帐户付100美金,但A帐户余额里只有欧元,若A帐户付欧元给B帐户,B帐户同意并接收了,则B帐户在提现美金的时候,需要把欧元转换成美金,这时则需要承担货币兑换手续费。

提现

之前在网上有看到可直接提现人民币到中国的银行的方法已经不支持【公司合作结束】,如有误请及时指出

提现方式费用优势
电汇至中国的银行账户(收款USD)$35 / 笔金额越大越划算
提现到香港银行(收款HKD)1000港币及以上,免费;1000港币以下,每笔3.50港币适合有香港银行账户,可以自行进行转账和人民币兑换的商户
通过支票提现(收款USD)$5 / 笔5-7周,试用不急用且可兑换人民币用户

退款

这里提到的退款是商家接收付款之后,再由商家发起退款产生的资费问题。
1、商家收款时会产生收款手续费;
2、收款的180天内商家可发起退款(可仅退部分款项或全部退款),按照退款比率退换收款时扣取的手续费,但是固定费用例如$ 0.3 不会退回,可以看做退款的损失是$ 0.3。
3、收款180天之后商家不可发起退款,此时如果需要退款需要采用汇款的方式完成退款,此时用户接受汇款形式的退款会征收用户收款手续费。

请看以下案例:
A账户给B账户汇款 $ 100,B账户收款支付收款手续费 $ 4.4 + $ 0.3,实际收款$ 95.3。180天内B账户发起全额退款,A账户收到款项$ 100【A账户不需要支付收款手续费】,手续费$ 4.4退换给B,但是$ 0.3固定费用不退

链接: 如何发放退款

退单

退单是由买家发起的:
买家向信用卡公司提出退单时,PayPal会向卖家收取一笔费用。如果交易受卖家保障规则保护,PayPal将支付退单金额并免除退单费。且卖家收款费用不退还

链接: 具体条约文档
链接: 退单费用
链接: 卖家如何处理退单?
链接: 调解中心

付款类型

sale(直接付款)

款项直接打到商户PayPal账户余额,商家直接接受货款并支付手续费。注:① 如果当前账户是开户以来第一次接受付款,商户需要确认货款,如果30天未接收货款则自动退回买家账户。② 如果当前账户接收的货币类型,商户账户余额中并未持有,则需要商户确认货款,如果30天未接收货款则自动退回买家账户

authorize(授权付款)

买家给商户授权一笔款项,商户账户需要在30天内领取(商户账号可以修改授权款项),若未确认领取则款项自动退还买家。商户可以在30天内作废次交易,则款项原路退还给买家

order(订单付款)

买家给商户支付一笔订单,商户账户需要在30天内领取(商户账号不可以修改订单款项),若未确认领取则款项自动退还买家。商户可以在30天内作废次交易,则款项原路退还给买家

收款方式

credit_card(信用卡收款)

使用信用卡收款

paypal(余额收款)

买家打款,商家使用paypal账户余额收款

订单对象

三级关联

这一部分需要代码debug测试时访问selectTransactionState接口传入paymentId查看支付订单payment对象。(查看代码controller中的selectTransactionState接口)
支付订单payment对象关联交易订单对象,其中交易订单分为Sale、Authorization、order三类,其中Authorization授权订单对象关联Capture商户确认授权单对象(保存了商户实际收款金额),其中交易订单Sale、order和确认授权对象Capture还关联了退款订单Refund对象(保存了商户实际退款金额)

对象类型作用通用ID
Payment支付订单,包含付款金额、paymentId
Sale直接付款订单,包含付款、saleId字段设计可以共用transactionId,用付款类型区分
Authorization授权付款订单,包含付款金额、authorizationId字段设计可以共用transactionId,用付款类型区分
order订购付款订单,包含付款金额、orderId字段设计可以共用transactionId,用付款类型区分
Capture商家确认授权对象,包含商家实际授权的金额、captureId
Refund商家退款对象,包含商家实际退款的金额、refundId

订单状态

1、Payment对象:支付订单对象

state状态
created创建
approved已支付(如果商家发起退款不会修改payment状态)
failed订单取消

2、Sale对象:直接付款对象

state状态
completed款项到达商户账户余额
refunded商家发起全部退款
partially_refunded商家发起部分退款

3、Authorization:授权付款对象

state状态
authorized等待商家领取款项
captured商家领取全部款项
partially_captured商家领取部分款项
voided商家作废此次交易,款项原路退还买家

4、Order:订单付款对象

state状态
PENDING等待商家领取款项
VOIDED商家作废此次交易,款项原路退还买家

5、Capture:商家授权对象

state状态
completed款项到达商户账户余额
refunded商家发起全部退款
partially_refunded商家发起部分退款

6、Refund:退款信息对象

state状态
completed款项到达买家账户(余额或者信用卡)
  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值