Java服务端实现Paypal支付功能

Hello,各位读者们,这一期讲解Java集成Paypal支付功能,但是要注意的是,我这边只是集成了Paypal单次购买(One-Time)订阅购买(Subscription),其实这两种类型的在线支付已经足够了。好了废话不多说,直接开整。

Paypal支付所需配置

这边就不细说了,配置这个东西还是很简单的,按照官网来就行了。对于咱们研发,要多看、研究所集成对象官网,这个才是靠谱的,不过没事,为了下一期发布Paypal支付配置,来给读者讲解。

Paypal支付注意事项

可能很多读者参考网上资料,Java集成的时候都是什么导入SDK依赖啊什么的,注意,Paypal官网已经不会对服务端SDK维护了,也就是说你们可以继续用,但是新功能你用不了,官网推荐:REST-API接入支付功能,并且有一些API也是废弃了,所以呢这些都是需要去详细了解。

Paypal支付流程图

Paypal支付流程
业务逻辑其实也不算复杂,老套的逻辑:

1. 下单
2. 创建第三方订单
3. 关联系统订单
4. 付款
5. 回调处理订单

不管支付逻辑有多难,我们都以以上的流程用最简单的方式把它搞定。
用自然语言给大家描述下:当用户购买商品时,客户端发起支付,服务端根据参数对其进行创建订单,请求第三方支付创建订单,创建成功后,第三方订单号和系统订单号关联,这两个操作是原子性的。返回支付链接给客户端,客户端访问支付地址,跳转到支付网关payment页面,进行付款,付款的结果,支付网关都会重定向到指定地址(比如官网配置的地址,调API设置好的地址)。第三方网关回调服务端,服务端校验订单、处理订单、通知用户。

上代码

使用最新版Paypal,调用REST-API(OAuth2.0认证),所以无需引如依赖。当然了httpclient依赖还是要有的哈哈,用于发送http请求。Httpclient依赖,我这边是使用的httpclient:

		<dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.1</version>
        </dependency>

1、Httpclient封装工具类

/**
 * @author xxx Http请求封装工具类(使用Http5.X版本)
 * 支持HTTP/2、新的异步HTTP接口、重构reactor io模式,改进基于reactor 的NIO、
 * 不论服务端是阻塞还是异步的实现,httpclient5均能支持服务端的过滤。例如横切协议(cross-cutting protocol)的握手,和用户认证授权
 * 支持reactive流的API、
 * @version 1.0
 * @date 2023/9/11 11:32
 */
public class HttpClientUtil {

    public static final Logger logger = LogManager.getLogger(HttpClientUtil.class);

    /**
     * 总最大连接数
     */
    private static final int MAX_TOTAL_CONNECTIONS = 600;
    /**
     * 每个http主机的最大连接数
     */
    private static final int MAX_CONNECTIONS_PER_ROUTE = 300;
    /**
     * 请求超时时间(以毫秒为单位)
     */
    private static final int CONNECTION_REQUEST_TIMEOUT = 10000;
    /**
     * 等待数据的超时时间(就是请求超时时间)(以毫秒为单位)
     */
    private static final int RESPONSE_TIMEOUT = 10000;
    /**
     * 空闲连接清理间隔(以毫秒为单位)
     */
    private static final int IDLE_CONNECTION_CLEAR_INTERVAL = 30000;

    //客户端 http请求对象
    private static CloseableHttpClient httpClient;
    //http连接池配置
    private static PoolingHttpClientConnectionManager connectionManager;
    //请求配置
    private static RequestConfig requestConfig;

