diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java index 97e967253..239ddda23 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java @@ -35,14 +35,4 @@ public class PayOrderNotifyRespDTO { */ private LocalDateTime successTime; - /** - * TODO @jason 结合其他的渠道定义成枚举, - * - * alipay - * TRADE_CLOSED,未付款交易超时关闭,或支付完成后全额退款。 - * TRADE_SUCCESS, 交易支付成功 - * TRADE_FINISHED 交易结束,不可退款。 - */ - private String tradeStatus; - } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java new file mode 100644 index 000000000..95fe7fb12 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.pay.core.client.exception; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class PayException extends RuntimeException { + + /** + * 第三方平台的错误码 + */ + private String code; + /** + * 第三方平台的错误提示 + */ + private String message; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java index 88fa4b100..262f9ab8e 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -7,18 +7,18 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDT import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO; -import com.alipay.api.AlipayResponse;import lombok.extern.slf4j.Slf4j; +import com.alipay.api.AlipayResponse; +import lombok.extern.slf4j.Slf4j; import javax.validation.Validation; import java.time.LocalDateTime; import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; -import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMATTER; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.PAY_EXCEPTION; +// TODO 芋艿:优化下,替换异常; /** * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 * diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java index 9abb1b6f1..c6ebd4f07 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.*; import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXLitePayClient; import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXNativePayClient; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPubPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPubPayClient; import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; import lombok.extern.slf4j.Slf4j; @@ -60,10 +60,10 @@ public class PayClientFactoryImpl implements PayClientFactory { // 创建客户端 // TODO @芋艿 WX_LITE WX_APP 如果不添加在 项目启动的时候去初始化会报错无法启动。所以我手动加了两个,具体需要你来配 switch (channelEnum) { - case WX_PUB: return (AbstractPayClient) new WXPubPayClient(channelId, (WXPayClientConfig) config); - case WX_LITE: return (AbstractPayClient) new WXLitePayClient(channelId, (WXPayClientConfig) config); //微信小程序请求支付 - case WX_APP: return (AbstractPayClient) new WXPubPayClient(channelId, (WXPayClientConfig) config); - case WX_NATIVE: return (AbstractPayClient) new WXNativePayClient(channelId, (WXPayClientConfig) config); + case WX_PUB: return (AbstractPayClient) new WxPubPayClient(channelId, (WxPayClientConfig) config); + case WX_LITE: return (AbstractPayClient) new WXLitePayClient(channelId, (WxPayClientConfig) config); //微信小程序请求支付 + case WX_APP: return (AbstractPayClient) new WxPubPayClient(channelId, (WxPayClientConfig) config); + case WX_NATIVE: return (AbstractPayClient) new WXNativePayClient(channelId, (WxPayClientConfig) config); case ALIPAY_WAP: return (AbstractPayClient) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config); case ALIPAY_QR: return (AbstractPayClient) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config); case ALIPAY_APP: return (AbstractPayClient) new AlipayAppPayClient(channelId, (AlipayPayClientConfig) config); 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/AbstractAlipayPayClient.java similarity index 88% rename from yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java rename to yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index 3e73cdc06..c326c2ba7 100644 --- 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/AbstractAlipayPayClient.java @@ -25,16 +25,16 @@ import java.util.Map; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; /** - * 支付宝抽象类, 实现支付宝统一的接口。如退款 + * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) * - * @author jason + * @author jason */ @Slf4j -public abstract class AbstractAlipayClient extends AbstractPayClient { +public abstract class AbstractAlipayPayClient extends AbstractPayClient { protected DefaultAlipayClient client; - public AbstractAlipayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { + public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { super(channelId, channelCode, config); } @@ -106,9 +106,11 @@ public abstract class AbstractAlipayClient extends AbstractPayClient { + + protected WxPayService client; + + public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + /** + * 初始化 client 客户端 + * + * @param tradeType 交易类型 + */ + protected void doInit(String tradeType) { + // 创建 config 配置 + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent"); + payConfig.setTradeType(tradeType); + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } + + // 创建 client 客户端 + client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + @Override + protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable { + try { + switch (config.getApiVersion()) { + case WxPayClientConfig.API_VERSION_V2: + return doUnifiedOrderV2(reqDTO); + case WxPayClientConfig.API_VERSION_V3: + return doUnifiedOrderV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[doUnifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e); + throw buildPayException(e); + } + } + + /** + * 【V2】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) + throws WxPayException; + + /** + * 【V3】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) + throws WxPayException; + + + @Override + public Object parseNotify(PayNotifyReqDTO rawNotify) { + log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody()); + try { + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case WxPayClientConfig.API_VERSION_V2: + return parseOrderNotifyV2(rawNotify); + case WxPayClientConfig.API_VERSION_V3: + return parseOrderNotifyV3(rawNotify); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e); + throw buildPayException(e); + } + } + + private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException { + WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); + Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); + // 转换结果 + return PayOrderNotifyRespDTO + .builder() + .orderExtensionNo(notifyResult.getOutTradeNo()) + .channelOrderNo(notifyResult.getTransactionId()) + .channelUserId(notifyResult.getOpenid()) + .successTime(parseDateV2(notifyResult.getTimeEnd())) + .build(); + } + + private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException { + WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult(); + // 转换结果 + Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"), + "支付结果非 SUCCESS"); + return PayOrderNotifyRespDTO.builder() + .orderExtensionNo(result.getOutTradeNo()) + .channelOrderNo(result.getTradeState()) + .channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null) + .successTime(parseDateV3(result.getSuccessTime())) + .build(); + } + + // ========== 各种工具方法 ========== + + static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw new IllegalArgumentException("支付请求的 openid 不能为空!"); + } + return openid; + } + + static PayException buildPayException(WxPayException e) { + return new PayException(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode()), + ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg())); + } + + static String formatDateV2(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyyMMddHHmmss"); + } + + static LocalDateTime parseDateV2(String time) { + return LocalDateTimeUtil.parse(time, "yyyyMMddHHmmss"); + } + + static String formatDateV3(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyy-MM-dd'T'HH:mm:ssXXX"); + } + + static LocalDateTime parseDateV3(String time) { + return LocalDateTimeUtil.parse(time, "yyyy-MM-dd'T'HH:mm:ssXXX"); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXLitePayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXLitePayClient.java index 65d087a54..99ae9d057 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXLitePayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXLitePayClient.java @@ -39,11 +39,11 @@ import java.util.Objects; * @author zwy */ @Slf4j -public class WXLitePayClient extends AbstractPayClient { +public class WXLitePayClient extends AbstractPayClient { private WxPayService client; - public WXLitePayClient(Long channelId, WXPayClientConfig config) { + public WXLitePayClient(Long channelId, WxPayClientConfig config) { super(channelId, PayChannelEnum.WX_LITE.getCode(), config); } @@ -99,7 +99,7 @@ public class WXLitePayClient extends AbstractPayClient { WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() .outTradeNo(reqDTO.getMerchantOrderId()) .body(reqDTO.getBody()) - .totalFee(reqDTO.getAmount().intValue()) // 单位分 + .totalFee(reqDTO.getAmount()) // 单位分 .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) // v2的时间格式 .spbillCreateIp(reqDTO.getUserIp()) .openid(getOpenid(reqDTO)) @@ -117,9 +117,7 @@ public class WXLitePayClient extends AbstractPayClient { request.setDescription(reqDTO.getBody()); request.setAmount(new WxPayUnifiedOrderV3Request .Amount() - .setTotal(reqDTO - .getAmount() - .intValue())); // 单位分 + .setTotal(reqDTO.getAmount())); // 单位分 request.setTimeExpire(DateUtil.format(Date.from(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()).toInstant()), "yyyy-MM-dd'T'HH:mm:ssXXX")); // v3的时间格式 request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); @@ -149,9 +147,9 @@ public class WXLitePayClient extends AbstractPayClient { log.info("[parseOrderNotify][微信支付回调data数据:{}]", data.getBody()); // 微信支付 v2 回调结果处理 switch (config.getApiVersion()) { - case WXPayClientConfig.API_VERSION_V2: + case WxPayClientConfig.API_VERSION_V2: return parseOrderNotifyV2(data); - case WXPayClientConfig.API_VERSION_V3: + case WxPayClientConfig.API_VERSION_V3: return parseOrderNotifyV3(data); default: throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXNativePayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXNativePayClient.java index a4f180712..f15c8b799 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXNativePayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXNativePayClient.java @@ -37,11 +37,11 @@ import java.util.Objects; * @author zwy */ @Slf4j -public class WXNativePayClient extends AbstractPayClient { +public class WXNativePayClient extends AbstractPayClient { private WxPayService client; - public WXNativePayClient(Long channelId, WXPayClientConfig config) { + public WXNativePayClient(Long channelId, WxPayClientConfig config) { super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config); } @@ -134,9 +134,9 @@ public class WXNativePayClient extends AbstractPayClient { log.info("微信支付回调data数据:{}", data.getBody()); // 微信支付 v2 回调结果处理 switch (config.getApiVersion()) { - case WXPayClientConfig.API_VERSION_V2: + case WxPayClientConfig.API_VERSION_V2: return parseOrderNotifyV2(data); - case WXPayClientConfig.API_VERSION_V3: + case WxPayClientConfig.API_VERSION_V3: return parseOrderNotifyV3(data); default: throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPubPayClient.java deleted file mode 100644 index 390428bf3..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPubPayClient.java +++ /dev/null @@ -1,201 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.date.TemporalAccessorUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.io.FileUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; -import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.PayDisplayModeEnum; -import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; -import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; -import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; -import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; -import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; -import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; -import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; -import com.github.binarywang.wxpay.config.WxPayConfig; -import com.github.binarywang.wxpay.constant.WxPayConstants; -import com.github.binarywang.wxpay.exception.WxPayException; -import com.github.binarywang.wxpay.service.WxPayService; -import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Objects; - -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - -/** - * 微信支付(公众号)的 PayClient 实现类 - * - * @author 芋道源码 - */ -@Slf4j -public class WXPubPayClient extends AbstractPayClient { - - private WxPayService client; - - public WXPubPayClient(Long channelId, WXPayClientConfig config) { - super(channelId, PayChannelEnum.WX_PUB.getCode(), config); - } - - @Override - protected void doInit() { - WxPayConfig payConfig = new WxPayConfig(); - BeanUtil.copyProperties(config, payConfig, "keyContent"); - payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式 -// if (StrUtil.isNotEmpty(config.getKeyContent())) { -// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8)); -// } - if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 - payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); - } - if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { - // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决 - payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); - } - // 真实客户端 - this.client = new WxPayServiceImpl(); - client.setConfig(payConfig); - } - - @Override - public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { - WxPayMpOrderResult response = null; - try { - switch (config.getApiVersion()) { - case WXPayClientConfig.API_VERSION_V2: - response = this.unifiedOrderV2(reqDTO); - break; - case WXPayClientConfig.API_VERSION_V3: - WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO); - // 将 V3 的结果,统一转换成 V2。返回的字段是一致的 - response = new WxPayMpOrderResult(); - BeanUtil.copyProperties(responseV3, response, true); - break; - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } catch (WxPayException e) { - log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e); -// return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"), -// ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping); - System.out.println(); - } - return new PayOrderUnifiedRespDTO().setDisplayMode(PayDisplayModeEnum.CUSTOM.getMode()) - .setDisplayContent(JsonUtils.toJsonString(response)); - } - - - private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() - .outTradeNo(reqDTO.getMerchantOrderId()) - .body(reqDTO.getBody()) - .totalFee(reqDTO.getAmount()) // 单位分 - .timeExpire(formatDate(reqDTO.getExpireTime())) - .spbillCreateIp(reqDTO.getUserIp()) - .openid(getOpenid(reqDTO)) - .notifyUrl(reqDTO.getNotifyUrl()) - .build(); - // 执行请求 - return client.createOrder(request); - } - - private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); - request.setOutTradeNo(reqDTO.getMerchantOrderId()); - request.setDescription(reqDTO.getBody()); - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 - request.setTimeExpire(formatDate(reqDTO.getExpireTime())); - request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); - request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - // 执行请求 - return client.createOrderV3(TradeTypeEnum.JSAPI, request); - } - - private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { - String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); - if (StrUtil.isEmpty(openid)) { - throw new IllegalArgumentException("支付请求的 openid 不能为空!"); - } - return openid; - } - - /** - * - * 微信支付回调 分v2 和v3 的处理方式 - * - * @param data 通知结果 - * @return 支付回调对象 - * @throws WxPayException 微信异常类 - */ -// @Override - public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyReqDTO data) throws WxPayException { - log.info("[parseOrderNotify][微信支付回调data数据: {}]", data.getBody()); - // 微信支付 v2 回调结果处理 - switch (config.getApiVersion()) { - case WXPayClientConfig.API_VERSION_V2: - return parseOrderNotifyV2(data); - case WXPayClientConfig.API_VERSION_V3: - return parseOrderNotifyV3(data); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } - - private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException { - WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result = client.parseOrderNotifyV3Result(data.getBody(), null); - WxPayOrderNotifyV3Result.DecryptNotifyResult result = wxPayOrderNotifyV3Result.getResult(); - // 转换结果 - Assert.isTrue(Objects.equals(wxPayOrderNotifyV3Result.getResult().getTradeState(), "SUCCESS"), - "支付结果非 SUCCESS"); - return PayOrderNotifyRespDTO - .builder() - .orderExtensionNo(result.getOutTradeNo()) - .channelOrderNo(result.getTradeState()) - .successTime(LocalDateTimeUtil.parse(result.getSuccessTime(), "yyyy-MM-dd'T'HH:mm:ssXXX")) - .build(); - } - - private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException { - WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); - Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); - // 转换结果 - return PayOrderNotifyRespDTO - .builder() - .orderExtensionNo(notifyResult.getOutTradeNo()) - .channelOrderNo(notifyResult.getTransactionId()) - .channelUserId(notifyResult.getOpenid()) - .successTime(LocalDateTimeUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss")) - .build(); - - } - - @Override - protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { - // TODO 需要实现 - throw new UnsupportedOperationException(); - } - - private static String formatDate(LocalDateTime time) { -// return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyy-MM-dd'T'HH:mm:ssXXX"); - return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyyMMddHHmmss"); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java similarity index 97% rename from yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPayClientConfig.java rename to yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java index 462f47ab1..48228f379 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WXPayClientConfig.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java @@ -18,15 +18,17 @@ import java.util.Set; * @author 芋道源码 */ @Data -public class WXPayClientConfig implements PayClientConfig { +public class WxPayClientConfig implements PayClientConfig { /** * API 版本 - V2 + * * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1 */ public static final String API_VERSION_V2 = "v2"; /** * API 版本 - V3 + * * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml */ public static final String API_VERSION_V3 = "v3"; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java new file mode 100644 index 000000000..0d508b7fa --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO; +import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.PayDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class WxPubPayClient extends AbstractWxPayClient { + + public WxPubPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_PUB.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.JSAPI); + } + + @Override + protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getMerchantOrderId()) + .body(reqDTO.getBody()) + .totalFee(reqDTO.getAmount()) // 单位分 + .timeExpire(formatDateV2(reqDTO.getExpireTime())) + .spbillCreateIp(reqDTO.getUserIp()) + .openid(getOpenid(reqDTO)) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return new PayOrderUnifiedRespDTO().setDisplayMode(PayDisplayModeEnum.CUSTOM.getMode()) + .setDisplayContent(JsonUtils.toJsonString(response)); + } + + @Override + protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getMerchantOrderId()); + request.setDescription(reqDTO.getBody()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 + request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + // 执行请求 + WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); + + // 转换结果 + return new PayOrderUnifiedRespDTO().setDisplayMode(PayDisplayModeEnum.CUSTOM.getMode()) + .setDisplayContent(JsonUtils.toJsonString(response)); + } + + @Override + protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + // TODO 需要实现 + throw new UnsupportedOperationException(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java index d018495a5..506ac893d 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.pay.core.enums; import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig; import lombok.AllArgsConstructor; import lombok.Getter; @@ -17,10 +17,10 @@ import lombok.Getter; @AllArgsConstructor public enum PayChannelEnum { - WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页 - WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class), - WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class), - WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class), + WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页 + WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class), + WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class), + WX_NATIVE("wx_native", "微信 native 支付", WxPayClientConfig.class), ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java index 4d560d86f..ce16cbaf9 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDT import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient; import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPubPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPubPayClient; import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,15 +27,15 @@ public class PayClientFactoryImplIntegrationTest { private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); /** - * {@link WXPubPayClient} 的 V2 版本 + * {@link WxPubPayClient} 的 V2 版本 */ @Test public void testCreatePayClient_WX_PUB_V2() { // 创建配置 - WXPayClientConfig config = new WXPayClientConfig(); + WxPayClientConfig config = new WxPayClientConfig(); config.setAppId("wx041349c6f39b268b"); config.setMchId("1545083881"); - config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setApiVersion(WxPayClientConfig.API_VERSION_V2); config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); // 创建客户端 Long channelId = RandomUtil.randomLong(); @@ -48,15 +48,15 @@ public class PayClientFactoryImplIntegrationTest { } /** - * {@link WXPubPayClient} 的 V3 版本 + * {@link WxPubPayClient} 的 V3 版本 */ @Test public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { // 创建配置 - WXPayClientConfig config = new WXPayClientConfig(); + WxPayClientConfig config = new WxPayClientConfig(); config.setAppId("wx041349c6f39b268b"); config.setMchId("1545083881"); - config.setApiVersion(WXPayClientConfig.API_VERSION_V3); + config.setApiVersion(WxPayClientConfig.API_VERSION_V3); config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/merchant/PayChannelServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/merchant/PayChannelServiceTest.java index 50f12baa0..eb621ea67 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/merchant/PayChannelServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/merchant/PayChannelServiceTest.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig; import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.pay.controller.admin.merchant.vo.channel.PayChannelCreateReqVO; @@ -49,7 +49,7 @@ public class PayChannelServiceTest extends BaseDbUnitTest { @Test public void testCreateWechatVersion2Channel_success() { // 准备参数 - WXPayClientConfig v2Config = getV2Config(); + WxPayClientConfig v2Config = getV2Config(); PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { o.setCode(PayChannelEnum.WX_PUB.getCode()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); @@ -70,7 +70,7 @@ public class PayChannelServiceTest extends BaseDbUnitTest { @Test public void testCreateWechatVersion3Channel_success() { // 准备参数 - WXPayClientConfig v3Config = getV3Config(); + WxPayClientConfig v3Config = getV3Config(); PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { o.setCode(PayChannelEnum.WX_PUB.getCode()); o.setStatus(CommonStatusEnum.ENABLE.getStatus()); @@ -346,22 +346,22 @@ public class PayChannelServiceTest extends BaseDbUnitTest { assertPojoEquals(payClientConfig, list.get(0).getConfig()); } - public WXPayClientConfig getV2Config() { - return new WXPayClientConfig() + public WxPayClientConfig getV2Config() { + return new WxPayClientConfig() .setAppId("APP00001") .setMchId("MCH00001") - .setApiVersion(WXPayClientConfig.API_VERSION_V2) + .setApiVersion(WxPayClientConfig.API_VERSION_V2) .setMchKey("dsa1d5s6a1d6sa16d1sa56d15a61das6") .setApiV3Key("") .setPrivateCertContent("") .setPrivateKeyContent(""); } - public WXPayClientConfig getV3Config() { - return new WXPayClientConfig() + public WxPayClientConfig getV3Config() { + return new WxPayClientConfig() .setAppId("APP00001") .setMchId("MCH00001") - .setApiVersion(WXPayClientConfig.API_VERSION_V3) + .setApiVersion(WxPayClientConfig.API_VERSION_V3) .setMchKey("") .setApiV3Key("sdadasdsadadsa") .setPrivateKeyContent("dsa445das415d15asd16ad156as")