PayPal支付介面開發java版

MrWanZH發表於2020-11-06

前言

經過n次debug和無數的查詢資料,終於摸清除了PayPal支付,請聽我一一道來

程式碼

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 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:/";
    }

    /**
     * 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>

相關文章