From 6dcb1277b322195bbba7340585592e23c4a2ca17 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 28 Feb 2024 21:03:40 +0800 Subject: [PATCH] ✨ 全局:迁移 pay 组件到 pay 模块,迁移 flowable 组件到 bpm 模块 --- yudao-dependencies/pom.xml | 10 ---------- yudao-framework/pom.xml | 6 +----- yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml | 76 ---------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java | 22 ---------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java | 98 -------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java | 26 -------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java | 38 -------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java | 141 --------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java | 92 -------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java | 115 ------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java | 70 ---------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java | 109 ------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java | 78 ------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java | 17 ----------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java | 250 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java | 31 ------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java | 95 ----------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java | 342 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java | 59 ----------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java | 85 ------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java | 107 ----------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java | 69 --------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java | 66 ------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java | 58 ---------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java | 80 -------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java | 484 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java | 63 --------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java | 107 ----------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java | 22 ---------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java | 58 ---------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java | 110 -------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java | 80 -------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java | 68 -------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java | 29 ----------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java | 56 -------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java | 32 -------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java | 45 --------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java | 44 -------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1 - yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java | 133 ------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java | 221 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java | 170 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java | 131 ----------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java | 147 --------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java | 111 --------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java | 123 --------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java | 83 ----------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/pom.xml | 37 ------------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java | 46 ---------------------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java | 40 ---------------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java | 22 ---------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java | 1 - yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java | 309 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java | 32 -------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java | 35 ----------------------------------- yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java | 1 - yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1 - yudao-module-bpm/pom.xml | 1 + yudao-module-bpm/yudao-module-bpm-biz/pom.xml | 2 ++ yudao-module-bpm/yudao-spring-boot-starter-flowable/pom.xml | 37 +++++++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java | 46 ++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java | 40 ++++++++++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java | 22 ++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java | 1 + yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java | 32 ++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java | 35 +++++++++++++++++++++++++++++++++++ yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java | 1 + yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1 + yudao-module-mall/yudao-module-trade-biz/pom.xml | 6 ------ yudao-module-pay/pom.xml | 2 +- yudao-module-pay/yudao-module-pay-biz/pom.xml | 3 ++- yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java | 22 ++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java | 26 ++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java | 38 ++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java | 17 +++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java | 31 +++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java | 342 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java | 484 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java | 22 ++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java | 29 +++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java | 32 ++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1 + yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 117 files changed, 4973 insertions(+), 4989 deletions(-) delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/pom.xml delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/pom.xml create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java create mode 100644 yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java create mode 100644 yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index befb06e..942edef 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -113,11 +113,6 @@ cn.iocoder.boot - yudao-spring-boot-starter-biz-pay - ${revision} - - - cn.iocoder.boot yudao-spring-boot-starter-biz-tenant ${revision} @@ -391,11 +386,6 @@ - cn.iocoder.boot - yudao-spring-boot-starter-flowable - ${revision} - - org.flowable flowable-spring-boot-starter-process ${flowable.version} diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml index c260ff9..aab0db3 100644 --- a/yudao-framework/pom.xml +++ b/yudao-framework/pom.xml @@ -15,6 +15,7 @@ yudao-spring-boot-starter-redis yudao-spring-boot-starter-web yudao-spring-boot-starter-security + yudao-spring-boot-starter-websocket yudao-spring-boot-starter-monitor yudao-spring-boot-starter-protection @@ -26,14 +27,9 @@ yudao-spring-boot-starter-biz-operatelog yudao-spring-boot-starter-biz-dict - - yudao-spring-boot-starter-biz-pay yudao-spring-boot-starter-biz-tenant yudao-spring-boot-starter-biz-data-permission yudao-spring-boot-starter-biz-ip - - yudao-spring-boot-starter-flowable - yudao-spring-boot-starter-websocket yudao-framework diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml deleted file mode 100644 index a13b7dd..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - yudao-framework - cn.iocoder.boot - ${revision} - - 4.0.0 - - yudao-spring-boot-starter-biz-pay - ${project.artifactId} - 支付拓展,接入国内多个支付渠道 - 1. 支付宝,基于官方 SDK 接入 - 2. 微信支付,基于 weixin-java-pay 接入 - - - - - cn.iocoder.boot - yudao-common - - - - - org.springframework.boot - spring-boot-starter - - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.slf4j - slf4j-api - - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-core - - - - - com.alipay.sdk - alipay-sdk-java - 4.35.79.ALL - - - org.bouncycastle - bcprov-jdk15on - - - - - com.github.binarywang - weixin-java-pay - - - - - cn.iocoder.boot - yudao-spring-boot-starter-test - test - - - - diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java deleted file mode 100644 index e57f4ac..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.framework.pay.config; - -import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; -import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * 支付配置类 - * - * @author 芋道源码 - */ -@AutoConfiguration -public class YudaoPayAutoConfiguration { - - @Bean - public PayClientFactory payClientFactory() { - return new PayClientFactoryImpl(); - } - -} 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 deleted file mode 100644 index 86e3566..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; - -import java.util.Map; - -/** - * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 - * - * @author 芋道源码 - */ -public interface PayClient { - - /** - * 获得渠道编号 - * - * @return 渠道编号 - */ - Long getId(); - - // ============ 支付相关 ========== - - /** - * 调用支付渠道,统一下单 - * - * @param reqDTO 下单信息 - * @return 支付订单信息 - */ - PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); - - /** - * 解析 order 回调数据 - * - * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 - * @param body HTTP 回调接口的 request body - * @return 支付订单信息 - */ - PayOrderRespDTO parseOrderNotify(Map params, String body); - - /** - * 获得支付订单信息 - * - * @param outTradeNo 外部订单号 - * @return 支付订单信息 - */ - PayOrderRespDTO getOrder(String outTradeNo); - - // ============ 退款相关 ========== - - /** - * 调用支付渠道,进行退款 - * - * @param reqDTO 统一退款请求信息 - * @return 退款信息 - */ - PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); - - /** - * 解析 refund 回调数据 - * - * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 - * @param body HTTP 回调接口的 request body - * @return 支付订单信息 - */ - PayRefundRespDTO parseRefundNotify(Map params, String body); - - /** - * 获得退款订单信息 - * - * @param outTradeNo 外部订单号 - * @param outRefundNo 外部退款号 - * @return 退款订单信息 - */ - PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo); - - /** - * 调用渠道,进行转账 - * - * @param reqDTO 统一转账请求信息 - * @return 转账信息 - */ - PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO); - - /** - * 获得转账订单信息 - * - * @param outTradeNo 外部订单号 - * @param type 转账类型 - * @return 转账信息 - */ - PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type); -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java deleted file mode 100644 index beb3b41..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client; - -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -import jakarta.validation.Validator; - -/** - * 支付客户端的配置,本质是支付渠道的配置 - * 每个不同的渠道,需要不同的配置,通过子类来定义 - * - * @author 芋道源码 - */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) -// @JsonTypeInfo 注解的作用,Jackson 多态 -// 1. 序列化到时数据库时,增加 @class 属性。 -// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 -public interface PayClientConfig { - - /** - * 参数校验 - * - * @param validator 校验对象 - */ - void validate(Validator validator); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java deleted file mode 100644 index 53f1a8c..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client; - -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; - -/** - * 支付客户端的工厂接口 - * - * @author 芋道源码 - */ -public interface PayClientFactory { - - /** - * 获得支付客户端 - * - * @param channelId 渠道编号 - * @return 支付客户端 - */ - PayClient getPayClient(Long channelId); - - /** - * 创建支付客户端 - * - * @param channelId 渠道编号 - * @param channelCode 渠道编码 - * @param config 支付配置 - */ - void createOrUpdatePayClient(Long channelId, String channelCode, - Config config); - - /** - * 注册支付客户端 Class,用于模块中实现的 PayClient - * - * @param channel 支付渠道的编码的枚举 - * @param payClientClass 支付客户端 class - */ - void registerPayClientClass(PayChannelEnum channel, Class payClientClass); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java deleted file mode 100644 index 82050a6..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java +++ /dev/null @@ -1,141 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.order; - -import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 渠道支付订单 Response DTO - * - * @author 芋道源码 - */ -@Data -public class PayOrderRespDTO { - - /** - * 支付状态 - * - * 枚举:{@link PayOrderStatusRespEnum} - */ - private Integer status; - - /** - * 外部订单号 - * - * 对应 PayOrderExtensionDO 的 no 字段 - */ - private String outTradeNo; - - /** - * 支付渠道编号 - */ - private String channelOrderNo; - /** - * 支付渠道用户编号 - */ - private String channelUserId; - - /** - * 支付成功时间 - */ - private LocalDateTime successTime; - - /** - * 原始的同步/异步通知结果 - */ - private Object rawData; - - // ========== 主动发起支付时,会返回的字段 ========== - - /** - * 展示模式 - * - * 枚举 {@link PayOrderDisplayModeEnum} 类 - */ - private String displayMode; - /** - * 展示内容 - */ - private String displayContent; - - /** - * 调用渠道的错误码 - * - * 注意:这里返回的是业务异常,而是不系统异常。 - * 如果是系统异常,则会抛出 {@link PayException} - */ - private String channelErrorCode; - /** - * 调用渠道报错时,错误信息 - */ - private String channelErrorMsg; - - public PayOrderRespDTO() { - } - - /** - * 创建【WAITING】状态的订单返回 - */ - public static PayOrderRespDTO waitingOf(String displayMode, String displayContent, - String outTradeNo, Object rawData) { - PayOrderRespDTO respDTO = new PayOrderRespDTO(); - respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus(); - respDTO.displayMode = displayMode; - respDTO.displayContent = displayContent; - // 相对通用的字段 - respDTO.outTradeNo = outTradeNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【SUCCESS】状态的订单返回 - */ - public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime, - String outTradeNo, Object rawData) { - PayOrderRespDTO respDTO = new PayOrderRespDTO(); - respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); - respDTO.channelOrderNo = channelOrderNo; - respDTO.channelUserId = channelUserId; - respDTO.successTime = successTime; - // 相对通用的字段 - respDTO.outTradeNo = outTradeNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建指定状态的订单返回,适合支付渠道回调时 - */ - public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, - String outTradeNo, Object rawData) { - PayOrderRespDTO respDTO = new PayOrderRespDTO(); - respDTO.status = status; - respDTO.channelOrderNo = channelOrderNo; - respDTO.channelUserId = channelUserId; - respDTO.successTime = successTime; - // 相对通用的字段 - respDTO.outTradeNo = outTradeNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 - */ - public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg, - String outTradeNo, Object rawData) { - PayOrderRespDTO respDTO = new PayOrderRespDTO(); - respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus(); - respDTO.channelErrorCode = channelErrorCode; - respDTO.channelErrorMsg = channelErrorMsg; - // 相对通用的字段 - respDTO.outTradeNo = outTradeNo; - respDTO.rawData = rawData; - return respDTO; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java deleted file mode 100644 index 15be7a7..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java +++ /dev/null @@ -1,92 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.order; - -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import lombok.Data; -import org.hibernate.validator.constraints.Length; -import org.hibernate.validator.constraints.URL; - -import jakarta.validation.constraints.DecimalMin; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDateTime; -import java.util.Map; - -/** - * 统一下单 Request DTO - * - * @author 芋道源码 - */ -@Data -public class PayOrderUnifiedReqDTO { - - /** - * 用户 IP - */ - @NotEmpty(message = "用户 IP 不能为空") - private String userIp; - - // ========== 商户相关字段 ========== - - /** - * 外部订单号 - * - * 对应 PayOrderExtensionDO 的 no 字段 - */ - @NotEmpty(message = "外部订单编号不能为空") - private String outTradeNo; - /** - * 商品标题 - */ - @NotEmpty(message = "商品标题不能为空") - @Length(max = 32, message = "商品标题不能超过 32") - private String subject; - /** - * 商品描述信息 - */ - @Length(max = 128, message = "商品描述信息长度不能超过128") - private String body; - /** - * 支付结果的 notify 回调地址 - */ - @NotEmpty(message = "支付结果的回调地址不能为空") - @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") - private String notifyUrl; - /** - * 支付结果的 return 回调地址 - */ - @URL(message = "支付结果的 return 回调地址必须是 URL 格式") - private String returnUrl; - - // ========== 订单相关字段 ========== - - /** - * 支付金额,单位:分 - */ - @NotNull(message = "支付金额不能为空") - @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") - private Integer price; - - /** - * 支付过期时间 - */ - @NotNull(message = "支付过期时间不能为空") - private LocalDateTime expireTime; - - // ========== 拓展参数 ========== - /** - * 支付渠道的额外参数 - * - * 例如说,微信公众号需要传递 openid 参数 - */ - private Map channelExtras; - - /** - * 展示模式 - * - * 如果不传递,则每个支付渠道使用默认的方式 - * - * 枚举 {@link PayOrderDisplayModeEnum} - */ - private String displayMode; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java deleted file mode 100644 index 3184f27..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java +++ /dev/null @@ -1,115 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.refund; - -import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 渠道退款订单 Response DTO - * - * @author jason - */ -@Data -public class PayRefundRespDTO { - - /** - * 退款状态 - * - * 枚举 {@link PayRefundStatusRespEnum} - */ - private Integer status; - - /** - * 外部退款号 - * - * 对应 PayRefundDO 的 no 字段 - */ - private String outRefundNo; - - /** - * 渠道退款单号 - * - * 对应 PayRefundDO.channelRefundNo 字段 - */ - private String channelRefundNo; - - /** - * 退款成功时间 - */ - private LocalDateTime successTime; - - /** - * 原始的异步通知结果 - */ - private Object rawData; - - /** - * 调用渠道的错误码 - * - * 注意:这里返回的是业务异常,而是不系统异常。 - * 如果是系统异常,则会抛出 {@link PayException} - */ - private String channelErrorCode; - /** - * 调用渠道报错时,错误信息 - */ - private String channelErrorMsg; - - private PayRefundRespDTO() { - } - - /** - * 创建【WAITING】状态的退款返回 - */ - public static PayRefundRespDTO waitingOf(String channelRefundNo, - String outRefundNo, Object rawData) { - PayRefundRespDTO respDTO = new PayRefundRespDTO(); - respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus(); - respDTO.channelRefundNo = channelRefundNo; - // 相对通用的字段 - respDTO.outRefundNo = outRefundNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【SUCCESS】状态的退款返回 - */ - public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime, - String outRefundNo, Object rawData) { - PayRefundRespDTO respDTO = new PayRefundRespDTO(); - respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus(); - respDTO.channelRefundNo = channelRefundNo; - respDTO.successTime = successTime; - // 相对通用的字段 - respDTO.outRefundNo = outRefundNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【FAILURE】状态的退款返回 - */ - public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) { - return failureOf(null, null, - outRefundNo, rawData); - } - - /** - * 创建【FAILURE】状态的退款返回 - */ - public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg, - String outRefundNo, Object rawData) { - PayRefundRespDTO respDTO = new PayRefundRespDTO(); - respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus(); - respDTO.channelErrorCode = channelErrorCode; - respDTO.channelErrorMsg = channelErrorMsg; - // 相对通用的字段 - respDTO.outRefundNo = outRefundNo; - respDTO.rawData = rawData; - return respDTO; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java deleted file mode 100644 index 3101633..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java +++ /dev/null @@ -1,70 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.refund; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.hibernate.validator.constraints.URL; - -import jakarta.validation.constraints.DecimalMin; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -/** - * 统一 退款 Request DTO - * - * @author jason - */ -@Accessors(chain = true) -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Data -public class PayRefundUnifiedReqDTO { - - /** - * 外部订单号 - * - * 对应 PayOrderExtensionDO 的 no 字段 - */ - @NotEmpty(message = "外部订单编号不能为空") - private String outTradeNo; - - /** - * 外部退款号 - * - * 对应 PayRefundDO 的 no 字段 - */ - @NotEmpty(message = "退款请求单号不能为空") - private String outRefundNo; - - /** - * 退款原因 - */ - @NotEmpty(message = "退款原因不能为空") - private String reason; - - /** - * 支付金额,单位:分 - * - * 目前微信支付在退款的时候,必须传递该字段 - */ - @NotNull(message = "支付金额不能为空") - @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") - private Integer payPrice; - /** - * 退款金额,单位:分 - */ - @NotNull(message = "退款金额不能为空") - @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") - private Integer refundPrice; - - /** - * 退款结果的 notify 回调地址 - */ - @NotEmpty(message = "支付结果的回调地址不能为空") - @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") - private String notifyUrl; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java deleted file mode 100644 index 0f9b482..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java +++ /dev/null @@ -1,109 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.transfer; - -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 统一转账 Response DTO - * - * @author jason - */ -@Data -public class PayTransferRespDTO { - - /** - * 转账状态 - * - * 关联 {@link PayTransferStatusRespEnum#getStatus()} - */ - private Integer status; - - /** - * 外部转账单号 - * - */ - private String outTransferNo; - - /** - * 支付渠道编号 - */ - private String channelTransferNo; - - /** - * 支付成功时间 - */ - private LocalDateTime successTime; - - /** - * 原始的返回结果 - */ - private Object rawData; - - /** - * 调用渠道的错误码 - */ - private String channelErrorCode; - /** - * 调用渠道报错时,错误信息 - */ - private String channelErrorMsg; - - /** - * 创建【WAITING】状态的转账返回 - */ - public static PayTransferRespDTO waitingOf(String channelTransferNo, - String outTransferNo, Object rawData) { - PayTransferRespDTO respDTO = new PayTransferRespDTO(); - respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus(); - respDTO.channelTransferNo = channelTransferNo; - respDTO.outTransferNo = outTransferNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【IN_PROGRESS】状态的转账返回 - */ - public static PayTransferRespDTO dealingOf(String channelTransferNo, - String outTransferNo, Object rawData) { - PayTransferRespDTO respDTO = new PayTransferRespDTO(); - respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus(); - respDTO.channelTransferNo = channelTransferNo; - respDTO.outTransferNo = outTransferNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【CLOSED】状态的转账返回 - */ - public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg, - String outTransferNo, Object rawData) { - PayTransferRespDTO respDTO = new PayTransferRespDTO(); - respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus(); - respDTO.channelErrorCode = channelErrorCode; - respDTO.channelErrorMsg = channelErrorMsg; - // 相对通用的字段 - respDTO.outTransferNo = outTransferNo; - respDTO.rawData = rawData; - return respDTO; - } - - /** - * 创建【SUCCESS】状态的转账返回 - */ - public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime, - String outTransferNo, Object rawData) { - PayTransferRespDTO respDTO = new PayTransferRespDTO(); - respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus(); - respDTO.channelTransferNo = channelTransferNo; - respDTO.successTime = successTime; - // 相对通用的字段 - respDTO.outTransferNo = outTransferNo; - respDTO.rawData = rawData; - return respDTO; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java deleted file mode 100644 index 43afa86..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.transfer; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; -import lombok.Data; -import org.hibernate.validator.constraints.Length; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import java.util.Map; - -import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*; - -/** - * 统一转账 Request DTO - * - * @author jason - */ -@Data -public class PayTransferUnifiedReqDTO { - - /** - * 转账类型 - * - * 关联 {@link PayTransferTypeEnum#getType()} - */ - @NotNull(message = "转账类型不能为空") - @InEnum(PayTransferTypeEnum.class) - private Integer type; - - /** - * 用户 IP - */ - @NotEmpty(message = "用户 IP 不能为空") - private String userIp; - - @NotEmpty(message = "外部转账单编号不能为空") - private String outTransferNo; - - /** - * 转账金额,单位:分 - */ - @NotNull(message = "转账金额不能为空") - @Min(value = 1, message = "转账金额必须大于零") - private Integer price; - - /** - * 转账标题 - */ - @NotEmpty(message = "转账标题不能为空") - @Length(max = 128, message = "转账标题不能超过 128") - private String subject; - - /** - * 收款人姓名 - */ - @NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class}) - private String userName; - - /** - * 支付宝登录号 - */ - @NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class}) - private String alipayLogonId; - - /** - * 微信 openId - */ - @NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class}) - private String openid; - - /** - * 支付渠道的额外参数 - */ - private Map channelExtras; -} 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 deleted file mode 100644 index 75f4c39..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.exception; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 支付系统异常 Exception - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class PayException extends RuntimeException { - - public PayException(Throwable cause) { - super(cause); - } - -} 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 deleted file mode 100644 index 82d68b5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ /dev/null @@ -1,250 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl; - -import cn.iocoder.yudao.framework.common.exception.ServiceException; -import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; -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.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - -/** - * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 - * - * @author 芋道源码 - */ -@Slf4j -public abstract class AbstractPayClient implements PayClient { - - /** - * 渠道编号 - */ - private final Long channelId; - /** - * 渠道编码 - */ - @SuppressWarnings("FieldCanBeLocal") - private final String channelCode; - /** - * 支付配置 - */ - protected Config config; - - public AbstractPayClient(Long channelId, String channelCode, Config config) { - this.channelId = channelId; - this.channelCode = channelCode; - this.config = config; - } - - /** - * 初始化 - */ - public final void init() { - doInit(); - log.debug("[init][客户端({}) 初始化完成]", getId()); - } - - /** - * 自定义初始化 - */ - protected abstract void doInit(); - - public final void refresh(Config config) { - // 判断是否更新 - if (config.equals(this.config)) { - return; - } - log.info("[refresh][客户端({})发生变化,重新初始化]", getId()); - this.config = config; - // 初始化 - this.init(); - } - - @Override - public Long getId() { - return channelId; - } - - // ============ 支付相关 ========== - - @Override - public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { - ValidationUtils.validate(reqDTO); - // 执行统一下单 - PayOrderRespDTO resp; - try { - resp = doUnifiedOrder(reqDTO); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - // 系统异常,则包装成 PayException 异常抛出 - log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]", - getId(), toJsonString(reqDTO), ex); - throw buildPayException(ex); - } - return resp; - } - - protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) - throws Throwable; - - @Override - public final PayOrderRespDTO parseOrderNotify(Map params, String body) { - try { - return doParseOrderNotify(params, body); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]", - getId(), params, body, ex); - throw buildPayException(ex); - } - } - - protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body) - throws Throwable; - - @Override - public final PayOrderRespDTO getOrder(String outTradeNo) { - try { - return doGetOrder(outTradeNo); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]", - getId(), outTradeNo, ex); - throw buildPayException(ex); - } - } - - protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) - throws Throwable; - - // ============ 退款相关 ========== - - @Override - public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { - ValidationUtils.validate(reqDTO); - // 执行统一退款 - PayRefundRespDTO resp; - try { - resp = doUnifiedRefund(reqDTO); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - // 系统异常,则包装成 PayException 异常抛出 - log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]", - getId(), toJsonString(reqDTO), ex); - throw buildPayException(ex); - } - return resp; - } - - protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; - - @Override - public final PayRefundRespDTO parseRefundNotify(Map params, String body) { - try { - return doParseRefundNotify(params, body); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]", - getId(), params, body, ex); - throw buildPayException(ex); - } - } - - protected abstract PayRefundRespDTO doParseRefundNotify(Map params, String body) - throws Throwable; - - @Override - public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) { - try { - return doGetRefund(outTradeNo, outRefundNo); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]", - getId(), outTradeNo, outRefundNo, ex); - throw buildPayException(ex); - } - } - - protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) - throws Throwable; - - @Override - public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { - validatePayTransferReqDTO(reqDTO); - PayTransferRespDTO resp; - try { - resp = doUnifiedTransfer(reqDTO); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - // 系统异常,则包装成 PayException 异常抛出 - log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]", - getId(), toJsonString(reqDTO), ex); - throw buildPayException(ex); - } - return resp; - } - private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) { - PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType()); - switch (transferType) { - case ALIPAY_BALANCE: { - ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class); - break; - } - case WX_BALANCE: { - ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class); - break; - } - default: { - throw exception(NOT_IMPLEMENTED); - } - } - } - - @Override - public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) { - try { - return doGetTransfer(outTradeNo, type); - } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; - } catch (Throwable ex) { - log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]", - getId(), outTradeNo, type, ex); - throw buildPayException(ex); - } - } - - protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) - throws Throwable; - - protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) - throws Throwable; - - // ========== 各种工具方法 ========== - - private PayException buildPayException(Throwable ex) { - if (ex instanceof PayException) { - return (PayException) ex; - } - throw new PayException(ex); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java deleted file mode 100644 index 80581f3..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl; - -import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; -import lombok.Data; - -import jakarta.validation.Validator; - -/** - * 无需任何配置 PayClientConfig 实现类 - * - * @author jason - */ -@Data -public class NonePayClientConfig implements PayClientConfig { - - /** - * 配置名称 - *

- * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称 - */ - private String name; - - public NonePayClientConfig(){ - this.name = "none-config"; - } - - @Override - public void validate(Validator validator) { - // 无任何配置不需要校验 - } -} 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 deleted file mode 100644 index 0b39587..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ReflectUtil; -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.PayClientFactory; -import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.*; -import cn.iocoder.yudao.framework.pay.core.client.impl.mock.MockPayClient; -import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.*; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import static cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum.*; - -/** - * 支付客户端的工厂实现类 - * - * @author 芋道源码 - */ -@Slf4j -public class PayClientFactoryImpl implements PayClientFactory { - - /** - * 支付客户端 Map - * - * key:渠道编号 - */ - private final ConcurrentMap> clients = new ConcurrentHashMap<>(); - - /** - * 支付客户端 Class Map - */ - private final Map> clientClass = new ConcurrentHashMap<>(); - - public PayClientFactoryImpl() { - // 微信支付客户端 - clientClass.put(WX_PUB, WxPubPayClient.class); - clientClass.put(WX_LITE, WxLitePayClient.class); - clientClass.put(WX_APP, WxAppPayClient.class); - clientClass.put(WX_BAR, WxBarPayClient.class); - clientClass.put(WX_NATIVE, WxNativePayClient.class); - // 支付包支付客户端 - clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class); - clientClass.put(ALIPAY_QR, AlipayQrPayClient.class); - clientClass.put(ALIPAY_APP, AlipayAppPayClient.class); - clientClass.put(ALIPAY_PC, AlipayPcPayClient.class); - clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class); - // Mock 支付客户端 - clientClass.put(MOCK, MockPayClient.class); - } - - @Override - public void registerPayClientClass(PayChannelEnum channel, Class payClientClass) { - clientClass.put(channel, payClientClass); - } - - @Override - public PayClient getPayClient(Long channelId) { - AbstractPayClient client = clients.get(channelId); - if (client == null) { - log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); - } - return client; - } - - @Override - @SuppressWarnings("unchecked") - public void createOrUpdatePayClient(Long channelId, String channelCode, - Config config) { - AbstractPayClient client = (AbstractPayClient) clients.get(channelId); - if (client == null) { - client = this.createPayClient(channelId, channelCode, config); - client.init(); - clients.put(client.getId(), client); - } else { - client.refresh(config); - } - } - - @SuppressWarnings("unchecked") - private AbstractPayClient createPayClient(Long channelId, String channelCode, - Config config) { - PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); - Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode)); - Class payClientClass = clientClass.get(channelEnum); - Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode)); - return (AbstractPayClient) ReflectUtil.newInstance(payClientClass, channelId, config); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java deleted file mode 100644 index 4dcf236..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ /dev/null @@ -1,342 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.AlipayConfig; -import com.alipay.api.AlipayResponse; -import com.alipay.api.DefaultAlipayClient; -import com.alipay.api.domain.*; -import com.alipay.api.internal.util.AlipaySignature; -import com.alipay.api.request.*; -import com.alipay.api.response.*; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - -import java.nio.charset.StandardCharsets; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.function.Supplier; - -import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; -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.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; - -/** - * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) - * - * @author jason - */ -@Slf4j -public abstract class AbstractAlipayPayClient extends AbstractPayClient { - - @Getter // 仅用于单测场景 - protected DefaultAlipayClient client; - - public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { - super(channelId, channelCode, config); - } - - @Override - @SneakyThrows - protected void doInit() { - AlipayConfig alipayConfig = new AlipayConfig(); - BeanUtil.copyProperties(config, alipayConfig, false); - this.client = new DefaultAlipayClient(alipayConfig); - } - - // ============ 支付相关 ========== - - /** - * 构造支付关闭的 {@link PayOrderRespDTO} 对象 - * - * @return 支付关闭的 {@link PayOrderRespDTO} 对象 - */ - protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) { - Assert.isFalse(response.isSuccess()); - return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - reqDTO.getOutTradeNo(), response); - } - - @Override - public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable { - // 1. 校验回调数据 - Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); - AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), - StandardCharsets.UTF_8.name(), config.getSignType()); - - // 2. 解析订单的状态 - // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 - Integer status = parseStatus(bodyObj.get("trade_status")); - // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功 - if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) { - status = PayOrderStatusRespEnum.REFUND.getStatus(); - } - Assert.notNull(status, (Supplier) () -> { - throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); - }); - return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), - bodyObj.get("out_trade_no"), body); - } - - @Override - protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { - // 1.1 构建 AlipayTradeRefundModel 请求 - AlipayTradeQueryModel model = new AlipayTradeQueryModel(); - model.setOutTradeNo(outTradeNo); - // 1.2 构建 AlipayTradeQueryRequest 请求 - AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); - request.setBizModel(model); - AlipayTradeQueryResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { - // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - if (!response.isSuccess()) { // 不成功,例如说订单不存在 - return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - outTradeNo, response); - } - // 2.2 解析订单的状态 - Integer status = parseStatus(response.getTradeStatus()); - Assert.notNull(status, () -> { - throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); - }); - return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), - outTradeNo, response); - } - - private static Integer parseStatus(String tradeStatus) { - return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus() - : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus() - : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null; - } - - // ============ 退款相关 ========== - - /** - * 支付宝统一的退款接口 alipay.trade.refund - * - * @param reqDTO 退款请求 request DTO - * @return 退款请求 Response - */ - @Override - protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 构建 AlipayTradeRefundModel 请求 - AlipayTradeRefundModel model = new AlipayTradeRefundModel(); - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setOutRequestNo(reqDTO.getOutRefundNo()); - model.setRefundAmount(formatAmount(reqDTO.getRefundPrice())); - model.setRefundReason(reqDTO.getReason()); - // 1.2 构建 AlipayTradePayRequest 请求 - AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); - request.setBizModel(model); - - // 2.1 执行请求 - AlipayTradeRefundResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - if (!response.isSuccess()) { - // 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询 - if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) { - return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response); - } - return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response); - } - // 2.2 创建返回结果 - // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。 - // 另外,支付宝没有退款单号,所以不用设置 - return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), - reqDTO.getOutRefundNo(), response); - } - - @Override - public PayRefundRespDTO doParseRefundNotify(Map params, String body) { - // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 - // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 - // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 - // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 - // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 - throw new UnsupportedOperationException("支付宝无退款回调"); - } - - @Override - protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException { - // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求 - AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); - model.setOutTradeNo(outTradeNo); - model.setOutRequestNo(outRefundNo); - model.setQueryOptions(Collections.singletonList("gmt_refund_pay")); - // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求 - AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); - request.setBizModel(model); - - // 2.1 执行请求 - AlipayTradeFastpayRefundQueryResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - if (!response.isSuccess()) { - // 明确不存在的情况,应该就是失败,可进行关闭 - if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) { - return PayRefundRespDTO.failureOf(outRefundNo, response); - } - // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待 - return PayRefundRespDTO.waitingOf(null, outRefundNo, response); - } - // 2.2 创建返回结果 - if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) { - return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), - outRefundNo, response); - } - return PayRefundRespDTO.waitingOf(null, outRefundNo, response); - } - - @Override - protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 校验公钥类型 必须使用公钥证书模式 - if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) { - throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式"); - } - // 1.2 构建 AlipayFundTransUniTransferModel - AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); - // ① 通用的参数 - model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额 - model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。 - model.setOutBizNo(reqDTO.getOutTransferNo()); - model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD - model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER - if (reqDTO.getChannelExtras() != null) { - model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); - } - // ② 个性化的参数 - Participant payeeInfo = new Participant(); - PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType()); - switch (transferType) { - // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦? - // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试?? - case ALIPAY_BALANCE: { - payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); - payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号 - payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名 - model.setPayeeInfo(payeeInfo); - break; - } - case BANK_CARD: { - payeeInfo.setIdentityType("BANKCARD_ACCOUNT"); - // TODO 待实现 - throw exception(NOT_IMPLEMENTED); - } - default: { - throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType); - } - } - // 1.3 构建 AlipayFundTransUniTransferRequest - AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); - request.setBizModel(model); - // 执行请求 - AlipayFundTransUniTransferResponse response = client.certificateExecute(request); - // 处理结果 - if (!response.isSuccess()) { - // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账 - // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询 - if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { - return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); - } - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - reqDTO.getOutTransferNo(), response); - } else { - if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - reqDTO.getOutTransferNo(), response); - } - if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 - return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response); - } - return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), - response.getOutBizNo(), response); - } - - } - - @Override - protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable { - // 1.1 构建 AlipayFundTransCommonQueryModel - AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); - model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD"); - model.setBizScene("DIRECT_TRANSFER"); //业务场景 - model.setOutBizNo(outTradeNo); - // 1.2 构建 AlipayFundTransCommonQueryRequest - AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); - request.setBizModel(model); - - // 2.1 执行请求 - AlipayFundTransCommonQueryResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - // 2.2 处理返回结果 - if (response.isSuccess()) { - if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - outTradeNo, response); - } - if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 - return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response); - } - return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()), - response.getOutBizNo(), response); - } else { - // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 - // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 - if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { - return PayTransferRespDTO.waitingOf(null, outTradeNo, response); - } - return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), - outTradeNo, response); - } - } - - // ========== 各种工具方法 ========== - - protected String formatAmount(Integer amount) { - return String.valueOf(amount / 100.0); - } - - protected String formatTime(LocalDateTime time) { - return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER); - } - - protected LocalDateTime parseTime(String str) { - return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java deleted file mode 100644 index 4e5a37e..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradeAppPayModel; -import com.alipay.api.request.AlipayTradeAppPayRequest; -import com.alipay.api.response.AlipayTradeAppPayResponse; -import lombok.extern.slf4j.Slf4j; - -/** - * 支付宝【App 支付】的 PayClient 实现类 - * - * 文档:App 支付 - * - * // TODO 芋艿:未详细测试,因为手头没 App - * - * @author 芋道源码 - */ -@Slf4j -public class AlipayAppPayClient extends AbstractAlipayPayClient { - - public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) { - super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config); - } - - @Override - public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 构建 AlipayTradeAppPayModel 请求 - AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); - // ① 通用的参数 - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setSubject(reqDTO.getSubject()); - model.setBody(reqDTO.getBody() + "test"); - model.setTotalAmount(formatAmount(reqDTO.getPrice())); - model.setTimeExpire(formatTime(reqDTO.getExpireTime())); - model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品 - // ② 个性化的参数【无】 - // ③ 支付宝扫码支付只有一种展示 - String displayMode = PayOrderDisplayModeEnum.APP.getMode(); - - // 1.2 构建 AlipayTradePrecreateRequest 请求 - AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); - request.setBizModel(model); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - request.setReturnUrl(reqDTO.getReturnUrl()); - - // 2.1 执行请求 - AlipayTradeAppPayResponse response = client.sdkExecute(request); - // 2.2 处理结果 - if (!response.isSuccess()) { - return buildClosedPayOrderRespDTO(reqDTO, response); - } - return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), - reqDTO.getOutTradeNo(), response); - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java deleted file mode 100644 index 1f90d6b..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradePayModel; -import com.alipay.api.request.AlipayTradePayRequest; -import com.alipay.api.response.AlipayTradePayResponse; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.Objects; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; -import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; - -/** - * 支付宝【条码支付】的 PayClient 实现类 - * - * 文档:当面付 - * - * @author 芋道源码 - */ -@Slf4j -public class AlipayBarPayClient extends AbstractAlipayPayClient { - - public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) { - super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config); - } - - @Override - public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { - String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); - if (StrUtil.isEmpty(authCode)) { - throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); - } - - // 1.1 构建 AlipayTradePayModel 请求 - AlipayTradePayModel model = new AlipayTradePayModel(); - // ① 通用的参数 - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setSubject(reqDTO.getSubject()); - model.setBody(reqDTO.getBody()); - model.setTotalAmount(formatAmount(reqDTO.getPrice())); - model.setScene("bar_code"); // 当面付条码支付场景 - // ② 个性化的参数 - model.setAuthCode(authCode); - // ③ 支付宝条码支付只有一种展示 - String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode(); - - // 1.2 构建 AlipayTradePayRequest 请求 - AlipayTradePayRequest request = new AlipayTradePayRequest(); - request.setBizModel(model); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - request.setReturnUrl(reqDTO.getReturnUrl()); - - // 2.1 执行请求 - AlipayTradePayResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { - // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - // 2.2 处理结果 - if (!response.isSuccess()) { - return buildClosedPayOrderRespDTO(reqDTO, response); - } - if ("10000".equals(response.getCode())) { // 免密支付 - LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment()); - return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime, - response.getOutTradeNo(), response) - .setDisplayMode(displayMode).setDisplayContent(""); - } - // 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询 - return PayOrderRespDTO.waitingOf(displayMode, "", - reqDTO.getOutTradeNo(), response); - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java deleted file mode 100644 index 13f2885..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java +++ /dev/null @@ -1,107 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; -import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; -import lombok.Data; - -import jakarta.validation.Validator; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -/** - * 支付宝的 PayClientConfig 实现类 - * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 - * - * @author 芋道源码 - */ -@Data -public class AlipayPayClientConfig implements PayClientConfig { - - /** - * 公钥类型 - 公钥模式 - */ - public static final Integer MODE_PUBLIC_KEY = 1; - /** - * 公钥类型 - 证书模式 - */ - public static final Integer MODE_CERTIFICATE = 2; - - /** - * 签名算法类型 - RSA - */ - public static final String SIGN_TYPE_DEFAULT = "RSA2"; - - /** - * 网关地址 - * - * 1. 生产环境 - * 2. 沙箱环境 - */ - @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) - private String serverUrl; - - /** - * 开放平台上创建的应用的 ID - */ - @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) - private String appId; - - /** - * 签名算法类型,推荐:RSA2 - *

- * {@link #SIGN_TYPE_DEFAULT} - */ - @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) - private String signType; - - /** - * 公钥类型 - * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey - * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent - */ - @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) - private Integer mode; - - // ========== 公钥模式 ========== - /** - * 商户私钥 - */ - @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) - private String privateKey; - - /** - * 支付宝公钥字符串 - */ - @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) - private String alipayPublicKey; - - // ========== 证书模式 ========== - /** - * 指定商户公钥应用证书内容字符串 - */ - @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) - private String appCertContent; - /** - * 指定支付宝公钥证书内容字符串 - */ - @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) - private String alipayPublicCertContent; - /** - * 指定根证书内容字符串 - */ - @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) - private String rootCertContent; - - public interface ModePublicKey { - } - - public interface ModeCertificate { - } - - @Override - public void validate(Validator validator) { - ValidationUtils.validate(validator, this, - MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java deleted file mode 100644 index 6dbd19b..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java +++ /dev/null @@ -1,69 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.http.Method; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradePagePayModel; -import com.alipay.api.request.AlipayTradePagePayRequest; -import com.alipay.api.response.AlipayTradePagePayResponse; -import lombok.extern.slf4j.Slf4j; - -import java.util.Objects; - -/** - * 支付宝【PC 网站】的 PayClient 实现类 - * - * 文档:电脑网站支付 - * - * @author XGD - */ -@Slf4j -public class AlipayPcPayClient extends AbstractAlipayPayClient { - - public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) { - super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config); - } - - @Override - public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 构建 AlipayTradePagePayModel 请求 - AlipayTradePagePayModel model = new AlipayTradePagePayModel(); - // ① 通用的参数 - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setSubject(reqDTO.getSubject()); - model.setBody(reqDTO.getBody()); - model.setTotalAmount(formatAmount(reqDTO.getPrice())); - model.setTimeExpire(formatTime(reqDTO.getExpireTime())); - model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY - // ② 个性化的参数 - // 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展 - model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/ - // ③ 支付宝 PC 支付有两种展示模式:FORM、URL - String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(), - PayOrderDisplayModeEnum.URL.getMode()); - - // 1.2 构建 AlipayTradePagePayRequest 请求 - AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); - request.setBizModel(model); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - request.setReturnUrl(reqDTO.getReturnUrl()); - - // 2.1 执行请求 - AlipayTradePagePayResponse response; - if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) { - response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求 - } else { - response = client.pageExecute(request, Method.GET.name()); - } - // 2.2 处理结果 - if (!response.isSuccess()) { - return buildClosedPayOrderRespDTO(reqDTO, response); - } - return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), - reqDTO.getOutTradeNo(), response); - } -} 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 deleted file mode 100644 index bb3ad17..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradePrecreateModel; -import com.alipay.api.request.AlipayTradePrecreateRequest; -import com.alipay.api.response.AlipayTradePrecreateResponse; -import lombok.extern.slf4j.Slf4j; - -import java.util.Objects; - -import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; - -/** - * 支付宝【扫码支付】的 PayClient 实现类 - * - * 文档:扫码支付 - * - * @author 芋道源码 - */ -@Slf4j -public class AlipayQrPayClient extends AbstractAlipayPayClient { - - public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { - super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); - } - - @Override - public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 构建 AlipayTradePrecreateModel 请求 - AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); - // ① 通用的参数 - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setSubject(reqDTO.getSubject()); - model.setBody(reqDTO.getBody()); - model.setTotalAmount(formatAmount(reqDTO.getPrice())); - model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT - // ② 个性化的参数【无】 - // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开 - String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode(); - - // 1.2 构建 AlipayTradePrecreateRequest 请求 - AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); - request.setBizModel(model); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - request.setReturnUrl(reqDTO.getReturnUrl()); - - // 2.1 执行请求 - AlipayTradePrecreateResponse response; - if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { - // 证书模式 - response = client.certificateExecute(request); - } else { - response = client.execute(request); - } - // 2.2 处理结果 - if (!response.isSuccess()) { - return buildClosedPayOrderRespDTO(reqDTO, response); - } - return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(), - reqDTO.getOutTradeNo(), response); - } -} 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 deleted file mode 100644 index f9dccf5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.http.Method; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradeWapPayModel; -import com.alipay.api.request.AlipayTradeWapPayRequest; -import com.alipay.api.response.AlipayTradeWapPayResponse; -import lombok.extern.slf4j.Slf4j; - -/** - * 支付宝【Wap 网站】的 PayClient 实现类 - * - * 文档:手机网站支付接口 - * - * @author 芋道源码 - */ -@Slf4j -public class AlipayWapPayClient extends AbstractAlipayPayClient { - - public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { - super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); - } - - @Override - public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { - // 1.1 构建 AlipayTradeWapPayModel 请求 - AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); - // ① 通用的参数 - model.setOutTradeNo(reqDTO.getOutTradeNo()); - model.setSubject(reqDTO.getSubject()); - model.setBody(reqDTO.getBody()); - model.setTotalAmount(formatAmount(reqDTO.getPrice())); - model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY - // ② 个性化的参数【无】 - // ③ 支付宝 Wap 支付只有一种展示:URL - String displayMode = PayOrderDisplayModeEnum.URL.getMode(); - - // 1.2 构建 AlipayTradeWapPayRequest 请求 - AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); - request.setBizModel(model); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - request.setReturnUrl(reqDTO.getReturnUrl()); - model.setQuitUrl(reqDTO.getReturnUrl()); - - // 2.1 执行请求 - AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); - // 2.2 处理结果 - if (!response.isSuccess()) { - return buildClosedPayOrderRespDTO(reqDTO, response); - } - return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), - reqDTO.getOutTradeNo(), response); - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java deleted file mode 100644 index 1ad1ad7..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.mock; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; -import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; - -import java.time.LocalDateTime; -import java.util.Map; - -/** - * 模拟支付的 PayClient 实现类 - * - * 模拟支付返回结果都是成功,方便大家日常流畅 - * - * @author jason - */ -public class MockPayClient extends AbstractPayClient { - - private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS"; - - public MockPayClient(Long channelId, NonePayClientConfig config) { - super(channelId, PayChannelEnum.MOCK.getCode(), config); - } - - @Override - protected void doInit() { - } - - @Override - protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { - return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(), - reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA); - } - - @Override - protected PayOrderRespDTO doGetOrder(String outTradeNo) { - return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(), - outTradeNo, MOCK_RESP_SUCCESS_DATA); - } - - @Override - protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { - return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), - reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); - } - - @Override - protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { - return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(), - outRefundNo, MOCK_RESP_SUCCESS_DATA); - } - - @Override - protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { - throw new UnsupportedOperationException("模拟支付无退款回调"); - } - - @Override - protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { - throw new UnsupportedOperationException("模拟支付无支付回调"); - } - - @Override - protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { - throw new UnsupportedOperationException("待实现"); - } - - @Override - protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { - 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/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java deleted file mode 100644 index a8c50cf..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ /dev/null @@ -1,484 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.codec.Base64; -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.date.TemporalAccessorUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.io.FileUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; -import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; -import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result; -import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; -import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; -import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result; -import com.github.binarywang.wxpay.bean.request.*; -import com.github.binarywang.wxpay.bean.result.*; -import com.github.binarywang.wxpay.config.WxPayConfig; -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.Map; -import java.util.Objects; - -import static cn.hutool.core.date.DatePattern.*; -import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2; - -/** - * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) - * - * @author 遇到源码 - */ -@Slf4j -public abstract class AbstractWxPayClient 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", "privateKeyContent", "privateCertContent"); - payConfig.setTradeType(tradeType); - // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 - if (Base64.isBase64(config.getKeyContent())) { - payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); - } - if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { - payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); - } - if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { - payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); - } -// payConfig.setCertSerialNo(); - - // 创建 client 客户端 - client = new WxPayServiceImpl(); - client.setConfig(payConfig); - } - - // ============ 支付相关 ========== - - @Override - protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { - try { - switch (config.getApiVersion()) { - case 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) { - String errorCode = getErrorCode(e); - String errorMessage = getErrorMessage(e); - return PayOrderRespDTO.closedOf(errorCode, errorMessage, - reqDTO.getOutTradeNo(), e.getXmlString()); - } - } - - /** - * 【V2】调用支付渠道,统一下单 - * - * @param reqDTO 下单信息 - * @return 各支付渠道的返回结果 - */ - protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) - throws Exception; - - /** - * 【V3】调用支付渠道,统一下单 - * - * @param reqDTO 下单信息 - * @return 各支付渠道的返回结果 - */ - protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) - throws WxPayException; - - /** - * 【V2】创建微信下单请求 - * - * @param reqDTO 下信息 - * @return 下单请求 - */ - protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) { - return WxPayUnifiedOrderRequest.newBuilder() - .outTradeNo(reqDTO.getOutTradeNo()) - .body(reqDTO.getSubject()) - .detail(reqDTO.getBody()) - .totalFee(reqDTO.getPrice()) // 单位分 - .timeExpire(formatDateV2(reqDTO.getExpireTime())) - .spbillCreateIp(reqDTO.getUserIp()) - .notifyUrl(reqDTO.getNotifyUrl()) - .build(); - } - - /** - * 【V3】创建微信下单请求 - * - * @param reqDTO 下信息 - * @return 下单请求 - */ - protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) { - WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); - request.setOutTradeNo(reqDTO.getOutTradeNo()); - request.setDescription(reqDTO.getSubject()); - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分 - request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); - request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); - request.setNotifyUrl(reqDTO.getNotifyUrl()); - return request; - } - - @Override - public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws WxPayException { - switch (config.getApiVersion()) { - case API_VERSION_V2: - return doParseOrderNotifyV2(body); - case WxPayClientConfig.API_VERSION_V3: - return doParseOrderNotifyV3(body); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } - - private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException { - // 1. 解析回调 - WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); - // 2. 构建结果 - // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂 - Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ? - PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus(); - return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), - response.getOutTradeNo(), body); - } - - private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException { - // 1. 解析回调 - WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null); - WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult(); - // 2. 构建结果 - Integer status = parseStatus(result.getTradeState()); - String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null; - return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()), - result.getOutTradeNo(), body); - } - - @Override - protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { - try { - switch (config.getApiVersion()) { - case API_VERSION_V2: - return doGetOrderV2(outTradeNo); - case WxPayClientConfig.API_VERSION_V3: - return doGetOrderV3(outTradeNo); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } catch (WxPayException e) { - if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) { - String errorCode = getErrorCode(e); - String errorMessage = getErrorMessage(e); - return PayOrderRespDTO.closedOf(errorCode, errorMessage, - outTradeNo, e.getXmlString()); - } - throw e; - } - } - - private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder() - .outTradeNo(outTradeNo).build(); - // 执行请求 - WxPayOrderQueryResult response = client.queryOrder(request); - - // 转换结果 - Integer status = parseStatus(response.getTradeState()); - return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), - outTradeNo, response); - } - - private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request() - .setOutTradeNo(outTradeNo); - // 执行请求 - WxPayOrderQueryV3Result response = client.queryOrderV3(request); - - // 转换结果 - Integer status = parseStatus(response.getTradeState()); - String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null; - return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()), - outTradeNo, response); - } - - private static Integer parseStatus(String tradeState) { - switch (tradeState) { - case "NOTPAY": - case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有) - return PayOrderStatusRespEnum.WAITING.getStatus(); - case "SUCCESS": - return PayOrderStatusRespEnum.SUCCESS.getStatus(); - case "REFUND": - return PayOrderStatusRespEnum.REFUND.getStatus(); - case "CLOSED": - case "REVOKED": // 已撤销(刷卡支付独有) - case "PAYERROR": // 支付失败(其它原因,如银行返回失败) - return PayOrderStatusRespEnum.CLOSED.getStatus(); - default: - throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState)); - } - } - - // ============ 退款相关 ========== - - @Override - protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { - try { - switch (config.getApiVersion()) { - case API_VERSION_V2: - return doUnifiedRefundV2(reqDTO); - case WxPayClientConfig.API_VERSION_V3: - return doUnifiedRefundV3(reqDTO); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } catch (WxPayException e) { - String errorCode = getErrorCode(e); - String errorMessage = getErrorMessage(e); - return PayRefundRespDTO.failureOf(errorCode, errorMessage, - reqDTO.getOutTradeNo(), e.getXmlString()); - } - } - - private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable { - // 1. 构建 WxPayRefundRequest 请求 - WxPayRefundRequest request = new WxPayRefundRequest() - .setOutTradeNo(reqDTO.getOutTradeNo()) - .setOutRefundNo(reqDTO.getOutRefundNo()) - .setRefundFee(reqDTO.getRefundPrice()) - .setRefundDesc(reqDTO.getReason()) - .setTotalFee(reqDTO.getPayPrice()) - .setNotifyUrl(reqDTO.getNotifyUrl()); - // 2.1 执行请求 - WxPayRefundResult response = client.refundV2(request); - // 2.2 创建返回结果 - if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知 - return PayRefundRespDTO.waitingOf(response.getRefundId(), - reqDTO.getOutRefundNo(), response); - } - return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); - } - - private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { - // 1. 构建 WxPayRefundRequest 请求 - WxPayRefundV3Request request = new WxPayRefundV3Request() - .setOutTradeNo(reqDTO.getOutTradeNo()) - .setOutRefundNo(reqDTO.getOutRefundNo()) - .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice()) - .setTotal(reqDTO.getPayPrice()).setCurrency("CNY")) - .setReason(reqDTO.getReason()) - .setNotifyUrl(reqDTO.getNotifyUrl()); - // 2.1 执行请求 - WxPayRefundV3Result response = client.refundV3(request); - // 2.2 创建返回结果 - if (Objects.equals("SUCCESS", response.getStatus())) { - return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), - reqDTO.getOutRefundNo(), response); - } - if (Objects.equals("PROCESSING", response.getStatus())) { - return PayRefundRespDTO.waitingOf(response.getRefundId(), - reqDTO.getOutRefundNo(), response); - } - return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); - } - - @Override - public PayRefundRespDTO doParseRefundNotify(Map params, String body) throws WxPayException { - switch (config.getApiVersion()) { - case API_VERSION_V2: - return doParseRefundNotifyV2(body); - case WxPayClientConfig.API_VERSION_V3: - return parseRefundNotifyV3(body); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } - - private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException { - // 1. 解析回调 - WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); - WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo(); - // 2. 构建结果 - if (Objects.equals("SUCCESS", result.getRefundStatus())) { - return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()), - result.getOutRefundNo(), response); - } - return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); - } - - private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { - // 1. 解析回调 - WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null); - WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult(); - // 2. 构建结果 - if (Objects.equals("SUCCESS", result.getRefundStatus())) { - return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()), - result.getOutRefundNo(), response); - } - return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); - } - - @Override - protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException { - try { - switch (config.getApiVersion()) { - case API_VERSION_V2: - return doGetRefundV2(outTradeNo, outRefundNo); - case WxPayClientConfig.API_VERSION_V3: - return doGetRefundV3(outTradeNo, outRefundNo); - default: - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); - } - } catch (WxPayException e) { - if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) { - String errorCode = getErrorCode(e); - String errorMessage = getErrorMessage(e); - return PayRefundRespDTO.failureOf(errorCode, errorMessage, - outRefundNo, e.getXmlString()); - } - throw e; - } - } - - private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException { - // 1. 构建 WxPayRefundRequest 请求 - WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder() - .outTradeNo(outTradeNo) - .outRefundNo(outRefundNo) - .build(); - // 2.1 执行请求 - WxPayRefundQueryResult response = client.refundQuery(request); - // 2.2 创建返回结果 - if (!Objects.equals("SUCCESS", response.getResultCode())) { - return PayRefundRespDTO.waitingOf(null, - outRefundNo, response); - } - WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(), - record -> record.getOutRefundNo().equals(outRefundNo)); - if (refund == null) { - return PayRefundRespDTO.failureOf(outRefundNo, response); - } - switch (refund.getRefundStatus()) { - case "SUCCESS": - return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()), - outRefundNo, response); - case "PROCESSING": - return PayRefundRespDTO.waitingOf(refund.getRefundId(), - outRefundNo, response); - case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款 - case "FAIL": - return PayRefundRespDTO.failureOf(outRefundNo, response); - default: - throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus())); - } - } - - private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException { - // 1. 构建 WxPayRefundRequest 请求 - WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request(); - request.setOutRefundNo(outRefundNo); - // 2.1 执行请求 - WxPayRefundQueryV3Result response = client.refundQueryV3(request); - // 2.2 创建返回结果 - switch (response.getStatus()) { - case "SUCCESS": - return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), - outRefundNo, response); - case "PROCESSING": - return PayRefundRespDTO.waitingOf(response.getRefundId(), - outRefundNo, response); - case "ABNORMAL": // 退款异常 - case "CLOSED": - return PayRefundRespDTO.failureOf(outRefundNo, response); - default: - throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus())); - } - } - - @Override - protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { - throw new UnsupportedOperationException("待实现"); - } - - @Override - protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { - throw new UnsupportedOperationException("待实现"); - } - - // ========== 各种工具方法 ========== - - static String formatDateV2(LocalDateTime time) { - return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); - } - - static LocalDateTime parseDateV2(String time) { - return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); - } - - static LocalDateTime parseDateV2B(String time) { - return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN); - } - - static String formatDateV3(LocalDateTime time) { - return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); - } - - static LocalDateTime parseDateV3(String time) { - return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); - } - - static String getErrorCode(WxPayException e) { - if (StrUtil.isNotEmpty(e.getErrCode())) { - return e.getErrCode(); - } - if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { - return "CUSTOM_ERROR"; - } - return e.getReturnCode(); - } - - static String getErrorMessage(WxPayException e) { - if (StrUtil.isNotEmpty(e.getErrCode())) { - return e.getErrCodeDes(); - } - if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { - return e.getCustomErrorMsg(); - } - return e.getReturnMsg(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java deleted file mode 100644 index 396694a..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -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; - -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - -/** - * 微信支付【App 支付】的 PayClient 实现类 - * - * 文档:App 支付 - * - * // TODO 芋艿:未详细测试,因为手头没 App - * - * @author 芋道源码 - */ -@Slf4j -public class WxAppPayClient extends AbstractWxPayClient { - - public WxAppPayClient(Long channelId, WxPayClientConfig config) { - super(channelId, PayChannelEnum.WX_APP.getCode(), config); - } - - @Override - protected void doInit() { - super.doInit(WxPayConstants.TradeType.APP); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); - // 执行请求 - WxPayMpOrderResult response = client.createOrder(request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), - reqDTO.getOutTradeNo(), response); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderV3Request 对象 - WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); - // 执行请求 - WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), - reqDTO.getOutTradeNo(), response); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java deleted file mode 100644 index d01b504..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java +++ /dev/null @@ -1,107 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.thread.ThreadUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; -import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; -import com.github.binarywang.wxpay.constant.WxPayConstants; -import com.github.binarywang.wxpay.exception.WxPayException; -import lombok.extern.slf4j.Slf4j; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.concurrent.TimeUnit; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - -/** - * 微信支付【付款码支付】的 PayClient 实现类 - * - * 文档:付款码支付 - * - * @author 芋道源码 - */ -@Slf4j -public class WxBarPayClient extends AbstractWxPayClient { - - /** - * 微信付款码的过期时间 - */ - private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3); - - public WxBarPayClient(Long channelId, WxPayClientConfig config) { - super(channelId, PayChannelEnum.WX_BAR.getCode(), config); - } - - @Override - protected void doInit() { - super.doInit(WxPayConstants.TradeType.MICROPAY); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 由于付款码需要不断轮询,所以需要在较短的时间完成支付 - LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); - if (expireTime.isAfter(reqDTO.getExpireTime())) { - expireTime = reqDTO.getExpireTime(); - } - // 构建 WxPayMicropayRequest 对象 - WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() - .outTradeNo(reqDTO.getOutTradeNo()) - .body(reqDTO.getSubject()) - .detail(reqDTO.getBody()) - .totalFee(reqDTO.getPrice()) // 单位分 - .timeExpire(formatDateV2(expireTime)) - .spbillCreateIp(reqDTO.getUserIp()) - .authCode(getAuthCode(reqDTO)) - .build(); - // 执行请求,重试直到失败(过期),或者成功 - WxPayException lastWxPayException = null; - for (int i = 1; i < Byte.MAX_VALUE; i++) { - try { - WxPayMicropayResult response = client.micropay(request); - // 支付成功,例如说:1)用户输入了密码;2)用户免密支付 - return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), - response.getOutTradeNo(), response) - .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode()); - } catch (WxPayException ex) { - lastWxPayException = ex; - // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理 - // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。 - // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 - // 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 - if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) { - throw ex; - } - // 等待 5 秒,继续下一轮重新发起支付 - log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, - toJsonString(request), ex.getMessage()); - ThreadUtil.sleep(5, TimeUnit.SECONDS); - } - } - throw lastWxPayException; - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - return doUnifiedOrderV2(reqDTO); - } - - // ========== 各种工具方法 ========== - - static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { - String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode"); - if (StrUtil.isEmpty(authCode)) { - throw invalidParamException("支付请求的 authCode 不能为空!"); - } - return authCode; - } - -} 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 deleted file mode 100644 index 9929955..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import lombok.extern.slf4j.Slf4j; - -/** - * 微信支付【小程序】的 PayClient 实现类 - * - * 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承 - * - * 文档:JSAPI 下单 - * - * @author zwy - */ -@Slf4j -public class WxLitePayClient extends WxPubPayClient { - - public WxLitePayClient(Long channelId, WxPayClientConfig config) { - super(channelId, PayChannelEnum.WX_LITE.getCode(), config); - } - -} 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 deleted file mode 100644 index 5a07350..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; -import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; -import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; -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; - -/** - * 微信支付【Native 二维码】的 PayClient 实现类 - * - * 文档:Native 下单 - * - * @author zwy - */ -@Slf4j -public class WxNativePayClient extends AbstractWxPayClient { - - public WxNativePayClient(Long channelId, WxPayClientConfig config) { - super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config); - } - - @Override - protected void doInit() { - super.doInit(WxPayConstants.TradeType.NATIVE); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); - // 执行请求 - WxPayNativeOrderResult response = client.createOrder(request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(), - reqDTO.getOutTradeNo(), response); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderV3Request 对象 - WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); - // 执行请求 - String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response, - reqDTO.getOutTradeNo(), response); - } - -} 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 deleted file mode 100644 index de5b023..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java +++ /dev/null @@ -1,110 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.hutool.core.io.IoUtil; -import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; -import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; -import lombok.Data; - -import jakarta.validation.Validator; -import jakarta.validation.constraints.NotBlank; -import java.io.FileInputStream; -import java.io.FileNotFoundException; - -/** - * 微信支付的 PayClientConfig 实现类 - * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 - * - * @author 芋道源码 - */ -@Data -public class WxPayClientConfig implements PayClientConfig { - - /** - * API 版本 - V2 - * - * V2 协议说明 - */ - public static final String API_VERSION_V2 = "v2"; - /** - * API 版本 - V3 - * - * V3 协议说明 - */ - public static final String API_VERSION_V3 = "v3"; - - /** - * 公众号或者小程序的 appid - * - * 只有公众号或小程序需要该字段 - */ - @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) - private String appId; - /** - * 商户号 - */ - @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class}) - private String mchId; - /** - * API 版本 - */ - @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class}) - private String apiVersion; - - // ========== V2 版本的参数 ========== - - /** - * 商户密钥 - */ - @NotBlank(message = "商户密钥不能为空", groups = V2.class) - private String mchKey; - /** - * apiclient_cert.p12 证书文件的对应字符串【base64 格式】 - * - * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储 - */ - @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class) - private String keyContent; - - // ========== V3 版本的参数 ========== - /** - * apiclient_key.pem 证书文件的对应字符串 - */ - @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) - private String privateKeyContent; - /** - * apiclient_cert.pem 证书文件的对应的字符串 - */ - @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) - private String privateCertContent; - /** - * apiV3 密钥值 - */ - @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class) - private String apiV3Key; - - /** - * 分组校验 v2版本 - */ - public interface V2 { - } - - /** - * 分组校验 v3版本 - */ - public interface V3 { - } - - @Override - public void validate(Validator validator) { - ValidationUtils.validate(validator, this, - API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class); - } - - 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/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 390c513..0000000 --- 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,80 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -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; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; - -/** - * 微信支付(公众号)的 PayClient 实现类 - * - * 文档:JSAPI 下单 - * - * @author 芋道源码 - */ -@Slf4j -public class WxPubPayClient extends AbstractWxPayClient { - - public WxPubPayClient(Long channelId, WxPayClientConfig config) { - super(channelId, PayChannelEnum.WX_PUB.getCode(), config); - } - - protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) { - super(channelId, channelCode, config); - } - - @Override - protected void doInit() { - super.doInit(WxPayConstants.TradeType.JSAPI); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO) - .setOpenid(getOpenid(reqDTO)); - // 执行请求 - WxPayMpOrderResult response = client.createOrder(request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), - reqDTO.getOutTradeNo(), response); - } - - @Override - protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { - // 构建 WxPayUnifiedOrderRequest 对象 - WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO) - .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); - // 执行请求 - WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); - - // 转换结果 - return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), - reqDTO.getOutTradeNo(), response); - } - - // ========== 各种工具方法 ========== - - static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { - String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); - if (StrUtil.isEmpty(openid)) { - throw invalidParamException("支付请求的 openid 不能为空!"); - } - return openid; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java deleted file mode 100644 index 83d2d13..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.channel; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; -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 lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 支付渠道的编码的枚举 - * - * @author 芋道源码 - */ -@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_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class), - - ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), - ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), - ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class), - ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class), - ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class), - MOCK("mock", "模拟支付", NonePayClientConfig.class), - - WALLET("wallet", "钱包支付", NonePayClientConfig.class); - - /** - * 编码 - * - * 参考 支付渠道属性值 - */ - private final String code; - /** - * 名字 - */ - private final String name; - - /** - * 配置类 - */ - private final Class configClass; - - /** - * 微信支付 - */ - public static final String WECHAT = "WECHAT"; - - /** - * 支付宝支付 - */ - public static final String ALIPAY = "ALIPAY"; - - public static PayChannelEnum getByCode(String code) { - return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); - } - - public static boolean isAlipay(String channelCode) { - return channelCode != null && channelCode.startsWith("alipay"); - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java deleted file mode 100644 index 129c406..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.order; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 支付 UI 展示模式 - * - * @author 芋道源码 - */ -@Getter -@AllArgsConstructor -public enum PayOrderDisplayModeEnum { - - URL("url"), // Redirect 跳转链接的方式 - IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】 - FORM("form"), // HTML 表单提交 - QR_CODE("qr_code"), // 二维码的文字内容 - QR_CODE_URL("qr_code_url"), // 二维码的图片链接 - BAR_CODE("bar_code"), // 条形码 - APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的 - ; - - /** - * 展示模式 - */ - private final String mode; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java deleted file mode 100644 index eac381c..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.order; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Objects; - -/** - * 渠道的支付状态枚举 - * - * @author 芋道源码 - */ -@Getter -@AllArgsConstructor -public enum PayOrderStatusRespEnum { - - WAITING(0, "未支付"), - SUCCESS(10, "支付成功"), - REFUND(20, "已退款"), - CLOSED(30, "支付关闭"), - ; - - private final Integer status; - private final String name; - - /** - * 判断是否支付成功 - * - * @param status 状态 - * @return 是否支付成功 - */ - public static boolean isSuccess(Integer status) { - return Objects.equals(status, SUCCESS.getStatus()); - } - - /** - * 判断是否已退款 - * - * @param status 状态 - * @return 是否支付成功 - */ - public static boolean isRefund(Integer status) { - return Objects.equals(status, REFUND.getStatus()); - } - - /** - * 判断是否支付关闭 - * - * @param status 状态 - * @return 是否支付关闭 - */ - public static boolean isClosed(Integer status) { - return Objects.equals(status, CLOSED.getStatus()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java deleted file mode 100644 index 8ad61a6..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.refund; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Objects; - -/** - * 渠道的退款状态枚举 - * - * @author jason - */ -@Getter -@AllArgsConstructor -public enum PayRefundStatusRespEnum { - - WAITING(0, "等待退款"), - SUCCESS(10, "退款成功"), - FAILURE(20, "退款失败"); - - private final Integer status; - private final String name; - - public static boolean isSuccess(Integer status) { - return Objects.equals(status, SUCCESS.getStatus()); - } - - public static boolean isFailure(Integer status) { - return Objects.equals(status, FAILURE.getStatus()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java deleted file mode 100644 index 35ea344..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.transfer; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Objects; - -/** - * 渠道的转账状态枚举 - * - * @author jason - */ -@Getter -@AllArgsConstructor -public enum PayTransferStatusRespEnum { - - WAITING(0, "等待转账"), - - /** - * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现 - * TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子? - */ - IN_PROGRESS(10, "转账进行中"), - - SUCCESS(20, "转账成功"), - /** - * 转账关闭 (失败,或者其它情况) - */ - CLOSED(30, "转账关闭"); - - private final Integer status; - private final String name; - - public static boolean isSuccess(Integer status) { - return Objects.equals(status, SUCCESS.getStatus()); - } - - public static boolean isClosed(Integer status) { - return Objects.equals(status, CLOSED.getStatus()); - } - - public static boolean isInProgress(Integer status) { - return Objects.equals(status, IN_PROGRESS.getStatus()); - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java deleted file mode 100644 index a7580f0..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.enums.transfer; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; - -/** - * 转账类型枚举 - * - * @author jason - */ -@AllArgsConstructor -@Getter -public enum PayTransferTypeEnum implements IntArrayValuable { - - ALIPAY_BALANCE(1, "支付宝余额"), - WX_BALANCE(2, "微信余额"), - BANK_CARD(3, "银行卡"), - WALLET_BALANCE(4, "钱包余额"); - - public interface WxPay { - } - - public interface Alipay { - } - - private final Integer type; - private final String name; - - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayTransferTypeEnum::getType).toArray(); - - @Override - public int[] array() { - return ARRAYS; - } - - public static PayTransferTypeEnum typeOf(Integer type) { - return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index f2a8bf1..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration \ No newline at end of file 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 deleted file mode 100644 index 9842560..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl; - -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.util.RandomUtil; -import cn.iocoder.yudao.framework.pay.core.client.PayClient; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -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.enums.channel.PayChannelEnum; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; - -/** - * {@link PayClientFactoryImpl} 的集成测试 - * - * @author 芋道源码 - */ -@Disabled -public class PayClientFactoryImplIntegrationTest { - - private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; - - private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); - - /** - * {@link WxPubPayClient} 的 V2 版本 - */ - @Test - public void testCreatePayClient_WX_PUB_V2() { - // 创建配置 - WxPayClientConfig config = new WxPayClientConfig(); - config.setAppId("wx041349c6f39b268b"); - config.setMchId("1545083881"); - config.setApiVersion(WxPayClientConfig.API_VERSION_V2); - config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); - // 创建客户端 - Long channelId = RandomUtil.randomLong(); - payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); - PayClient client = payClientFactory.getPayClient(channelId); - // 发起支付 - PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); -// CommonResult result = client.unifiedOrder(reqDTO); -// System.out.println(result); - } - - /** - * {@link WxPubPayClient} 的 V3 版本 - */ - @Test - public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { - // 创建配置 - WxPayClientConfig config = new WxPayClientConfig(); - config.setAppId("wx041349c6f39b268b"); - config.setMchId("1545083881"); - 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"); - // 创建客户端 - Long channelId = RandomUtil.randomLong(); - payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); - PayClient client = payClientFactory.getPayClient(channelId); - // 发起支付 - PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); -// CommonResult result = client.unifiedOrder(reqDTO); -// System.out.println(result); - } - - /** - * {@link AlipayQrPayClient} - */ - @Test - @SuppressWarnings("unchecked") - public void testCreatePayClient_ALIPAY_QR() { - // 创建配置 - AlipayPayClientConfig config = new AlipayPayClientConfig(); - config.setAppId("2021000118634035"); - config.setServerUrl(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"); - // 创建客户端 - Long channelId = RandomUtil.randomLong(); - payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); - PayClient client = payClientFactory.getPayClient(channelId); - // 发起支付 - PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); - reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址 -// CommonResult result = (CommonResult) client.unifiedOrder(reqDTO); -// System.out.println(JsonUtils.toJsonString(result)); -// System.out.println(result.getData().getQrCode()); - } - - /** - * {@link AlipayWapPayClient} - */ - @Test - public void testCreatePayClient_ALIPAY_WAP() { - // 创建配置 - AlipayPayClientConfig config = new AlipayPayClientConfig(); - config.setAppId("2021000118634035"); - config.setServerUrl(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"); - // 创建客户端 - Long channelId = RandomUtil.randomLong(); - payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); - PayClient client = payClientFactory.getPayClient(channelId); - // 发起支付 - PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); -// CommonResult result = client.unifiedOrder(reqDTO); -// System.out.println(JsonUtils.toJsonString(result)); - } - - private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { - PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); - reqDTO.setPrice(123); - reqDTO.setSubject("IPhone 13"); - reqDTO.setBody("biubiubiu"); - reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); - reqDTO.setUserIp("127.0.0.1"); - reqDTO.setNotifyUrl("http://127.0.0.1:8080"); - return reqDTO; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java deleted file mode 100644 index 7c45b6a..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.util.RandomUtil; -import cn.iocoder.yudao.framework.common.exception.ServiceException; -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import com.alipay.api.AlipayApiException; -import com.alipay.api.DefaultAlipayClient; -import com.alipay.api.DefaultSigner; -import com.alipay.api.domain.AlipayTradeRefundModel; -import com.alipay.api.request.AlipayTradeRefundRequest; -import com.alipay.api.response.AlipayTradeRefundResponse; -import lombok.Setter; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.Mock; - -import jakarta.validation.ConstraintViolationException; -import java.util.Date; - -import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; - -/** - * 支付宝 Client 的测试基类 - * - * @author jason - */ -public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest { - - protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> { - o.setServerUrl(randomURL()); - o.setPrivateKey(randomString()); - o.setMode(MODE_PUBLIC_KEY); - o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); - o.setAppCertContent(""); - o.setAlipayPublicCertContent(""); - o.setRootCertContent(""); - }); - - @Mock - protected DefaultAlipayClient defaultAlipayClient; - - @Setter - private AbstractAlipayPayClient client; - - /** - * 子类需要实现该方法. 设置 client 的具体实现 - */ - @BeforeEach - public abstract void setUp(); - - @Test - @DisplayName("支付宝 Client 初始化") - public void testDoInit() { - // 调用 - client.doInit(); - // 断言 - DefaultAlipayClient realClient = client.getClient(); - assertNotSame(defaultAlipayClient, realClient); - assertInstanceOf(DefaultSigner.class, realClient.getSigner()); - assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey()); - } - - @Test - @DisplayName("支付宝 Client 统一退款:成功") - public void testUnifiedRefund_success() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - Date refundTime = randomDate(); - String outRefundNo = randomString(); - String outTradeNo = randomString(); - Integer refundAmount = randomInteger(); - AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { - o.setSubCode(""); - o.setGmtRefundPay(refundTime); - }); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); - AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel(); - assertEquals(outRefundNo, bizModel.getOutRequestNo()); - assertEquals(outTradeNo, bizModel.getOutTradeNo()); - assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount()); - return true; - }))).thenReturn(response); - // 准备请求参数 - PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { - o.setOutRefundNo(outRefundNo); - o.setOutTradeNo(outTradeNo); - o.setNotifyUrl(notifyUrl); - o.setRefundPrice(refundAmount); - }); - - // 调用 - PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); - // 断言 - assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); - assertEquals(outRefundNo, resp.getOutRefundNo()); - assertNull(resp.getChannelRefundNo()); - assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝 Client 统一退款:渠道返回失败") - public void test_unified_refund_channel_failed() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - String subCode = randomString(); - String subMsg = randomString(); - AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { - o.setSubCode(subCode); - o.setSubMsg(subMsg); - }); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); - return true; - }))).thenReturn(response); - // 准备请求参数 - String outRefundNo = randomString(); - String outTradeNo = randomString(); - PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { - o.setOutRefundNo(outRefundNo); - o.setOutTradeNo(outTradeNo); - o.setNotifyUrl(notifyUrl); - }); - - // 调用 - PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); - // 断言 - assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus()); - assertEquals(outRefundNo, resp.getOutRefundNo()); - assertNull(resp.getChannelRefundNo()); - assertNull(resp.getSuccessTime()); - assertSame(response, resp.getRawData()); - assertEquals(subCode, resp.getChannelErrorCode()); - assertEquals(subMsg, resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝 Client 统一退款:参数校验不通过") - public void testUnifiedRefund_paramInvalidate() { - // 准备请求参数 - String notifyUrl = randomURL(); - PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { - o.setOutTradeNo(""); - o.setNotifyUrl(notifyUrl); - }); - - // 调用,并断言 - assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO)); - } - - @Test - @DisplayName("支付宝 Client 统一退款:抛出业务异常") - public void testUnifiedRefund_throwServiceException() throws AlipayApiException { - // mock 方法 - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) - .thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); - // 准备请求参数 - String notifyUrl = randomURL(); - PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); - - // 调用,并断言 - assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO)); - } - - @Test - @DisplayName("支付宝 Client 统一退款:抛出系统异常") - public void testUnifiedRefund_throwPayException() throws AlipayApiException { - // mock 方法 - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) - .thenThrow(new RuntimeException("系统异常")); - // 准备请求参数 - String notifyUrl = randomURL(); - PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); - - // 调用,并断言 - assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO)); - } - - @Test - @DisplayName("支付宝 Client 统一下单:参数校验不通过") - public void testUnifiedOrder_paramInvalidate() { - // 准备请求参数 - String outTradeNo = randomString(); - String notifyUrl = randomURL(); - PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> { - o.setOutTradeNo(outTradeNo); - o.setNotifyUrl(notifyUrl); - }); - - // 调用,并断言 - assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO)); - } - - protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) { - return randomPojo(PayOrderUnifiedReqDTO.class, o -> { - o.setOutTradeNo(outTradeNo); - o.setNotifyUrl(notifyUrl); - o.setPrice(price); - o.setSubject(RandomUtil.randomString(32)); - o.setBody(RandomUtil.randomString(32)); - }); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java deleted file mode 100644 index 47f1008..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.iocoder.yudao.framework.common.exception.ServiceException; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradePayModel; -import com.alipay.api.request.AlipayTradePayRequest; -import com.alipay.api.response.AlipayTradePayResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; - -/** - * {@link AlipayBarPayClient} 单元测试 - * - * @author jason - */ -public class AlipayBarPayClientTest extends AbstractAlipayClientTest { - - @InjectMocks - private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config); - - @Override - @BeforeEach - public void setUp() { - setClient(client); - } - - @Test - @DisplayName("支付宝条码支付:非免密码支付下单成功") - public void testUnifiedOrder_success() throws AlipayApiException { - // mock 方法 - String outTradeNo = randomString(); - String notifyUrl = randomURL(); - Integer price = randomInteger(); - String authCode = randomString(); - AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode("")); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertInstanceOf(AlipayTradePayModel.class, request.getBizModel()); - assertEquals(notifyUrl, request.getNotifyUrl()); - AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel(); - assertEquals(outTradeNo, model.getOutTradeNo()); - assertEquals(String.valueOf(price / 100.0), model.getTotalAmount()); - assertEquals(authCode, model.getAuthCode()); - return true; - }))).thenReturn(response); - // 准备请求参数 - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - Map extraParam = new HashMap<>(); - extraParam.put("auth_code", authCode); - reqDTO.setChannelExtras(extraParam); - - // 调用方法 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(WAITING.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); - assertEquals("", resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝条码支付:免密码支付下单成功") - public void testUnifiedOrder_code10000Success() throws AlipayApiException { - // mock 方法 - String outTradeNo = randomString(); - String channelNo = randomString(); - String channelUserId = randomString(); - Date payTime = randomDate(); - AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { - o.setSubCode(""); - o.setCode("10000"); - o.setOutTradeNo(outTradeNo); - o.setTradeNo(channelNo); - o.setBuyerUserId(channelUserId); - o.setGmtPayment(payTime); - }); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) - .thenReturn(response); - // 准备请求参数 - String authCode = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); - Map extraParam = new HashMap<>(); - extraParam.put("auth_code", authCode); - reqDTO.setChannelExtras(extraParam); - - // 下单请求 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(PayOrderStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertEquals(channelNo, resp.getChannelOrderNo()); - assertEquals(channelUserId, resp.getChannelUserId()); - assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); - assertEquals("", resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝条码支付:没有传条码") - public void testUnifiedOrder_emptyAuthCode() { - // 准备参数 - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger()); - - // 调用,并断言 - assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); - } - - @Test - @DisplayName("支付宝条码支付:渠道返回失败") - public void test_unified_order_channel_failed() throws AlipayApiException { - // mock 方法 - String subCode = randomString(); - String subMsg = randomString(); - AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { - o.setSubCode(subCode); - o.setSubMsg(subMsg); - }); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) - .thenReturn(response); - // 准备请求参数 - String authCode = randomString(); - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); - Map extraParam = new HashMap<>(); - extraParam.put("auth_code", authCode); - reqDTO.setChannelExtras(extraParam); - - // 调用方法 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(CLOSED.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertNull(resp.getDisplayMode()); - assertNull(resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertEquals(subCode, resp.getChannelErrorCode()); - assertEquals(subMsg, resp.getChannelErrorMsg()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java deleted file mode 100644 index d78caf2..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.http.Method; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.request.AlipayTradePagePayRequest; -import com.alipay.api.response.AlipayTradePagePayResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; - -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -/** - * {@link AlipayPcPayClient} 单元测试 - * - * @author jason - */ -public class AlipayPcPayClientTest extends AbstractAlipayClientTest { - - @InjectMocks - private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config); - - @Override - @BeforeEach - public void setUp() { - setClient(client); - } - - @Test - @DisplayName("支付宝 PC 网站支付:URL Display Mode 下单成功") - public void testUnifiedOrder_urlSuccess() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); - when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), - eq(Method.GET.name()))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - Integer price = randomInteger(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - reqDTO.setDisplayMode(null); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(WAITING.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); - assertEquals(response.getBody(), resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝 PC 网站支付:Form Display Mode 下单成功") - public void testUnifiedOrder_formSuccess() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); - when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), - eq(Method.POST.name()))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - Integer price = randomInteger(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode()); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(WAITING.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode()); - assertEquals(response.getBody(), resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝 PC 网站支付:渠道返回失败") - public void testUnifiedOrder_channelFailed() throws AlipayApiException { - // mock 方法 - String subCode = randomString(); - String subMsg = randomString(); - AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> { - o.setSubCode(subCode); - o.setSubMsg(subMsg); - }); - when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), - eq(Method.GET.name()))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); - reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(CLOSED.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertNull(resp.getDisplayMode()); - assertNull(resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertEquals(subCode, resp.getChannelErrorCode()); - assertEquals(subMsg, resp.getChannelErrorMsg()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java deleted file mode 100644 index c7e1eb3..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.iocoder.yudao.framework.common.exception.ServiceException; -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.request.AlipayTradePrecreateRequest; -import com.alipay.api.response.AlipayTradePrecreateResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; - -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; - -/** - * {@link AlipayQrPayClient} 单元测试 - * - * @author jason - */ -public class AlipayQrPayClientTest extends AbstractAlipayClientTest { - - @InjectMocks - private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config); - - @BeforeEach - public void setUp() { - setClient(client); - } - - @Test - @DisplayName("支付宝扫描支付:下单成功") - public void testUnifiedOrder_success() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - String qrCode = randomString(); - Integer price = randomInteger(); - AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { - o.setQrCode(qrCode); - o.setSubCode(""); - }); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertEquals(notifyUrl, request.getNotifyUrl()); - return true; - }))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(WAITING.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode()); - assertEquals(response.getQrCode(), resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝扫描支付:渠道返回失败") - public void testUnifiedOrder_channelFailed() throws AlipayApiException { - // mock 方法 - String notifyUrl = randomURL(); - String subCode = randomString(); - String subMsg = randomString(); - Integer price = randomInteger(); - AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { - o.setSubCode(subCode); - o.setSubMsg(subMsg); - }); - // mock - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertEquals(notifyUrl, request.getNotifyUrl()); - return true; - }))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(CLOSED.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertNull(resp.getDisplayMode()); - assertNull(resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertEquals(subCode, resp.getChannelErrorCode()); - assertEquals(subMsg, resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝扫描支付, 抛出系统异常") - public void testUnifiedOrder_throwPayException() throws AlipayApiException { - // mock 方法 - String outTradeNo = randomString(); - String notifyUrl = randomURL(); - Integer price = randomInteger(); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertEquals(notifyUrl, request.getNotifyUrl()); - return true; - }))).thenThrow(new RuntimeException("系统异常")); - // 准备请求参数 - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price); - - // 调用,并断言 - assertThrows(PayException.class, () -> client.unifiedOrder(reqDTO)); - } - - @Test - @DisplayName("支付宝 Client 统一下单:抛出业务异常") - public void testUnifiedOrder_throwServiceException() throws AlipayApiException { - // mock 方法 - String outTradeNo = randomString(); - String notifyUrl = randomURL(); - Integer price = randomInteger(); - when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { - assertEquals(notifyUrl, request.getNotifyUrl()); - return true; - }))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); - // 准备请求参数 - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - - // 调用,并断言 - assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java deleted file mode 100644 index af0c717..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; - -import cn.hutool.http.Method; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.alipay.api.AlipayApiException; -import com.alipay.api.domain.AlipayTradeWapPayModel; -import com.alipay.api.request.AlipayTradeWapPayRequest; -import com.alipay.api.response.AlipayTradeWapPayResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; - -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; -import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -/** - * {@link AlipayWapPayClient} 单元测试 - * - * @author jason - */ -public class AlipayWapPayClientTest extends AbstractAlipayClientTest { - - /** - * 支付宝 H5 支付 Client - */ - @InjectMocks - private AlipayWapPayClient client = new AlipayWapPayClient(randomLongId(), config); - - @BeforeEach - public void setUp() { - setClient(client); - } - - @Test - @DisplayName("支付宝 H5 支付:下单成功") - public void testUnifiedOrder_success() throws AlipayApiException { - // mock 方法 - String h5Body = randomString(); - Integer price = randomInteger(); - AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { - o.setSubCode(""); - o.setBody(h5Body); - }); - String notifyUrl = randomURL(); - when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> { - assertInstanceOf(AlipayTradeWapPayModel.class, request.getBizModel()); - AlipayTradeWapPayModel bizModel = (AlipayTradeWapPayModel) request.getBizModel(); - assertEquals(String.valueOf(price / 100.0), bizModel.getTotalAmount()); - assertEquals(notifyUrl, request.getNotifyUrl()); - return true; - }), eq(Method.GET.name()))).thenReturn(response); - // 准备请求参数 - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(WAITING.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); - assertEquals(response.getBody(), resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertNull(resp.getChannelErrorCode()); - assertNull(resp.getChannelErrorMsg()); - } - - @Test - @DisplayName("支付宝 H5 支付:渠道返回失败") - public void test_unified_order_channel_failed() throws AlipayApiException { - // mock 方法 - String subCode = randomString(); - String subMsg = randomString(); - AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { - o.setSubCode(subCode); - o.setSubMsg(subMsg); - }); - when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), - eq(Method.GET.name()))).thenReturn(response); - String outTradeNo = randomString(); - PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); - - // 调用 - PayOrderRespDTO resp = client.unifiedOrder(reqDTO); - // 断言 - assertEquals(CLOSED.getStatus(), resp.getStatus()); - assertEquals(outTradeNo, resp.getOutTradeNo()); - assertNull(resp.getChannelOrderNo()); - assertNull(resp.getChannelUserId()); - assertNull(resp.getSuccessTime()); - assertNull(resp.getDisplayMode()); - assertNull(resp.getDisplayContent()); - assertSame(response, resp.getRawData()); - assertEquals(subCode, resp.getChannelErrorCode()); - assertEquals(subMsg, resp.getChannelErrorMsg()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java deleted file mode 100644 index 9af11ac..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; -import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; -import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; -import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; -import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; -import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; -import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; -import com.github.binarywang.wxpay.config.WxPayConfig; -import com.github.binarywang.wxpay.exception.WxPayException; -import com.github.binarywang.wxpay.service.WxPayService; -import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2; - -/** - * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付 - * - * @author 芋道源码 - */ -@Disabled -public class WxBarPayClientIntegrationTest { - - @Test - public void testPayV2() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV2(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行发起支付 - WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() - .outTradeNo(String.valueOf(System.currentTimeMillis())) - .body("测试支付-body") - .detail("测试支付-detail") - .totalFee(1) // 单位分 - .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) - .spbillCreateIp("127.0.0.1") - .authCode("134298744426278497") - .build(); - System.out.println("========= request =========="); - System.out.println(JsonUtils.toJsonPrettyString(request)); - WxPayMicropayResult response = client.micropay(request); - System.out.println("========= response =========="); - System.out.println(JsonUtils.toJsonPrettyString(response)); - } - - @Test - public void testParseRefundNotifyV2() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV2(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行解析 - String xml = "SUCCESS"; - WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); - System.out.println(response.getReqInfo()); - } - - @Test - public void testRefundV2() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV2(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行发起退款 - WxPayRefundRequest request = new WxPayRefundRequest() - .setOutTradeNo("1689545667276") - .setOutRefundNo(String.valueOf(System.currentTimeMillis())) - .setRefundFee(1) - .setRefundDesc("就是想退了") - .setTotalFee(1); - System.out.println("========= request =========="); - System.out.println(JsonUtils.toJsonPrettyString(request)); - WxPayRefundResult response = client.refund(request); - System.out.println("========= response =========="); - System.out.println(JsonUtils.toJsonPrettyString(response)); - } - - @Test - public void testRefundV3() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV2(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行发起退款 - WxPayRefundV3Request request = new WxPayRefundV3Request() - .setOutTradeNo("1689506325635") - .setOutRefundNo(String.valueOf(System.currentTimeMillis())) - .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) - .setReason("就是想退了"); - System.out.println("========= request =========="); - System.out.println(JsonUtils.toJsonPrettyString(request)); - WxPayRefundV3Result response = client.refundV3(request); - System.out.println("========= response =========="); - System.out.println(JsonUtils.toJsonPrettyString(response)); - } - - private WxPayConfig buildWxPayConfigV2() { - WxPayConfig config = new WxPayConfig(); - config.setAppId("wx62056c0d5e8db250"); - config.setMchId("1545083881"); - config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); -// config.setSignType(WxPayConstants.SignType.MD5); - config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); - return config; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java deleted file mode 100644 index 5e73601..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; - -import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; -import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; -import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; -import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; -import com.github.binarywang.wxpay.config.WxPayConfig; -import com.github.binarywang.wxpay.exception.WxPayException; -import com.github.binarywang.wxpay.service.WxPayService; -import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3; - -/** - * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付 - * - * @author 芋道源码 - */ -@Disabled -public class WxNativePayClientIntegrationTest { - - @Test - public void testPayV3() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV3(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行发起支付 - WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request() - .setOutTradeNo(String.valueOf(System.currentTimeMillis())) - .setDescription("测试支付-body") - .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分 - .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) - .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1")) - .setNotifyUrl("http://127.0.0.1:48080"); - System.out.println("========= request =========="); - System.out.println(JsonUtils.toJsonPrettyString(request)); - String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); - System.out.println("========= response =========="); - System.out.println(JsonUtils.toJsonPrettyString(response)); - } - - @Test - public void testRefundV3() throws WxPayException { - // 创建 config 配置 - WxPayConfig config = buildWxPayConfigV3(); - // 创建 WxPayService 客户端 - WxPayService client = new WxPayServiceImpl(); - client.setConfig(config); - - // 执行发起退款 - WxPayRefundV3Request request = new WxPayRefundV3Request() - .setOutTradeNo("1689545729695") - .setOutRefundNo(String.valueOf(System.currentTimeMillis())) - .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) - .setReason("就是想退了"); - System.out.println("========= request =========="); - System.out.println(JsonUtils.toJsonPrettyString(request)); - WxPayRefundV3Result response = client.refundV3(request); - System.out.println("========= response =========="); - System.out.println(JsonUtils.toJsonPrettyString(response)); - } - - private WxPayConfig buildWxPayConfigV3() { - WxPayConfig config = new WxPayConfig(); - config.setAppId("wx62056c0d5e8db250"); - config.setMchId("1545083881"); - config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4"); -// config.setCertSerialNo(serialNo); - config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"); - config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"); - return config; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml b/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml deleted file mode 100644 index ac24f27..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - yudao-framework - cn.iocoder.boot - ${revision} - - 4.0.0 - - yudao-spring-boot-starter-flowable - - - - cn.iocoder.boot - yudao-common - - - - - cn.iocoder.boot - yudao-spring-boot-starter-security - - - - - org.flowable - flowable-spring-boot-starter-process - - - org.flowable - flowable-spring-boot-starter-actuator - - - - diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java deleted file mode 100644 index 859eca5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.config; - -import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; -import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.core.task.AsyncListenableTaskExecutor; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -@AutoConfiguration -public class YudaoFlowableConfiguration { - - /** - * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean - * - * 如果不创建,会导致项目启动时,Flowable 报错的问题 - */ - @Bean(name = "applicationTaskExecutor") - @ConditionalOnMissingBean(name = "applicationTaskExecutor") - public AsyncListenableTaskExecutor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(8); - executor.setMaxPoolSize(8); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("flowable-task-Executor-"); - executor.setAwaitTerminationSeconds(30); - executor.setWaitForTasksToCompleteOnShutdown(true); - executor.setAllowCoreThreadTimeOut(true); - executor.initialize(); - return executor; - } - - /** - * 配置 flowable Web 过滤器 - */ - @Bean - public FilterRegistrationBean flowableWebFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new FlowableWebFilter()); - registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER); - return registrationBean; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java deleted file mode 100644 index efc6d53..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core.context; - -import cn.hutool.core.collection.CollUtil; -import com.alibaba.ttl.TransmittableThreadLocal; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * 工作流--用户用到的上下文相关信息 - */ -public class FlowableContextHolder { - - private static final ThreadLocal>> ASSIGNEE = new TransmittableThreadLocal<>(); - - /** - * 通过流程任务的定义 key ,拿到提前选好的审批人 - * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取 - * - * @param taskDefinitionKey 流程任务 key - * @return 审批人 ID 集合 - */ - public static List getAssigneeByTaskDefinitionKey(String taskDefinitionKey) { - if (CollUtil.isNotEmpty(ASSIGNEE.get())) { - return ASSIGNEE.get().get(taskDefinitionKey); - } - return Collections.emptyList(); - } - - /** - * 存入提前选好的审批人到上下文线程变量中 - * - * @param assignee 流程任务 key -> 审批人 ID 炅和 - */ - public static void setAssignee(Map> assignee) { - ASSIGNEE.set(assignee); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java deleted file mode 100644 index c3cce42..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core.enums; - -/** - * 流程常量信息 - */ -public interface BpmnModelConstants { - - String BPMN_FILE_SUFFIX = ".bpmn"; - - /** - * BPMN 中的命名空间 - * - * 这个东西有可能导致无法切换工作流程的实现 - */ - String NAMESPACE = "http://flowable.org/bpmn"; - - /** - * 自定义属性 dataType - */ - String PROCESS_CUSTOM_DATA_TYPE = "dataType"; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java deleted file mode 100644 index de8d627..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core; diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java deleted file mode 100644 index 7b23a46..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java +++ /dev/null @@ -1,309 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core.util; - -import cn.hutool.core.collection.CollUtil; -import org.flowable.bpmn.converter.BpmnXMLConverter; -import org.flowable.bpmn.model.Process; -import org.flowable.bpmn.model.*; - -import java.util.*; - -/** - * 流程模型转操作工具类 - */ -public class BpmnModelUtils { - - /** - * 根据节点,获取入口连线 - * - * @param source 起始节点 - * @return 入口连线列表 - */ - public static List getElementIncomingFlows(FlowElement source) { - if (source instanceof FlowNode) { - return ((FlowNode) source).getIncomingFlows(); - } - return new ArrayList<>(); - } - - /** - * 根据节点,获取出口连线 - * - * @param source 起始节点 - * @return 出口连线列表 - */ - public static List getElementOutgoingFlows(FlowElement source) { - if (source instanceof FlowNode) { - return ((FlowNode) source).getOutgoingFlows(); - } - return new ArrayList<>(); - } - - /** - * 获取流程元素信息 - * - * @param model bpmnModel 对象 - * @param flowElementId 元素 ID - * @return 元素信息 - */ - public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { - Process process = model.getMainProcess(); - return process.getFlowElement(flowElementId); - } - - /** - * 获得 BPMN 流程中,指定的元素们 - * - * @param model - * @param clazz 指定元素。例如说,{@link UserTask}、{@link Gateway} 等等 - * @return 元素们 - */ - public static List getBpmnModelElements(BpmnModel model, Class clazz) { - List result = new ArrayList<>(); - model.getProcesses().forEach(process -> { - process.getFlowElements().forEach(flowElement -> { - if (flowElement.getClass().isAssignableFrom(clazz)) { - result.add((T) flowElement); - } - }); - }); - return result; - } - - /** - * 比较 两个bpmnModel 是否相同 - * @param oldModel 老的bpmn model - * @param newModel 新的bpmn model - */ - public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { - // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较 - return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); - } - - /** - * 把 bpmnModel 转换成 byte[] - * @param model bpmnModel - */ - public static byte[] getBpmnBytes(BpmnModel model) { - if (model == null) { - return new byte[0]; - } - BpmnXMLConverter converter = new BpmnXMLConverter(); - return converter.convertToXML(model); - } - - // ========== 遍历相关的方法 ========== - - /** - * 找到 source 节点之前的所有用户任务节点 - * - * @param source 起始节点 - * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 - * @param userTaskList 已找到的用户任务节点 - * @return 用户任务节点 数组 - */ - public static List getPreviousUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { - userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; - hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; - // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 - if (source instanceof StartEvent && source.getSubProcess() != null) { - userTaskList = getPreviousUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); - } - - // 根据类型,获取入口连线 - List sequenceFlows = getElementIncomingFlows(source); - if (sequenceFlows == null) { - return userTaskList; - } - // 循环找到目标元素 - for (SequenceFlow sequenceFlow : sequenceFlows) { - // 如果发现连线重复,说明循环了,跳过这个循环 - if (hasSequenceFlow.contains(sequenceFlow.getId())) { - continue; - } - // 添加已经走过的连线 - hasSequenceFlow.add(sequenceFlow.getId()); - // 类型为用户节点,则新增父级节点 - if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { - userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); - } - // 类型为子流程,则添加子流程开始节点出口处相连的节点 - if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { - // 获取子流程用户任务节点 - List childUserTaskList = findChildProcessUserTaskList((StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); - // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 - if (CollUtil.isNotEmpty(childUserTaskList)) { - userTaskList.addAll(childUserTaskList); - } - } - // 继续迭代 - userTaskList = getPreviousUserTaskList(sequenceFlow.getSourceFlowElement(), hasSequenceFlow, userTaskList); - } - return userTaskList; - } - - /** - * 迭代获取子流程用户任务节点 - * - * @param source 起始节点 - * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 - * @param userTaskList 需要撤回的用户任务列表 - * @return 用户任务节点 - */ - public static List findChildProcessUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { - hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; - userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; - - // 根据类型,获取出口连线 - List sequenceFlows = getElementOutgoingFlows(source); - if (sequenceFlows == null) { - return userTaskList; - } - // 循环找到目标元素 - for (SequenceFlow sequenceFlow : sequenceFlows) { - // 如果发现连线重复,说明循环了,跳过这个循环 - if (hasSequenceFlow.contains(sequenceFlow.getId())) { - continue; - } - // 添加已经走过的连线 - hasSequenceFlow.add(sequenceFlow.getId()); - // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 - if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { - userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); - continue; - } - // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 - if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { - List childUserTaskList = findChildProcessUserTaskList((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); - // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 - if (CollUtil.isNotEmpty(childUserTaskList)) { - userTaskList.addAll(childUserTaskList); - continue; - } - } - // 继续迭代 - userTaskList = findChildProcessUserTaskList(sequenceFlow.getTargetFlowElement(), hasSequenceFlow, userTaskList); - } - return userTaskList; - } - - - /** - * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行 - * 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况 - * - * @param source 起始节点 - * @param target 目标节点 - * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复 - * @return 结果 - */ - public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set visitedElements) { - visitedElements = visitedElements == null ? new HashSet<>() : visitedElements; - // 不能是开始事件和子流程 - if (source instanceof StartEvent && isInEventSubprocess(source)) { - return false; - } - - // 根据类型,获取入口连线 - List sequenceFlows = getElementIncomingFlows(source); - if (CollUtil.isEmpty(sequenceFlows)) { - return true; - } - // 循环找到目标元素 - for (SequenceFlow sequenceFlow : sequenceFlows) { - // 如果发现连线重复,说明循环了,跳过这个循环 - if (visitedElements.contains(sequenceFlow.getId())) { - continue; - } - // 添加已经走过的连线 - visitedElements.add(sequenceFlow.getId()); - // 这条线路存在目标节点,这条线路完成,进入下个线路 - FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); - if (target.getId().equals(sourceFlowElement.getId())) { - continue; - } - // 如果目标节点为并行网关,则不继续 - if (sourceFlowElement instanceof ParallelGateway) { - return false; - } - // 否则就继续迭代 - if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) { - return false; - } - } - return true; - } - - /** - * 判断当前节点是否属于不同的子流程 - * - * @param flowElement 被判断的节点 - * @return true 表示属于子流程 - */ - private static boolean isInEventSubprocess(FlowElement flowElement) { - FlowElementsContainer flowElementsContainer = flowElement.getParentContainer(); - while (flowElementsContainer != null) { - if (flowElementsContainer instanceof EventSubProcess) { - return true; - } - - if (flowElementsContainer instanceof FlowElement) { - flowElementsContainer = ((FlowElement) flowElementsContainer).getParentContainer(); - } else { - flowElementsContainer = null; - } - } - return false; - } - - /** - * 根据正在运行的任务节点,迭代获取子级任务节点列表,向后找 - * - * @param source 起始节点 - * @param runTaskKeyList 正在运行的任务 Key,用于校验任务节点是否是正在运行的节点 - * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 - * @param userTaskList 需要撤回的用户任务列表 - * @return 子级任务节点列表 - */ - public static List iteratorFindChildUserTasks(FlowElement source, List runTaskKeyList, - Set hasSequenceFlow, List userTaskList) { - hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; - userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; - // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 - if (source instanceof StartEvent && source.getSubProcess() != null) { - userTaskList = iteratorFindChildUserTasks(source.getSubProcess(), runTaskKeyList, hasSequenceFlow, userTaskList); - } - - // 根据类型,获取出口连线 - List sequenceFlows = getElementOutgoingFlows(source); - if (sequenceFlows == null) { - return userTaskList; - } - // 循环找到目标元素 - for (SequenceFlow sequenceFlow : sequenceFlows) { - // 如果发现连线重复,说明循环了,跳过这个循环 - if (hasSequenceFlow.contains(sequenceFlow.getId())) { - continue; - } - // 添加已经走过的连线 - hasSequenceFlow.add(sequenceFlow.getId()); - // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 - if (sequenceFlow.getTargetFlowElement() instanceof UserTask && runTaskKeyList.contains((sequenceFlow.getTargetFlowElement()).getId())) { - userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); - continue; - } - // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 - if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { - List childUserTaskList = iteratorFindChildUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runTaskKeyList, hasSequenceFlow, null); - // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 - if (CollUtil.isNotEmpty(childUserTaskList)) { - userTaskList.addAll(childUserTaskList); - continue; - } - } - // 继续迭代 - userTaskList = iteratorFindChildUserTasks(sequenceFlow.getTargetFlowElement(), runTaskKeyList, hasSequenceFlow, userTaskList); - } - return userTaskList; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java deleted file mode 100644 index 2a0fc38..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core.util; - -import org.flowable.common.engine.impl.identity.Authentication; - -/** - * Flowable 相关的工具方法 - * - * @author 芋道源码 - */ -public class FlowableUtils { - - // ========== User 相关的工具方法 ========== - - public static void setAuthenticatedUserId(Long userId) { - Authentication.setAuthenticatedUserId(String.valueOf(userId)); - } - - public static void clearAuthenticatedUserId() { - Authentication.setAuthenticatedUserId(null); - } - - // ========== Execution 相关的工具方法 ========== - - public static String formatCollectionVariable(String activityId) { - return activityId + "_assignees"; - } - - public static String formatCollectionElementVariable(String activityId) { - return activityId + "_assignee"; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java deleted file mode 100644 index d213529..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.framework.flowable.core.web; - -import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -/** - * flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中 - * - * @author jason - */ -public class FlowableWebFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - try { - // 设置工作流的用户 - Long userId = SecurityFrameworkUtils.getLoginUserId(); - if (userId != null) { - FlowableUtils.setAuthenticatedUserId(userId); - } - // 过滤 - chain.doFilter(request, response); - } finally { - // 清理 - FlowableUtils.clearAuthenticatedUserId(); - } - } -} diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java deleted file mode 100644 index 324d3de..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.yudao.framework.flowable; diff --git a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 1df6159..0000000 --- a/yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration \ No newline at end of file diff --git a/yudao-module-bpm/pom.xml b/yudao-module-bpm/pom.xml index 95b7989..491c997 100644 --- a/yudao-module-bpm/pom.xml +++ b/yudao-module-bpm/pom.xml @@ -11,6 +11,7 @@ yudao-module-bpm-api yudao-module-bpm-biz + yudao-spring-boot-starter-flowable yudao-module-bpm pom diff --git a/yudao-module-bpm/yudao-module-bpm-biz/pom.xml b/yudao-module-bpm/yudao-module-bpm-biz/pom.xml index 12d5c5b..f1ac4c0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/pom.xml +++ b/yudao-module-bpm/yudao-module-bpm-biz/pom.xml @@ -63,10 +63,12 @@ cn.iocoder.boot yudao-spring-boot-starter-test + cn.iocoder.boot yudao-spring-boot-starter-flowable + ${revision} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/pom.xml b/yudao-module-bpm/yudao-spring-boot-starter-flowable/pom.xml new file mode 100644 index 0000000..ae0ed46 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/pom.xml @@ -0,0 +1,37 @@ + + + + cn.iocoder.boot + yudao-module-bpm + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-flowable + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + + org.flowable + flowable-spring-boot-starter-process + + + org.flowable + flowable-spring-boot-starter-actuator + + + + diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java new file mode 100644 index 0000000..859eca5 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.flowable.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@AutoConfiguration +public class YudaoFlowableConfiguration { + + /** + * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean + * + * 如果不创建,会导致项目启动时,Flowable 报错的问题 + */ + @Bean(name = "applicationTaskExecutor") + @ConditionalOnMissingBean(name = "applicationTaskExecutor") + public AsyncListenableTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("flowable-task-Executor-"); + executor.setAwaitTerminationSeconds(30); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAllowCoreThreadTimeOut(true); + executor.initialize(); + return executor; + } + + /** + * 配置 flowable Web 过滤器 + */ + @Bean + public FilterRegistrationBean flowableWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new FlowableWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER); + return registrationBean; + } + +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java new file mode 100644 index 0000000..efc6d53 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.flowable.core.context; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.ttl.TransmittableThreadLocal; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 工作流--用户用到的上下文相关信息 + */ +public class FlowableContextHolder { + + private static final ThreadLocal>> ASSIGNEE = new TransmittableThreadLocal<>(); + + /** + * 通过流程任务的定义 key ,拿到提前选好的审批人 + * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取 + * + * @param taskDefinitionKey 流程任务 key + * @return 审批人 ID 集合 + */ + public static List getAssigneeByTaskDefinitionKey(String taskDefinitionKey) { + if (CollUtil.isNotEmpty(ASSIGNEE.get())) { + return ASSIGNEE.get().get(taskDefinitionKey); + } + return Collections.emptyList(); + } + + /** + * 存入提前选好的审批人到上下文线程变量中 + * + * @param assignee 流程任务 key -> 审批人 ID 炅和 + */ + public static void setAssignee(Map> assignee) { + ASSIGNEE.set(assignee); + } + +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java new file mode 100644 index 0000000..c3cce42 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/enums/BpmnModelConstants.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.flowable.core.enums; + +/** + * 流程常量信息 + */ +public interface BpmnModelConstants { + + String BPMN_FILE_SUFFIX = ".bpmn"; + + /** + * BPMN 中的命名空间 + * + * 这个东西有可能导致无法切换工作流程的实现 + */ + String NAMESPACE = "http://flowable.org/bpmn"; + + /** + * 自定义属性 dataType + */ + String PROCESS_CUSTOM_DATA_TYPE = "dataType"; + +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java new file mode 100644 index 0000000..de8d627 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.flowable.core; diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java new file mode 100644 index 0000000..7b23a46 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/BpmnModelUtils.java @@ -0,0 +1,309 @@ +package cn.iocoder.yudao.framework.flowable.core.util; + +import cn.hutool.core.collection.CollUtil; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.*; + +import java.util.*; + +/** + * 流程模型转操作工具类 + */ +public class BpmnModelUtils { + + /** + * 根据节点,获取入口连线 + * + * @param source 起始节点 + * @return 入口连线列表 + */ + public static List getElementIncomingFlows(FlowElement source) { + if (source instanceof FlowNode) { + return ((FlowNode) source).getIncomingFlows(); + } + return new ArrayList<>(); + } + + /** + * 根据节点,获取出口连线 + * + * @param source 起始节点 + * @return 出口连线列表 + */ + public static List getElementOutgoingFlows(FlowElement source) { + if (source instanceof FlowNode) { + return ((FlowNode) source).getOutgoingFlows(); + } + return new ArrayList<>(); + } + + /** + * 获取流程元素信息 + * + * @param model bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 元素信息 + */ + public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { + Process process = model.getMainProcess(); + return process.getFlowElement(flowElementId); + } + + /** + * 获得 BPMN 流程中,指定的元素们 + * + * @param model + * @param clazz 指定元素。例如说,{@link UserTask}、{@link Gateway} 等等 + * @return 元素们 + */ + public static List getBpmnModelElements(BpmnModel model, Class clazz) { + List result = new ArrayList<>(); + model.getProcesses().forEach(process -> { + process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + }); + }); + return result; + } + + /** + * 比较 两个bpmnModel 是否相同 + * @param oldModel 老的bpmn model + * @param newModel 新的bpmn model + */ + public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { + // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较 + return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); + } + + /** + * 把 bpmnModel 转换成 byte[] + * @param model bpmnModel + */ + public static byte[] getBpmnBytes(BpmnModel model) { + if (model == null) { + return new byte[0]; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToXML(model); + } + + // ========== 遍历相关的方法 ========== + + /** + * 找到 source 节点之前的所有用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 已找到的用户任务节点 + * @return 用户任务节点 数组 + */ + public static List getPreviousUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = getPreviousUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList); + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 类型为用户节点,则新增父级节点 + if (sequenceFlow.getSourceFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement()); + } + // 类型为子流程,则添加子流程开始节点出口处相连的节点 + if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) { + // 获取子流程用户任务节点 + List childUserTaskList = findChildProcessUserTaskList((StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + } + } + // 继续迭代 + userTaskList = getPreviousUserTaskList(sequenceFlow.getSourceFlowElement(), hasSequenceFlow, userTaskList); + } + return userTaskList; + } + + /** + * 迭代获取子流程用户任务节点 + * + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return 用户任务节点 + */ + public static List findChildProcessUserTaskList(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = findChildProcessUserTaskList((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = findChildProcessUserTaskList(sequenceFlow.getTargetFlowElement(), hasSequenceFlow, userTaskList); + } + return userTaskList; + } + + + /** + * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行 + * 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况 + * + * @param source 起始节点 + * @param target 目标节点 + * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复 + * @return 结果 + */ + public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set visitedElements) { + visitedElements = visitedElements == null ? new HashSet<>() : visitedElements; + // 不能是开始事件和子流程 + if (source instanceof StartEvent && isInEventSubprocess(source)) { + return false; + } + + // 根据类型,获取入口连线 + List sequenceFlows = getElementIncomingFlows(source); + if (CollUtil.isEmpty(sequenceFlows)) { + return true; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (visitedElements.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + visitedElements.add(sequenceFlow.getId()); + // 这条线路存在目标节点,这条线路完成,进入下个线路 + FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); + if (target.getId().equals(sourceFlowElement.getId())) { + continue; + } + // 如果目标节点为并行网关,则不继续 + if (sourceFlowElement instanceof ParallelGateway) { + return false; + } + // 否则就继续迭代 + if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) { + return false; + } + } + return true; + } + + /** + * 判断当前节点是否属于不同的子流程 + * + * @param flowElement 被判断的节点 + * @return true 表示属于子流程 + */ + private static boolean isInEventSubprocess(FlowElement flowElement) { + FlowElementsContainer flowElementsContainer = flowElement.getParentContainer(); + while (flowElementsContainer != null) { + if (flowElementsContainer instanceof EventSubProcess) { + return true; + } + + if (flowElementsContainer instanceof FlowElement) { + flowElementsContainer = ((FlowElement) flowElementsContainer).getParentContainer(); + } else { + flowElementsContainer = null; + } + } + return false; + } + + /** + * 根据正在运行的任务节点,迭代获取子级任务节点列表,向后找 + * + * @param source 起始节点 + * @param runTaskKeyList 正在运行的任务 Key,用于校验任务节点是否是正在运行的节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 需要撤回的用户任务列表 + * @return 子级任务节点列表 + */ + public static List iteratorFindChildUserTasks(FlowElement source, List runTaskKeyList, + Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow; + userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList; + // 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代 + if (source instanceof StartEvent && source.getSubProcess() != null) { + userTaskList = iteratorFindChildUserTasks(source.getSubProcess(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + + // 根据类型,获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (sequenceFlows == null) { + return userTaskList; + } + // 循环找到目标元素 + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + // 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加 + if (sequenceFlow.getTargetFlowElement() instanceof UserTask && runTaskKeyList.contains((sequenceFlow.getTargetFlowElement()).getId())) { + userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement()); + continue; + } + // 如果节点为子流程节点情况,则从节点中的第一个节点开始获取 + if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) { + List childUserTaskList = iteratorFindChildUserTasks((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), runTaskKeyList, hasSequenceFlow, null); + // 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续 + if (CollUtil.isNotEmpty(childUserTaskList)) { + userTaskList.addAll(childUserTaskList); + continue; + } + } + // 继续迭代 + userTaskList = iteratorFindChildUserTasks(sequenceFlow.getTargetFlowElement(), runTaskKeyList, hasSequenceFlow, userTaskList); + } + return userTaskList; + } + +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java new file mode 100644 index 0000000..2a0fc38 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.flowable.core.util; + +import org.flowable.common.engine.impl.identity.Authentication; + +/** + * Flowable 相关的工具方法 + * + * @author 芋道源码 + */ +public class FlowableUtils { + + // ========== User 相关的工具方法 ========== + + public static void setAuthenticatedUserId(Long userId) { + Authentication.setAuthenticatedUserId(String.valueOf(userId)); + } + + public static void clearAuthenticatedUserId() { + Authentication.setAuthenticatedUserId(null); + } + + // ========== Execution 相关的工具方法 ========== + + public static String formatCollectionVariable(String activityId) { + return activityId + "_assignees"; + } + + public static String formatCollectionElementVariable(String activityId) { + return activityId + "_assignee"; + } + +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java new file mode 100644 index 0000000..d213529 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.flowable.core.web; + +import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +/** + * flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中 + * + * @author jason + */ +public class FlowableWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + // 设置工作流的用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId != null) { + FlowableUtils.setAuthenticatedUserId(userId); + } + // 过滤 + chain.doFilter(request, response); + } finally { + // 清理 + FlowableUtils.clearAuthenticatedUserId(); + } + } +} diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java new file mode 100644 index 0000000..324d3de --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.flowable; diff --git a/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..1df6159 --- /dev/null +++ b/yudao-module-bpm/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-trade-biz/pom.xml b/yudao-module-mall/yudao-module-trade-biz/pom.xml index 912135e..277fa48 100644 --- a/yudao-module-mall/yudao-module-trade-biz/pom.xml +++ b/yudao-module-mall/yudao-module-trade-biz/pom.xml @@ -74,12 +74,6 @@ yudao-spring-boot-starter-security - - - cn.iocoder.boot - yudao-spring-boot-starter-biz-pay - - cn.iocoder.boot diff --git a/yudao-module-pay/pom.xml b/yudao-module-pay/pom.xml index af42593..85bcb1e 100644 --- a/yudao-module-pay/pom.xml +++ b/yudao-module-pay/pom.xml @@ -13,6 +13,7 @@ yudao-module-pay-api yudao-module-pay-biz + yudao-spring-boot-starter-biz-pay ${project.artifactId} @@ -21,5 +22,4 @@ 例如说:商户、应用、支付、退款等等 - diff --git a/yudao-module-pay/yudao-module-pay-biz/pom.xml b/yudao-module-pay/yudao-module-pay-biz/pom.xml index 8531e86..f991ccd 100644 --- a/yudao-module-pay/yudao-module-pay-biz/pom.xml +++ b/yudao-module-pay/yudao-module-pay-biz/pom.xml @@ -3,8 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - yudao-module-pay cn.iocoder.boot + yudao-module-pay ${revision} 4.0.0 @@ -37,6 +37,7 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-pay + ${revision} cn.iocoder.boot diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml new file mode 100644 index 0000000..93193d0 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/pom.xml @@ -0,0 +1,76 @@ + + + + cn.iocoder.boot + yudao-module-pay + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-biz-pay + ${project.artifactId} + 支付拓展,接入国内多个支付渠道 + 1. 支付宝,基于官方 SDK 接入 + 2. 微信支付,基于 weixin-java-pay 接入 + + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.alipay.sdk + alipay-sdk-java + 4.35.79.ALL + + + org.bouncycastle + bcprov-jdk15on + + + + + com.github.binarywang + weixin-java-pay + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java new file mode 100644 index 0000000..e57f4ac --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.pay.config; + +import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 支付配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YudaoPayAutoConfiguration { + + @Bean + public PayClientFactory payClientFactory() { + return new PayClientFactoryImpl(); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java new file mode 100644 index 0000000..86e3566 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; + +import java.util.Map; + +/** + * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 + * + * @author 芋道源码 + */ +public interface PayClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + // ============ 支付相关 ========== + + /** + * 调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 支付订单信息 + */ + PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + + /** + * 解析 order 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @return 支付订单信息 + */ + PayOrderRespDTO parseOrderNotify(Map params, String body); + + /** + * 获得支付订单信息 + * + * @param outTradeNo 外部订单号 + * @return 支付订单信息 + */ + PayOrderRespDTO getOrder(String outTradeNo); + + // ============ 退款相关 ========== + + /** + * 调用支付渠道,进行退款 + * + * @param reqDTO 统一退款请求信息 + * @return 退款信息 + */ + PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); + + /** + * 解析 refund 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @return 支付订单信息 + */ + PayRefundRespDTO parseRefundNotify(Map params, String body); + + /** + * 获得退款订单信息 + * + * @param outTradeNo 外部订单号 + * @param outRefundNo 外部退款号 + * @return 退款订单信息 + */ + PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo); + + /** + * 调用渠道,进行转账 + * + * @param reqDTO 统一转账请求信息 + * @return 转账信息 + */ + PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO); + + /** + * 获得转账订单信息 + * + * @param outTradeNo 外部订单号 + * @param type 转账类型 + * @return 转账信息 + */ + PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type); +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java new file mode 100644 index 0000000..beb3b41 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import jakarta.validation.Validator; + +/** + * 支付客户端的配置,本质是支付渠道的配置 + * 每个不同的渠道,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface PayClientConfig { + + /** + * 参数校验 + * + * @param validator 校验对象 + */ + void validate(Validator validator); + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java new file mode 100644 index 0000000..53f1a8c --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.pay.core.client; + +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; + +/** + * 支付客户端的工厂接口 + * + * @author 芋道源码 + */ +public interface PayClientFactory { + + /** + * 获得支付客户端 + * + * @param channelId 渠道编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long channelId); + + /** + * 创建支付客户端 + * + * @param channelId 渠道编号 + * @param channelCode 渠道编码 + * @param config 支付配置 + */ + void createOrUpdatePayClient(Long channelId, String channelCode, + Config config); + + /** + * 注册支付客户端 Class,用于模块中实现的 PayClient + * + * @param channel 支付渠道的编码的枚举 + * @param payClientClass 支付客户端 class + */ + void registerPayClientClass(PayChannelEnum channel, Class payClientClass); + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java new file mode 100644 index 0000000..82050a6 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -0,0 +1,141 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.order; + +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道支付订单 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 支付状态 + * + * 枚举:{@link PayOrderStatusRespEnum} + */ + private Integer status; + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + private String outTradeNo; + + /** + * 支付渠道编号 + */ + private String channelOrderNo; + /** + * 支付渠道用户编号 + */ + private String channelUserId; + + /** + * 支付成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的同步/异步通知结果 + */ + private Object rawData; + + // ========== 主动发起支付时,会返回的字段 ========== + + /** + * 展示模式 + * + * 枚举 {@link PayOrderDisplayModeEnum} 类 + */ + private String displayMode; + /** + * 展示内容 + */ + private String displayContent; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + public PayOrderRespDTO() { + } + + /** + * 创建【WAITING】状态的订单返回 + */ + public static PayOrderRespDTO waitingOf(String displayMode, String displayContent, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus(); + respDTO.displayMode = displayMode; + respDTO.displayContent = displayContent; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的订单返回 + */ + public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建指定状态的订单返回,适合支付渠道回调时 + */ + public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = status; + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 + */ + public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java new file mode 100644 index 0000000..15be7a7 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.order; + +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 统一下单 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderUnifiedReqDTO { + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述信息 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + /** + * 支付结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + /** + * 支付结果的 return 回调地址 + */ + @URL(message = "支付结果的 return 回调地址必须是 URL 格式") + private String returnUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + + // ========== 拓展参数 ========== + /** + * 支付渠道的额外参数 + * + * 例如说,微信公众号需要传递 openid 参数 + */ + private Map channelExtras; + + /** + * 展示模式 + * + * 如果不传递,则每个支付渠道使用默认的方式 + * + * 枚举 {@link PayOrderDisplayModeEnum} + */ + private String displayMode; + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java new file mode 100644 index 0000000..3184f27 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java @@ -0,0 +1,115 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.refund; + +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道退款订单 Response DTO + * + * @author jason + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusRespEnum} + */ + private Integer status; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + private String outRefundNo; + + /** + * 渠道退款单号 + * + * 对应 PayRefundDO.channelRefundNo 字段 + */ + private String channelRefundNo; + + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的异步通知结果 + */ + private Object rawData; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + private PayRefundRespDTO() { + } + + /** + * 创建【WAITING】状态的退款返回 + */ + public static PayRefundRespDTO waitingOf(String channelRefundNo, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的退款返回 + */ + public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) { + return failureOf(null, null, + outRefundNo, rawData); + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java new file mode 100644 index 0000000..3101633 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.refund; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 统一 退款 Request DTO + * + * @author jason + */ +@Accessors(chain = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class PayRefundUnifiedReqDTO { + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + @NotEmpty(message = "退款请求单号不能为空") + private String outRefundNo; + + /** + * 退款原因 + */ + @NotEmpty(message = "退款原因不能为空") + private String reason; + + /** + * 支付金额,单位:分 + * + * 目前微信支付在退款的时候,必须传递该字段 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer refundPrice; + + /** + * 退款结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java new file mode 100644 index 0000000..0f9b482 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.transfer; + +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 统一转账 Response DTO + * + * @author jason + */ +@Data +public class PayTransferRespDTO { + + /** + * 转账状态 + * + * 关联 {@link PayTransferStatusRespEnum#getStatus()} + */ + private Integer status; + + /** + * 外部转账单号 + * + */ + private String outTransferNo; + + /** + * 支付渠道编号 + */ + private String channelTransferNo; + + /** + * 支付成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的返回结果 + */ + private Object rawData; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 创建【WAITING】状态的转账返回 + */ + public static PayTransferRespDTO waitingOf(String channelTransferNo, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【IN_PROGRESS】状态的转账返回 + */ + public static PayTransferRespDTO dealingOf(String channelTransferNo, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【CLOSED】状态的转账返回 + */ + public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusRespEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的转账返回 + */ + public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime, + String outTransferNo, Object rawData) { + PayTransferRespDTO respDTO = new PayTransferRespDTO(); + respDTO.status = PayTransferStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelTransferNo = channelTransferNo; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTransferNo = outTransferNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java new file mode 100644 index 0000000..43afa86 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferUnifiedReqDTO.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.framework.pay.core.client.dto.transfer; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.Map; + +import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.*; + +/** + * 统一转账 Request DTO + * + * @author jason + */ +@Data +public class PayTransferUnifiedReqDTO { + + /** + * 转账类型 + * + * 关联 {@link PayTransferTypeEnum#getType()} + */ + @NotNull(message = "转账类型不能为空") + @InEnum(PayTransferTypeEnum.class) + private Integer type; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @NotEmpty(message = "外部转账单编号不能为空") + private String outTransferNo; + + /** + * 转账金额,单位:分 + */ + @NotNull(message = "转账金额不能为空") + @Min(value = 1, message = "转账金额必须大于零") + private Integer price; + + /** + * 转账标题 + */ + @NotEmpty(message = "转账标题不能为空") + @Length(max = 128, message = "转账标题不能超过 128") + private String subject; + + /** + * 收款人姓名 + */ + @NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class}) + private String userName; + + /** + * 支付宝登录号 + */ + @NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class}) + private String alipayLogonId; + + /** + * 微信 openId + */ + @NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class}) + private String openid; + + /** + * 支付渠道的额外参数 + */ + private Map channelExtras; +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java new file mode 100644 index 0000000..75f4c39 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/exception/PayException.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.pay.core.client.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 支付系统异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PayException extends RuntimeException { + + public PayException(Throwable cause) { + super(cause); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java new file mode 100644 index 0000000..82d68b5 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -0,0 +1,250 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +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.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayClient implements PayClient { + + /** + * 渠道编号 + */ + private final Long channelId; + /** + * 渠道编码 + */ + @SuppressWarnings("FieldCanBeLocal") + private final String channelCode; + /** + * 支付配置 + */ + protected Config config; + + public AbstractPayClient(Long channelId, String channelCode, Config config) { + this.channelId = channelId; + this.channelCode = channelCode; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.debug("[init][客户端({}) 初始化完成]", getId()); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][客户端({})发生变化,重新初始化]", getId()); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return channelId; + } + + // ============ 支付相关 ========== + + @Override + public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一下单 + PayOrderRespDTO resp; + try { + resp = doUnifiedOrder(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable; + + @Override + public final PayOrderRespDTO parseOrderNotify(Map params, String body) { + try { + return doParseOrderNotify(params, body); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]", + getId(), params, body, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body) + throws Throwable; + + @Override + public final PayOrderRespDTO getOrder(String outTradeNo) { + try { + return doGetOrder(outTradeNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) + throws Throwable; + + // ============ 退款相关 ========== + + @Override + public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一退款 + PayRefundRespDTO resp; + try { + resp = doUnifiedRefund(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; + + @Override + public final PayRefundRespDTO parseRefundNotify(Map params, String body) { + try { + return doParseRefundNotify(params, body); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]", + getId(), params, body, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doParseRefundNotify(Map params, String body) + throws Throwable; + + @Override + public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) { + try { + return doGetRefund(outTradeNo, outRefundNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]", + getId(), outTradeNo, outRefundNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) + throws Throwable; + + @Override + public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + validatePayTransferReqDTO(reqDTO); + PayTransferRespDTO resp; + try { + resp = doUnifiedTransfer(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) { + PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType()); + switch (transferType) { + case ALIPAY_BALANCE: { + ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class); + break; + } + case WX_BALANCE: { + ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class); + break; + } + default: { + throw exception(NOT_IMPLEMENTED); + } + } + } + + @Override + public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) { + try { + return doGetTransfer(outTradeNo, type); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]", + getId(), outTradeNo, type, ex); + throw buildPayException(ex); + } + } + + protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) + throws Throwable; + + protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) + throws Throwable; + + // ========== 各种工具方法 ========== + + private PayException buildPayException(Throwable ex) { + if (ex instanceof PayException) { + return (PayException) ex; + } + throw new PayException(ex); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java new file mode 100644 index 0000000..80581f3 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/NonePayClientConfig.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import jakarta.validation.Validator; + +/** + * 无需任何配置 PayClientConfig 实现类 + * + * @author jason + */ +@Data +public class NonePayClientConfig implements PayClientConfig { + + /** + * 配置名称 + *

+ * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称 + */ + private String name; + + public NonePayClientConfig(){ + this.name = "none-config"; + } + + @Override + public void validate(Validator validator) { + // 无任何配置不需要校验 + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java new file mode 100644 index 0000000..0b39587 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +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.PayClientFactory; +import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.*; +import cn.iocoder.yudao.framework.pay.core.client.impl.mock.MockPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.*; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum.*; + +/** + * 支付客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class PayClientFactoryImpl implements PayClientFactory { + + /** + * 支付客户端 Map + * + * key:渠道编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + /** + * 支付客户端 Class Map + */ + private final Map> clientClass = new ConcurrentHashMap<>(); + + public PayClientFactoryImpl() { + // 微信支付客户端 + clientClass.put(WX_PUB, WxPubPayClient.class); + clientClass.put(WX_LITE, WxLitePayClient.class); + clientClass.put(WX_APP, WxAppPayClient.class); + clientClass.put(WX_BAR, WxBarPayClient.class); + clientClass.put(WX_NATIVE, WxNativePayClient.class); + // 支付包支付客户端 + clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class); + clientClass.put(ALIPAY_QR, AlipayQrPayClient.class); + clientClass.put(ALIPAY_APP, AlipayAppPayClient.class); + clientClass.put(ALIPAY_PC, AlipayPcPayClient.class); + clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class); + // Mock 支付客户端 + clientClass.put(MOCK, MockPayClient.class); + } + + @Override + public void registerPayClientClass(PayChannelEnum channel, Class payClientClass) { + clientClass.put(channel, payClientClass); + } + + @Override + public PayClient getPayClient(Long channelId) { + AbstractPayClient client = clients.get(channelId); + if (client == null) { + log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdatePayClient(Long channelId, String channelCode, + Config config) { + AbstractPayClient client = (AbstractPayClient) clients.get(channelId); + if (client == null) { + client = this.createPayClient(channelId, channelCode, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractPayClient createPayClient(Long channelId, String channelCode, + Config config) { + PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); + Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode)); + Class payClientClass = clientClass.get(channelEnum); + Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode)); + return (AbstractPayClient) ReflectUtil.newInstance(payClientClass, channelId, config); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java new file mode 100644 index 0000000..4dcf236 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -0,0 +1,342 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConfig; +import com.alipay.api.AlipayResponse; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.*; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.*; +import com.alipay.api.response.*; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; +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.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; + +/** + * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) + * + * @author jason + */ +@Slf4j +public abstract class AbstractAlipayPayClient extends AbstractPayClient { + + @Getter // 仅用于单测场景 + protected DefaultAlipayClient client; + + public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + @SneakyThrows + protected void doInit() { + AlipayConfig alipayConfig = new AlipayConfig(); + BeanUtil.copyProperties(config, alipayConfig, false); + this.client = new DefaultAlipayClient(alipayConfig); + } + + // ============ 支付相关 ========== + + /** + * 构造支付关闭的 {@link PayOrderRespDTO} 对象 + * + * @return 支付关闭的 {@link PayOrderRespDTO} 对象 + */ + protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) { + Assert.isFalse(response.isSuccess()); + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTradeNo(), response); + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable { + // 1. 校验回调数据 + Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); + AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), + StandardCharsets.UTF_8.name(), config.getSignType()); + + // 2. 解析订单的状态 + // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 + Integer status = parseStatus(bodyObj.get("trade_status")); + // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功 + if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) { + status = PayOrderStatusRespEnum.REFUND.getStatus(); + } + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); + }); + return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), + bodyObj.get("out_trade_no"), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeQueryModel model = new AlipayTradeQueryModel(); + model.setOutTradeNo(outTradeNo); + // 1.2 构建 AlipayTradeQueryRequest 请求 + AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); + request.setBizModel(model); + AlipayTradeQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { // 不成功,例如说订单不存在 + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + // 2.2 解析订单的状态 + Integer status = parseStatus(response.getTradeStatus()); + Assert.notNull(status, () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); + }); + return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeStatus) { + return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus() + : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus() + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null; + } + + // ============ 退款相关 ========== + + /** + * 支付宝统一的退款接口 alipay.trade.refund + * + * @param reqDTO 退款请求 request DTO + * @return 退款请求 Response + */ + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeRefundModel model = new AlipayTradeRefundModel(); + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setOutRequestNo(reqDTO.getOutRefundNo()); + model.setRefundAmount(formatAmount(reqDTO.getRefundPrice())); + model.setRefundReason(reqDTO.getReason()); + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeRefundResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询 + if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) { + return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response); + } + // 2.2 创建返回结果 + // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。 + // 另外,支付宝没有退款单号,所以不用设置 + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body) { + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 + throw new UnsupportedOperationException("支付宝无退款回调"); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException { + // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求 + AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); + model.setOutTradeNo(outTradeNo); + model.setOutRequestNo(outRefundNo); + model.setQueryOptions(Collections.singletonList("gmt_refund_pay")); + // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求 + AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeFastpayRefundQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + if (!response.isSuccess()) { + // 明确不存在的情况,应该就是失败,可进行关闭 + if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待 + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + // 2.2 创建返回结果 + if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) { + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + outRefundNo, response); + } + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 校验公钥类型 必须使用公钥证书模式 + if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式"); + } + // 1.2 构建 AlipayFundTransUniTransferModel + AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); + // ① 通用的参数 + model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额 + model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。 + model.setOutBizNo(reqDTO.getOutTransferNo()); + model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD + model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER + if (reqDTO.getChannelExtras() != null) { + model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); + } + // ② 个性化的参数 + Participant payeeInfo = new Participant(); + PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType()); + switch (transferType) { + // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦? + // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试?? + case ALIPAY_BALANCE: { + payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); + payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号 + payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名 + model.setPayeeInfo(payeeInfo); + break; + } + case BANK_CARD: { + payeeInfo.setIdentityType("BANKCARD_ACCOUNT"); + // TODO 待实现 + throw exception(NOT_IMPLEMENTED); + } + default: { + throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType); + } + } + // 1.3 构建 AlipayFundTransUniTransferRequest + AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); + request.setBizModel(model); + // 执行请求 + AlipayFundTransUniTransferResponse response = client.certificateExecute(request); + // 处理结果 + if (!response.isSuccess()) { + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账 + // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询 + if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { + return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); + } + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTransferNo(), response); + } else { + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTransferNo(), response); + } + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 + return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response); + } + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), + response.getOutBizNo(), response); + } + + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable { + // 1.1 构建 AlipayFundTransCommonQueryModel + AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); + model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD"); + model.setBizScene("DIRECT_TRANSFER"); //业务场景 + model.setOutBizNo(outTradeNo); + // 1.2 构建 AlipayFundTransCommonQueryRequest + AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayFundTransCommonQueryResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + // 2.2 处理返回结果 + if (response.isSuccess()) { + if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 + return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response); + } + return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()), + response.getOutBizNo(), response); + } else { + // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 + // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 + if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { + return PayTransferRespDTO.waitingOf(null, outTradeNo, response); + } + return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + } + + // ========== 各种工具方法 ========== + + protected String formatAmount(Integer amount) { + return String.valueOf(amount / 100.0); + } + + protected String formatTime(LocalDateTime time) { + return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER); + } + + protected LocalDateTime parseTime(String str) { + return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java new file mode 100644 index 0000000..4e5a37e --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeAppPayModel; +import com.alipay.api.request.AlipayTradeAppPayRequest; +import com.alipay.api.response.AlipayTradeAppPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayAppPayClient extends AbstractAlipayPayClient { + + public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeAppPayModel 请求 + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody() + "test"); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品 + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.APP.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradeAppPayResponse response = client.sdkExecute(request); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java new file mode 100644 index 0000000..1f90d6b --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePayModel; +import com.alipay.api.request.AlipayTradePayRequest; +import com.alipay.api.response.AlipayTradePayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; + +/** + * 支付宝【条码支付】的 PayClient 实现类 + * + * 文档:当面付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayBarPayClient extends AbstractAlipayPayClient { + + public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); + if (StrUtil.isEmpty(authCode)) { + throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); + } + + // 1.1 构建 AlipayTradePayModel 请求 + AlipayTradePayModel model = new AlipayTradePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setScene("bar_code"); // 当面付条码支付场景 + // ② 个性化的参数 + model.setAuthCode(authCode); + // ③ 支付宝条码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode(); + + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradePayRequest request = new AlipayTradePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePayResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + if ("10000".equals(response.getCode())) { // 免密支付 + LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment()); + return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime, + response.getOutTradeNo(), response) + .setDisplayMode(displayMode).setDisplayContent(""); + } + // 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询 + return PayOrderRespDTO.waitingOf(displayMode, "", + reqDTO.getOutTradeNo(), response); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java new file mode 100644 index 0000000..13f2885 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 支付宝的 PayClientConfig 实现类 + * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class AlipayPayClientConfig implements PayClientConfig { + + /** + * 公钥类型 - 公钥模式 + */ + public static final Integer MODE_PUBLIC_KEY = 1; + /** + * 公钥类型 - 证书模式 + */ + public static final Integer MODE_CERTIFICATE = 2; + + /** + * 签名算法类型 - RSA + */ + public static final String SIGN_TYPE_DEFAULT = "RSA2"; + + /** + * 网关地址 + * + * 1. 生产环境 + * 2. 沙箱环境 + */ + @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String serverUrl; + + /** + * 开放平台上创建的应用的 ID + */ + @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String appId; + + /** + * 签名算法类型,推荐:RSA2 + *

+ * {@link #SIGN_TYPE_DEFAULT} + */ + @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String signType; + + /** + * 公钥类型 + * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey + * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent + */ + @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private Integer mode; + + // ========== 公钥模式 ========== + /** + * 商户私钥 + */ + @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) + private String privateKey; + + /** + * 支付宝公钥字符串 + */ + @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) + private String alipayPublicKey; + + // ========== 证书模式 ========== + /** + * 指定商户公钥应用证书内容字符串 + */ + @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) + private String appCertContent; + /** + * 指定支付宝公钥证书内容字符串 + */ + @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) + private String alipayPublicCertContent; + /** + * 指定根证书内容字符串 + */ + @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) + private String rootCertContent; + + public interface ModePublicKey { + } + + public interface ModeCertificate { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java new file mode 100644 index 0000000..6dbd19b --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.Method; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePagePayModel; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +/** + * 支付宝【PC 网站】的 PayClient 实现类 + * + * 文档:电脑网站支付 + * + * @author XGD + */ +@Slf4j +public class AlipayPcPayClient extends AbstractAlipayPayClient { + + public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePagePayModel 请求 + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY + // ② 个性化的参数 + // 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展 + model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/ + // ③ 支付宝 PC 支付有两种展示模式:FORM、URL + String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(), + PayOrderDisplayModeEnum.URL.getMode()); + + // 1.2 构建 AlipayTradePagePayRequest 请求 + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePagePayResponse response; + if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) { + response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求 + } else { + response = client.pageExecute(request, Method.GET.name()); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java new file mode 100644 index 0000000..bb3ad17 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePrecreateModel; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; + +/** + * 支付宝【扫码支付】的 PayClient 实现类 + * + * 文档:扫码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayQrPayClient extends AbstractAlipayPayClient { + + public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePrecreateModel 请求 + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开 + String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePrecreateResponse response; + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { + // 证书模式 + response = client.certificateExecute(request); + } else { + response = client.execute(request); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java new file mode 100644 index 0000000..f9dccf5 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【Wap 网站】的 PayClient 实现类 + * + * 文档:手机网站支付接口 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayWapPayClient extends AbstractAlipayPayClient { + + public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeWapPayModel 请求 + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY + // ② 个性化的参数【无】 + // ③ 支付宝 Wap 支付只有一种展示:URL + String displayMode = PayOrderDisplayModeEnum.URL.getMode(); + + // 1.2 构建 AlipayTradeWapPayRequest 请求 + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + model.setQuitUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java new file mode 100644 index 0000000..1ad1ad7 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.mock; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 模拟支付的 PayClient 实现类 + * + * 模拟支付返回结果都是成功,方便大家日常流畅 + * + * @author jason + */ +public class MockPayClient extends AbstractPayClient { + + private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS"; + + public MockPayClient(Long channelId, NonePayClientConfig config) { + super(channelId, PayChannelEnum.MOCK.getCode(), config); + } + + @Override + protected void doInit() { + } + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(), + reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) { + return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(), + outTradeNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), + reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { + return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(), + outRefundNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { + throw new UnsupportedOperationException("模拟支付无退款回调"); + } + + @Override + protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { + throw new UnsupportedOperationException("模拟支付无支付回调"); + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + throw new UnsupportedOperationException("待实现"); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { + throw new UnsupportedOperationException("待实现"); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java new file mode 100644 index 0000000..a8c50cf --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -0,0 +1,484 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum; +import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result; +import com.github.binarywang.wxpay.bean.request.*; +import com.github.binarywang.wxpay.bean.result.*; +import com.github.binarywang.wxpay.config.WxPayConfig; +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.Map; +import java.util.Objects; + +import static cn.hutool.core.date.DatePattern.*; +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2; + +/** + * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) + * + * @author 遇到源码 + */ +@Slf4j +public abstract class AbstractWxPayClient 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", "privateKeyContent", "privateCertContent"); + payConfig.setTradeType(tradeType); + // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 + if (Base64.isBase64(config.getKeyContent())) { + payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } +// payConfig.setCertSerialNo(); + + // 创建 client 客户端 + client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + // ============ 支付相关 ========== + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { + try { + switch (config.getApiVersion()) { + case 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) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + reqDTO.getOutTradeNo(), e.getXmlString()); + } + } + + /** + * 【V2】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) + throws Exception; + + /** + * 【V3】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) + throws WxPayException; + + /** + * 【V2】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) { + return WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(reqDTO.getExpireTime())) + .spbillCreateIp(reqDTO.getUserIp()) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + } + + /** + * 【V3】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) { + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getOutTradeNo()); + request.setDescription(reqDTO.getSubject()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分 + request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + return request; + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseOrderNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return doParseOrderNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); + // 2. 构建结果 + // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂 + Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ? + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus(); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), body); + } + + private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException { + // 1. 解析回调 + WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null); + WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + Integer status = parseStatus(result.getTradeState()); + String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()), + result.getOutTradeNo(), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetOrderV2(outTradeNo); + case WxPayClientConfig.API_VERSION_V3: + return doGetOrderV3(outTradeNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + outTradeNo, e.getXmlString()); + } + throw e; + } + } + + private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder() + .outTradeNo(outTradeNo).build(); + // 执行请求 + WxPayOrderQueryResult response = client.queryOrder(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + outTradeNo, response); + } + + private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request() + .setOutTradeNo(outTradeNo); + // 执行请求 + WxPayOrderQueryV3Result response = client.queryOrderV3(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeState) { + switch (tradeState) { + case "NOTPAY": + case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有) + return PayOrderStatusRespEnum.WAITING.getStatus(); + case "SUCCESS": + return PayOrderStatusRespEnum.SUCCESS.getStatus(); + case "REFUND": + return PayOrderStatusRespEnum.REFUND.getStatus(); + case "CLOSED": + case "REVOKED": // 已撤销(刷卡支付独有) + case "PAYERROR": // 支付失败(其它原因,如银行返回失败) + return PayOrderStatusRespEnum.CLOSED.getStatus(); + default: + throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState)); + } + } + + // ============ 退款相关 ========== + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doUnifiedRefundV2(reqDTO); + case WxPayClientConfig.API_VERSION_V3: + return doUnifiedRefundV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + reqDTO.getOutTradeNo(), e.getXmlString()); + } + } + + private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setRefundFee(reqDTO.getRefundPrice()) + .setRefundDesc(reqDTO.getReason()) + .setTotalFee(reqDTO.getPayPrice()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundResult response = client.refundV2(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知 + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice()) + .setTotal(reqDTO.getPayPrice()).setCurrency("CNY")) + .setReason(reqDTO.getReason()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundV3Result response = client.refundV3(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getStatus())) { + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + reqDTO.getOutRefundNo(), response); + } + if (Objects.equals("PROCESSING", response.getStatus())) { + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseRefundNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return parseRefundNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); + WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null); + WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetRefundV2(outTradeNo, outRefundNo); + case WxPayClientConfig.API_VERSION_V3: + return doGetRefundV3(outTradeNo, outRefundNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + outRefundNo, e.getXmlString()); + } + throw e; + } + } + + private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder() + .outTradeNo(outTradeNo) + .outRefundNo(outRefundNo) + .build(); + // 2.1 执行请求 + WxPayRefundQueryResult response = client.refundQuery(request); + // 2.2 创建返回结果 + if (!Objects.equals("SUCCESS", response.getResultCode())) { + return PayRefundRespDTO.waitingOf(null, + outRefundNo, response); + } + WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(), + record -> record.getOutRefundNo().equals(outRefundNo)); + if (refund == null) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + switch (refund.getRefundStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(refund.getRefundId(), + outRefundNo, response); + case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款 + case "FAIL": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus())); + } + } + + private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request(); + request.setOutRefundNo(outRefundNo); + // 2.1 执行请求 + WxPayRefundQueryV3Result response = client.refundQueryV3(request); + // 2.2 创建返回结果 + switch (response.getStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(response.getRefundId(), + outRefundNo, response); + case "ABNORMAL": // 退款异常 + case "CLOSED": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus())); + } + } + + @Override + protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) { + throw new UnsupportedOperationException("待实现"); + } + + @Override + protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) { + throw new UnsupportedOperationException("待实现"); + } + + // ========== 各种工具方法 ========== + + static String formatDateV2(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2(String time) { + return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2B(String time) { + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN); + } + + static String formatDateV3(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); + } + + static LocalDateTime parseDateV3(String time) { + return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); + } + + static String getErrorCode(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCode(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return "CUSTOM_ERROR"; + } + return e.getReturnCode(); + } + + static String getErrorMessage(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCodeDes(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return e.getCustomErrorMsg(); + } + return e.getReturnMsg(); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java new file mode 100644 index 0000000..396694a --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +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; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class WxAppPayClient extends AbstractWxPayClient { + + public WxAppPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_APP.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.APP); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java new file mode 100644 index 0000000..d01b504 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java @@ -0,0 +1,107 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【付款码支付】的 PayClient 实现类 + * + * 文档:付款码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class WxBarPayClient extends AbstractWxPayClient { + + /** + * 微信付款码的过期时间 + */ + private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3); + + public WxBarPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_BAR.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.MICROPAY); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 由于付款码需要不断轮询,所以需要在较短的时间完成支付 + LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); + if (expireTime.isAfter(reqDTO.getExpireTime())) { + expireTime = reqDTO.getExpireTime(); + } + // 构建 WxPayMicropayRequest 对象 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(expireTime)) + .spbillCreateIp(reqDTO.getUserIp()) + .authCode(getAuthCode(reqDTO)) + .build(); + // 执行请求,重试直到失败(过期),或者成功 + WxPayException lastWxPayException = null; + for (int i = 1; i < Byte.MAX_VALUE; i++) { + try { + WxPayMicropayResult response = client.micropay(request); + // 支付成功,例如说:1)用户输入了密码;2)用户免密支付 + return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), response) + .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode()); + } catch (WxPayException ex) { + lastWxPayException = ex; + // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理 + // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。 + // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + // 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) { + throw ex; + } + // 等待 5 秒,继续下一轮重新发起支付 + log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, + toJsonString(request), ex.getMessage()); + ThreadUtil.sleep(5, TimeUnit.SECONDS); + } + } + throw lastWxPayException; + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + return doUnifiedOrderV2(reqDTO); + } + + // ========== 各种工具方法 ========== + + static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode"); + if (StrUtil.isEmpty(authCode)) { + throw invalidParamException("支付请求的 authCode 不能为空!"); + } + return authCode; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java new file mode 100644 index 0000000..9929955 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxLitePayClient.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付【小程序】的 PayClient 实现类 + * + * 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承 + * + * 文档:JSAPI 下单 + * + * @author zwy + */ +@Slf4j +public class WxLitePayClient extends WxPubPayClient { + + public WxLitePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_LITE.getCode(), config); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java new file mode 100644 index 0000000..5a07350 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +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; + +/** + * 微信支付【Native 二维码】的 PayClient 实现类 + * + * 文档:Native 下单 + * + * @author zwy + */ +@Slf4j +public class WxNativePayClient extends AbstractWxPayClient { + + public WxNativePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.NATIVE); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayNativeOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response, + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java new file mode 100644 index 0000000..de5b023 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.io.IoUtil; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotBlank; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * 微信支付的 PayClientConfig 实现类 + * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class WxPayClientConfig implements PayClientConfig { + + /** + * API 版本 - V2 + * + * V2 协议说明 + */ + public static final String API_VERSION_V2 = "v2"; + /** + * API 版本 - V3 + * + * V3 协议说明 + */ + public static final String API_VERSION_V3 = "v3"; + + /** + * 公众号或者小程序的 appid + * + * 只有公众号或小程序需要该字段 + */ + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) + private String appId; + /** + * 商户号 + */ + @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class}) + private String mchId; + /** + * API 版本 + */ + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class}) + private String apiVersion; + + // ========== V2 版本的参数 ========== + + /** + * 商户密钥 + */ + @NotBlank(message = "商户密钥不能为空", groups = V2.class) + private String mchKey; + /** + * apiclient_cert.p12 证书文件的对应字符串【base64 格式】 + * + * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储 + */ + @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class) + private String keyContent; + + // ========== V3 版本的参数 ========== + /** + * apiclient_key.pem 证书文件的对应字符串 + */ + @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) + private String privateKeyContent; + /** + * apiclient_cert.pem 证书文件的对应的字符串 + */ + @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) + private String privateCertContent; + /** + * apiV3 密钥值 + */ + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class) + private String apiV3Key; + + /** + * 分组校验 v2版本 + */ + public interface V2 { + } + + /** + * 分组校验 v3版本 + */ + public interface V3 { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class); + } + + 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-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java b/yudao-module-pay/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 0000000..390c513 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +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; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * 文档:JSAPI 下单 + * + * @author 芋道源码 + */ +@Slf4j +public class WxPubPayClient extends AbstractWxPayClient { + + public WxPubPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_PUB.getCode(), config); + } + + protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.JSAPI); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO) + .setOpenid(getOpenid(reqDTO)); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO) + .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + // 执行请求 + WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + // ========== 各种工具方法 ========== + + static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw invalidParamException("支付请求的 openid 不能为空!"); + } + return openid; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java new file mode 100644 index 0000000..83d2d13 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/channel/PayChannelEnum.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.framework.pay.core.enums.channel; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig; +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 lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付渠道的编码的枚举 + * + * @author 芋道源码 + */ +@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_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class), + + ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), + ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), + ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class), + ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class), + ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class), + MOCK("mock", "模拟支付", NonePayClientConfig.class), + + WALLET("wallet", "钱包支付", NonePayClientConfig.class); + + /** + * 编码 + * + * 参考 支付渠道属性值 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + /** + * 配置类 + */ + private final Class configClass; + + /** + * 微信支付 + */ + public static final String WECHAT = "WECHAT"; + + /** + * 支付宝支付 + */ + public static final String ALIPAY = "ALIPAY"; + + public static PayChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + + public static boolean isAlipay(String channelCode) { + return channelCode != null && channelCode.startsWith("alipay"); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java new file mode 100644 index 0000000..129c406 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.pay.core.enums.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付 UI 展示模式 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderDisplayModeEnum { + + URL("url"), // Redirect 跳转链接的方式 + IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】 + FORM("form"), // HTML 表单提交 + QR_CODE("qr_code"), // 二维码的文字内容 + QR_CODE_URL("qr_code_url"), // 二维码的图片链接 + BAR_CODE("bar_code"), // 条形码 + APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的 + ; + + /** + * 展示模式 + */ + private final String mode; + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java new file mode 100644 index 0000000..eac381c --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.pay.core.enums.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的支付状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusRespEnum { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), + ; + + private final Integer status; + private final String name; + + /** + * 判断是否支付成功 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + /** + * 判断是否已退款 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isRefund(Integer status) { + return Objects.equals(status, REFUND.getStatus()); + } + + /** + * 判断是否支付关闭 + * + * @param status 状态 + * @return 是否支付关闭 + */ + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java new file mode 100644 index 0000000..8ad61a6 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.pay.core.enums.refund; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的退款状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusRespEnum { + + WAITING(0, "等待退款"), + SUCCESS(10, "退款成功"), + FAILURE(20, "退款失败"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isFailure(Integer status) { + return Objects.equals(status, FAILURE.getStatus()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java new file mode 100644 index 0000000..35ea344 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.pay.core.enums.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的转账状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayTransferStatusRespEnum { + + WAITING(0, "等待转账"), + + /** + * TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现 + * TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子? + */ + IN_PROGRESS(10, "转账进行中"), + + SUCCESS(20, "转账成功"), + /** + * 转账关闭 (失败,或者其它情况) + */ + CLOSED(30, "转账关闭"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + + public static boolean isInProgress(Integer status) { + return Objects.equals(status, IN_PROGRESS.getStatus()); + } +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java new file mode 100644 index 0000000..a7580f0 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferTypeEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.pay.core.enums.transfer; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 转账类型枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum PayTransferTypeEnum implements IntArrayValuable { + + ALIPAY_BALANCE(1, "支付宝余额"), + WX_BALANCE(2, "微信余额"), + BANK_CARD(3, "银行卡"), + WALLET_BALANCE(4, "钱包余额"); + + public interface WxPay { + } + + public interface Alipay { + } + + private final Integer type; + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PayTransferTypeEnum::getType).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + + public static PayTransferTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f2a8bf1 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration \ No newline at end of file diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java new file mode 100644 index 0000000..9842560 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -0,0 +1,133 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.pay.core.client.PayClient; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +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.enums.channel.PayChannelEnum; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * {@link PayClientFactoryImpl} 的集成测试 + * + * @author 芋道源码 + */ +@Disabled +public class PayClientFactoryImplIntegrationTest { + + private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; + + private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); + + /** + * {@link WxPubPayClient} 的 V2 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V2() { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WxPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link WxPubPayClient} 的 V3 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + 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"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link AlipayQrPayClient} + */ + @Test + @SuppressWarnings("unchecked") + public void testCreatePayClient_ALIPAY_QR() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(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"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址 +// CommonResult result = (CommonResult) client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); +// System.out.println(result.getData().getQrCode()); + } + + /** + * {@link AlipayWapPayClient} + */ + @Test + public void testCreatePayClient_ALIPAY_WAP() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(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"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); + } + + private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { + PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); + reqDTO.setPrice(123); + reqDTO.setSubject("IPhone 13"); + reqDTO.setBody("biubiubiu"); + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setNotifyUrl("http://127.0.0.1:8080"); + return reqDTO; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java new file mode 100644 index 0000000..7c45b6a --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java @@ -0,0 +1,221 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.RandomUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.alipay.api.AlipayApiException; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.DefaultSigner; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeRefundResponse; +import lombok.Setter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; + +import jakarta.validation.ConstraintViolationException; +import java.util.Date; + +import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * 支付宝 Client 的测试基类 + * + * @author jason + */ +public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest { + + protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> { + o.setServerUrl(randomURL()); + o.setPrivateKey(randomString()); + o.setMode(MODE_PUBLIC_KEY); + o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + o.setAppCertContent(""); + o.setAlipayPublicCertContent(""); + o.setRootCertContent(""); + }); + + @Mock + protected DefaultAlipayClient defaultAlipayClient; + + @Setter + private AbstractAlipayPayClient client; + + /** + * 子类需要实现该方法. 设置 client 的具体实现 + */ + @BeforeEach + public abstract void setUp(); + + @Test + @DisplayName("支付宝 Client 初始化") + public void testDoInit() { + // 调用 + client.doInit(); + // 断言 + DefaultAlipayClient realClient = client.getClient(); + assertNotSame(defaultAlipayClient, realClient); + assertInstanceOf(DefaultSigner.class, realClient.getSigner()); + assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:成功") + public void testUnifiedRefund_success() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + Date refundTime = randomDate(); + String outRefundNo = randomString(); + String outTradeNo = randomString(); + Integer refundAmount = randomInteger(); + AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { + o.setSubCode(""); + o.setGmtRefundPay(refundTime); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); + AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel(); + assertEquals(outRefundNo, bizModel.getOutRequestNo()); + assertEquals(outTradeNo, bizModel.getOutTradeNo()); + assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount()); + return true; + }))).thenReturn(response); + // 准备请求参数 + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutRefundNo(outRefundNo); + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + o.setRefundPrice(refundAmount); + }); + + // 调用 + PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); + // 断言 + assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); + assertEquals(outRefundNo, resp.getOutRefundNo()); + assertNull(resp.getChannelRefundNo()); + assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:渠道返回失败") + public void test_unified_refund_channel_failed() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outRefundNo = randomString(); + String outTradeNo = randomString(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutRefundNo(outRefundNo); + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + }); + + // 调用 + PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO); + // 断言 + assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus()); + assertEquals(outRefundNo, resp.getOutRefundNo()); + assertNull(resp.getChannelRefundNo()); + assertNull(resp.getSuccessTime()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 Client 统一退款:参数校验不通过") + public void testUnifiedRefund_paramInvalidate() { + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> { + o.setOutTradeNo(""); + o.setNotifyUrl(notifyUrl); + }); + + // 调用,并断言 + assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一退款:抛出业务异常") + public void testUnifiedRefund_throwServiceException() throws AlipayApiException { + // mock 方法 + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一退款:抛出系统异常") + public void testUnifiedRefund_throwPayException() throws AlipayApiException { + // mock 方法 + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenThrow(new RuntimeException("系统异常")); + // 准备请求参数 + String notifyUrl = randomURL(); + PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl)); + + // 调用,并断言 + assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一下单:参数校验不通过") + public void testUnifiedOrder_paramInvalidate() { + // 准备请求参数 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> { + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + }); + + // 调用,并断言 + assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO)); + } + + protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) { + return randomPojo(PayOrderUnifiedReqDTO.class, o -> { + o.setOutTradeNo(outTradeNo); + o.setNotifyUrl(notifyUrl); + o.setPrice(price); + o.setSubject(RandomUtil.randomString(32)); + o.setBody(RandomUtil.randomString(32)); + }); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java new file mode 100644 index 0000000..47f1008 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClientTest.java @@ -0,0 +1,170 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePayModel; +import com.alipay.api.request.AlipayTradePayRequest; +import com.alipay.api.response.AlipayTradePayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayBarPayClient} 单元测试 + * + * @author jason + */ +public class AlipayBarPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config); + + @Override + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝条码支付:非免密码支付下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + String authCode = randomString(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradePayModel.class, request.getBizModel()); + assertEquals(notifyUrl, request.getNotifyUrl()); + AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel(); + assertEquals(outTradeNo, model.getOutTradeNo()); + assertEquals(String.valueOf(price / 100.0), model.getTotalAmount()); + assertEquals(authCode, model.getAuthCode()); + return true; + }))).thenReturn(response); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 调用方法 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); + assertEquals("", resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝条码支付:免密码支付下单成功") + public void testUnifiedOrder_code10000Success() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String channelNo = randomString(); + String channelUserId = randomString(); + Date payTime = randomDate(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { + o.setSubCode(""); + o.setCode("10000"); + o.setOutTradeNo(outTradeNo); + o.setTradeNo(channelNo); + o.setBuyerUserId(channelUserId); + o.setGmtPayment(payTime); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenReturn(response); + // 准备请求参数 + String authCode = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 下单请求 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(PayOrderStatusRespEnum.SUCCESS.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertEquals(channelNo, resp.getChannelOrderNo()); + assertEquals(channelUserId, resp.getChannelUserId()); + assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode()); + assertEquals("", resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝条码支付:没有传条码") + public void testUnifiedOrder_emptyAuthCode() { + // 准备参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger()); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); + } + + @Test + @DisplayName("支付宝条码支付:渠道返回失败") + public void test_unified_order_channel_failed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> true))) + .thenReturn(response); + // 准备请求参数 + String authCode = randomString(); + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + Map extraParam = new HashMap<>(); + extraParam.put("auth_code", authCode); + reqDTO.setChannelExtras(extraParam); + + // 调用方法 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java new file mode 100644 index 0000000..d78caf2 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClientTest.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayPcPayClient} 单元测试 + * + * @author jason + */ +public class AlipayPcPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config); + + @Override + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝 PC 网站支付:URL Display Mode 下单成功") + public void testUnifiedOrder_urlSuccess() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + Integer price = randomInteger(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + reqDTO.setDisplayMode(null); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:Form Display Mode 下单成功") + public void testUnifiedOrder_formSuccess() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode("")); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.POST.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + Integer price = randomInteger(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 PC 网站支付:渠道返回失败") + public void testUnifiedOrder_channelFailed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java new file mode 100644 index 0000000..c7e1eb3 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java @@ -0,0 +1,147 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayQrPayClient} 单元测试 + * + * @author jason + */ +public class AlipayQrPayClientTest extends AbstractAlipayClientTest { + + @InjectMocks + private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config); + + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝扫描支付:下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String qrCode = randomString(); + Integer price = randomInteger(); + AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { + o.setQrCode(qrCode); + o.setSubCode(""); + }); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode()); + assertEquals(response.getQrCode(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝扫描支付:渠道返回失败") + public void testUnifiedOrder_channelFailed() throws AlipayApiException { + // mock 方法 + String notifyUrl = randomURL(); + String subCode = randomString(); + String subMsg = randomString(); + Integer price = randomInteger(); + AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + // mock + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝扫描支付, 抛出系统异常") + public void testUnifiedOrder_throwPayException() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenThrow(new RuntimeException("系统异常")); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price); + + // 调用,并断言 + assertThrows(PayException.class, () -> client.unifiedOrder(reqDTO)); + } + + @Test + @DisplayName("支付宝 Client 统一下单:抛出业务异常") + public void testUnifiedOrder_throwServiceException() throws AlipayApiException { + // mock 方法 + String outTradeNo = randomString(); + String notifyUrl = randomURL(); + Integer price = randomInteger(); + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request -> { + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR)); + // 准备请求参数 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用,并断言 + assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO)); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java new file mode 100644 index 0000000..af0c717 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClientTest.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; + +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED; +import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link AlipayWapPayClient} 单元测试 + * + * @author jason + */ +public class AlipayWapPayClientTest extends AbstractAlipayClientTest { + + /** + * 支付宝 H5 支付 Client + */ + @InjectMocks + private AlipayWapPayClient client = new AlipayWapPayClient(randomLongId(), config); + + @BeforeEach + public void setUp() { + setClient(client); + } + + @Test + @DisplayName("支付宝 H5 支付:下单成功") + public void testUnifiedOrder_success() throws AlipayApiException { + // mock 方法 + String h5Body = randomString(); + Integer price = randomInteger(); + AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { + o.setSubCode(""); + o.setBody(h5Body); + }); + String notifyUrl = randomURL(); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> { + assertInstanceOf(AlipayTradeWapPayModel.class, request.getBizModel()); + AlipayTradeWapPayModel bizModel = (AlipayTradeWapPayModel) request.getBizModel(); + assertEquals(String.valueOf(price / 100.0), bizModel.getTotalAmount()); + assertEquals(notifyUrl, request.getNotifyUrl()); + return true; + }), eq(Method.GET.name()))).thenReturn(response); + // 准备请求参数 + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(WAITING.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode()); + assertEquals(response.getBody(), resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertNull(resp.getChannelErrorCode()); + assertNull(resp.getChannelErrorMsg()); + } + + @Test + @DisplayName("支付宝 H5 支付:渠道返回失败") + public void test_unified_order_channel_failed() throws AlipayApiException { + // mock 方法 + String subCode = randomString(); + String subMsg = randomString(); + AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> { + o.setSubCode(subCode); + o.setSubMsg(subMsg); + }); + when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher) request -> true), + eq(Method.GET.name()))).thenReturn(response); + String outTradeNo = randomString(); + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger()); + + // 调用 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO); + // 断言 + assertEquals(CLOSED.getStatus(), resp.getStatus()); + assertEquals(outTradeNo, resp.getOutTradeNo()); + assertNull(resp.getChannelOrderNo()); + assertNull(resp.getChannelUserId()); + assertNull(resp.getSuccessTime()); + assertNull(resp.getDisplayMode()); + assertNull(resp.getDisplayContent()); + assertSame(response, resp.getRawData()); + assertEquals(subCode, resp.getChannelErrorCode()); + assertEquals(subMsg, resp.getChannelErrorMsg()); + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java new file mode 100644 index 0000000..9af11ac --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2; + +/** + * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxBarPayClientIntegrationTest { + + @Test + public void testPayV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(String.valueOf(System.currentTimeMillis())) + .body("测试支付-body") + .detail("测试支付-detail") + .totalFee(1) // 单位分 + .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .spbillCreateIp("127.0.0.1") + .authCode("134298744426278497") + .build(); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayMicropayResult response = client.micropay(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testParseRefundNotifyV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行解析 + String xml = "SUCCESS"; + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); + System.out.println(response.getReqInfo()); + } + + @Test + public void testRefundV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo("1689545667276") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setRefundFee(1) + .setRefundDesc("就是想退了") + .setTotalFee(1); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundResult response = client.refund(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689506325635") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV2() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); +// config.setSignType(WxPayConstants.SignType.MD5); + config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); + return config; + } + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java new file mode 100644 index 0000000..5e73601 --- /dev/null +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; + +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3; + +/** + * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxNativePayClientIntegrationTest { + + @Test + public void testPayV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request() + .setOutTradeNo(String.valueOf(System.currentTimeMillis())) + .setDescription("测试支付-body") + .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分 + .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1")) + .setNotifyUrl("http://127.0.0.1:48080"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689545729695") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV3() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4"); +// config.setCertSerialNo(serialNo); + config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"); + config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"); + return config; + } + +} -- libgit2 0.21.4