    static {
        //注册访问协议相关的Socket工厂
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", trustHttpsCertificates())
                .build();
        connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(CONNECTION_REQUEST_TIMEOUT))
                .setResponseTimeout(Timeout.ofMilliseconds(RESPONSE_TIMEOUT))
                .build();
        httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                //每隔多久清理一次空闲连接,正在使用的连接不会被清理
                .evictIdleConnections(Timeout.ofMilliseconds(IDLE_CONNECTION_CLEAR_INTERVAL))
                .build();
    }

    /**
     * Https证书管理
     *
     * @return 可识别证书集合
     */
    private static ConnectionSocketFactory trustHttpsCertificates() {
        TrustManager[] trustAllCertificates = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        };
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCertificates, new SecureRandom());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    }

    /**
     * 重写response handler,确保客户端自动释放资源
     *
     * @return
     */
    private static HttpClientResponseHandler<String> getResponseHandler() {
        return new HttpClientResponseHandler<String>() {
            @Override
            public String handleResponse(ClassicHttpResponse response) throws IOException {
                int status = response.getCode();
                HttpEntity entity = response.getEntity();
                String responseBodyStr = null;
                try {
                    responseBodyStr = entity != null ? EntityUtils.toString(entity) : null;
                } catch (ParseException e) {
                    throw new ClientProtocolException("Http entity parse exception: " + e);
                }
                if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
                    return responseBodyStr;
                } else {
                    throw new ClientProtocolException("Unexpected response status: " + status + "Response body: " + responseBodyStr);
                }
            }
        };
    }

    /**
     * Get请求
     *
     * @param url     请求地址
     * @param headers 请求头
     * @param params  请求参数
     * @return
     */
    public static String get(String url, Map<String, Object> headers, Map<String, Object> params) throws PaymentCoreException {
        logger.info("Httpclient5 GET request url={} headers={} params={}", url, headers, params);
        String responseStr = null;
        HttpGet httpGet = new HttpGet(url);
        //设置header
        if (headers != null) {
            for (Map.Entry<String, Object> entry : headers.entrySet()) {
                httpGet.addHeader(entry.getKey(), entry.getValue());
            }
        }
        if (params != null && params.size() > 0) {
            // 表单参数
            List<NameValuePair> nvps = new ArrayList<>();
            // GET 请求参数,如果中文出现乱码需要加上URLEncoder.encode
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                nvps.add(new BasicNameValuePair(entry.getKey(), String.valueOf(entry.getValue())));
            }
            // 增加到请求 URL 中
            try {
                URI uri = new URIBuilder(new URI(url)).addParameters(nvps).build();
                httpGet.setUri(uri);
            } catch (URISyntaxException e) {
                throw new PaymentCoreException("URISyntaxException: " + e.getMessage());
            }
        }
        try {
            responseStr = httpClient.execute(httpGet, getResponseHandler());
        } catch (IOException e) {
            throw new PaymentCoreException("IOException url=: " + url + ">>>exception= " + e.getMessage());
        }
        logger.info("Httpclient5 GET response: {}", responseStr);
        return responseStr;
    }

    /**
     * Post请求
     *
     * @param url     请求地址
     * @param headers 请求头
     * @param body    请求参数(JSON字符串)
     * @return
     */
    public static String post(String url, Map<String, Object> headers, String body) throws PaymentCoreException {
        logger.info("Httpclient5 POST request url={} headers={} params={}", url, headers, body);
        String responseStr = null;
        HttpPost httpPost = new HttpPost(url);
        //设置header
        if (headers != null) {
            for (Map.Entry<String, Object> entry : headers.entrySet()) {
                httpPost.addHeader(entry.getKey(), entry.getValue());
            }
        }
        //设置请求参数
        if (!StringUtils.isEmpty(body)) {
            StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
            httpPost.setEntity(entity);
        }
        //执行请求
        try {
            responseStr = httpClient.execute(httpPost, getResponseHandler());
        } catch (IOException e) {
            throw new PaymentCoreException("IOException url=: " + url + ">>>exception= " + e.getMessage());
        }
        logger.info("Httpclient5 POST response: {}", responseStr);
        return responseStr;
    }

    /**
     * Post请求,无请求头
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return
     */
    public static String post(String url, Map<String, Object> params) throws PaymentCoreException {
        logger.info("Httpclient5 POST params request url={} params={}", url, params);
        String responseStr = null;
        HttpPost httpPost = new HttpPost(url);
        httpPost.setEntity(getFormEntity(params));
        try {
            responseStr = httpClient.execute(httpPost, getResponseHandler());
        } catch (IOException e) {
            throw new PaymentCoreException("IOException url=: " + url + ">>>exception= " + e.getMessage());
        }
        logger.info("Httpclient5 POST params response: {}", responseStr);
        return responseStr;
    }

    /**
     * OAuth2.0授权验证请求
     *
     * @param url              请求地址
     * @param authorizationStr 请求认证签名
     * @return
     */
    public static String postOAuth2(String url, String authorizationStr) throws PaymentCoreException {
        logger.info("Httpclient5 POST oAuth request url={} authorizationStr={}", url, authorizationStr);
        String responseStr = null;
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("Authorization", "Basic " + authorizationStr);
        httpPost.setEntity(new StringEntity("grant_type=client_credentials", ContentType.APPLICATION_FORM_URLENCODED));
        try {
            responseStr = httpClient.execute(httpPost, getResponseHandler());
        } catch (IOException e) {
            throw new PaymentCoreException("IOException url=: " + url + ">>>exception= " + e.getMessage());
        }
        logger.info("Httpclient5 POST oAuth response: {}", responseStr);
        return responseStr;
    }

    /**
     * 封装Post请求的form表单
     *
     * @param paramMap
     * @return UrlEncodedFormEntity
     */
    private static UrlEncodedFormEntity getFormEntity(Map<String, Object> paramMap) {
        //参数定义
        List<NameValuePair> params = new ArrayList<>();
        if (!CollectionUtils.isEmpty(paramMap)) {
            paramMap.forEach((k, v) -> {
                params.add(new BasicNameValuePair(k, String.valueOf(v)));
            });
            //创建提交的表单对象
            return new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
        }
        return new UrlEncodedFormEntity(params);
    }

    /**
     * 表单请求
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return
     */
    public static String postForm(String url, Map<String, Object> params) throws PaymentCoreException {
        logger.info("Httpclient5 POST form request url={} params={}", url, params);
        String responseStr = null;
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
        httpPost.setEntity(getFormEntity(params));
        try {
            responseStr = httpClient.execute(httpPost, getResponseHandler());
        } catch (IOException e) {
            throw new PaymentCoreException("IOException url=: " + url + ">>>exception= " + e.getMessage());
        }
        logger.info("Httpclient5 POST form response: {}", responseStr);
        return responseStr;
    }

