Hello,各位读者们,这一期讲解Java集成Paypal支付功能,但是要注意的是,我这边只是集成了Paypal单次购买(One-Time) 和 订阅购买(Subscription),其实这两种类型的在线支付已经足够了。好了废话不多说,直接开整。
Paypal支付所需配置
这边就不细说了,配置这个东西还是很简单的,按照官网来就行了。对于咱们研发,要多看、研究所集成对象官网,这个才是靠谱的,不过没事,为了下一期发布Paypal支付配置,来给读者讲解。
Paypal支付注意事项
可能很多读者参考网上资料,Java集成的时候都是什么导入SDK依赖啊什么的,注意,Paypal官网已经不会对服务端SDK维护了,也就是说你们可以继续用,但是新功能你用不了,官网推荐:REST-API接入支付功能,并且有一些API也是废弃了,所以呢这些都是需要去详细了解。
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;
}
/**
* 构造请求头
*
* @param requestId 请求唯一Id,可以不用,自己实现调用日志归档
* @param accessToken OAuth accessToken
* @return
*/
public Map<String, Object> createRequestHeaders(String requestId, String accessToken) {
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("Authorization", "Bearer " + accessToken);
headerMap.put(ConstantUtil.CONTENT_TYPE, ConstantUtil.APPLICATION_JSON_REQUEST);
if (!StringUtils.isEmpty(requestId)) {
headerMap.put("PayPal-Request-Id", requestId);
}
return headerMap;
}
/**
* 获取订单详情,这里只取了关键信息,用于判断支付是否支付成功,判断是否为 【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即可,注意创建订单要设置returnUrl和cancelUrl,完成支付将会依据地址重定向。
订阅(Subscription) 订阅下单有些特殊,直接说步骤:创建商品–>创建计划–>创建订阅,根据上述API调用即可,系统订单将与订阅Id关联,系统订单将与订阅Id关联,系统订单将与订阅Id关联(重要说三遍),注意创建订单要设置returnUrl和cancelUrl,完成支付将会依据地址重定向。
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了,可以去看看,给你们看下:
所有的代码均已讲解,各位读者对这个支付功能还有什么疑惑呢,在评论区探讨的,码字不易~讲解不易!!!点个赞再走。记住,我还是那个会撩头发的程序猿!!!
转载请标明出处谢谢大家~