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;