2、Token认证对象创建
这个我自己实现的。仅供参考:

    /**
     * oauth2生成token请求地址
     */
    private final String V1_GET_TOKEN_URL = "/v1/oauth2/token";
    /**
     * 创建订单请求地址
     */
    private final String V2_CHECK_ORDER_URL = "/v2/checkout/orders";
    /**
     * 创建订阅计划请求地址
     */
    private final String V1_BILLING_PLANS_URL = "/v1/billing/plans";
    /**
     * 创建订阅请求地址
     */
    private final String V1_BILLING_SUBSCRIPRION_URL = "/v1/billing/subscriptions";
    /**
     * 创建订阅商品请求地址
     */
    private final String V1_CATALOGS_PRODUCT_URL = "/v1/catalogs/products";

    /**
     * 获取Token对象
     *
     * @param appName 应用唯一标识
     * @return
     * @throws PaymentCoreException
     */
    @Override
    public synchronized TokenContext getAuthInstance(String appName) throws PaymentCoreException {
        TokenContext tokenContext = TOKEN_CONTEXT_MAP.get(appName);
        if (tokenContext != null && tokenContext.expiresIn() > 0) {
            return tokenContext;
        }
        //获取access token
        PayPalConfigEntity paymentConfig = paymentConfigFactory.getPaymentConfig(ConstantUtil.PAY_PAL_PAYMENT_CONFIG_IMPL, appName);
        JSONObject response = this.getAccessToken(paymentConfig);
        tokenContext = new TokenContext();
        tokenContext.setAccessToken(response.getString("access_token"));
        tokenContext.setExpire((System.currentTimeMillis() / 1000) + response.getLongValue("expires_in"));
        tokenContext.setTokenType(response.getString("token_type"));
        tokenContext.setAppId(response.getString("app_id"));
        tokenContext.setDomainUrl(paymentConfig.getDomainUrl());
        TOKEN_CONTEXT_MAP.put(appName, tokenContext);
        return tokenContext;
    }

    /**
     * 请求token API 获取accessToken
     * {
     * "scope": "https://uri.paypal.com/services/applications/webhooks",
     * "access_token": "xxxxxxxxxxxxxxxxxx",
     * "token_type": "Bearer",
     * "app_id": "APP-xxxxxxxxx",
     * "expires_in": 32400,
     * "nonce": "2023-09-12T12:43:35Z6Yw0-JDPHF1FA84QudGxxxxxxxTKT5cus"
     * }
     *
     * @param paymentConfig 配置信息
     * @return JSONObject
     * @throws PaymentCoreException
     */
    private JSONObject getAccessToken(PayPalConfigEntity paymentConfig) throws PaymentCoreException {
        String base64Str = EncryptionUtil.base64Encode(paymentConfig.getSecretId() + ":" + paymentConfig.getSecretKey());
        String str = HttpClientUtil.postOAuth2(paymentConfig.getDomainUrl() + V1_GET_TOKEN_URL, base64Str);
        if (StringUtils.isEmpty(str)) {
            throw new PaymentCoreException("[PayPal] get access token fail, Pls check log");
        }
        return JSONObject.parseObject(str);
    }

TokenContext 是我自己实现存放accessToken的,判断是否过期,不必频繁去拿Token。

