diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java new file mode 100644 index 000000000..56baaed95 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.common.util.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; + +import java.io.File; + +/** + * 文件工具类 + * + * @author 芋道源码 + */ +public class FileUtils { + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml index 862a41e45..668ce5d3d 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml @@ -11,7 +11,10 @@ yudao-spring-boot-starter-biz-pay ${artifactId} - 支付拓展,基于 IJPay 简单封装,支持微信、支付宝等常见支付渠道 + 支付拓展,接入国内多个支付渠道 + 1. 支付宝,基于官方 SDK 接入 + 2. 微信支付,基于 weixin-java-pay 接入 + @@ -34,17 +37,36 @@ slf4j-api - - com.github.javen205 - IJPay-AliPay - 2.7.8 + com.fasterxml.jackson.core + jackson-databind - com.github.javen205 - IJPay-WxPay - 2.7.8 + com.fasterxml.jackson.core + jackson-core + + + + + + + + + + + + + com.alipay.sdk + alipay-sdk-java + 4.17.9.ALL + + + com.github.binarywang + weixin-java-pay + 4.1.9.B + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java new file mode 100644 index 000000000..88be97f00 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; +import lombok.extern.slf4j.Slf4j; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see PayCommonResult + * @see PayFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayCodeMapping { + + public final ErrorCode apply(String apiCode, String apiMsg) { + if (apiCode == null) { + log.error("[apply][API 错误码为空,请排查]"); + return PayFrameworkErrorCodeConstants.EXCEPTION; + } + ErrorCode errorCode = this.apply0(apiCode, apiMsg); + if (errorCode == null) { + log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg); + return PayFrameworkErrorCodeConstants.PAY_UNKNOWN; + } + return errorCode; + } + + protected abstract ErrorCode apply0(String apiCode, String apiMsg); + +} 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 1fe3690e1..05278a9ce 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 @@ -18,6 +18,6 @@ public interface PayClient { Long getId(); // TODO 缺少注释 - CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCodeMapping.java deleted file mode 100644 index 42238af77..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCodeMapping.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client; - -import cn.iocoder.yudao.framework.common.exception.ErrorCode; -import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; - -import java.util.function.Function; - -/** - * 将 API 的错误码,转换为通用的错误码 - * - * @see PayCommonResult - * @see PayFrameworkErrorCodeConstants - * - * @author 芋道源码 - */ -public interface PayCodeMapping extends Function { -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java index 8346a845f..8837a0ac9 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java @@ -35,16 +35,13 @@ public class PayCommonResult extends CommonResult { private PayCommonResult() { } - public static PayCommonResult build(String apiCode, String apiMsg, T data, PayCodeMapping codeMapping) { + public static PayCommonResult build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) { Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); PayCommonResult result = new PayCommonResult().setApiCode(apiCode).setApiMsg(apiMsg); result.setData(data); // 翻译错误码 if (codeMapping != null) { - ErrorCode errorCode = codeMapping.apply(apiCode); - if (errorCode == null) { - errorCode = PayFrameworkErrorCodeConstants.EXCEPTION; - } + ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg); result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); } return result; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java index 9e1f0412e..5d8c087ee 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.dto; import lombok.Data; import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.NotEmpty; @@ -39,6 +40,12 @@ public class PayOrderUnifiedReqDTO { @NotEmpty(message = "商品描述信息不能为空") @Length(max = 128, message = "商品描述信息长度不能超过128") private String body; + /** + * 支付结果的回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的回调地址必须是 URL 格式") + private String notifyUrl; // ========== 订单相关字段 ========== @@ -55,4 +62,7 @@ public class PayOrderUnifiedReqDTO { @NotNull(message = "支付过期时间不能为空") private Date expireTime; + // ========== 拓展参数 ========== + // TODO 芋艿:待完善 + } 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 77defbc4e..97850bdcf 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 @@ -2,7 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl; import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; -import cn.iocoder.yudao.framework.pay.core.client.PayCodeMapping; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; import lombok.extern.slf4j.Slf4j; /** @@ -24,7 +24,7 @@ public abstract class AbstractPayClient implemen /** * 错误码枚举类 */ - protected PayCodeMapping codeMapping; + protected AbstractPayCodeMapping codeMapping; /** * 支付配置 */ @@ -34,7 +34,7 @@ public abstract class AbstractPayClient implemen return amount / 100.0; } - public AbstractPayClient(Long channelId, String channelCode, Config config, PayCodeMapping codeMapping) { + public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) { this.channelId = channelId; this.channelCode = channelCode; this.codeMapping = codeMapping; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.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/alipay/AlipayPayConfig.java rename to yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java index 26b1fc8d3..f5106ecaa 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayConfig.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -11,7 +11,7 @@ import lombok.Data; * @author 芋道源码 */ @Data -public class AlipayPayConfig implements PayClientConfig { +public class AlipayPayClientConfig implements PayClientConfig { /** * 网关地址 - 线上 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java index a61a8cce1..16ded752e 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java @@ -1,12 +1,17 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; import cn.iocoder.yudao.framework.common.exception.ErrorCode; -import cn.iocoder.yudao.framework.pay.core.client.PayCodeMapping; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; -public class AlipayPayCodeMapping implements PayCodeMapping { +/** + * 支付宝的 PayCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class AlipayPayCodeMapping extends AbstractPayCodeMapping { @Override - public ErrorCode apply(String s) { + protected ErrorCode apply0(String apiCode, String apiMsg) { return null; } 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 1be679ca4..58473185d 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 @@ -12,6 +12,9 @@ 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 static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; /** * 支付宝【扫码支付】的 PayClient 实现类 @@ -19,11 +22,12 @@ import lombok.SneakyThrows; * * @author 芋道源码 */ -public class AlipayQrPayClient extends AbstractPayClient { +@Slf4j +public class AlipayQrPayClient extends AbstractPayClient { private DefaultAlipayClient client; - public AlipayQrPayClient(Long channelId, String channelCode, AlipayPayConfig config) { + public AlipayQrPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { super(channelId, channelCode, config, new AlipayPayCodeMapping()); } @@ -32,17 +36,18 @@ public class AlipayQrPayClient extends AbstractPayClient { protected void doInit() { AlipayConfig alipayConfig = new AlipayConfig(); BeanUtil.copyProperties(config, alipayConfig, false); + // 真实客户端 this.client = new DefaultAlipayClient(alipayConfig); } @Override - public CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + public CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { // 构建 AlipayTradePrecreateModel 请求 AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setSubject(reqDTO.getSubject()); model.setBody(reqDTO.getBody()); - model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); + model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元 // TODO 芋艿:clientIp + expireTime // 构建 AlipayTradePrecreateRequest AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); @@ -53,6 +58,7 @@ public class AlipayQrPayClient extends AbstractPayClient { try { response = client.execute(request); } catch (AlipayApiException e) { + log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e); return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); } @@ -64,10 +70,10 @@ public class AlipayQrPayClient extends AbstractPayClient { } public static void main(String[] args) { - AlipayPayConfig config = new AlipayPayConfig(); + AlipayPayClientConfig config = new AlipayPayClientConfig(); config.setAppId("2021000118634035"); - config.setServerUrl(AlipayPayConfig.SERVER_URL_SANDBOX); - config.setSignType(AlipayPayConfig.SIGN_TYPE_DEFAULT); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); AlipayQrPayClient client = new AlipayQrPayClient(1L, "biu", config); @@ -79,4 +85,5 @@ public class AlipayQrPayClient extends AbstractPayClient { reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); client.unifiedOrder(reqDTO); } + } 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 d2102ab6c..b89d1208b 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 @@ -19,11 +19,11 @@ import lombok.SneakyThrows; * * @author 芋道源码 */ -public class AlipayWapPayClient extends AbstractPayClient { +public class AlipayWapPayClient extends AbstractPayClient { private DefaultAlipayClient client; - public AlipayWapPayClient(Long channelId, String channelCode, AlipayPayConfig config) { + public AlipayWapPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { super(channelId, channelCode, config, new AlipayPayCodeMapping()); } @@ -36,7 +36,7 @@ public class AlipayWapPayClient extends AbstractPayClient { } @Override - public CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + public CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { // 构建 AlipayTradeWapPayModel 请求 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); model.setOutTradeNo(reqDTO.getMerchantOrderId()); @@ -65,10 +65,10 @@ public class AlipayWapPayClient extends AbstractPayClient { } public static void main(String[] args) { - AlipayPayConfig config = new AlipayPayConfig(); + AlipayPayClientConfig config = new AlipayPayClientConfig(); config.setAppId("2021000118634035"); - config.setServerUrl(AlipayPayConfig.SERVER_URL_SANDBOX); - config.setSignType(AlipayPayConfig.SIGN_TYPE_DEFAULT); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); AlipayWapPayClient client = new AlipayWapPayClient(1L, "biubiubiu", config); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java new file mode 100644 index 000000000..d52e83a5c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*; + +/** + * 微信支付 PayCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class WXCodeMapping extends AbstractPayCodeMapping { + + /** + * 错误码 - 成功 + * 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下 + */ + public static final String CODE_SUCCESS = "SUCCESS"; + /** + * 错误提示 - 成功 + */ + public static final String MESSAGE_SUCCESS = "成功"; + + @Override + protected ErrorCode apply0(String apiCode, String apiMsg) { + if (Objects.equals(apiCode, CODE_SUCCESS)) { + return GlobalErrorCodeConstants.SUCCESS; + } + if (Objects.equals(apiCode, "FAIL")) { + if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) { + return PAY_CONFIG_APP_ID_ERROR; + } + if (Objects.equals(apiMsg, "签名错误,请检查后再试") + || Objects.equals(apiMsg, "签名错误")) { + return PAY_CONFIG_SIGN_ERROR; + } + } + if (Objects.equals(apiCode, "PARAM_ERROR")) { + if (Objects.equals(apiMsg, "无效的openid")) { + return PAY_OPENID_ERROR; + } + } + return null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java new file mode 100644 index 000000000..27bab4c1b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +// TODO 芋艿:参数校验 +/** + * 微信支付的 PayClientConfig 实现类 + * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class WXPayClientConfig implements PayClientConfig { + + // TODO 芋艿:V2 or V3 客户端 + /** + * 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"; + + /** + * 公众号或者小程序的 appid + */ + private String appId; + /** + * 商户号 + */ + private String mchId; + /** + * API 版本 + */ + private String apiVersion; + + // ========== V2 版本的参数 ========== + + /** + * 商户密钥 + */ + private String mchKey; +// /** +// * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径. +// * 对应的字符串 +// * +// * 注意,可通过 {@link #main(String[])} 读取 +// */ +// private String keyContent; + + // ========== V3 版本的参数 ========== + /** + * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. + * 对应的字符串 + * + * 注意,可通过 {@link #main(String[])} 读取 + */ + private String privateKeyContent; + /** + * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. + * 对应的字符串 + * + * 注意,可通过 {@link #main(String[])} 读取 + */ + private String privateCertContent; + /** + * apiV3 秘钥值 + */ + private String apiV3Key; + + public static void main(String[] args) throws FileNotFoundException { + String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"; +// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem"; +// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"; + System.out.println(IoUtil.readUtf8(new FileInputStream(path))); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java new file mode 100644 index 000000000..cb0bce6fb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java @@ -0,0 +1,148 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.wx; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult; +import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +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.io.FileInputStream; +import java.io.FileNotFoundException; + +import static cn.hutool.core.util.ObjectUtil.defaultIfNull; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS; +import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class WXPubPayClient extends AbstractPayClient { + + private WxPayService client; + + public WXPubPayClient(Long channelId, String channelCode, WXPayClientConfig config) { + super(channelId, channelCode, config, new WXCodeMapping()); + } + + @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 CommonResult unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + WxPayMpOrderResult response; + 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(defaultIfNull(e.getErrCode(), e.getReturnCode()), + defaultIfNull(e.getErrCodeDes(), e.getReturnMsg()),null, codeMapping); + } + return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping); + } + + private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getMerchantOrderId()) + // TODO 芋艿:貌似没 title? + .body(reqDTO.getBody()) + .totalFee(reqDTO.getAmount()) // 单位分 + .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")) + .spbillCreateIp(reqDTO.getClientIp()) + .openid("ockUAwIZ-0OeMZl9ogcZ4ILrGba0") // TODO 芋艿:先随便写死 + .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()); + // TODO 芋艿:貌似没 title? + request.setDescription(reqDTO.getBody()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 + request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss")); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid("ockUAwIZ-0OeMZl9ogcZ4ILrGba0")); // TODO 芋艿:先随便写死 + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getClientIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + // 执行请求 + return client.createOrderV3(TradeTypeEnum.JSAPI, request); + } + + public static void main(String[] args) throws FileNotFoundException { + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V3); +// config.setKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"))); + 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"); + + WXPubPayClient client = new WXPubPayClient(1L, "biu", config); + client.init(); + + PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); + reqDTO.setAmount(123); + reqDTO.setSubject("IPhone 13"); + reqDTO.setBody("biubiubiu"); + reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); + reqDTO.setClientIp("127.0.0.1"); + reqDTO.setNotifyUrl("http://127.0.0.1:8080"); + CommonResult result = client.unifiedOrder(reqDTO); + System.out.println(result); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java index 524653911..92b516029 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java @@ -11,36 +11,15 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; */ public interface PayFrameworkErrorCodeConstants { - ErrorCode PAY_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); + ErrorCode PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析"); -// // ========== 权限 / 限流等相关 2001000100 ========== -// -// ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); -// // 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信 -// ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); -// -// // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 -// ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); -// // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 -// ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); -// -// ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); -// -// // ========== 模板相关 2001000200 ========== -// ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 -// ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); -// -// // ========== 签名相关 2001000300 ========== -// ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); -// -// // ========== 账户相关 2001000400 ========== -// ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); -// ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); -// -// // ========== 其它相关 2001000900 开头 ========== -// ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); -// ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); -// ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); + // ========== 配置相关相关 2002000100 ========== + ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确"); + ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey + + + // ========== 其它相关 2002000900 开头 ========== + ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过 ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");