diff --git a/docs/sql/init.sql b/docs/sql/init.sql index c59f0d80e4ac92dcd7d2eafcaa8ae1923305bf2b..61f8150c3a878c91d12536b13f8b6d83fca8e9b6 100644 --- a/docs/sql/init.sql +++ b/docs/sql/init.sql @@ -360,7 +360,7 @@ CREATE TABLE `t_refund_order` ( `pay_amount` BIGINT(20) NOT NULL COMMENT '支付金额,单位分', `refund_amount` BIGINT(20) NOT NULL COMMENT '退款金额,单位分', `currency` VARCHAR(3) NOT NULL DEFAULT 'cny' COMMENT '三位货币代码,人民币:cny', - `state` TINYINT(6) NOT NULL DEFAULT '0' COMMENT '退款状态:0-订单生成,1-退款中,2-退款成功,3-退款失败', + `state` TINYINT(6) NOT NULL DEFAULT '0' COMMENT '退款状态:0-订单生成,1-退款中,2-退款成功,3-退款失败,4-退款任务关闭', `client_ip` VARCHAR(32) DEFAULT NULL COMMENT '客户端IP', `refund_reason` VARCHAR(256) NOT NULL COMMENT '退款原因', `channel_order_no` VARCHAR(32) DEFAULT NULL COMMENT '渠道订单号', @@ -370,6 +370,7 @@ CREATE TABLE `t_refund_order` ( `notify_url` VARCHAR(128) DEFAULT NULL COMMENT '通知地址', `ext_param` VARCHAR(64) DEFAULT NULL COMMENT '扩展参数', `success_time` DATETIME DEFAULT NULL COMMENT '订单退款成功时间', + `expired_time` DATETIME DEFAULT NULL COMMENT '退款失效时间(失效后系统更改为退款任务关闭状态)', `created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', `updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', PRIMARY KEY (`refund_order_id`), diff --git a/jeepay-core/src/main/java/com/jeequan/jeepay/core/entity/RefundOrder.java b/jeepay-core/src/main/java/com/jeequan/jeepay/core/entity/RefundOrder.java index 1842d09a4e9a673ffb0699cff7acef146dd3ff10..92ff0f1237629b3f440553e2032e797f7335ff24 100644 --- a/jeepay-core/src/main/java/com/jeequan/jeepay/core/entity/RefundOrder.java +++ b/jeepay-core/src/main/java/com/jeequan/jeepay/core/entity/RefundOrder.java @@ -43,6 +43,7 @@ public class RefundOrder extends BaseModel { public static final byte STATE_ING = 1; //退款中 public static final byte STATE_SUCCESS = 2; //退款成功 public static final byte STATE_FAIL = 3; //退款失败 + public static final byte STATE_CLOSED = 4; //退款任务关闭 public static final LambdaQueryWrapper gw(){ return new LambdaQueryWrapper<>(); @@ -122,7 +123,7 @@ public class RefundOrder extends BaseModel { private String currency; /** - * 退款状态:0-订单生成,1-退款中,2-退款成功,3-退款失败 + * 退款状态:0-订单生成,1-退款中,2-退款成功,3-退款失败,4-退款任务关闭 */ private Byte state; @@ -171,6 +172,11 @@ public class RefundOrder extends BaseModel { */ private Date successTime; + /** + * 退款失效时间(失效后系统更改为退款任务关闭状态) + */ + private Date expiredTime; + /** * 创建时间 */ diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/IRefundService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/IRefundService.java index e8e77aea4960f06fedc2dab81ca08de2e81ec70d..56d5185145cbe9f575be4cba7f07ad06c57b354a 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/IRefundService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/IRefundService.java @@ -39,4 +39,7 @@ public interface IRefundService { /** 调起退款接口,并响应数据; 内部处理普通商户和服务商模式 **/ ChannelRetMsg refund(RefundOrderRQ bizRQ, RefundOrder refundOrder, PayOrder payOrder, MchAppConfigContext mchAppConfigContext) throws Exception; + /** 退款查单接口 **/ + ChannelRetMsg query(RefundOrder refundOrder, MchAppConfigContext mchAppConfigContext) throws Exception; + } diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java index ad17ef08f0d7d47522d30035858df00097cf6507..8dff8540d1ed2634be1a25270118b4b4d3980b62 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java @@ -56,6 +56,7 @@ public class AlipayKit { else if(req instanceof AlipayTradeWapPayRequest) ((AlipayTradeWapPayRequest)req).putOtherTextParam("app_auth_token", isvsubMchParams.getAppAuthToken()); else if(req instanceof AlipayTradeQueryRequest) ((AlipayTradeQueryRequest)req).putOtherTextParam("app_auth_token", isvsubMchParams.getAppAuthToken()); else if(req instanceof AlipayTradeRefundRequest) ((AlipayTradeRefundRequest)req).putOtherTextParam("app_auth_token", isvsubMchParams.getAppAuthToken()); + else if(req instanceof AlipayTradeFastpayRefundQueryRequest) ((AlipayTradeFastpayRefundQueryRequest)req).putOtherTextParam("app_auth_token", isvsubMchParams.getAppAuthToken()); // 服务商信息 ExtendParams extendParams = new ExtendParams(); diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayRefundService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayRefundService.java index 15e989d89729c89f78a3c864dbe73a89867919b3..a5a56bb2c66994172e5cb6962ff475493f909083 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayRefundService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayRefundService.java @@ -15,8 +15,11 @@ */ package com.jeequan.jeepay.pay.channel.alipay; +import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel; import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest; import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse; import com.alipay.api.response.AlipayTradeRefundResponse; import com.jeequan.jeepay.core.constants.CS; import com.jeequan.jeepay.core.entity.PayOrder; @@ -82,5 +85,37 @@ public class AlipayRefundService extends AbstractRefundService { return channelRetMsg; } + @Override + public ChannelRetMsg query(RefundOrder refundOrder, MchAppConfigContext mchAppConfigContext) throws Exception { + + AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); + AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); + model.setTradeNo(refundOrder.getChannelPayOrderNo()); + model.setOutTradeNo(refundOrder.getPayOrderId()); + model.setOutRequestNo(refundOrder.getRefundOrderId()); + request.setBizModel(model); + + //统一放置 isv接口必传信息 + AlipayKit.putApiIsvInfo(mchAppConfigContext, request, model); + + AlipayTradeFastpayRefundQueryResponse response = mchAppConfigContext.getAlipayClientWrapper().execute(request); + + ChannelRetMsg channelRetMsg = new ChannelRetMsg(); + channelRetMsg.setChannelAttach(response.getBody()); + channelRetMsg.setChannelOrderId(response.getTradeNo()); + + // 调用成功 & 金额相等 (传入不存在的outRequestNo支付宝仍然返回响应成功只是数据不存在, 调用isSuccess() 仍是成功, 此处需判断金额是否相等) + Long channelRefundAmount = response.getRefundAmount() == null ? null : Long.parseLong(AmountUtil.convertDollar2Cent(response.getRefundAmount())); + if(response.isSuccess() && refundOrder.getRefundAmount().equals(channelRefundAmount)){ + channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS); + }else{ + + channelRetMsg.setChannelState(ChannelRetMsg.ChannelState.WAITING); //认为是处理中 + + } + + return channelRetMsg; + } + } diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/payorder/QueryOrderController.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/payorder/QueryOrderController.java index 02a06006ba9ca13566ffb61c561a4494e0c5cc22..7db4f96f1f06dcf8330c00a5346a188e9fa67c06 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/payorder/QueryOrderController.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/payorder/QueryOrderController.java @@ -19,8 +19,8 @@ import com.jeequan.jeepay.core.entity.PayOrder; import com.jeequan.jeepay.core.exception.BizException; import com.jeequan.jeepay.core.model.ApiRes; import com.jeequan.jeepay.pay.ctrl.ApiController; -import com.jeequan.jeepay.pay.rqrs.QueryPayOrderRQ; -import com.jeequan.jeepay.pay.rqrs.QueryPayOrderRS; +import com.jeequan.jeepay.pay.rqrs.payorder.QueryPayOrderRQ; +import com.jeequan.jeepay.pay.rqrs.payorder.QueryPayOrderRS; import com.jeequan.jeepay.pay.service.ConfigContextService; import com.jeequan.jeepay.service.impl.PayOrderService; import lombok.extern.slf4j.Slf4j; diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/QueryRefundOrderController.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/QueryRefundOrderController.java new file mode 100644 index 0000000000000000000000000000000000000000..50aedeea19bd8ead0d5a4039c467a2e56e619f5e --- /dev/null +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/QueryRefundOrderController.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jeequan.jeepay.pay.ctrl.refund; + +import com.jeequan.jeepay.core.entity.RefundOrder; +import com.jeequan.jeepay.core.exception.BizException; +import com.jeequan.jeepay.core.model.ApiRes; +import com.jeequan.jeepay.pay.ctrl.ApiController; +import com.jeequan.jeepay.pay.rqrs.refund.QueryRefundOrderRQ; +import com.jeequan.jeepay.pay.rqrs.refund.QueryRefundOrderRS; +import com.jeequan.jeepay.pay.service.ConfigContextService; +import com.jeequan.jeepay.service.impl.RefundOrderService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** +* 商户退款单查询controller +* +* @author terrfly +* @site https://www.jeepay.vip +* @date 2021/6/17 15:20 +*/ +@Slf4j +@RestController +public class QueryRefundOrderController extends ApiController { + + @Autowired private RefundOrderService refundOrderService; + @Autowired private ConfigContextService configContextService; + + /** + * 查单接口 + * **/ + @RequestMapping("/api/refund/query") + public ApiRes queryRefundOrder(){ + + //获取参数 & 验签 + QueryRefundOrderRQ rq = getRQByWithMchSign(QueryRefundOrderRQ.class); + + if(StringUtils.isAllEmpty(rq.getMchRefundNo(), rq.getRefundOrderId())){ + throw new BizException("mchRefundNo 和 refundOrderId不能同时为空"); + } + + RefundOrder refundOrder = refundOrderService.queryMchOrder(rq.getMchNo(), rq.getMchRefundNo(), rq.getRefundOrderId()); + if(refundOrder == null){ + throw new BizException("订单不存在"); + } + + QueryRefundOrderRS bizRes = QueryRefundOrderRS.buildByRefundOrder(refundOrder); + return ApiRes.okWithSign(bizRes, configContextService.getMchAppConfigContext(rq.getMchNo(), rq.getAppId()).getMchApp().getAppSecret()); + } +} diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/RefundOrderController.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/RefundOrderController.java index 9091d6b4f4e9aacbef94b465d660a70094dc36ee..4b64b18e768b5891294e5b15609dca1d745cb7ed 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/RefundOrderController.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/ctrl/refund/RefundOrderController.java @@ -15,6 +15,7 @@ */ package com.jeequan.jeepay.pay.ctrl.refund; +import cn.hutool.core.date.DateUtil; import com.jeequan.jeepay.core.entity.MchApp; import com.jeequan.jeepay.core.entity.MchInfo; import com.jeequan.jeepay.core.entity.PayOrder; @@ -32,6 +33,7 @@ import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg; import com.jeequan.jeepay.pay.rqrs.refund.RefundOrderRQ; import com.jeequan.jeepay.pay.rqrs.refund.RefundOrderRS; import com.jeequan.jeepay.pay.service.ConfigContextService; +import com.jeequan.jeepay.pay.service.PayMchNotifyService; import com.jeequan.jeepay.service.impl.PayOrderService; import com.jeequan.jeepay.service.impl.RefundOrderService; import lombok.extern.slf4j.Slf4j; @@ -56,6 +58,7 @@ public class RefundOrderController extends ApiController { @Autowired private PayOrderService payOrderService; @Autowired private RefundOrderService refundOrderService; @Autowired private ConfigContextService configContextService; + @Autowired private PayMchNotifyService payMchNotifyService; /** 申请退款 **/ @PostMapping("/api/refund/refundOrder") @@ -69,7 +72,6 @@ public class RefundOrderController extends ApiController { try { - if(StringUtils.isAllEmpty(rq.getMchOrderNo(), rq.getPayOrderId())){ throw new BizException("mchOrderNo 和 payOrderId不能同时为空"); } @@ -174,6 +176,7 @@ public class RefundOrderController extends ApiController { private RefundOrder genRefundOrder(RefundOrderRQ rq, PayOrder payOrder, MchInfo mchInfo, MchApp mchApp){ + Date nowTime = new Date(); RefundOrder refundOrder = new RefundOrder(); refundOrder.setRefundOrderId(SeqKit.genPayOrderId()); //退款订单号 refundOrder.setPayOrderId(payOrder.getPayOrderId()); //支付订单号 @@ -198,8 +201,9 @@ public class RefundOrderController extends ApiController { refundOrder.setChannelExtra(rq.getChannelExtra()); //特定渠道发起时额外参数 refundOrder.setNotifyUrl(rq.getNotifyUrl()); //通知地址 refundOrder.setExtParam(rq.getExtParam()); //扩展参数 + refundOrder.setExpiredTime(DateUtil.offsetHour(nowTime, 2)); //订单超时关闭时间 默认两个小时 refundOrder.setSuccessTime(null); //订单退款成功时间 - refundOrder.setCreatedAt(new Date()); //创建时间 + refundOrder.setCreatedAt(nowTime); //创建时间 return refundOrder; } @@ -219,7 +223,7 @@ public class RefundOrderController extends ApiController { if(ChannelRetMsg.ChannelState.CONFIRM_SUCCESS == channelRetMsg.getChannelState()) { this.updateInitOrderStateThrowException(RefundOrder.STATE_SUCCESS, refundOrder, channelRetMsg); -// payMchNotifyService.payOrderNotify(payOrder); + payMchNotifyService.refundOrderNotify(refundOrder); //明确失败 }else if(ChannelRetMsg.ChannelState.CONFIRM_FAIL == channelRetMsg.getChannelState()) { @@ -235,7 +239,7 @@ public class RefundOrderController extends ApiController { this.updateInitOrderStateThrowException(RefundOrder.STATE_ING, refundOrder, channelRetMsg); // 系统异常: 退款单不再处理。 为: 生成状态 - }else if( ChannelRetMsg.ChannelState.SYS_ERROR == channelRetMsg.getChannelState()){ + }else if( ChannelRetMsg.ChannelState.SYS_ERROR == channelRetMsg.getChannelState() ){ }else{ diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRQ.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRQ.java similarity index 86% rename from jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRQ.java rename to jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRQ.java index f78a6a04585e25cd9b39ce8166d9459c42b08f83..e6b194f20b1396f3f7731e9aa0b23896c5060b61 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRQ.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRQ.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jeequan.jeepay.pay.rqrs; +package com.jeequan.jeepay.pay.rqrs.payorder; +import com.jeequan.jeepay.pay.rqrs.AbstractMchAppRQ; import lombok.Data; import javax.validation.constraints.NotBlank; @@ -27,7 +28,7 @@ import javax.validation.constraints.NotBlank; * @date 2021/6/8 17:40 */ @Data -public class QueryPayOrderRQ extends AbstractMchAppRQ{ +public class QueryPayOrderRQ extends AbstractMchAppRQ { /** 商户订单号 **/ private String mchOrderNo; diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRS.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRS.java similarity index 94% rename from jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRS.java rename to jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRS.java index 86ceb398a3b8828266553b4abd15d8ba3eeeb8dd..dabcebf9f42d854d9973b1a08214d8b7170f0303 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/QueryPayOrderRS.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/payorder/QueryPayOrderRS.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jeequan.jeepay.pay.rqrs; +package com.jeequan.jeepay.pay.rqrs.payorder; import com.jeequan.jeepay.core.entity.PayOrder; +import com.jeequan.jeepay.pay.rqrs.AbstractRS; import lombok.Data; import org.springframework.beans.BeanUtils; @@ -27,7 +28,7 @@ import org.springframework.beans.BeanUtils; * @date 2021/6/8 17:40 */ @Data -public class QueryPayOrderRS extends AbstractRS{ +public class QueryPayOrderRS extends AbstractRS { /** * 支付订单号 diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRQ.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRQ.java new file mode 100644 index 0000000000000000000000000000000000000000..e2fab2c96b29210cc117af3854ae60492d135421 --- /dev/null +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRQ.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jeequan.jeepay.pay.rqrs.refund; + +import com.jeequan.jeepay.pay.rqrs.AbstractMchAppRQ; +import lombok.Data; + +/* +* 查询退款单请求参数对象 +* +* @author terrfly +* @site https://www.jeepay.vip +* @date 2021/6/17 14:07 +*/ +@Data +public class QueryRefundOrderRQ extends AbstractMchAppRQ { + + /** 商户退款单号 **/ + private String mchRefundNo; + + /** 支付系统退款订单号 **/ + private String refundOrderId; + +} diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRS.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRS.java new file mode 100644 index 0000000000000000000000000000000000000000..9510adc8694a2de05cfefdc28f8ba3c64d53fc46 --- /dev/null +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/QueryRefundOrderRS.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jeequan.jeepay.pay.rqrs.refund; + +import com.jeequan.jeepay.core.entity.RefundOrder; +import com.jeequan.jeepay.pay.rqrs.AbstractRS; +import lombok.Data; +import org.springframework.beans.BeanUtils; + +/* +* 查询退款单 响应参数 +* +* @author terrfly +* @site https://www.jeepay.vip +* @date 2021/6/17 14:08 +*/ +@Data +public class QueryRefundOrderRS extends AbstractRS { + + /** + * 退款订单号(支付系统生成订单号) + */ + private String refundOrderId; + + /** + * 支付订单号(与t_pay_order对应) + */ + private String payOrderId; + + /** + * 商户号 + */ + private String mchNo; + + /** + * 应用ID + */ + private String appId; + + /** + * 商户退款单号(商户系统的订单号) + */ + private String mchRefundNo; + + /** + * 支付金额,单位分 + */ + private Long payAmount; + + /** + * 退款金额,单位分 + */ + private Long refundAmount; + + /** + * 三位货币代码,人民币:cny + */ + private String currency; + + /** + * 退款状态:0-订单生成,1-退款中,2-退款成功,3-退款失败 + */ + private Byte state; + + /** + * 渠道订单号 + */ + private String channelOrderNo; + + /** + * 渠道错误码 + */ + private String channelErrCode; + + /** + * 渠道错误描述 + */ + private String channelErrMsg; + + /** + * 特定渠道发起时额外参数 + */ + private String channelExtra; + + /** + * 扩展参数 + */ + private String extParam; + + /** + * 订单退款成功时间 + */ + private Long successTime; + + /** + * 创建时间 + */ + private Long createdAt; + + + public static QueryRefundOrderRS buildByRefundOrder(RefundOrder refundOrder){ + + if(refundOrder == null){ + return null; + } + + QueryRefundOrderRS result = new QueryRefundOrderRS(); + BeanUtils.copyProperties(refundOrder, result); + result.setSuccessTime(refundOrder.getSuccessTime() == null ? null : refundOrder.getSuccessTime().getTime()); + result.setCreatedAt(refundOrder.getCreatedAt() == null ? null : refundOrder.getCreatedAt().getTime()); + return result; + } + + +} diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/RefundOrderRS.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/RefundOrderRS.java index e75b27a9f75dc6b5c8a1ee83b56a798dd9f3a3f0..928815e38514f48d75f13dfef66ee98b8ef3dd2f 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/RefundOrderRS.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/rqrs/refund/RefundOrderRS.java @@ -15,10 +15,8 @@ */ package com.jeequan.jeepay.pay.rqrs.refund; -import com.jeequan.jeepay.core.entity.PayOrder; import com.jeequan.jeepay.core.entity.RefundOrder; import com.jeequan.jeepay.pay.rqrs.AbstractRS; -import com.jeequan.jeepay.pay.rqrs.QueryPayOrderRS; import lombok.Data; import org.springframework.beans.BeanUtils; diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ChannelOrderReissueService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ChannelOrderReissueService.java index c9289a313868e0e3868a6fae852e69d4a7e6bec3..25a1047f248f843ce656fe171be33c97a56b9dad 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ChannelOrderReissueService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/ChannelOrderReissueService.java @@ -16,11 +16,14 @@ package com.jeequan.jeepay.pay.service; import com.jeequan.jeepay.core.entity.PayOrder; +import com.jeequan.jeepay.core.entity.RefundOrder; import com.jeequan.jeepay.core.utils.SpringBeansUtil; import com.jeequan.jeepay.pay.channel.IPayOrderQueryService; +import com.jeequan.jeepay.pay.channel.IRefundService; import com.jeequan.jeepay.pay.model.MchAppConfigContext; import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg; import com.jeequan.jeepay.service.impl.PayOrderService; +import com.jeequan.jeepay.service.impl.RefundOrderService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -33,12 +36,14 @@ import org.springframework.stereotype.Service; * @site https://www.jeepay.vip * @date 2021/6/8 17:40 */ + @Service @Slf4j public class ChannelOrderReissueService { @Autowired private ConfigContextService configContextService; @Autowired private PayOrderService payOrderService; + @Autowired private RefundOrderService refundOrderService; @Autowired private PayMchNotifyService payMchNotifyService; @@ -95,5 +100,58 @@ public class ChannelOrderReissueService { } + /** 处理退款订单 **/ + public ChannelRetMsg processRefundOrder(RefundOrder refundOrder){ + + try { + + String refundOrderId = refundOrder.getRefundOrderId(); + + //查询支付接口是否存在 + IRefundService queryService = SpringBeansUtil.getBean(refundOrder.getIfCode() + "RefundService", IRefundService.class); + + // 支付通道接口实现不存在 + if(queryService == null){ + log.error("退款补单:{} interface not exists!", refundOrder.getIfCode()); + return null; + } + + //查询出商户应用的配置信息 + MchAppConfigContext mchAppConfigContext = configContextService.getMchAppConfigContext(refundOrder.getMchNo(), refundOrder.getAppId()); + + ChannelRetMsg channelRetMsg = queryService.query(refundOrder, mchAppConfigContext); + if(channelRetMsg == null){ + log.error("退款补单:channelRetMsg is null"); + return null; + } + + log.info("退款补单:[{}]查询结果为:{}", refundOrderId, channelRetMsg); + + // 查询成功 + if(channelRetMsg.getChannelState() == ChannelRetMsg.ChannelState.CONFIRM_SUCCESS) { + if (refundOrderService.updateIng2Success(refundOrderId, channelRetMsg.getChannelOrderId())) { + + // 通知商户系统 + if(StringUtils.isNotEmpty(refundOrder.getNotifyUrl())){ + payMchNotifyService.refundOrderNotify(refundOrderService.getById(refundOrderId)); + } + + } + }else if(channelRetMsg.getChannelState() == ChannelRetMsg.ChannelState.CONFIRM_FAIL){ //确认失败 + + //1. 更新支付订单表为失败状态 + refundOrderService.updateIng2Fail(refundOrderId, channelRetMsg.getChannelOrderId(), channelRetMsg.getChannelErrCode(), channelRetMsg.getChannelErrMsg()); + + } + + return channelRetMsg; + + } catch (Exception e) { //继续下一次迭代查询 + log.error("退款补单:error refundOrderId = {}", refundOrder.getRefundOrderId(), e); + return null; + } + + } + } diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/PayMchNotifyService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/PayMchNotifyService.java index 589950c18f187ba827076192d34af6424e8d775b..2289e50d486e2acfcfa18237a42beb4a40918d2a 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/PayMchNotifyService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/service/PayMchNotifyService.java @@ -16,14 +16,14 @@ package com.jeequan.jeepay.pay.service; import com.alibaba.fastjson.JSONObject; -import com.jeequan.jeepay.core.entity.MchInfo; import com.jeequan.jeepay.core.entity.MchNotifyRecord; import com.jeequan.jeepay.core.entity.PayOrder; +import com.jeequan.jeepay.core.entity.RefundOrder; import com.jeequan.jeepay.core.utils.JeepayKit; import com.jeequan.jeepay.core.utils.StringKit; import com.jeequan.jeepay.pay.mq.queue.MqQueue4PayOrderMchNotify; -import com.jeequan.jeepay.pay.rqrs.QueryPayOrderRS; -import com.jeequan.jeepay.service.impl.MchInfoService; +import com.jeequan.jeepay.pay.rqrs.payorder.QueryPayOrderRS; +import com.jeequan.jeepay.pay.rqrs.refund.QueryRefundOrderRS; import com.jeequan.jeepay.service.impl.MchNotifyRecordService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -91,6 +91,51 @@ public class PayMchNotifyService { } } + /** 商户通知信息,退款成功的发送通知 **/ + public void refundOrderNotify(RefundOrder dbRefundOrder){ + + try { + // 通知地址为空 + if(StringUtils.isEmpty(dbRefundOrder.getNotifyUrl())){ + return ; + } + + //获取到通知对象 + MchNotifyRecord mchNotifyRecord = mchNotifyRecordService.findByRefundOrder(dbRefundOrder.getRefundOrderId()); + + if(mchNotifyRecord != null){ + + log.info("当前已存在通知消息, 不再发送。"); + return ; + } + + //商户app私钥 + String appSecret = configContextService.getMchAppConfigContext(dbRefundOrder.getMchNo(), dbRefundOrder.getAppId()).getMchApp().getAppSecret(); + + // 封装通知url + String notifyUrl = createNotifyUrl(dbRefundOrder, appSecret); + mchNotifyRecord = new MchNotifyRecord(); + mchNotifyRecord.setOrderId(dbRefundOrder.getRefundOrderId()); + mchNotifyRecord.setOrderType(MchNotifyRecord.TYPE_REFUND_ORDER); + mchNotifyRecord.setMchNo(dbRefundOrder.getMchNo()); + mchNotifyRecord.setMchOrderNo(dbRefundOrder.getMchRefundNo()); //商户订单号 + mchNotifyRecord.setIsvNo(dbRefundOrder.getIsvNo()); + mchNotifyRecord.setAppId(dbRefundOrder.getAppId()); + mchNotifyRecord.setNotifyUrl(notifyUrl); + mchNotifyRecord.setResResult(""); + mchNotifyRecord.setNotifyCount(0); + mchNotifyRecord.setState(MchNotifyRecord.STATE_ING); // 通知中 + mchNotifyRecordService.save(mchNotifyRecord); + + //推送到MQ + Long notifyId = mchNotifyRecord.getNotifyId(); + mqPayOrderMchNotifyQueue.send(notifyId + ""); + + } catch (Exception e) { + log.error("推送失败!", e); + } + } + /** * 创建响应URL @@ -109,6 +154,23 @@ public class PayMchNotifyService { } + /** + * 创建响应URL + */ + public String createNotifyUrl(RefundOrder refundOrder, String appSecret) { + + QueryRefundOrderRS queryPayOrderRS = QueryRefundOrderRS.buildByRefundOrder(refundOrder); + JSONObject jsonObject = (JSONObject)JSONObject.toJSON(queryPayOrderRS); + jsonObject.put("reqTime", System.currentTimeMillis()); //添加请求时间 + + // 报文签名 + jsonObject.put("sign", JeepayKit.getSign(jsonObject, appSecret)); + + // 生成通知 + return StringKit.appendUrlQuery(refundOrder.getNotifyUrl(), jsonObject); + } + + /** * 创建响应URL */ diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderExpiredTask.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderExpiredTask.java new file mode 100644 index 0000000000000000000000000000000000000000..af05b35e40fb6413f8007dda96570cc26d1850ea --- /dev/null +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderExpiredTask.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jeequan.jeepay.pay.task; + +import com.jeequan.jeepay.service.impl.PayOrderService; +import com.jeequan.jeepay.service.impl.RefundOrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/* +* 退款订单过期定时任务 +* +* @author terrfly +* @site https://www.jeepay.vip +* @date 2021/6/17 14:36 +*/ +@Slf4j +@Component +public class RefundOrderExpiredTask { + + @Autowired private RefundOrderService refundOrderService; + + @Scheduled(cron="0 0/1 * * * ?") // 每分钟执行一次 + public void start() { + + int updateCount = refundOrderService.updateOrderExpired(); + log.info("处理退款订单超时{}条.", updateCount); + } + + +} diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderReissueTask.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderReissueTask.java new file mode 100644 index 0000000000000000000000000000000000000000..73482390935ece2ade8593e75ee44d5d348041d7 --- /dev/null +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/task/RefundOrderReissueTask.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jeequan.jeepay.pay.task; + +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.jeequan.jeepay.core.entity.RefundOrder; +import com.jeequan.jeepay.pay.service.ChannelOrderReissueService; +import com.jeequan.jeepay.service.impl.RefundOrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/* +* 补单定时任务(退款单) +* +* @author terrfly +* @site https://www.jeepay.vip +* @date 2021/6/17 14:22 +*/ +@Slf4j +@Component +public class RefundOrderReissueTask { + + private static final int QUERY_PAGE_SIZE = 100; //每次查询数量 + + @Autowired private RefundOrderService refundOrderService; + @Autowired private ChannelOrderReissueService channelOrderReissueService; + + @Scheduled(cron="0 0/1 * * * ?") // 每分钟执行一次 + public void start() { + + //当前时间 减去10分钟。 + Date offsetDate = DateUtil.offsetMinute(new Date(), -10); + + //查询条件: 支付中的订单 & ( 订单创建时间 + 10分钟 >= 当前时间 ) + LambdaQueryWrapper lambdaQueryWrapper = RefundOrder.gw().eq(RefundOrder::getState, RefundOrder.STATE_ING).le(RefundOrder::getCreatedAt, offsetDate); + + int currentPageIndex = 1; //当前页码 + while(true){ + + try { + IPage refundOrderIPage = refundOrderService.page(new Page(currentPageIndex, QUERY_PAGE_SIZE), lambdaQueryWrapper); + + if(refundOrderIPage == null || refundOrderIPage.getRecords().isEmpty()){ //本次查询无结果, 不再继续查询; + break; + } + + for(RefundOrder refundOrder: refundOrderIPage.getRecords()){ + channelOrderReissueService.processRefundOrder(refundOrder); + } + + //已经到达页码最大量,无需再次查询 + if(refundOrderIPage.getPages() <= currentPageIndex){ + break; + } + currentPageIndex++; + + + } catch (Exception e) { //出现异常,直接退出,避免死循环。 + log.error("error", e); + break; + } + + } + } + + + +} diff --git a/jeepay-service/src/main/java/com/jeequan/jeepay/service/impl/RefundOrderService.java b/jeepay-service/src/main/java/com/jeequan/jeepay/service/impl/RefundOrderService.java index c261e2218ae2f06e46f74ff610dce32959fd32a6..9304991c00a85998c39d06be6713ac380cb5a760 100644 --- a/jeepay-service/src/main/java/com/jeequan/jeepay/service/impl/RefundOrderService.java +++ b/jeepay-service/src/main/java/com/jeequan/jeepay/service/impl/RefundOrderService.java @@ -21,10 +21,12 @@ import com.jeequan.jeepay.core.entity.RefundOrder; import com.jeequan.jeepay.core.exception.BizException; import com.jeequan.jeepay.service.mapper.PayOrderMapper; import com.jeequan.jeepay.service.mapper.RefundOrderMapper; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; import java.util.Date; /** @@ -40,6 +42,18 @@ public class RefundOrderService extends ServiceImpl +