/**
 * @author liujiang 封装授权Token上下文对象,基于OAuth认证标准
 * @version 1.0
 * @date 2023/9/12 15:55
 */
public class TokenContext implements Serializable {

    /**
     * Access token宽限时长(单位/s),防止拿到token后,再去调用刚好失效了
     **/
    private long graceTime = 20;

    /**
     * Access token,用于访问Rest api
     **/
    private String accessToken;

    /**
     * expire,access token过期时间,当OAuth2.0返回时间+当前时间(单位/s)
     **/
    private long expire = 0;

    /**
     * Token范围,常见的:Bearer
     **/
    private String tokenType;

    /**
     * App id,可自定义
     **/
    private String appId;

    /**
     * paypal请求域名(生产或者沙盒)
     * 示例:https://api-m.sandbox.paypal.com
     **/
    private String domainUrl;

    /**
     * 计算过期时长还剩多少并减去宽限时长(时间必须统一)
     *
     * @return
     */
    public long expiresIn() {
        return expire - System.currentTimeMillis() / 1000 - graceTime;
    }
 }

3、以下是各种API处理方法:

/**
     * 创建paypal支付订单,注意需自行实现系统OrderId和paypal orderId关联
     *
     * @param appName  应用唯一标识
     * @param orderReq 请求body
     * @return OrderDetailResp
     */
    public OrderDetailResp createOrder(String appName, @NonNull CreateOrderReq orderReq) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        String resp = HttpClientUtil.post(authInstance.getDomainUrl() + V2_CHECK_ORDER_URL, requestHeaders, JSONObject.toJSONString(orderReq));
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] create order fail, Pls check log");
        }
        //返回实体请看官网示例,这里只取重要信息
        JSONObject responseBody = JSONObject.parseObject(resp);
        OrderDetailResp orderDetailResp = new OrderDetailResp();
        this.dataHandle(responseBody, orderDetailResp);
        if (responseBody.containsKey("links")) {
            List<LinkDto> links = JSONArray.parseArray(responseBody.getString("links"), LinkDto.class);
            LinkDto linkDto = links.stream().filter(e -> "payer-action".equals(e.getRel())).findAny().get();
            orderDetailResp.setPaymentUrl(linkDto.getHref());
        }
        return orderDetailResp;
    }

    /**
     * 获取订单详情,这里只取了关键信息,用于判断支付是否支付成功,判断是否为 【APPROVED】
     *
     * @param appName   应用唯一标识
     * @param paymentId paypal支付订单Id
     * @return
     */
    public OrderDetailResp getOrderDetail(String appName, @NonNull String paymentId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V2_CHECK_ORDER_URL);
        stringBuilder.append("/");
        stringBuilder.append(paymentId);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.get(url, requestHeaders, null);
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] Get order detail fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        OrderDetailResp orderDetailResp = new OrderDetailResp();
        this.dataHandle(responseBody, orderDetailResp);
        return orderDetailResp;
    }

    /**
     * 用户确认订单,不建议使用,该方法是用来用户确认并通知的,这不应该交由第三方实现,子系统实现
     * https://api-m.paypal.com/v2/checkout/orders/{id}/confirm-payment-source
     *
     * @param appName          应用唯一标识
     * @param paymentSourceDto 确认订单请求参数
     * @return OrderDetailResp
     */
    public OrderDetailResp confirmOrder(String appName, @NonNull PaymentSourceDto paymentSourceDto, @NonNull String paymentId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V2_CHECK_ORDER_URL);
        stringBuilder.append("/");
        stringBuilder.append(paymentId);
        stringBuilder.append("/");
        stringBuilder.append("confirm-payment-source");
        String url = stringBuilder.toString();
        JSONObject body = new JSONObject();
        body.put("paymentSource", paymentSourceDto);
        String resp = HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(body));
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] confirm order fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        OrderDetailResp orderDetailResp = new OrderDetailResp();
        this.dataHandle(responseBody, orderDetailResp);
        return orderDetailResp;
    }

    /**
     * 创建订阅计划方法,创建订阅计划
     *
     * @param appName       应用唯一标识
     * @param createPlanReq 创建订阅计划的请求参数
     * @return PlanDetailResp
     * @throws PaymentCoreException
     */
    public PlanDetailResp createPlan(String appName, @NonNull CreatePlanReq createPlanReq) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_PLANS_URL);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(createPlanReq));
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] create plan fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        PlanDetailResp planDetailResp = new PlanDetailResp();
        this.dataHandle(responseBody, planDetailResp);
        return planDetailResp;
    }

    /**
     * 获取订阅计划详情
     *
     * @param appName 应用唯一标识
     * @param planId  计划Id,创建Plan已经返回
     * @return
     * @throws PaymentCoreException
     */
    public PlanDetailResp getPlanDetail(String appName, @NonNull String planId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_PLANS_URL);
        stringBuilder.append("/");
        stringBuilder.append(planId);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.get(url, requestHeaders, null);
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] Get plan detail fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        PlanDetailResp planDetailResp = new PlanDetailResp();
        this.dataHandle(responseBody, planDetailResp);
        return planDetailResp;
    }

    /**
     * 激活订阅计划,该方法并没有返回响应体,出现异常则失败
     *
     * @param appName 应用唯一标识
     * @param planId  计划Id,创建Plan已经返回
     * @throws PaymentCoreException
     */
    public void activePlan(String appName, @NonNull String planId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_PLANS_URL);
        stringBuilder.append("/");
        stringBuilder.append(planId);
        stringBuilder.append("/");
        stringBuilder.append("activate");
        String url = stringBuilder.toString();
        HttpClientUtil.post(url, requestHeaders, null);
    }

    /**
     * 停止订阅计划,该方法并没有返回响应体,出现异常则失败
     *
     * @param appName 应用唯一标识
     * @param planId  计划Id,创建Plan已经返回
     * @throws PaymentCoreException
     */
    public void deactivatePlan(String appName, @NonNull String planId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_PLANS_URL);
        stringBuilder.append("/");
        stringBuilder.append(planId);
        stringBuilder.append("/");
        stringBuilder.append("deactivate");
        String url = stringBuilder.toString();
        HttpClientUtil.post(url, requestHeaders, null);
    }

    /**
     * 更新计划的定价。例如,您可以将常规计费周期从每月5美元更新为每月7美元。
     *
     * @param appName          应用唯一标识
     * @param planId           计划Id,创建Plan已经返回
     * @param pricingSchemeDto
     */
    public void updatePlanPrice(String appName, @NonNull String planId, @NonNull List<PricingSchemeDto> pricingSchemeDto) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_PLANS_URL);
        stringBuilder.append("/");
        stringBuilder.append(planId);
        stringBuilder.append("/");
        stringBuilder.append("update-pricing-schemes");
        String url = stringBuilder.toString();
        JSONObject requestBody = new JSONObject();
        requestBody.put("pricing_schemes", pricingSchemeDto);
        HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(requestBody));
    }

    /**
     * 创建订阅
     *
     * @param appName               应用唯一标识
     * @param createSubscriptionReq 创建订阅请求实体
     * @return SubscriptionDetailResp
     * @throws PaymentCoreException
     */
    public SubscriptionDetailResp createSubscription(String appName, @NonNull CreateSubscriptionReq createSubscriptionReq) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_SUBSCRIPRION_URL);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(createSubscriptionReq));
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] create subscription fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        SubscriptionDetailResp subscriptionDetailResp = new SubscriptionDetailResp();
        this.dataHandle(responseBody, subscriptionDetailResp);
        return subscriptionDetailResp;
    }

    /**
     * 获取订阅详情
     *
     * @param appName        应用唯一标识
     * @param subscriptionId 订阅Id
     * @return SubscriptionDetailResp
     * @throws PaymentCoreException
     */
    public SubscriptionDetailResp getSubscriptionDetail(String appName, @NonNull String subscriptionId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_SUBSCRIPRION_URL);
        stringBuilder.append("/");
        stringBuilder.append(subscriptionId);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.get(url, requestHeaders, null);
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] create subscription fail, Pls check log");
        }
        JSONObject responseBody = JSONObject.parseObject(resp);
        SubscriptionDetailResp subscriptionDetailResp = new SubscriptionDetailResp();
        this.dataHandle(responseBody, subscriptionDetailResp);
        return subscriptionDetailResp;
    }

    /***
     * 挂起订阅
     * @param appName 应用唯一标识
     * @param subscriptionId  订阅Id
     * @param reason  挂起原因
     * @throws PaymentCoreException
     */
    public void suspendSubscription(String appName, @NonNull String subscriptionId, String reason) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_SUBSCRIPRION_URL);
        stringBuilder.append("/");
        stringBuilder.append(subscriptionId);
        stringBuilder.append("/");
        stringBuilder.append("suspend");
        String url = stringBuilder.toString();
        JSONObject jsonObject = new JSONObject();
        if (StringUtils.isEmpty(reason)) {
            reason = "Other";
        }
        jsonObject.put("reason", reason);
        HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(jsonObject));
    }

    /***
     * 取消订阅
     * @param appName 应用唯一标识
     * @param subscriptionId  订阅Id
     * @param reason  取消原因
     * @throws PaymentCoreException
     */
    public void cancelSubscription(String appName, @NonNull String subscriptionId, String reason) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_SUBSCRIPRION_URL);
        stringBuilder.append("/");
        stringBuilder.append(subscriptionId);
        stringBuilder.append("/");
        stringBuilder.append("cancel");
        String url = stringBuilder.toString();
        JSONObject jsonObject = new JSONObject();
        if (StringUtils.isEmpty(reason)) {
            reason = "Other";
        }
        jsonObject.put("reason", reason);
        HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(jsonObject));
    }

    /***
     * 激活订阅
     * @param appName 应用唯一标识
     * @param subscriptionId  订阅Id
     * @throws PaymentCoreException
     */
    public void activateSubscription(String appName, @NonNull String subscriptionId) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_BILLING_SUBSCRIPRION_URL);
        stringBuilder.append("/");
        stringBuilder.append(subscriptionId);
        stringBuilder.append("/");
        stringBuilder.append("activate");
        String url = stringBuilder.toString();
        HttpClientUtil.post(url, requestHeaders, null);
    }

    /**
     * 创建商品(需创建商品才能使用用订阅服务)
     *
     * @param appName
     * @param catalogsProductReq
     * @return
     * @throws PaymentCoreException
     */
    public CatalogsProductResp createProduct(String appName, @NonNull CatalogsProductReq catalogsProductReq) throws PaymentCoreException {
        TokenContext authInstance = this.getAuthInstance(appName);
        if (authInstance == null) {
            throw new PaymentCoreException("Get token context info fail, Pls check config");
        }
        Map<String, Object> requestHeaders = this.createRequestHeaders(null, authInstance.getAccessToken());
        StringBuilder stringBuilder = new StringBuilder(authInstance.getDomainUrl());
        stringBuilder.append(V1_CATALOGS_PRODUCT_URL);
        String url = stringBuilder.toString();
        String resp = HttpClientUtil.post(url, requestHeaders, JSONObject.toJSONString(catalogsProductReq));
        if (StringUtils.isEmpty(resp)) {
            throw new PaymentCoreException("[PayPal] create order fail, Pls check log");
        }
        CatalogsProductResp catalogsProductResp = JSONObject.parseObject(resp, CatalogsProductResp.class);
        return catalogsProductResp;
    }

    /**
     * 数据处理(抽取复用代码)
     *
     * @param responseBody
     * @param subscriptionDetailResp
     */
    private void dataHandle(JSONObject responseBody, SubscriptionDetailResp subscriptionDetailResp) {
        if (responseBody.containsKey("id")) {
            subscriptionDetailResp.setSubscriptionId(responseBody.getString("id"));
        }
        if (responseBody.containsKey("status")) {
            subscriptionDetailResp.setStatus(responseBody.getString("status"));
        }
        if (responseBody.containsKey("plan_id")) {
            subscriptionDetailResp.setPlanId(responseBody.getString("plan_id"));
        }
        if (responseBody.containsKey("links")) {
            List<LinkDto> links = JSONArray.parseArray(responseBody.getString("links"), LinkDto.class);
            Optional<LinkDto> any = links.stream().filter(e -> "approve".equals(e.getRel())).findAny();
            if(any.isPresent()) {
                LinkDto linkDto = any.get();
                subscriptionDetailResp.setPaymentUrl(linkDto.getHref());
            }
        }
    }

    /**
     * 数据处理(抽取复用代码)
     *
     * @param responseBody
     * @param orderDetailResp
     */
    private void dataHandle(JSONObject responseBody, OrderDetailResp orderDetailResp) {
        if (responseBody.containsKey("id")) {
            orderDetailResp.setPaymentId(responseBody.getString("id"));
        }
        if (responseBody.containsKey("status")) {
            orderDetailResp.setOrderStatus(responseBody.getString("status"));
        }
    }

    /**
     * 数据处理(抽取复用代码)
     *
     * @param responseBody
     * @param planDetailResp
     */
    private void dataHandle(JSONObject responseBody, PlanDetailResp planDetailResp) {
        if (responseBody.containsKey("id")) {
            planDetailResp.setPlanId(responseBody.getString("id"));
        }
        if (responseBody.containsKey("status")) {
            planDetailResp.setStatus(responseBody.getString("status"));
        }
    }

    /**
     * 检查该订单是否成功(其实就批准)
     *
     * @param orderDetail
     * @return
     */
    public boolean isSuccess(@NonNull OrderDetailResp orderDetail) {
        if (PayPalOrderStatusEnum.APPROVED.toString().equals(orderDetail.getOrderStatus())) {
            return true;
        }
        return false;
    }

    /**
     * 校验Paypal 的IPN通知消息是否合法
     *
     * @param verifyUrl  校验地址: 生产:https://ipnpb.paypal.com/cgi-bin/webscr  沙盒:https://ipnpb.sandbox.paypal.com/cgi-bin/webscr
     * @param requestMap Map集合
     * @return 参考 IPNVerifyStateEnum
     * @throws PaymentCoreException
     */
    public String verifyIPNMessage(String verifyUrl, Map<String, Object> requestMap) throws PaymentCoreException {
        return HttpClientUtil.postForm(verifyUrl, requestMap);
    }

