diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java index de0a6d91..687d1d28 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java @@ -24,9 +24,9 @@ public interface PayErrorCodeCoreConstants { ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007001003, "支付渠道不存在"); ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001005, "已存在相同的渠道"); ErrorCode CHANNEL_WECHAT_VERSION_2_MCH_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v2版本中商户密钥不可为空"); - ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本apiclient_key.pem不可为空"); - ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001006,"微信渠道v3版本中apiclient_cert.pem不可为空"); - + ErrorCode CHANNEL_WECHAT_VERSION_3_PRIVATE_KEY_IS_NULL = new ErrorCode(1007001007,"微信渠道v3版本apiclient_key.pem不可为空"); + ErrorCode CHANNEL_WECHAT_VERSION_3_CERT_KEY_IS_NULL = new ErrorCode(1007001008,"微信渠道v3版本中apiclient_cert.pem不可为空"); + ErrorCode PAY_CHANNEL_NOTIFY_VERIFY_FAILED = new ErrorCode(1007001009, "渠道通知校验失败"); /** * ========== ORDER 模块 1-007-002-000 ========== */ diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayCommonCoreService.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayCommonCoreService.java new file mode 100644 index 00000000..38d1c7a4 --- /dev/null +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayCommonCoreService.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.coreservice.modules.pay.service.order; + +import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO; + +/** + * 支付通用 Core Service + * + * @author jason + */ +public interface PayCommonCoreService { + + /** + * 验证是否是渠道通知 + * @param notifyData 通知数据 + */ + void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData); + + /** + * 支付宝的支付回调通知,和退款回调通知 地址是同一个 + * 是否是退款回调通知 + * @param notifyData 通知数据 + * @return + */ + boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData); +} diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java index 3a1e4774..9a7b95fd 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java @@ -43,10 +43,9 @@ public interface PayOrderCoreService { * 通知支付单成功 * * @param channelId 渠道编号 - * @param channelCode 渠道编码 * @param notifyData 通知数据 */ - void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception; + void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception; diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java index 525f69fb..adfd768c 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayRefundCoreService.java @@ -23,11 +23,10 @@ public interface PayRefundCoreService { /** * 渠道的退款通知 * @param channelId 渠道编号 - * @param channelCode 渠道编码 * @param notifyData 通知数据 * @throws Exception 退款通知异常 */ - void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception; + void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) throws Exception; diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayCommonCoreServiceImpl.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayCommonCoreServiceImpl.java new file mode 100644 index 00000000..3825c515 --- /dev/null +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayCommonCoreServiceImpl.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl; + +import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO; +import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService; +import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; +import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.PAY_CHANNEL_NOTIFY_VERIFY_FAILED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * 支付通用 Core Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +public class PayCommonCoreServiceImpl implements PayCommonCoreService { + + @Resource + private PayChannelCoreService payChannelCoreService; + + @Resource + private PayClientFactory payClientFactory; + + @Override + public void verifyNotifyData(Long channelId, PayNotifyDataDTO notifyData) { + // 校验支付渠道是否有效 + PayChannelDO channel = payChannelCoreService.validPayChannel(channelId); + // 校验支付客户端是否正确初始化 + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND); + } + boolean verifyResult = client.verifyNotifyData(notifyData); + if(!verifyResult){ + //渠道通知验证失败 + throw exception(PAY_CHANNEL_NOTIFY_VERIFY_FAILED); + } + } + + @Override + public boolean isRefundNotify(Long channelId, PayNotifyDataDTO notifyData) { + // 校验支付渠道是否有效 + PayChannelDO channel = payChannelCoreService.validPayChannel(channelId); + // 校验支付客户端是否正确初始化 + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND); + } + return client.isRefundNotify(notifyData); + } +} diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java index bef3d36d..dea1a5c0 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java @@ -155,24 +155,22 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService { /** * 根据支付渠道的编码,生成支付渠道的返回地址 - * @param channel - * @return + * @param channel 支付渠道 + * @return 支付成功返回的地址。 配置地址 + "/" + channel id */ private String genChannelReturnUrl(PayChannelDO channel) { - return payProperties.getPayReturnUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-") - + "/" + channel.getId(); + return payProperties.getPayReturnUrl() + "/" + channel.getId(); } /** * 根据支付渠道的编码,生成支付渠道的回调地址 * * @param channel 支付渠道 - * @return 支付渠道的回调地址 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id */ private String genChannelPayNotifyUrl(PayChannelDO channel) { - // _ 转化为 - 的原因,是因为 URL 我们统一采用中划线的原则 - return payProperties.getPayNotifyUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-") - + "/" + channel.getId(); + //去掉channel code, 似乎没啥用, 用统一的回调地址 + return payProperties.getPayNotifyUrl() + "/" + channel.getId(); } private String generateOrderExtensionNo() { @@ -195,7 +193,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService { @Override @Transactional - public void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception { + public void notifyPayOrder(Long channelId, PayNotifyDataDTO notifyData) throws Exception { // TODO 芋艿,记录回调日志 log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData.getBody()); @@ -207,7 +205,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService { log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND); } - //TODO @jason 校验 是否支付宝调用。 使用 支付宝publickey 或者payclient 加一个校验方法 + // 解析支付结果 PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData); @@ -222,7 +220,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService { throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); } // 1.2 更新 PayOrderExtensionDO - //TODO @jason notifyRespDTO.getTradeStatus() 需要根据不同的状态更新成不同的值 PayOrderStatusEnum + //TODO 支付宝交易超时 TRADE_FINISHED 需要更新交易关闭 int updateCounts = payOrderExtensionCoreMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId()) .status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData.getBody()).build()); @@ -241,7 +239,7 @@ public class PayOrderCoreServiceImpl implements PayOrderCoreService { } // 2.2 更新 PayOrderDO updateCounts = payOrderCoreMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(), - PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channelCode) + PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channel.getCode()) .successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId()) .channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId()) .notifyTime(new Date()).build()); diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java index e39ebb9d..b64c2000 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundChannelQueryHandler.java @@ -40,7 +40,9 @@ public class PayRefundChannelQueryHandler extends PayRefundAbstractChannelPostHa //更新退款单表 PayRefundDO updateRefundDO = new PayRefundDO(); updateRefundDO.setId(respBO.getRefundId()) - .setStatus(refundStatus.getStatus()); + .setStatus(refundStatus.getStatus()) + .setChannelErrorCode(respBO.getChannelErrCode()) + .setChannelErrorMsg(respBO.getChannelErrMsg()); updatePayRefund(updateRefundDO); PayOrderDO updateOrderDO = new PayOrderDO(); diff --git a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java index 68ba7ae3..3b2b4fbb 100644 --- a/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java +++ b/yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java @@ -182,7 +182,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService { @Override - public void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) { + public void notifyPayRefund(Long channelId, PayNotifyDataDTO notifyData) { log.info("[notifyPayRefund][channelId({}) 回调数据({})]", channelId, notifyData.getBody()); // 校验支付渠道是否有效 PayChannelDO channel = payChannelCoreService.validPayChannel(channelId); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java index ceea4b45..5eac6d10 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -49,4 +49,22 @@ public interface PayClient { */ PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData); + + /** + * 验证是否渠道通知 + * @param notifyData 通知数据 + * @return 默认是 true + */ + default boolean verifyNotifyData(PayNotifyDataDTO notifyData){ + return true; + } + + /** + * 是否退款通知 + * @param notifyData 通知数据 + * @return 默认是 false + */ + default boolean isRefundNotify(PayNotifyDataDTO notifyData){ + return false; + } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java new file mode 100644 index 00000000..11701944 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java @@ -0,0 +1,162 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; +import cn.iocoder.yudao.framework.pay.core.client.dto.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum; +import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConfig; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeRefundResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付宝抽象类, 实现支付宝统一的接口。如退款 + * + * @author jason + */ +@Slf4j +public abstract class AbstractAlipayClient extends AbstractPayClient { + + protected DefaultAlipayClient client; + + public AbstractAlipayClient(Long channelId, String channelCode, + AlipayPayClientConfig config, AbstractPayCodeMapping codeMapping) { + super(channelId, channelCode, config, codeMapping); + } + + @Override + @SneakyThrows + protected void doInit() { + AlipayConfig alipayConfig = new AlipayConfig(); + BeanUtil.copyProperties(config, alipayConfig, false); + this.client = new DefaultAlipayClient(alipayConfig); + } + + /** + * 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考 + * //https://opendocs.alipay.com/open/203/105286 + * @param data 通知结果 + * @return 解析结果 PayOrderNotifyRespDTO + * @throws Exception 解析失败,抛出异常 + */ + @Override + public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception { + Map params = data.getParams(); + return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no")) + .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id")) + .tradeStatus(params.get("trade_status")) + .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss")) + .data(data.getBody()).build(); + } + + @Override + public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { + Map params = notifyData.getParams(); + PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no")) + .tradeNo(params.get("out_trade_no")) + .reqNo(params.get("out_biz_no")) + .status(PayNotifyRefundStatusEnum.SUCCESS) + .refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss")) + .build(); + return notifyDTO; + } + + @Override + public boolean isRefundNotify(PayNotifyDataDTO notifyData) { + if (notifyData.getParams().containsKey("refund_fee")) { + return true; + } else { + return false; + } + } + + @Override + public boolean verifyNotifyData(PayNotifyDataDTO notifyData) { + boolean verifyResult = false; + try { + verifyResult = AlipaySignature.rsaCheckV1(notifyData.getParams(), config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), "RSA2"); + } catch (AlipayApiException e) { + log.error("[AlipayClient verifyNotifyData][(notify param is :{}) 验证失败]", toJsonString(notifyData.getParams()), e); + } + return verifyResult; + } + + /** + * 支付宝统一的退款接口 alipay.trade.refund + * @param reqDTO 退款请求 request DTO + * @return 退款请求 Response + */ + @Override + protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + AlipayTradeRefundModel model=new AlipayTradeRefundModel(); + model.setTradeNo(reqDTO.getChannelOrderNo()); + model.setOutTradeNo(reqDTO.getPayTradeNo()); + model.setOutRequestNo(reqDTO.getRefundReqNo()); + model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString()); + model.setRefundReason(reqDTO.getReason()); + AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest(); + refundRequest.setBizModel(model); + PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO(); + try { + AlipayTradeRefundResponse response = client.execute(refundRequest); + log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response)); + if (response.isSuccess()) { + //退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知, + //退款导致触发的异步通知是发送到支付接口中设置的notify_url + //TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对? + respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY); + }else{ + //特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务 + //沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain + if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) { + respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE) + .setChannelErrMsg(response.getSubMsg()) + .setChannelErrCode(response.getSubCode()); + }else{ + //交易已关闭,需要查询确认退款是否已经完成 + if("ACQ.TRADE_HAS_CLOSE".equals(response.getSubCode())){ + respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_QUERY) + .setChannelErrMsg(response.getSubMsg()) + .setChannelErrCode(response.getSubCode()); + }else { + //其他当做不可以重试的错误 + respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE) + .setChannelErrCode(response.getSubCode()) + .setChannelErrMsg(response.getSubMsg()); + } + } + } + return respDTO; + } catch (AlipayApiException e) { + //TODO 记录异常日志 + log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e); + Throwable cause = e.getCause(); + //网络 read time out 异常, 退款状态未知 + if (cause instanceof SocketTimeoutException) { + respDTO.setExceptionMsg(e.getMessage()) + .setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION); + }else{ + respDTO.setExceptionMsg(e.getMessage()) + .setChannelErrCode(e.getErrCode()) + .setChannelErrMsg(e.getErrMsg()) + .setRespEnum(PayChannelRespEnum.CALL_EXCEPTION); + } + return respDTO; + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java index d046c955..acdf28e7 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java @@ -1,22 +1,14 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.date.DateUtil; import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; -import cn.iocoder.yudao.framework.pay.core.client.dto.*; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; import com.alipay.api.AlipayApiException; -import com.alipay.api.AlipayConfig; -import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.AlipayTradePrecreateModel; import com.alipay.api.request.AlipayTradePrecreateRequest; import com.alipay.api.response.AlipayTradePrecreateResponse; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.util.Map; - import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; /** @@ -26,23 +18,12 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString * @author 芋道源码 */ @Slf4j -public class AlipayQrPayClient extends AbstractPayClient { - - private DefaultAlipayClient client; +public class AlipayQrPayClient extends AbstractAlipayClient { public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping()); } - @Override - @SneakyThrows - protected void doInit() { - AlipayConfig alipayConfig = new AlipayConfig(); - BeanUtil.copyProperties(config, alipayConfig, false); - // 真实客户端 - this.client = new DefaultAlipayClient(alipayConfig); - } - @Override public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { // 构建 AlipayTradePrecreateModel 请求 @@ -56,7 +37,7 @@ public class AlipayQrPayClient extends AbstractPayClient AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); request.setBizModel(model); request.setNotifyUrl(reqDTO.getNotifyUrl()); - + request.setReturnUrl(reqDTO.getReturnUrl()); // 执行请求 AlipayTradePrecreateResponse response; try { @@ -68,30 +49,4 @@ public class AlipayQrPayClient extends AbstractPayClient // TODO 芋艿:sub Code 需要测试下各种失败的情况 return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping); } - - - - @Override - public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception { - //结果转换 - Map params = data.getParams(); - return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no")) - .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id")) - .tradeStatus(params.get("trade_status")) - .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss")) - .data(data.getBody()).build(); - - } - - @Override - public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { - //TODO 需要实现 - throw new UnsupportedOperationException("需要实现"); - } - - @Override - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { - //TODO 需要实现 - throw new UnsupportedOperationException(); - } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java index 07291d74..0e410ba0 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java @@ -1,31 +1,17 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; -import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateUtil; import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; -import cn.iocoder.yudao.framework.pay.core.client.dto.*; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum; -import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum; import com.alipay.api.AlipayApiException; -import com.alipay.api.AlipayConfig; -import com.alipay.api.DefaultAlipayClient; -import com.alipay.api.domain.AlipayTradeRefundModel; import com.alipay.api.domain.AlipayTradeWapPayModel; -import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.request.AlipayTradeWapPayRequest; -import com.alipay.api.response.AlipayTradeRefundResponse; import com.alipay.api.response.AlipayTradeWapPayResponse; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.net.SocketTimeoutException; -import java.util.Map; import java.util.Objects; -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - /** * 支付宝【手机网站】的 PayClient 实现类 * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay @@ -33,22 +19,13 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString * @author 芋道源码 */ @Slf4j -public class AlipayWapPayClient extends AbstractPayClient { +public class AlipayWapPayClient extends AbstractAlipayClient { - private DefaultAlipayClient client; public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping()); } - @Override - @SneakyThrows - protected void doInit() { - AlipayConfig alipayConfig = new AlipayConfig(); - BeanUtil.copyProperties(config, alipayConfig, false); - this.client = new DefaultAlipayClient(alipayConfig); - } - @Override public PayCommonResult doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { // 构建 AlipayTradeWapPayModel 请求 @@ -69,6 +46,7 @@ public class AlipayWapPayClient extends AbstractPayClient request.setBizModel(model); request.setNotifyUrl(reqDTO.getNotifyUrl()); request.setReturnUrl(reqDTO.getReturnUrl()); + // 执行请求 AlipayTradeWapPayResponse response; try { @@ -87,85 +65,11 @@ public class AlipayWapPayClient extends AbstractPayClient } - /** - * 从支付宝通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考 - * //https://opendocs.alipay.com/open/203/105286 - * @param data 通知结果 - * @return 解析结果 PayOrderNotifyRespDTO - * @throws Exception 解析失败,抛出异常 - */ - @Override - public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception { - Map params = data.getParams(); - return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no")) - .channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id")) - .tradeStatus(params.get("trade_status")) - .successTime(DateUtil.parse(params.get("notify_time"), "yyyy-MM-dd HH:mm:ss")) - .data(data.getBody()).build(); - } - @Override - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { - AlipayTradeRefundModel model=new AlipayTradeRefundModel(); - model.setTradeNo(reqDTO.getChannelOrderNo()); - model.setOutTradeNo(reqDTO.getPayTradeNo()); - model.setOutRequestNo(reqDTO.getRefundReqNo()); - model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString()); - model.setRefundReason(reqDTO.getReason()); - AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest(); - refundRequest.setBizModel(model); - PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO(); - try { - AlipayTradeRefundResponse response = client.execute(refundRequest); - log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response)); - if (response.isSuccess()) { - //退款成功,更新为PROCESSING_NOTIFY, 而不是 SYNC_SUCCESS 通过支付宝回调接口处理。退款导致触发的异步通知, - //退款导致触发的异步通知是发送到支付接口中设置的notify_url - //TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对? - respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY); - }else{ - //特殊处理 sub_code ACQ.SYSTEM_ERROR(系统错误), 需要调用重试任务 - //沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“, 用contain - if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) { - respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE) - .setChannelErrMsg(response.getSubMsg()) - .setChannelErrCode(response.getSubCode()); - }else{ - //其他当做不可以重试的错误 - respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE) - .setChannelErrCode(response.getSubCode()) - .setChannelErrMsg(response.getSubMsg()); - } - } - return respDTO; - } catch (AlipayApiException e) { - //TODO 记录异常日志 - log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e); - Throwable cause = e.getCause(); - //网络 read time out 异常, 退款状态未知 - if (cause instanceof SocketTimeoutException) { - respDTO.setExceptionMsg(e.getMessage()) - .setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION); - }else{ - respDTO.setExceptionMsg(e.getMessage()) - .setChannelErrCode(e.getErrCode()) - .setChannelErrMsg(e.getErrMsg()) - .setRespEnum(PayChannelRespEnum.CALL_EXCEPTION); - } - return respDTO; - } - } - @Override - public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) { - Map params = notifyData.getParams(); - PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no")) - .tradeNo(params.get("out_trade_no")) - .reqNo(params.get("out_biz_no")) - .status(PayNotifyRefundStatusEnum.SUCCESS) - .refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss")) - .build(); - return notifyDTO; - } + + + + } diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/UserServerApplication.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/UserServerApplication.java index 18a927af..b0ba53e9 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/UserServerApplication.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/UserServerApplication.java @@ -4,7 +4,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yudao.info.base-package} 和 ${yudao.core-service.base-package} -@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"})public class UserServerApplication { +@SpringBootApplication(scanBasePackages = {"${yudao.info.base-package}", "${yudao.core-service.base-package}"}) +public class UserServerApplication { public static void main(String[] args) { SpringApplication.run(UserServerApplication.class, args); diff --git a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayOrderController.java b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayOrderController.java index 909a4fa6..0a80d4e6 100644 --- a/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayOrderController.java +++ b/yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/pay/controller/order/PayOrderController.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.userserver.modules.pay.controller.order; import cn.hutool.core.bean.BeanUtil; import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO; +import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayCommonCoreService; import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService; import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService; import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO; @@ -38,6 +39,9 @@ public class PayOrderController { @Resource private PayRefundCoreService payRefundCoreService; + @Resource PayCommonCoreService commonCoreService; + + @PostMapping("/submit") @ApiOperation("提交支付订单") // @PreAuthenticated // TODO 暂时不加登陆验证,前端暂时没做好 @@ -57,82 +61,53 @@ public class PayOrderController { } // ========== 支付渠道的回调 ========== - + //TODO 芋道源码 换成了统一的地址了 /notify/{channelId},测试通过可以删除 @PostMapping("/notify/wx-pub/{channelId}") @ApiOperation("通知微信公众号支付的结果") public String notifyWxPayOrder(@PathVariable("channelId") Long channelId, @RequestBody String xmlData) throws Exception { - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.WX_PUB.getCode(), PayNotifyDataDTO.builder().body(xmlData).build()); + payOrderCoreService.notifyPayOrder(channelId, PayNotifyDataDTO.builder().body(xmlData).build()); return "success"; } - @PostMapping("/notify/alipay-qr/{channelId}") - @ApiOperation("通知支付宝扫码支付的结果") - public String notifyAlipayQrPayOrder(@PathVariable("channelId") Long channelId, - @RequestParam Map params, - @RequestBody String originData) throws Exception{ - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_QR.getCode(), - PayNotifyDataDTO.builder().params(params).body(originData).build()); - return "success"; - } - - @GetMapping(value = "/return/alipay-qr/{channelId}") - @ApiOperation("支付宝 wap 页面回跳") - public String returnAliPayQrPayOrder(@PathVariable("channelId") Long channelId){ - //TODO @jason 校验 是否支付宝调用。 支付宝publickey 可以根据 appId 跳转不同的页面 - System.out.println("支付成功"); - return "支付成功"; - } - - @PostMapping(value = "/notify/alipay-wap/{channelId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - @ApiOperation("支付宝 wap 页面回调") - public String notifyAliPayWapPayOrder(@PathVariable("channelId") Long channelId, - @RequestParam Map params, - @RequestBody String originData) throws Exception { - //TODO 校验是否支付宝调用。 payclient 中加一个校验方法 - //支付宝退款交易也会触发支付回调接口 - //参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851 - //判断是否为支付宝的退款交易 - if(isAliPayRefund(params)) { - //退款通知 - payRefundCoreService.notifyPayRefund(channelId,PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build()); - }else{ - //支付通知 - payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build()); - } - return "success"; - } - - /** + * 统一的跳转页面, 支付宝跳转参数说明 * https://opendocs.alipay.com/open/203/105285#%E5%89%8D%E5%8F%B0%E5%9B%9E%E8%B7%B3%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E * @param channelId 渠道id * @return 返回跳转页面 */ - @GetMapping(value = "/return/alipay-wap/{channelId}") - @ApiOperation("支付宝 wap 页面回跳") - public String returnAliPayWapPayOrder(@PathVariable("channelId") Long channelId){ - //TODO 校验 是否支付宝调用。 可以根据 appId 跳转不同的页面 - return "支付成功"; + @GetMapping(value = "/return/{channelId}") + @ApiOperation("渠道统一的支付成功返回地址") + public String returnAliPayOrder(@PathVariable("channelId") Long channelId, @RequestParam Map params){ + //TODO 可以根据渠道和 app_id 返回不同的页面 + log.info("app_id is {}", params.get("app_id")); + return String.format("渠道[%s]支付成功", String.valueOf(channelId)); } /** - * 是否是支付宝的退款交易 - * @param params http content-type application/x-www-form-urlencoded 的参数 - * @return + * 统一的渠道支付回调,支付宝的退款回调 + * @param channelId 渠道编号 + * @param params form 参数 + * @param originData http request body + * @return 成功返回 "success" */ - private boolean isAliPayRefund(Map params) { - if (params.containsKey("refund_fee")) { - return true; - } else { - return false; + @PostMapping(value = "/notify/{channelId}") + @ApiOperation("渠道统一的支付成功,或退款成功 通知url") + public String notifyChannelPay(@PathVariable("channelId") Long channelId, + @RequestParam Map params, + @RequestBody String originData) throws Exception { + //校验是否是渠道回调 + commonCoreService.verifyNotifyData(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build()); + //支付宝退款交易也会触发支付回调接口 + //参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851 + //判断是否为退款通知 + if(commonCoreService.isRefundNotify(channelId, PayNotifyDataDTO.builder().params(params).body(originData).build())) { + //退款通知 + payRefundCoreService.notifyPayRefund(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build()); + }else{ + //支付通知 + payOrderCoreService.notifyPayOrder(channelId,PayNotifyDataDTO.builder().params(params).body(originData).build()); } - } - - @RequestMapping("/notify/test") - @ApiOperation("通知的测试接口") - public String notifyTest() { -// System.out.println(data); return "success"; }