diff --git a/docs/sql/init.sql b/docs/sql/init.sql
index 283467c..6a13936 100644
--- a/docs/sql/init.sql
+++ b/docs/sql/init.sql
@@ -718,6 +718,8 @@ INSERT INTO t_pay_way (way_code, way_name) VALUES ('WX_LITE', '微信小程序')
INSERT INTO t_pay_way (way_code, way_name) VALUES ('YSF_BAR', '云闪付条码');
INSERT INTO t_pay_way (way_code, way_name) VALUES ('YSF_JSAPI', '云闪付jsapi');
+INSERT INTO t_pay_way (way_code, way_name) VALUES ('PP_PC', 'Paypal PC 支付');
+
-- 初始化支付接口定义
INSERT INTO t_pay_interface_define (if_code, if_name, is_mch_mode, is_isv_mode, config_page_type, isv_params, isvsub_mch_params, normal_mch_params, way_codes, icon, bg_color, state, remark)
VALUES ('alipay', '支付宝官方', 1, 1, 2,
@@ -742,3 +744,11 @@ VALUES ('ysfpay', '云闪付官方', 0, 1, 1,
NULL,
'[{"wayCode": "YSF_BAR"}, {"wayCode": "ALI_JSAPI"}, {"wayCode": "WX_JSAPI"}, {"wayCode": "ALI_BAR"}, {"wayCode": "WX_BAR"}]',
'http://jeequan.oss-cn-beijing.aliyuncs.com/jeepay/img/ysfpay.png', 'red', 1, '云闪付官方通道');
+
+INSERT INTO t_pay_interface_define (if_code, if_name, is_mch_mode, is_isv_mode, config_page_type, isv_params, isvsub_mch_params, normal_mch_params, way_codes, icon, bg_color, state, remark)
+VALUES ('pppay', 'Paypal 支付', 1, 0, 1,
+ NULL,
+ NULL,
+ '[{"name":"sandbox","desc":"环境配置","type":"radio","verify":"required","values":"1,0","titles":"沙箱环境, 生产环境"},{"name":"clientId","desc":"Client ID","type":"text","verify":"required"},{"name":"secret","desc":"Secret","type":"text","verify":"required"},{"name":"refundWebhook","desc":"退款 Webhook id","type":"text","verify":"required"},{"name":"notifyWebhook","desc":"通知 Webhook id","type":"text","verify":"required"}]',
+ '[{"wayCode": "PP_PC"}]',
+ 'https://payment-public.oss-cn-shenzhen.aliyuncs.com/ifBG/0b6c2cc3-d31b-4f5c-b076-f13c74d80b85.png', '#005ea6', 1, 'Paypal官方通道');
diff --git a/jeepay-core/src/main/java/com/jeequan/jeepay/core/constants/CS.java b/jeepay-core/src/main/java/com/jeequan/jeepay/core/constants/CS.java
index f423b70..e196c61 100644
--- a/jeepay-core/src/main/java/com/jeequan/jeepay/core/constants/CS.java
+++ b/jeepay-core/src/main/java/com/jeequan/jeepay/core/constants/CS.java
@@ -144,6 +144,7 @@ public class CS {
String WXPAY = "wxpay"; // 微信官方支付
String YSFPAY = "ysfpay"; // 云闪付开放平台
String XXPAY = "xxpay"; // 小新支付
+ String PPPAY = "pppay"; // Paypal 支付
}
@@ -169,6 +170,8 @@ public class CS {
String WX_BAR = "WX_BAR"; //微信条码支付
String WX_H5 = "WX_H5"; //微信H5支付
String WX_NATIVE = "WX_NATIVE"; //微信扫码支付
+
+ String PP_PC = "PP_PC"; // Paypal 支付
}
//支付数据包 类型
diff --git a/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/NormalMchParams.java b/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/NormalMchParams.java
index a2f03c6..0c0d9c3 100644
--- a/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/NormalMchParams.java
+++ b/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/NormalMchParams.java
@@ -18,6 +18,7 @@ package com.jeequan.jeepay.core.model.params;
import com.alibaba.fastjson.JSONObject;
import com.jeequan.jeepay.core.constants.CS;
import com.jeequan.jeepay.core.model.params.alipay.AlipayNormalMchParams;
+import com.jeequan.jeepay.core.model.params.pppay.PpPayNormalMchParams;
import com.jeequan.jeepay.core.model.params.wxpay.WxpayNormalMchParams;
import com.jeequan.jeepay.core.model.params.xxpay.XxpayNormalMchParams;
@@ -38,6 +39,8 @@ public abstract class NormalMchParams {
return JSONObject.parseObject(paramsStr, AlipayNormalMchParams.class);
}else if(CS.IF_CODE.XXPAY.equals(ifCode)){
return JSONObject.parseObject(paramsStr, XxpayNormalMchParams.class);
+ }else if (CS.IF_CODE.PPPAY.equals(ifCode)){
+ return JSONObject.parseObject(paramsStr, PpPayNormalMchParams.class);
}
return null;
}
diff --git a/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/pppay/PpPayNormalMchParams.java b/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/pppay/PpPayNormalMchParams.java
new file mode 100644
index 0000000..e8b9e50
--- /dev/null
+++ b/jeepay-core/src/main/java/com/jeequan/jeepay/core/model/params/pppay/PpPayNormalMchParams.java
@@ -0,0 +1,54 @@
+package com.jeequan.jeepay.core.model.params.pppay;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.jeequan.jeepay.core.model.params.NormalMchParams;
+import com.jeequan.jeepay.core.utils.StringKit;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.core.model.params.pppay
+ * @create 2021/11/15 18:10
+ */
+@Data
+public class PpPayNormalMchParams extends NormalMchParams {
+ /**
+ * 是否沙箱环境
+ */
+ private Byte sandbox;
+
+ /**
+ * clientId
+ * 客户端 ID
+ */
+ private String clientId;
+
+ /**
+ * secret
+ * 密钥
+ */
+ private String secret;
+
+ /**
+ * 支付 Webhook 通知 ID
+ */
+ private String notifyWebhook;
+
+ /**
+ * 退款 Webhook 通知 ID
+ */
+ private String refundWebhook;
+
+ @Override
+ public String deSenData() {
+ PpPayNormalMchParams mchParams = this;
+ if (StringUtils.isNotBlank(this.secret)) {
+ mchParams.setSecret(StringKit.str2Star(this.secret, 6, 6, 6));
+ }
+ return ((JSONObject) JSON.toJSON(mchParams)).toJSONString();
+ }
+}
diff --git a/jeepay-payment/pom.xml b/jeepay-payment/pom.xml
index 3af1065..a3ece86 100644
--- a/jeepay-payment/pom.xml
+++ b/jeepay-payment/pom.xml
@@ -105,6 +105,13 @@
alipay-sdk-java
+
+
+ com.paypal.sdk
+ checkout-sdk
+ 1.0.5
+
+
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/AbstractPaymentService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/AbstractPaymentService.java
index c34d0df..754b12b 100644
--- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/AbstractPaymentService.java
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/AbstractPaymentService.java
@@ -58,4 +58,8 @@ public abstract class AbstractPaymentService implements IPaymentService{
return sysConfigService.getDBApplicationConfig().getPaySiteUrl() + "/api/pay/return/" + getIfCode();
}
+ protected String getReturnUrl(String payOrderId){
+ return sysConfigService.getDBApplicationConfig().getPaySiteUrl() + "/api/pay/return/" + getIfCode() + "/" + payOrderId;
+ }
+
}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelNoticeService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelNoticeService.java
new file mode 100644
index 0000000..1dc81dd
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelNoticeService.java
@@ -0,0 +1,77 @@
+package com.jeequan.jeepay.pay.channel.pppay;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.core.exception.ResponseException;
+import com.jeequan.jeepay.pay.channel.AbstractChannelNoticeService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.MutablePair;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay
+ * @create 2021/11/15 20:58
+ */
+@Service
+@Slf4j
+public class PppayChannelNoticeService extends AbstractChannelNoticeService {
+ @Override
+ public String getIfCode() {
+ return CS.IF_CODE.PPPAY;
+ }
+
+ @Override
+ public MutablePair parseParams(HttpServletRequest request, String urlOrderId, NoticeTypeEnum noticeTypeEnum) {
+ if (noticeTypeEnum == NoticeTypeEnum.DO_NOTIFY) {
+ JSONObject params = JSONUtil.parseObj(getReqParamJSON().toJSONString());
+ String orderId = params.getByPath("resource.purchase_units[0].invoice_id", String.class);
+ return MutablePair.of(orderId, params);
+ } else {
+ if (urlOrderId == null || urlOrderId.isEmpty()) {
+ throw ResponseException.buildText("ERROR");
+ }
+ try {
+ JSONObject params = JSONUtil.parseObj(getReqParamJSON().toString());
+ return MutablePair.of(urlOrderId, params);
+ } catch (Exception e) {
+ log.error("error", e);
+ throw ResponseException.buildText("ERROR");
+ }
+ }
+ }
+
+ @Override
+ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrder payOrder, MchAppConfigContext mchAppConfigContext, NoticeTypeEnum noticeTypeEnum) {
+ try {
+ if (noticeTypeEnum == NoticeTypeEnum.DO_RETURN) {
+ return doReturn(request, params, payOrder, mchAppConfigContext);
+ }
+ return doNotify(request, params, payOrder, mchAppConfigContext);
+ } catch (Exception e) {
+ log.error("error", e);
+ throw ResponseException.buildText("ERROR");
+ }
+ }
+
+ public ChannelRetMsg doReturn(HttpServletRequest request, Object params, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws IOException {
+ JSONObject object = (JSONObject) params;
+ String ppOrderId = object.getStr("token");
+ return mchAppConfigContext.getPaypalWrapper().processOrder(ppOrderId, payOrder);
+ }
+
+ public ChannelRetMsg doNotify(HttpServletRequest request, Object params, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws IOException {
+ JSONObject object = (JSONObject) params;
+ String ppOrderId = object.getByPath("resource.id", String.class);
+ return mchAppConfigContext.getPaypalWrapper().processOrder(ppOrderId, payOrder, true);
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelRefundNoticeService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelRefundNoticeService.java
new file mode 100644
index 0000000..8bc8e63
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayChannelRefundNoticeService.java
@@ -0,0 +1,78 @@
+package com.jeequan.jeepay.pay.channel.pppay;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.core.entity.RefundOrder;
+import com.jeequan.jeepay.core.exception.ResponseException;
+import com.jeequan.jeepay.pay.channel.AbstractChannelRefundNoticeService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.model.PaypalWrapper;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import com.paypal.core.PayPalHttpClient;
+import com.paypal.http.HttpResponse;
+import com.paypal.http.serializer.Json;
+import com.paypal.payments.Refund;
+import com.paypal.payments.RefundsGetRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.MutablePair;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay
+ * @create 2021/11/16 20:39
+ */
+@Service
+@Slf4j
+public class PppayChannelRefundNoticeService extends AbstractChannelRefundNoticeService {
+ @Override
+ public String getIfCode() {
+ return CS.IF_CODE.PPPAY;
+ }
+
+ @Override
+ public MutablePair parseParams(HttpServletRequest request, String urlOrderId, NoticeTypeEnum noticeTypeEnum) {
+ JSONObject params = JSONUtil.parseObj(getReqParamJSON().toJSONString());
+ String orderId = params.getByPath("resource.invoice_id", String.class);
+ return MutablePair.of(orderId, params);
+ }
+
+ @Override
+ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, RefundOrder refundOrder, MchAppConfigContext mchAppConfigContext, NoticeTypeEnum noticeTypeEnum) {
+ try {
+ JSONObject object = (JSONObject) params;
+ String orderId = object.getByPath("resource.id", String.class);
+
+ PaypalWrapper wrapper = mchAppConfigContext.getPaypalWrapper();
+ PayPalHttpClient client = wrapper.getClient();
+
+ RefundsGetRequest refundRequest = new RefundsGetRequest(orderId);
+ HttpResponse response = client.execute(refundRequest);
+
+ ChannelRetMsg channelRetMsg = ChannelRetMsg.waiting();
+ channelRetMsg.setResponseEntity(wrapper.textResp("ERROR"));
+
+ if (response.statusCode() == 200) {
+ String responseJson = new Json().serialize(response.result());
+ channelRetMsg = wrapper.dispatchCode(response.result().status(), channelRetMsg);
+ channelRetMsg.setChannelAttach(responseJson);
+ channelRetMsg.setChannelOrderId(response.result().id());
+ channelRetMsg.setResponseEntity(wrapper.textResp("SUCCESS"));
+ } else {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("异步退款失败,Paypal 响应非 200");
+ }
+
+ return channelRetMsg;
+ } catch (Exception e) {
+ log.error("error", e);
+ throw ResponseException.buildText("ERROR");
+ }
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPayOrderQueryService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPayOrderQueryService.java
new file mode 100644
index 0000000..0a5cfa6
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPayOrderQueryService.java
@@ -0,0 +1,28 @@
+package com.jeequan.jeepay.pay.channel.pppay;
+
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.pay.channel.IPayOrderQueryService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import org.springframework.stereotype.Service;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay
+ * @create 2021/11/15 21:02
+ */
+@Service
+public class PppayPayOrderQueryService implements IPayOrderQueryService {
+ @Override
+ public String getIfCode() {
+ return CS.IF_CODE.PPPAY;
+ }
+
+ @Override
+ public ChannelRetMsg query(PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws Exception {
+ return mchAppConfigContext.getPaypalWrapper().processOrder(null, payOrder);
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPaymentService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPaymentService.java
new file mode 100644
index 0000000..879f7ab
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayPaymentService.java
@@ -0,0 +1,40 @@
+package com.jeequan.jeepay.pay.channel.pppay;
+
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.pay.channel.AbstractPaymentService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.rqrs.AbstractRS;
+import com.jeequan.jeepay.pay.rqrs.payorder.UnifiedOrderRQ;
+import com.jeequan.jeepay.pay.util.PaywayUtil;
+import org.springframework.stereotype.Service;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay
+ * @create 2021/11/15 18:17
+ */
+@Service
+public class PppayPaymentService extends AbstractPaymentService {
+ @Override
+ public String getIfCode() {
+ return CS.IF_CODE.PPPAY;
+ }
+
+ @Override
+ public boolean isSupport(String wayCode) {
+ return true;
+ }
+
+ @Override
+ public String preCheck(UnifiedOrderRQ bizRQ, PayOrder payOrder) {
+ return PaywayUtil.getRealPaywayService(this, payOrder.getWayCode()).preCheck(bizRQ, payOrder);
+ }
+
+ @Override
+ public AbstractRS pay(UnifiedOrderRQ bizRQ, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws Exception {
+ return PaywayUtil.getRealPaywayService(this, payOrder.getWayCode()).pay(bizRQ, payOrder, mchAppConfigContext);
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayRefundService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayRefundService.java
new file mode 100644
index 0000000..3722a4c
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/PppayRefundService.java
@@ -0,0 +1,118 @@
+package com.jeequan.jeepay.pay.channel.pppay;
+
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.core.entity.RefundOrder;
+import com.jeequan.jeepay.pay.channel.AbstractRefundService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.model.PaypalWrapper;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import com.jeequan.jeepay.pay.rqrs.refund.RefundOrderRQ;
+import com.paypal.core.PayPalHttpClient;
+import com.paypal.http.HttpResponse;
+import com.paypal.http.serializer.Json;
+import com.paypal.payments.*;
+import org.springframework.stereotype.Service;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay
+ * @create 2021/11/16 20:20
+ */
+@Service
+public class PppayRefundService extends AbstractRefundService {
+ @Override
+ public String getIfCode() {
+ return CS.IF_CODE.PPPAY;
+ }
+
+ @Override
+ public String preCheck(RefundOrderRQ bizRQ, RefundOrder refundOrder, PayOrder payOrder) {
+ return null;
+ }
+
+ @Override
+ public ChannelRetMsg refund(RefundOrderRQ bizRQ, RefundOrder refundOrder, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws Exception {
+ if (payOrder.getChannelOrderNo() == null) {
+ return ChannelRetMsg.confirmFail();
+ }
+
+ PaypalWrapper paypalWrapper = mchAppConfigContext.getPaypalWrapper();
+
+ String ppOrderId = paypalWrapper.processOrder(payOrder.getChannelOrderNo()).get(0);
+ String ppCatptId = paypalWrapper.processOrder(payOrder.getChannelOrderNo()).get(1);
+
+ if (ppOrderId == null || ppCatptId == null) {
+ return ChannelRetMsg.confirmFail();
+ }
+
+ PayPalHttpClient client = paypalWrapper.getClient();
+
+ long amount = (bizRQ.getRefundAmount() / 100);
+ String amountStr = Long.toString(amount, 10);
+ String currency = bizRQ.getCurrency().toUpperCase();
+
+ RefundRequest refundRequest = new RefundRequest();
+ Money money = new Money();
+ money.currencyCode(currency);
+ money.value(amountStr);
+
+ refundRequest.invoiceId(refundOrder.getRefundOrderId());
+ refundRequest.amount(money);
+ refundRequest.noteToPayer(bizRQ.getRefundReason());
+
+ CapturesRefundRequest request = new CapturesRefundRequest(ppCatptId);
+ request.prefer("return=representation");
+ request.requestBody(refundRequest);
+ HttpResponse response = client.execute(request);
+
+ ChannelRetMsg channelRetMsg = ChannelRetMsg.waiting();
+ channelRetMsg.setResponseEntity(paypalWrapper.textResp("ERROR"));
+
+ if (response.statusCode() == 201) {
+ String responseJson = new Json().serialize(response.result());
+ channelRetMsg = paypalWrapper.dispatchCode(response.result().status(), channelRetMsg);
+ channelRetMsg.setChannelAttach(responseJson);
+ channelRetMsg.setChannelOrderId(response.result().id());
+ channelRetMsg.setResponseEntity(paypalWrapper.textResp("SUCCESS"));
+ } else {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("请求退款失败,Paypal 响应非 201");
+ }
+
+ return channelRetMsg;
+ }
+
+ @Override
+ public ChannelRetMsg query(RefundOrder refundOrder, MchAppConfigContext mchAppConfigContext) throws Exception {
+ if (refundOrder.getChannelOrderNo() == null) {
+ return ChannelRetMsg.confirmFail();
+ }
+
+ PaypalWrapper wrapper = mchAppConfigContext.getPaypalWrapper();
+ PayPalHttpClient client = wrapper.getClient();
+
+ RefundsGetRequest refundRequest = new RefundsGetRequest(refundOrder.getPayOrderId());
+ HttpResponse response = client.execute(refundRequest);
+
+ ChannelRetMsg channelRetMsg = ChannelRetMsg.waiting();
+ channelRetMsg.setResponseEntity(wrapper.textResp("ERROR"));
+
+ if (response.statusCode() == 201) {
+ String responseJson = new Json().serialize(response.result());
+ channelRetMsg = wrapper.dispatchCode(response.result().status(), channelRetMsg);
+ channelRetMsg.setChannelAttach(responseJson);
+ channelRetMsg.setChannelOrderId(response.result().id());
+ channelRetMsg.setResponseEntity(wrapper.textResp("SUCCESS"));
+ } else {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("请求退款详情失败,Paypal 响应非 200");
+ }
+
+ return channelRetMsg;
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/payway/PpPc.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/payway/PpPc.java
new file mode 100644
index 0000000..cea45c2
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/pppay/payway/PpPc.java
@@ -0,0 +1,141 @@
+package com.jeequan.jeepay.pay.channel.pppay.payway;
+
+import cn.hutool.json.JSONUtil;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.core.exception.BizException;
+import com.jeequan.jeepay.pay.channel.pppay.PppayPaymentService;
+import com.jeequan.jeepay.pay.model.MchAppConfigContext;
+import com.jeequan.jeepay.pay.model.PaypalWrapper;
+import com.jeequan.jeepay.pay.rqrs.AbstractRS;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import com.jeequan.jeepay.pay.rqrs.payorder.UnifiedOrderRQ;
+import com.jeequan.jeepay.pay.rqrs.payorder.payway.PPPcOrderRQ;
+import com.jeequan.jeepay.pay.rqrs.payorder.payway.PPPcOrderRS;
+import com.paypal.http.HttpResponse;
+import com.paypal.http.serializer.Json;
+import com.paypal.orders.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.channel.pppay.payway
+ * @create 2021/11/15 18:59
+ */
+@Slf4j
+@Service("pppayPaymentByPPPCService")
+public class PpPc extends PppayPaymentService {
+ @Override
+ public String preCheck(UnifiedOrderRQ bizRQ, PayOrder payOrder) {
+ PPPcOrderRQ rq = (PPPcOrderRQ) bizRQ;
+ if (StringUtils.isEmpty(rq.getCancelUrl())) {
+ throw new BizException("用户取消支付回调[cancelUrl]不可为空");
+ }
+ return null;
+ }
+
+ @Override
+ public AbstractRS pay(UnifiedOrderRQ rq, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws Exception {
+ PPPcOrderRQ bizRQ = (PPPcOrderRQ) rq;
+
+ OrderRequest orderRequest = new OrderRequest();
+
+ ApplicationContext applicationContext = new ApplicationContext()
+ .brandName(mchAppConfigContext.getMchApp().getAppName())
+ .landingPage("NO_PREFERENCE")
+ .cancelUrl(bizRQ.getCancelUrl())
+ .returnUrl(getReturnUrl(payOrder.getPayOrderId()))
+ .userAction("PAY_NOW")
+ .shippingPreference("NO_SHIPPING");
+
+ orderRequest.applicationContext(applicationContext);
+ orderRequest.checkoutPaymentIntent("CAPTURE");
+
+ List purchaseUnitRequests = new ArrayList<>();
+
+ long amount = (payOrder.getAmount() / 100);
+ String amountStr = Long.toString(amount, 10);
+ String currency = payOrder.getCurrency().toUpperCase();
+
+ PurchaseUnitRequest purchaseUnitRequest = new PurchaseUnitRequest()
+ .customId(payOrder.getPayOrderId())
+ .invoiceId(payOrder.getPayOrderId())
+ .amountWithBreakdown(new AmountWithBreakdown()
+ .currencyCode(currency)
+ .value(amountStr)
+ .amountBreakdown(
+ new AmountBreakdown().itemTotal(new Money().currencyCode(currency).value(amountStr))
+ )
+ )
+ .items(new ArrayList- () {
+ {
+ add(
+ new Item()
+ .name(payOrder.getSubject())
+ .description(payOrder.getBody())
+ .sku(payOrder.getPayOrderId())
+ .unitAmount(new Money().currencyCode(currency).value(amountStr))
+ .quantity("1")
+ );
+ }
+ });
+
+ purchaseUnitRequests.add(purchaseUnitRequest);
+ orderRequest.purchaseUnits(purchaseUnitRequests);
+
+ PaypalWrapper palApiConfig = mchAppConfigContext.getPaypalWrapper();
+
+ OrdersCreateRequest request = new OrdersCreateRequest();
+ request.header("prefer", "return=representation");
+ request.requestBody(orderRequest);
+ HttpResponse response = palApiConfig.getClient().execute(request);
+
+ PPPcOrderRS res = new PPPcOrderRS();
+ ChannelRetMsg channelRetMsg = new ChannelRetMsg();
+
+ if (response.statusCode() == 201) {
+ Order order = response.result();
+ String status = response.result().status();
+ String tradeNo = response.result().id();
+
+ LinkDescription paypalLink = order.links().stream().reduce(null, (result, curr) -> {
+ if (curr.rel().equalsIgnoreCase("approve") && curr.method().equalsIgnoreCase("get")) {
+ result = curr;
+ }
+ return result;
+ });
+
+ channelRetMsg.setChannelAttach(JSONUtil.toJsonStr(new Json().serialize(order)));
+ channelRetMsg.setChannelOrderId(tradeNo + "," + "null");
+
+ if (status.equalsIgnoreCase("SAVED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING);
+ } else if (status.equalsIgnoreCase("APPROVED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING);
+ } else if (status.equalsIgnoreCase("VOIDED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ } else if (status.equalsIgnoreCase("COMPLETED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS);
+ } else if (status.equalsIgnoreCase("PAYER_ACTION_REQUIRED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING);
+ } else if (status.equalsIgnoreCase("CREATED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING);
+ }
+
+ res.setPayUrl(paypalLink.href());
+ } else {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("请求失败,Paypal 响应非 201");
+ }
+
+ res.setChannelRetMsg(channelRetMsg);
+ return res;
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/MchAppConfigContext.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/MchAppConfigContext.java
index 5159dde..bcda558 100644
--- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/MchAppConfigContext.java
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/MchAppConfigContext.java
@@ -50,6 +50,8 @@ public class MchAppConfigContext {
/** 放置所属服务商的信息 **/
private IsvConfigContext isvConfigContext;
+ /** 缓存 Paypal 对象 **/
+ private PaypalWrapper paypalWrapper;
/** 缓存支付宝client 对象 **/
private AlipayClientWrapper alipayClientWrapper;
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/PaypalWrapper.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/PaypalWrapper.java
new file mode 100644
index 0000000..88ec610
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/model/PaypalWrapper.java
@@ -0,0 +1,175 @@
+package com.jeequan.jeepay.pay.model;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.jeequan.jeepay.core.entity.PayOrder;
+import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
+import com.paypal.core.PayPalEnvironment;
+import com.paypal.core.PayPalHttpClient;
+import com.paypal.http.HttpResponse;
+import com.paypal.http.serializer.Json;
+import com.paypal.orders.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.model
+ * @create 2021/11/15 19:10
+ */
+@Slf4j
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PaypalWrapper {
+ private PayPalEnvironment environment;
+ private PayPalHttpClient client;
+
+ private String notifyWebhook;
+ private String refundWebhook;
+
+
+ public ChannelRetMsg processOrder(String token, PayOrder payOrder) throws IOException {
+ return processOrder(token, payOrder, false);
+ }
+
+
+ public List processOrder(String order) {
+ return processOrder(order, "null");
+ }
+
+ public List processOrder(String order, String afterOrderId) {
+ String ppOrderId = "null";
+ String ppCatptId = "null";
+ if (order != null) {
+ if (order.contains(",")) {
+ String[] split = order.split(",");
+ if (split.length == 2) {
+ ppCatptId = split[1];
+ ppOrderId = split[0];
+ }
+ }
+ }
+ if (afterOrderId != null && !afterOrderId.equalsIgnoreCase("null")) {
+ ppOrderId = afterOrderId;
+ }
+
+ if (ppCatptId.equalsIgnoreCase("null")) {
+ ppCatptId = null;
+ }
+ if (ppOrderId.equalsIgnoreCase("null")) {
+ ppOrderId = null;
+ }
+
+ return Arrays.asList(ppOrderId, ppCatptId);
+ }
+
+ public ChannelRetMsg processOrder(String token, PayOrder payOrder, boolean isCapture) throws IOException {
+ String ppOrderId = this.processOrder(payOrder.getChannelOrderNo(), token).get(0);
+ String ppCatptId = this.processOrder(payOrder.getChannelOrderNo()).get(1);
+
+ ChannelRetMsg channelRetMsg = ChannelRetMsg.waiting();
+ channelRetMsg.setResponseEntity(textResp("ERROR"));
+
+ // 如果订单 ID 还不存在,等待
+ if (ppOrderId == null) {
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("捕获订单请求失败");
+ return channelRetMsg;
+ } else {
+ Order order;
+
+ channelRetMsg.setChannelOrderId(ppOrderId + "," + "null");
+
+ // 如果 捕获 ID 不存在
+ if (ppCatptId == null && isCapture) {
+ OrderRequest orderRequest = new OrderRequest();
+ OrdersCaptureRequest ordersCaptureRequest = new OrdersCaptureRequest(ppOrderId);
+ ordersCaptureRequest.requestBody(orderRequest);
+
+ HttpResponse response = this.getClient().execute(ordersCaptureRequest);
+
+ if (response.statusCode() != 201) {
+ channelRetMsg.setChannelErrCode("201");
+ channelRetMsg.setChannelErrMsg("捕获订单请求失败");
+ return channelRetMsg;
+ }
+ order = response.result();
+ } else {
+ OrdersGetRequest request = new OrdersGetRequest(ppOrderId);
+ HttpResponse response = this.getClient().execute(request);
+
+ if (response.statusCode() != 200) {
+ channelRetMsg.setChannelOrderId(ppOrderId);
+ channelRetMsg.setChannelErrCode("200");
+ channelRetMsg.setChannelErrMsg("请求订单详情失败");
+ return channelRetMsg;
+ }
+
+ order = response.result();
+ }
+
+ String status = order.status();
+ String orderJsonStr = new Json().serialize(order);
+ JSONObject orderJson = JSONUtil.parseObj(orderJsonStr);
+
+ for (PurchaseUnit purchaseUnit : order.purchaseUnits()) {
+ if (purchaseUnit.payments() != null) {
+ for (Capture capture : purchaseUnit.payments().captures()) {
+ ppCatptId = capture.id();
+ break;
+ }
+ }
+ }
+
+ String orderUserId = orderJson.getByPath("payer.payer_id", String.class);
+
+ ChannelRetMsg result = new ChannelRetMsg();
+ result.setNeedQuery(true);
+ result.setChannelOrderId(ppOrderId + "," + ppCatptId); // 渠道订单号
+ result.setChannelUserId(orderUserId); // 支付用户ID
+ result.setChannelAttach(orderJsonStr); // Capture 响应数据
+ result.setResponseEntity(textResp("SUCCESS")); // 响应数据
+ result.setChannelState(ChannelRetMsg.ChannelState.WAITING); // 默认支付中
+
+ if (status.equalsIgnoreCase("COMPLETED")) {
+ result.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS);
+ } else if (status.equalsIgnoreCase("VOIDED")) {
+ result.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ }
+
+ return result;
+ }
+ }
+
+ public ChannelRetMsg dispatchCode(String status, ChannelRetMsg channelRetMsg) {
+ if (status.equalsIgnoreCase("CANCELLED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_FAIL);
+ } else if (status.equalsIgnoreCase("PENDING")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING);
+ } else if (status.equalsIgnoreCase("COMPLETED")) {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS);
+ } else {
+ channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.UNKNOWN);
+ }
+ return channelRetMsg;
+ }
+
+ public ResponseEntity textResp(String text) {
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.setContentType(MediaType.TEXT_HTML);
+ return new ResponseEntity(text, httpHeaders, HttpStatus.OK);
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/UnifiedOrderRQ.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/UnifiedOrderRQ.java
index 03925f1..d3d940e 100644
--- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/UnifiedOrderRQ.java
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/UnifiedOrderRQ.java
@@ -146,6 +146,10 @@ public class UnifiedOrderRQ extends AbstractMchAppRQ {
AliQrOrderRQ bizRQ = JSONObject.parseObject(StringUtils.defaultIfEmpty(this.channelExtra, "{}"), AliQrOrderRQ.class);
BeanUtils.copyProperties(this, bizRQ);
return bizRQ;
+ }else if (CS.PAY_WAY_CODE.PP_PC.equals(wayCode)){
+ PPPcOrderRQ bizRQ = JSONObject.parseObject(StringUtils.defaultIfEmpty(this.channelExtra, "{}"), PPPcOrderRQ.class);
+ BeanUtils.copyProperties(this, bizRQ);
+ return bizRQ;
}
return this;
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRQ.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRQ.java
new file mode 100644
index 0000000..4a52daa
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRQ.java
@@ -0,0 +1,28 @@
+package com.jeequan.jeepay.pay.rqrs.payorder.payway;
+
+import com.jeequan.jeepay.core.constants.CS;
+import com.jeequan.jeepay.pay.rqrs.payorder.CommonPayDataRQ;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.rqrs.payorder.payway
+ * @create 2021/11/15 17:52
+ */
+@Data
+public class PPPcOrderRQ extends CommonPayDataRQ {
+
+ /**
+ * 商品描述信息
+ **/
+ @NotBlank(message = "取消支付返回站点")
+ private String cancelUrl;
+
+ public PPPcOrderRQ() {
+ this.setWayCode(CS.PAY_WAY_CODE.PP_PC);
+ }
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRS.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRS.java
new file mode 100644
index 0000000..0c6bb91
--- /dev/null
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/payway/PPPcOrderRS.java
@@ -0,0 +1,16 @@
+package com.jeequan.jeepay.pay.rqrs.payorder.payway;
+
+import com.jeequan.jeepay.pay.rqrs.payorder.CommonPayDataRS;
+import lombok.Data;
+
+/**
+ * none.
+ *
+ * @author 陈泉
+ * @package com.jeequan.jeepay.pay.rqrs.payorder.payway
+ * @create 2021/11/15 19:56
+ */
+@Data
+public class PPPcOrderRS extends CommonPayDataRS {
+
+}
diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ConfigContextService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ConfigContextService.java
index cc8c194..29f5dc6 100644
--- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ConfigContextService.java
+++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ConfigContextService.java
@@ -25,10 +25,13 @@ import com.jeequan.jeepay.core.model.params.IsvsubMchParams;
import com.jeequan.jeepay.core.model.params.NormalMchParams;
import com.jeequan.jeepay.core.model.params.alipay.AlipayIsvParams;
import com.jeequan.jeepay.core.model.params.alipay.AlipayNormalMchParams;
+import com.jeequan.jeepay.core.model.params.pppay.PpPayNormalMchParams;
import com.jeequan.jeepay.core.model.params.wxpay.WxpayIsvParams;
import com.jeequan.jeepay.core.model.params.wxpay.WxpayNormalMchParams;
import com.jeequan.jeepay.pay.model.*;
import com.jeequan.jeepay.service.impl.*;
+import com.paypal.core.PayPalEnvironment;
+import com.paypal.core.PayPalHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -223,6 +226,12 @@ public class ConfigContextService {
mchAppConfigContext.setWxServiceWrapper(WxServiceWrapper.buildWxServiceWrapper(wxpayParams));
}
+ //放置 paypal client
+ PpPayNormalMchParams ppPayMchParams = mchAppConfigContext.getNormalMchParamsByIfCode(CS.IF_CODE.PPPAY, PpPayNormalMchParams.class);
+ if (ppPayMchParams != null) {
+ mchAppConfigContext.setPaypalWrapper(buildPaypalWrapper(ppPayMchParams.getSandbox(), ppPayMchParams.getSecret(), ppPayMchParams.getClientId(), ppPayMchParams.getNotifyWebhook(), ppPayMchParams.getRefundWebhook()));
+ }
+
}else{ //服务商模式商户
for (PayInterfaceConfig payInterfaceConfig : allConfigList) {
@@ -317,6 +326,22 @@ public class ConfigContextService {
}
}
+ private PaypalWrapper buildPaypalWrapper(Byte sandbox, String secret, String clientID, String notifyHook, String refundHook) {
+ PaypalWrapper paypalWrapper = new PaypalWrapper();
+
+ PayPalEnvironment environment = new PayPalEnvironment.Live(clientID, secret);
+
+ if (sandbox == 1) {
+ environment = new PayPalEnvironment.Sandbox(clientID, secret);
+ }
+
+ paypalWrapper.setEnvironment(environment);
+ paypalWrapper.setClient(new PayPalHttpClient(environment));
+ paypalWrapper.setNotifyWebhook(notifyHook);
+ paypalWrapper.setRefundWebhook(refundHook);
+
+ return paypalWrapper;
+ }
private boolean isCache(){
return SysConfigService.IS_USE_CACHE;