以上就是支付功能相关方法(部分)。其中的请求参数完全可以参考官网创建Bean,当然你也可以用JSONObject。

4、下单
单次(One-Time): 调用上述API即可,创建完会返回一个Paypal订单Id,关联系统订单Id即可,注意创建订单要设置returnUrlcancelUrl,完成支付将会依据地址重定向。

订阅(Subscription) 订阅下单有些特殊,直接说步骤:创建商品–>创建计划–>创建订阅,根据上述API调用即可,系统订单将与订阅Id关联,系统订单将与订阅Id关联,系统订单将与订阅Id关联(重要说三遍),注意创建订单要设置returnUrlcancelUrl,完成支付将会依据地址重定向。

5、重定向回调

    /**
     * Paypal支付成功回调  https://domain/page/paypal/success?token=xxx或者https://domain/page/paypal/success?subscription_id=xxx
     *
     * @param model      视图对象
     * @param paramMap   请求参数
     * @return
     */
    @RequestMapping(value = "/page/paypal/success", method = RequestMethod.GET)
    public String paymentSuccessCallback(Model model,
                                         @RequestParam() Map<String, String> paramMap,
                                         ) {
        model.addAttribute("basePath", basePath);
        String token = paramMap.get("token");
        String subscriptionId = paramMap.get("subscription_id");
        if (!StringUtils.isEmpty(subscriptionId)) {
            token = subscriptionId;
        }
        try {
            return payPalPaymentService.paymentSuccessCallback(token, model);
        } finally {
        }
    }

    /**
     * Paypal取消支付回调回调 https://domain/page/paypal/cancel?token=xxx或者https://domain/page/paypal/cancel?subscription_id=xxx
     *
     * @param model      视图对象
     * @param paramMap   请求参数
     * @return
     */
    @RequestMapping(value = "/page/paypal/cancel", method = RequestMethod.GET)
    public String paymentCancelCallback(Model model,
                                        @RequestParam() Map<String, String> paramMap,
                                        ) {
        model.addAttribute("basePath", basePath);
        String token = paramMap.get("token");
        String subscriptionId = paramMap.get("subscription_id");
        if (!StringUtils.isEmpty(subscriptionId)) {
            token = subscriptionId;
        }
        try {
            payPalPaymentService.paymentCancelCallback(token);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
        return ConstantUtil.PAYPAL_CANCEL_PAGE;
    }

以上token值就是单次购买paypal的订单,可以用于获取订单详情,subscription_id就是订阅Id,获取订阅详情,来判断订单是否支付成功。

校验方式:

//订阅
SubscriptionDetailResp subscriptionDetail = getSubscriptionDetail(subscriptionId);
if (PayPalSubscriptionStatusEnum.ACTIVE.toString().equals(subscriptionDetail.getStatus())) {
	//购买成功
}

//单次
OrderDetailResp payPalOrder = getOrderDetail(token);
if (PayPalOrderStatusEnum.APPROVED.toString().equals(payPalOrder .getOrderStatus())) {
	//购买成功
}

6、Paypal回调(IPN回调)
这个是paypal的即时消息通讯,其实就是第三方回调,上述的是网页的重定向,使用该功能监听用户续订、用户退订等通知。

/**
     * Paypal支付回调处理
     * @param request
     * @param response
     */
    @RequestMapping(value = "/notify/payment/paypal")
    public void payPalNotify(HttpServletRequest request, HttpServletResponse response) String tenantCode) {
        TreeMap<String, Object> bodyMap = new TreeMap<>();
        bodyMap.put("cmd", "_notify-validate");
        //遍历请求参数,放入Map
        Enumeration<?> enu = request.getParameterNames();
        while (enu.hasMoreElements()) {
            String paraName = (String) enu.nextElement();
            String paraValue = request.getParameter(paraName);
            bodyMap.put(paraName, paraValue);
        }
        DataBaseSelector.remove();
        //异步处理消息,官网推荐使用异步,收到通知先响应,但是肯定要先保存回调记录,再异步处理,防止消息丢失
        payPalPaymentService.payPalNotifyHandle(bodyMap);
        response.setStatus(HttpServletResponse.SC_OK);
        return;
    }

在这我们接收这个IPN回调,只处理续订和退订、订阅过期业务:

payPalNotifyHandle(Map<String, Object> treeMap) {
	//校验回调消息是否是Paypal的通知
	String result= verifyIPNMessage(VerifyUrl, treeMap);
	 //消息有效VERIFIED, 消息无效INVALID
 	if (IPNVerifyStateEnum.INVALID.toString().equals(result)) {
	       log.error(">>>>>> Paypal IPN verify fail: {}", result);
           return;
    }
	//发送IPN消息的事务类型。
    String txnType = (String) treeMap.get("txn_type");
    //事务Id,和订单关联,可能读者会奇怪怎么又和订单关联,注意这里是订阅关系关联,你需要重新建立一个表来管理这个关系
    //因为当为订阅时,多个订单才是一个订阅,那么每个订单要和txnId关联,这是为了防止之后有没有漏单,有漏单可以补单
    String txnId = (String) treeMap.get("txn_id");
	//订阅Id,代表订阅的一个事务
    String recurringPaymentId = (String) treeMap.get("recurring_payment_id");
    //订单状态 Completed
    String paymentStatus = (String) treeMap.get("payment_status");
	(IPNPaymentStatusEnum.Completed.toString().equals(paymentStatus) {
		//成功
	}	
	//回调类型  IPNTxnTypeEnum 
	//订阅创建
    if (IPNTxnTypeEnum.recurring_payment_profile_created.toString().equals(txnType)) {
         log.info("IPN message create subscription >>>>>>");
    }
    //取消订阅
    if (IPNTxnTypeEnum.recurring_payment_profile_cancel.toString().equals(txnType)) {
    }
    //订阅过期
    if (IPNTxnTypeEnum.recurring_payment_expired.toString().equals(txnType)) {    
    }
    //订阅付款(包括第一次扣款和续订扣款)
    if (IPNTxnTypeEnum.recurring_payment.toString().equals(txnType)) {
    }
}

public enum IPNTxnTypeEnum {

    /**
     * 单次购买,为单个项目收到的款项;来源是Express Checkout。
     */
    express_checkout,

    /**
     * 收到经常性付款
     */
    recurring_payment,

    /**
     * 经常性付款已过期
     */
    recurring_payment_expired,

    /**
     * 经常性付款失败。在下列情况下发送此事务类型:
     * 1、尝试收取定期付款失败
     * 2、客户经常性付款配置文件中的“最大失败付款”设置为0。在这种情况下,PayPal尝试无限次地收集定期付款,而不会暂停客户的定期付款配置文件
     */
    recurring_payment_failed,

    /**
     * 定期付款配置文件已取消,取消订阅
     */
    recurring_payment_profile_cancel,

    /**
     * 创建定期付款配置文件,创建订阅
     */
    recurring_payment_profile_created,

    /**
     * 定期付款失败,相关的定期付款配置文件已暂停。在下列情况下发送此事务类型:
     * 贝宝试图收取定期付款失败。
     * 客户经常性付款配置文件中的“最大失败付款”设置为1或更大。
     * 尝试收取付款的次数已超过为“最大失败付款”指定的值。在这种情况下,PayPal会暂停客户的定期支付配置文件。
     * 暂停订阅
     */
    recurring_payment_suspended_due_to_max_failed_payment
}

至此所有流程均已完成,由于我封装时把所有的请求参数转成了JavaBean,大家可能看不太理解,大家可以参考官网地址:REST-API,因为个人是符合我的功能去掉了一些相关参数,可能不适用于大家,按照文档上构建参数即可,对了再偷偷告诉你一个绝招:下载ApiFox,点击上面的开源项目,有Paypal api集成的,就会把请求导入到ApiFox了,可以去看看,给你们看下:
在这里插入图片描述
在这里插入图片描述
所有的代码均已讲解,各位读者对这个支付功能还有什么疑惑呢,在评论区探讨的,码字不易~讲解不易!!!点个赞再走。记住,我还是那个会撩头发的程序猿!!!

转载请标明出处谢谢大家~

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值