From 18ebfcc04f93a0f47992929ef19f49a417f4f428 Mon Sep 17 00:00:00 2001 From: JIAN Date: Sun, 8 Sep 2024 19:20:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor(trade):=E5=AF=BC=E5=85=A5=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=94=AF=E4=BB=98=E6=A8=A1=E5=9D=97=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=B7=A5=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jzo2o-trade/Dockerfile | 8 + jzo2o-trade/pom.xml | 111 +++++++ .../com/jzo2o/trade/TradeApplication.java | 23 ++ .../jzo2o/trade/annotation/PayChannel.java | 17 ++ .../com/jzo2o/trade/config/QRCodeConfig.java | 58 ++++ .../trade/config/RedissonConfiguration.java | 39 +++ .../com/jzo2o/trade/constant/Constants.java | 19 ++ .../trade/constant/TradingCacheConstant.java | 42 +++ .../jzo2o/trade/constant/TradingConstant.java | 71 +++++ .../controller/inner/NativePayController.java | 49 +++ .../inner/RefundRecordController.java | 53 ++++ .../controller/inner/TradingController.java | 84 +++++ .../controller/open/NotifyController.java | 86 ++++++ .../jzo2o/trade/enums/RefundStatusEnum.java | 36 +++ .../com/jzo2o/trade/enums/TradingEnum.java | 66 ++++ .../jzo2o/trade/enums/TradingStateEnum.java | 39 +++ .../jzo2o/trade/handler/BasicPayHandler.java | 45 +++ .../jzo2o/trade/handler/BeforePayHandler.java | 52 ++++ .../jzo2o/trade/handler/HandlerFactory.java | 33 ++ .../jzo2o/trade/handler/JsapiPayHandler.java | 18 ++ .../jzo2o/trade/handler/NativePayHandler.java | 21 ++ .../handler/alipay/AliBasicPayHandler.java | 165 ++++++++++ .../handler/alipay/AliNativePayHandler.java | 63 ++++ .../trade/handler/alipay/AlipayConfig.java | 50 +++ .../handler/impl/BeforePayHandlerImpl.java | 126 ++++++++ .../handler/wechat/WeChatBasicPayHandler.java | 198 ++++++++++++ .../handler/wechat/WechatJsapiPayHandler.java | 117 +++++++ .../wechat/WechatNativePayHandler.java | 67 ++++ .../handler/wechat/WechatPayHttpClient.java | 161 ++++++++++ .../handler/wechat/bean/JsapiPayParam.java | 50 +++ .../wechat/response/WeChatResponse.java | 35 +++ .../java/com/jzo2o/trade/job/TradeJob.java | 163 ++++++++++ .../jzo2o/trade/mapper/PayChannelMapper.java | 13 + .../trade/mapper/RefundRecordMapper.java | 13 + .../com/jzo2o/trade/mapper/TradingMapper.java | 13 + .../jzo2o/trade/model/domain/PayChannel.java | 96 ++++++ .../trade/model/domain/RefundRecord.java | 103 +++++++ .../com/jzo2o/trade/model/domain/Trading.java | 162 ++++++++++ .../jzo2o/trade/model/dto/PayChannelDTO.java | 55 ++++ .../trade/model/dto/RefundRecordDTO.java | 53 ++++ .../com/jzo2o/trade/model/dto/TradingDTO.java | 82 +++++ .../model/dto/request/JsapiPayReqDTO.java | 32 ++ .../model/dto/response/JsapiPayResDTO.java | 26 ++ .../dto/response/TradingStateResDTO.java | 23 ++ .../jzo2o/trade/service/BasicPayService.java | 68 +++++ .../jzo2o/trade/service/JsapiPayService.java | 22 ++ .../jzo2o/trade/service/NativePayService.java | 19 ++ .../jzo2o/trade/service/NotifyService.java | 34 +++ .../trade/service/PayChannelService.java | 60 ++++ .../jzo2o/trade/service/QRCodeService.java | 23 ++ .../trade/service/RefundRecordService.java | 47 +++ .../jzo2o/trade/service/TradingService.java | 60 ++++ .../service/impl/BasicPayServiceImpl.java | 286 ++++++++++++++++++ .../service/impl/JsapiPayServiceImpl.java | 84 +++++ .../service/impl/NativePayServiceImpl.java | 132 ++++++++ .../trade/service/impl/NotifyServiceImpl.java | 188 ++++++++++++ .../service/impl/PayChannelServiceImpl.java | 78 +++++ .../trade/service/impl/QRCodeServiceImpl.java | 51 ++++ .../service/impl/RefundRecordServiceImpl.java | 54 ++++ .../service/impl/TradingServiceImpl.java | 98 ++++++ .../src/main/resources/bootstrap-dev.yml | 17 ++ .../src/main/resources/bootstrap-prod.yml | 16 + .../src/main/resources/bootstrap-test.yml | 16 + jzo2o-trade/src/main/resources/bootstrap.yml | 88 ++++++ .../src/main/resources/logos/alipay.png | Bin 0 -> 1614 bytes .../src/main/resources/logos/wechat.png | Bin 0 -> 2702 bytes 66 files changed, 4277 insertions(+) create mode 100644 jzo2o-trade/Dockerfile create mode 100644 jzo2o-trade/pom.xml create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/TradeApplication.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/annotation/PayChannel.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/config/QRCodeConfig.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/config/RedissonConfiguration.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/constant/Constants.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingCacheConstant.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingConstant.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/NativePayController.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/RefundRecordController.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/TradingController.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/controller/open/NotifyController.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/enums/RefundStatusEnum.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingEnum.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingStateEnum.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BasicPayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BeforePayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/HandlerFactory.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/JsapiPayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/NativePayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliBasicPayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliNativePayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AlipayConfig.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/impl/BeforePayHandlerImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WeChatBasicPayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatJsapiPayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatNativePayHandler.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatPayHttpClient.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/bean/JsapiPayParam.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/response/WeChatResponse.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/job/TradeJob.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/PayChannelMapper.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/RefundRecordMapper.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/TradingMapper.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/PayChannel.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/RefundRecord.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/Trading.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/PayChannelDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/RefundRecordDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/TradingDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/request/JsapiPayReqDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/JsapiPayResDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/TradingStateResDTO.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/BasicPayService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/JsapiPayService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/NativePayService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/NotifyService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/PayChannelService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/QRCodeService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/RefundRecordService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/TradingService.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/BasicPayServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/JsapiPayServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NativePayServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NotifyServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/PayChannelServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/QRCodeServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/RefundRecordServiceImpl.java create mode 100644 jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/TradingServiceImpl.java create mode 100644 jzo2o-trade/src/main/resources/bootstrap-dev.yml create mode 100644 jzo2o-trade/src/main/resources/bootstrap-prod.yml create mode 100644 jzo2o-trade/src/main/resources/bootstrap-test.yml create mode 100644 jzo2o-trade/src/main/resources/bootstrap.yml create mode 100644 jzo2o-trade/src/main/resources/logos/alipay.png create mode 100644 jzo2o-trade/src/main/resources/logos/wechat.png diff --git a/jzo2o-trade/Dockerfile b/jzo2o-trade/Dockerfile new file mode 100644 index 0000000..f15b796 --- /dev/null +++ b/jzo2o-trade/Dockerfile @@ -0,0 +1,8 @@ +FROM openjdk:11-jdk +LABEL maintainer="研究院研发组 " +RUN echo "Asia/Shanghai" > /etc/timezone +ARG PACKAGE_PATH=./target/jzo2o-trade.jar + +ADD ${PACKAGE_PATH:-./} app.jar +EXPOSE 11505 +ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS app.jar"] \ No newline at end of file diff --git a/jzo2o-trade/pom.xml b/jzo2o-trade/pom.xml new file mode 100644 index 0000000..59bf080 --- /dev/null +++ b/jzo2o-trade/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + jzo2o-trade + 1.0-SNAPSHOT + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + + + + 11 + 11 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + com.jzo2o + jzo2o-mvc + + + + com.jzo2o + jzo2o-knife4j-web + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.jzo2o + jzo2o-api + + + + com.jzo2o + jzo2o-mysql + + + com.jzo2o + jzo2o-xxl-job + + + com.jzo2o + jzo2o-rabbitmq + + + com.jzo2o + jzo2o-redis + + + + com.alipay.sdk + alipay-easysdk + 2.2.2 + + + com.github.wechatpay-apiv3 + wechatpay-apache-httpclient + 0.4.7 + + + + com.google.zxing + core + 3.5.0 + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + com.jzo2o.trade.TradeApplication + + + + + \ No newline at end of file diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/TradeApplication.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/TradeApplication.java new file mode 100644 index 0000000..c1f714d --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/TradeApplication.java @@ -0,0 +1,23 @@ +package com.jzo2o.trade; + +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * @author itcast + */ +@Slf4j +@MapperScan("com.jzo2o.trade.mapper") +@SpringBootApplication +public class TradeApplication { + public static void main(String[] args) { + new SpringApplicationBuilder(TradeApplication.class) + .build(args) + .run(args); + log.info("家政服务-支付服务启动"); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/annotation/PayChannel.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/annotation/PayChannel.java new file mode 100644 index 0000000..57251f0 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/annotation/PayChannel.java @@ -0,0 +1,17 @@ +package com.jzo2o.trade.annotation; + +import com.jzo2o.api.trade.enums.PayChannelEnum; + +import java.lang.annotation.*; + +/** + * @author itcast + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented //标记注解 +public @interface PayChannel { + + PayChannelEnum type(); + +} \ No newline at end of file diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/config/QRCodeConfig.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/config/QRCodeConfig.java new file mode 100644 index 0000000..1f97181 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/config/QRCodeConfig.java @@ -0,0 +1,58 @@ +package com.jzo2o.trade.config; + +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.awt.*; + +/** + * 二维码生成参数配置 + * + * @author zzj + * @version 1.0 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "jzo2o.qrcode") +public class QRCodeConfig { + + private static Image WECHAT_LOGO; + private static Image ALIPAY_LOGO; + + static { + WECHAT_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/wechat.png")); + ALIPAY_LOGO = ImgUtil.read(ResourceUtil.getResource("logos/alipay.png")); + } + + //边距,二维码和背景之间的边距 + private Integer margin = 2; + // 二维码颜色,默认黑色 + private String foreColor = "#000000"; + //背景色,默认白色 + private String backColor = "#ffffff"; + //纠错级别,可选参数:L、M、Q、H,默认:M + //低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。 + private String errorCorrectionLevel = "M"; + //宽 + private Integer width = 300; + //高 + private Integer height = 300; + + public Image getLogo(PayChannelEnum payChannelEnum) { + switch (payChannelEnum) { + case ALI_PAY: { + return ALIPAY_LOGO; + } + case WECHAT_PAY: { + return WECHAT_LOGO; + } + default: { + return null; + } + } + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/config/RedissonConfiguration.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/config/RedissonConfiguration.java new file mode 100644 index 0000000..33bde07 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/config/RedissonConfiguration.java @@ -0,0 +1,39 @@ +//package com.jzo2o.trade.config; +// +//import cn.hutool.core.convert.Convert; +//import cn.hutool.core.util.StrUtil; +//import lombok.Data; +//import org.redisson.Redisson; +//import org.redisson.api.RedissonClient; +//import org.redisson.config.Config; +//import org.redisson.config.SingleServerConfig; +//import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +//import org.springframework.boot.context.properties.EnableConfigurationProperties; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +// +//import javax.annotation.Resource; +// +//@Configuration +//@EnableConfigurationProperties(RedisProperties.class) +//@Data +//public class RedissonConfiguration { +// +// @Resource +// private RedisProperties redisProperties; +// +// @Bean +// public RedissonClient redissonSingle() { +// Config config = new Config(); +// SingleServerConfig serverConfig = config.useSingleServer() +// .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()); +// if (null != (redisProperties.getTimeout())) { +// serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds())); +// } +// if (StrUtil.isNotEmpty(redisProperties.getPassword())) { +// serverConfig.setPassword(redisProperties.getPassword()); +// } +// return Redisson.create(config); +// } +// +//} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/Constants.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/Constants.java new file mode 100644 index 0000000..9cd0e3d --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/Constants.java @@ -0,0 +1,19 @@ +package com.jzo2o.trade.constant; + +/** + * 静态变量 + * + * @author zzj + * @version 1.0 + */ +public interface Constants { + /** + * 常量是 + */ + String YES = "YES"; + + /** + * 常量否 + */ + String NO = "NO"; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingCacheConstant.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingCacheConstant.java new file mode 100644 index 0000000..ec77457 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingCacheConstant.java @@ -0,0 +1,42 @@ +package com.jzo2o.trade.constant; + +/** + * @ClassName TradingCacheConstant.java + * @Description 交易缓存维护 + */ +public class TradingCacheConstant { + + //默认redis等待时间 + public static final int REDIS_WAIT_TIME = 5; + + //默认redis自动释放时间 + public static final int REDIS_LEASETIME = 4; + + //安全组前缀 + public static final String PREFIX = "trading:"; + + //分布式锁前缀 + public static final String LOCK_PREFIX = PREFIX + "lock:"; + + //创建交易加锁 + public static final String CREATE_PAY = LOCK_PREFIX + "create_pay"; + + //查询交易状态加锁 + public static final String QUERY_PAY = LOCK_PREFIX + "query_pay"; + + //创建退款加锁 + public static final String REFUND_PAY = LOCK_PREFIX + "refund_pay"; + + //退款查询加锁 + public static final String REFUND_QUERY_PAY = LOCK_PREFIX + "refund_query_pay"; + + //创建退款加锁 + public static final String PAY_CHANNEL_LIST = PREFIX + "pay_channel_list&ttl=-1"; + + //创建退款加锁 + public static final String CLOSE_PAY = LOCK_PREFIX + "close_pay"; + + + //page分页 + public static final String PAGE = PREFIX + "page"; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingConstant.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingConstant.java new file mode 100644 index 0000000..9ac4afa --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/constant/TradingConstant.java @@ -0,0 +1,71 @@ +package com.jzo2o.trade.constant; + +import com.jzo2o.api.trade.enums.PayChannelEnum; + +/** + * @ClassName TardingConstant.java + * @Description 交易常量类 + */ +public class TradingConstant { + + //【阿里云退款返回状态】 + //REFUND_SUCCESS:成功 + public static final String REFUND_SUCCESS = "REFUND_SUCCESS"; + + //【阿里云返回付款状态】 + //TRADE_CLOSED:未付款交易超时关闭,或支付完成后全额退款 + public static final String ALI_TRADE_CLOSED = "TRADE_CLOSED"; + //TRADE_SUCCESS:交易支付成功 + public static final String ALI_TRADE_SUCCESS = "TRADE_SUCCESS"; + //TRADE_FINISHED:交易结束不可退款 + public static final String ALI_TRADE_FINISHED = "TRADE_FINISHED"; + + + //【微信退款返回状态】 + //SUCCESS:退款成功 + public static final String WECHAT_REFUND_SUCCESS = "SUCCESS"; + //CLOSED:退款关闭 + public static final String WECHAT_REFUND_CLOSED = "CLOSED"; + //PROCESSING:退款处理中 + public static final String WECHAT_REFUND_PROCESSING = "PROCESSING"; + //ABNORMAL:退款异常 + public static final String WECHAT_REFUND_ABNORMAL = "TRADE_CLOSED"; + + //【微信返回付款状态】 + //SUCCESS:支付成功 + public static final String WECHAT_TRADE_SUCCESS = "SUCCESS"; + //REFUND:转入退款 + public static final String WECHAT_TRADE_REFUND = "REFUND"; + //NOTPAY:未支付 + public static final String WECHAT_TRADE_NOTPAY = "NOTPAY"; + //CLOSED:已关闭 + public static final String WECHAT_TRADE_CLOSED = "CLOSED"; + //REVOKED:已撤销(仅付款码支付会返回) + public static final String WECHAT_TRADE_REVOKED = "REVOKED"; + //USERPAYING:用户支付中(仅付款码支付会返回) + public static final String WECHAT_TRADE_USERPAYING = "USERPAYING"; + //PAYERROR:支付失败(仅付款码支付会返回) + public static final String WECHAT_TRADE_PAYERROR = "PAYERROR"; + + //【平台:交易渠道】 + //阿里支付 + public static final String TRADING_CHANNEL_ALI_PAY = PayChannelEnum.ALI_PAY.name(); + //微信支付 + public static final String TRADING_CHANNEL_WECHAT_PAY = PayChannelEnum.WECHAT_PAY.name(); + //现金 + public static final String TRADING_CHANNEL_CASH_PAY = "CASH_PAY"; + //免单挂账【信用渠道】 + public static final String TRADING_CHANNEL_CREDIT_PAY = "CREDIT_PAY"; + + //【平台:交易动作】 + //付款 + public static final String TRADING_TYPE_FK = "FK"; + //退款 + public static final String TRADING_TYPE_TK = "TK"; + //免单 + public static final String TRADING_TYPE_MD = "MD"; + //挂账 + public static final String TRADING_TYPE_GZ = "GZ"; + + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/NativePayController.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/NativePayController.java new file mode 100644 index 0000000..5290b23 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/NativePayController.java @@ -0,0 +1,49 @@ +package com.jzo2o.trade.controller.inner; + +import cn.hutool.core.bean.BeanUtil; +import com.jzo2o.api.trade.NativePayApi; +import com.jzo2o.api.trade.dto.request.NativePayReqDTO; +import com.jzo2o.api.trade.dto.response.NativePayResDTO; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.service.NativePayService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * Native支付方式Face接口:商户生成二维码,用户扫描支付 + * + * @author itcast + */ +@Validated +@RestController("innerNativePayController") +@Api(tags = "内部接口 - Native支付") +@RequestMapping("/inner/native") +public class NativePayController implements NativePayApi { + + @Resource + private NativePayService nativePayService; + + /*** + * 扫码支付,收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。 + * + * @param nativePayDTO 扫码支付提交参数 + * @return 扫码支付响应数据,其中包含二维码路径 + */ + @Override + @PostMapping + @ApiOperation(value = "统一收单线下交易", notes = "统一收单线下交易") + @ApiImplicitParam(name = "nativePayDTO", value = "扫码支付提交参数", required = true) + public NativePayResDTO createDownLineTrading(@RequestBody NativePayReqDTO nativePayDTO) { + Trading tradingEntity = BeanUtil.toBean(nativePayDTO, Trading.class); + Trading trading = this.nativePayService.createDownLineTrading(nativePayDTO.isChangeChannel(),tradingEntity); + return BeanUtil.toBean(trading, NativePayResDTO.class); + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/RefundRecordController.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/RefundRecordController.java new file mode 100644 index 0000000..633d168 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/RefundRecordController.java @@ -0,0 +1,53 @@ +package com.jzo2o.trade.controller.inner; + +import cn.hutool.core.bean.BeanUtil; +import com.jzo2o.api.trade.RefundRecordApi; +import com.jzo2o.api.trade.dto.response.ExecutionResultResDTO; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.service.BasicPayService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.math.BigDecimal; + +/** + * 基础支付控制器 + * + * @author itcast + */ +@RequestMapping("/inner/refund-record") +@RestController("innerRefundRecordController") +@Api(tags = "内部接口 - 退款") +public class RefundRecordController implements RefundRecordApi { + + @Resource + private BasicPayService basicPayService; + + /*** + * 统一收单交易退款接口 + * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家, + * 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 + * @param tradingOrderNo 交易单号 + * @param refundAmount 退款金额 + * @return + */ + @Override + @PostMapping("refund") + @ApiOperation(value = "统一收单交易退款", notes = "统一收单交易退款") + @ApiImplicitParams({ + @ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true), + @ApiImplicitParam(name = "refundAmount", value = "退款金额", required = true) + }) + public ExecutionResultResDTO refundTrading(@RequestParam("tradingOrderNo") Long tradingOrderNo, + @RequestParam("refundAmount") BigDecimal refundAmount) { + RefundRecord refundRecord = this.basicPayService.refundTrading(tradingOrderNo, refundAmount); + return BeanUtil.toBean(refundRecord,ExecutionResultResDTO.class); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/TradingController.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/TradingController.java new file mode 100644 index 0000000..5f57172 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/inner/TradingController.java @@ -0,0 +1,84 @@ +package com.jzo2o.trade.controller.inner; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; +import com.jzo2o.api.trade.TradingApi; +import com.jzo2o.api.trade.dto.response.TradingResDTO; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; +import com.jzo2o.trade.service.BasicPayService; +import com.jzo2o.trade.service.TradingService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author zzj + * @version 1.0 + */ +@RestController("innerTradingController") +@Api(tags = "内部接口 - 交易单服务") +@RequestMapping("/inner/tradings") +public class TradingController implements TradingApi { + + @Resource + private TradingService tradingService; + @Resource + private BasicPayService basicPayService; + +// /** +// * 根据订单号查询已结算交易单 +// * +// * @param productOrderNo 订单号 +// * @return 交易单数据 +// */ +// @Override +// @GetMapping("/findYjsTradByProductOrderNo") +// @ApiOperation(value = "根据订单号查询已结算交易单", notes = "根据业务订单号查询交易单") +// @ApiImplicitParams({ +// @ApiImplicitParam(name = "productOrderNo", value = "业务订单号", required = true, dataTypeClass = Long.class) +// }) +// public TradingResDTO findYjsTradByProductOrderNo(@RequestParam("productOrderNo") Long productOrderNo) { +// List yjsTradByProductOrderNo = tradingService.findYjsTradByProductOrderNo(productOrderNo); +// if(ObjectUtil.isNotEmpty(yjsTradByProductOrderNo)){ +// Trading trading = yjsTradByProductOrderNo.get(0); +// return BeanUtil.toBean(trading, TradingResDTO.class); +// } +// return null; +// } + +// @Override +// @GetMapping("/findTradByTradingOrderNo") +// @ApiOperation(value = "根据交易单号查询交易单", notes = "根据交易单号查询交易单") +// @ApiImplicitParams({ +// @ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true, dataTypeClass = Long.class) +// }) +// public TradingResDTO findTradByTradingOrderNo(Long tradingOrderNo) { +// Trading tradByTradingOrderNo = tradingService.findTradByTradingOrderNo(tradingOrderNo); +// TradingResDTO tradingResDTO = BeanUtil.toBean(tradByTradingOrderNo, TradingResDTO.class); +// return tradingResDTO; +// } + + @Override + @GetMapping("/findTradResultByTradingOrderNo") + @ApiOperation(value = "根据交易单号查询交易单的交易结果", notes = "根据交易单号查询交易单的交易结果") + @ApiImplicitParams({ + @ApiImplicitParam(name = "tradingOrderNo", value = "交易单号", required = true, dataTypeClass = Long.class) + }) + public TradingResDTO findTradResultByTradingOrderNo(Long tradingOrderNo) { + TradingDTO tradingDTO = basicPayService.queryTradingResult(tradingOrderNo); + TradingResDTO tradingResDTO = BeanUtil.toBean(tradingDTO, TradingResDTO.class); + return tradingResDTO; + } + + + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/open/NotifyController.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/open/NotifyController.java new file mode 100644 index 0000000..317ae10 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/controller/open/NotifyController.java @@ -0,0 +1,86 @@ +package com.jzo2o.trade.controller.open; + +import cn.hutool.core.map.MapUtil; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.service.NotifyService; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; +import io.swagger.annotations.Api; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 支付结果的通知 + */ +@RestController("openNotifyController") +@Api(tags = "白名单接口 - 支付通知") +@RequestMapping("/open/notify") +public class NotifyController { + + @Resource + private NotifyService notifyService; + + /** + * 微信支付成功回调(成功后无需响应内容) + * + * @param httpEntity 微信请求信息 + * @param enterpriseId 商户id + * @return 正常响应200,否则响应500 + */ + @PostMapping("wx/{enterpriseId}") + public ResponseEntity wxPayNotify(HttpEntity httpEntity, @PathVariable("enterpriseId") Long enterpriseId) { + try { + //获取请求头 + HttpHeaders headers = httpEntity.getHeaders(); + + //构建微信请求数据对象 + NotificationRequest request = new NotificationRequest.Builder() + .withSerialNumber(headers.getFirst("Wechatpay-Serial")) //证书序列号(微信平台) + .withNonce(headers.getFirst("Wechatpay-Nonce")) //随机串 + .withTimestamp(headers.getFirst("Wechatpay-Timestamp")) //时间戳 + .withSignature(headers.getFirst("Wechatpay-Signature")) //签名字符串 + .withBody(httpEntity.getBody()) + .build(); + + //微信通知的业务处理 + this.notifyService.wxPayNotify(request, enterpriseId); + + } catch (CommonException e) { + Map result = MapUtil.builder() + .put("code", "FAIL") + .put("message", e.getMessage()) + .build(); + //响应500 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + return ResponseEntity.ok(null); + } + + /** + * 支付宝支付成功回调(成功后需要响应success) + * + * @param enterpriseId 商户id + * @return 正常响应200,否则响应500 + */ + @PostMapping("alipay/{enterpriseId}") + public ResponseEntity aliPayNotify(HttpServletRequest request, + @PathVariable("enterpriseId") Long enterpriseId) { + try { + //支付宝通知的业务处理 + this.notifyService.aliPayNotify(request, enterpriseId); + } catch (CommonException e) { + //响应500 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + return ResponseEntity.ok("success"); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/RefundStatusEnum.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/RefundStatusEnum.java new file mode 100644 index 0000000..6e5730e --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/RefundStatusEnum.java @@ -0,0 +1,36 @@ +package com.jzo2o.trade.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 退款状态枚举 + * + * @author zzj + * @version 1.0 + */ +public enum RefundStatusEnum { + + APPLY_REFUND(0, "发起退款"), + SENDING(1, "退款中"), + SUCCESS(2, "成功"), + FAIL(3, "失败"); + + @EnumValue + @JsonValue + private final Integer code; + private final String value; + + RefundStatusEnum(Integer code, String value) { + this.code = code; + this.value = value; + } + + public Integer getCode() { + return this.code; + } + + public String getValue() { + return this.value; + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingEnum.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingEnum.java new file mode 100644 index 0000000..a311c13 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingEnum.java @@ -0,0 +1,66 @@ +package com.jzo2o.trade.enums; + +/** + * 交易枚举 + */ +public enum TradingEnum { + + SUCCEED(1001, 200, "操作成功"), + ERROR(1002, "操作失败"), + CHECK_TRADING_FAIL(1003, "交易单校验失败"), + TRY_LOCK_TRADING_FAIL(1004, "交易单加锁失败"), + PAYING_TRADING_FAIL(1005, "交易单支付失败"), + TRADING_STATE_SUCCEED(1006, "交易单已完成"), + TRADING_STATE_PAYING(1007, "交易单交易中"), + CONFIG_EMPTY(1008, "支付配置为空"), + CONFIG_ERROR(1009, "支付配置错误"), + NATIVE_PAY_FAIL(1010, "统一下单交易失败"), + NATIVE_QRCODE_FAIL(1011, "生成二维码失败"), + REFUND_FAIL(1012, "查询统一下单交易退款失败"), + SAVE_OR_UPDATE_FAIL(1013, "交易单保存或修改失败"), + TRADING_TYPE_FAIL(1014, "未定义的交易类型"), + NATIVE_QUERY_FAIL(1015, "查询统一下单交易失败"), + NATIVE_REFUND_FAIL(1016, "统一下单退款交易失败"), + NATIVE_QUERY_REFUND_FAIL(1017, "统一下单查询退款失败"), + CASH_PAY_FAIL(1018, "现金交易失败"), + CASH_REFUND_FAIL(1019, "统一下单退款交易失败"), + CREDIT_PAY_FAIL(1020, "信用交易失败"), + LIST_TRADE_STATE_FAIL(1021, "按交易状态查询交易单失败"), + NOT_FOUND(1022, "交易单不存在"), + CLOSE_FAIL(1023, "关闭交易单失败"), + BASIC_REFUND_OUT_FAIL(1024, "退款金额超过订单总金额"), + REFUND_NOT_FOUND(1025, "退款记录不存在"), + REFUND_ALREADY_COMPLETED(1026, "退款记录已经完成"), + BASIC_REFUND_COUNT_OUT_FAIL(1027, "退款次数超出限制,最多20次"), + TRADING_QUERY_PARAM_ERROR(1028, "查询交易单错误,交易单号为空"), + REFUND_QUERY_PARAM_ERROR(1029, "查询退款单错误,订单号或交易单号至少传递一个"), + REFUND_DURING(1030, "退款进行中"); + + private final Integer code; + private final Integer status; + private final String value; + + TradingEnum(Integer code, String value) { + this.code = code; + this.value = value; + this.status = 500; + } + + TradingEnum(Integer code, Integer status, String value) { + this.code = code; + this.value = value; + this.status = status; + } + + public Integer getCode() { + return code; + } + + public String getValue() { + return this.value; + } + + public Integer getStatus() { + return this.status; + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingStateEnum.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingStateEnum.java new file mode 100644 index 0000000..5f2acc6 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/enums/TradingStateEnum.java @@ -0,0 +1,39 @@ +package com.jzo2o.trade.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 交易单状态枚举 + * + * @author zzj + * @version 1.0 + */ +public enum TradingStateEnum{ + +// DFK(1, "待付款"), + FKZ(2, "付款中"), + FKSB(3, "付款失败"), + YJS(4, "已付款"), + QXDD(5, "取消订单"), + MD(6, "免单"), + GZ(7, "挂账"); + + @EnumValue + @JsonValue + private final Integer code; + private final String value; + + TradingStateEnum(Integer code, String value) { + this.code = code; + this.value = value; + } + + public Integer getCode() { + return this.code; + } + + public String getValue() { + return this.value; + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BasicPayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BasicPayHandler.java new file mode 100644 index 0000000..163a955 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BasicPayHandler.java @@ -0,0 +1,45 @@ +package com.jzo2o.trade.handler; + +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; + +/** + * 基础支付功能的定义,具体业务由不同的支付渠道实现 + * + * @author zzj + * @version 1.0 + */ +public interface BasicPayHandler { + + /*** + * 统一收单线下交易查询 + * 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。 + * @return 是否有变化 + */ + Boolean queryTrading(Trading trading) throws CommonException; + + /*** + * 关闭交易 + * @return 是否成功 + */ + Boolean closeTrading(Trading trading) throws CommonException; + + /*** + * 统一收单交易退款接口 + * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家, + * 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 + * @param refundRecord 退款记录对象 + * @return 是否有变化 + */ + Boolean refundTrading(RefundRecord refundRecord) throws CommonException; + + /*** + * 统一收单交易退款查询接口 + * + * @param refundRecord 退款交易单号 + * @return 是否有变化 + */ + Boolean queryRefundTrading(RefundRecord refundRecord) throws CommonException; + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BeforePayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BeforePayHandler.java new file mode 100644 index 0000000..2820b74 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/BeforePayHandler.java @@ -0,0 +1,52 @@ +package com.jzo2o.trade.handler; + +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; + +import java.math.BigDecimal; + +/** + * 交易前置处理接口 + * + * @author itcast + */ +public interface BeforePayHandler { + + + + /*** + * 交易单参数校验 + * @param tradingEntity 交易订单 + * @return 是否符合要求 + */ + void checkCreateTrading(Trading tradingEntity); + + /*** + * QueryTrading交易单参数校验 + * @param trading 交易订单 + */ + void checkQueryTrading(Trading trading); + + + /*** + * RefundTrading退款交易单参数校验 + * @param trading 交易订单 + * @param refundAmount 退款金额 + */ + void checkRefundTrading(Trading trading,BigDecimal refundAmount); + + + /*** + * QueryRefundTrading交易单参数校验 + * @param refundRecord 退款记录 + */ + void checkQueryRefundTrading(RefundRecord refundRecord); + + /*** + * CloseTrading交易单参数校验 + * @param trading 交易订单 + */ + void checkCloseTrading(Trading trading); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/HandlerFactory.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/HandlerFactory.java new file mode 100644 index 0000000..275d619 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/HandlerFactory.java @@ -0,0 +1,33 @@ +package com.jzo2o.trade.handler; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.api.trade.enums.PayChannelEnum; + +import java.util.Map; + +/** + * Handler工厂,用于获取指定类型的具体渠道的实例对象 + */ +public class HandlerFactory { + + private HandlerFactory() { + + } + + public static T get(PayChannelEnum payChannel, Class handler) { + Map beans = SpringUtil.getBeansOfType(handler); + for (Map.Entry entry : beans.entrySet()) { + PayChannel payChannelAnnotation = entry.getValue().getClass().getAnnotation(PayChannel.class); + if (ObjectUtil.isNotEmpty(payChannelAnnotation) && ObjectUtil.equal(payChannel, payChannelAnnotation.type())) { + return entry.getValue(); + } + } + return null; + } + + public static T get(String payChannel, Class handler) { + return get(PayChannelEnum.valueOf(payChannel), handler); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/JsapiPayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/JsapiPayHandler.java new file mode 100644 index 0000000..1b3699b --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/JsapiPayHandler.java @@ -0,0 +1,18 @@ +package com.jzo2o.trade.handler; + +import com.jzo2o.trade.model.domain.Trading; + +/** + * jsapi下单处理 + * + * @author itcast + */ +public interface JsapiPayHandler { + + /** + * 创建交易 + * + * @param tradingEntity 交易单 + */ + void createJsapiTrading(Trading tradingEntity); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/NativePayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/NativePayHandler.java new file mode 100644 index 0000000..9a86e64 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/NativePayHandler.java @@ -0,0 +1,21 @@ +package com.jzo2o.trade.handler; + +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.model.domain.Trading; + +/** + * @author itcast + * @ClassName NativePayHandler.java + * @Description Native支付方式Handler:商户生成二维码,用户扫描支付 + */ +public interface NativePayHandler { + + + /*** + * @description 统一收单线下交易预创建 + * 收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。 + * @param tradingEntity 交易单 + */ + void createDownLineTrading(Trading tradingEntity) throws CommonException; + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliBasicPayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliBasicPayHandler.java new file mode 100644 index 0000000..a0905c9 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliBasicPayHandler.java @@ -0,0 +1,165 @@ +package com.jzo2o.trade.handler.alipay; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alipay.easysdk.factory.Factory; +import com.alipay.easysdk.kernel.Config; +import com.alipay.easysdk.kernel.util.ResponseChecker; +import com.alipay.easysdk.payment.common.models.AlipayTradeCloseResponse; +import com.alipay.easysdk.payment.common.models.AlipayTradeFastpayRefundQueryResponse; +import com.alipay.easysdk.payment.common.models.AlipayTradeQueryResponse; +import com.alipay.easysdk.payment.common.models.AlipayTradeRefundResponse; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 支付宝基础支付功能的实现 + * + * @author zzj + * @version 1.0 + */ +@Slf4j +@Component("aliBasicPayHandler") +@PayChannel(type = PayChannelEnum.ALI_PAY) +public class AliBasicPayHandler implements BasicPayHandler { + + @Override + public Boolean queryTrading(Trading trading) throws CommonException { + //查询配置 + Config config = AlipayConfig.getConfig(trading.getEnterpriseId()); + //Factory使用配置 + Factory.setOptions(config); + AlipayTradeQueryResponse queryResponse; + try { + //调用支付宝API:通用查询支付情况 + queryResponse = Factory + .Payment + .Common() + .query(String.valueOf(trading.getTradingOrderNo())); + } catch (Exception e) { + String msg = StrUtil.format("查询支付宝统一下单失败:trading = {}", trading); + log.error(msg, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } + + //修改交易单状态 + trading.setTransactionId(queryResponse.getTradeNo()); + trading.setResultCode(queryResponse.getSubCode()); + trading.setResultMsg(queryResponse.getSubMsg()); + trading.setResultJson(JSONUtil.toJsonStr(queryResponse)); + + boolean success = ResponseChecker.success(queryResponse); + //响应成功,分析交易状态 + if (success) { + String tradeStatus = queryResponse.getTradeStatus(); + if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatus)) { + //支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款) + trading.setTradingState(TradingStateEnum.QXDD); + } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) { + // TRADE_SUCCESS(交易支付成功) + // TRADE_FINISHED(交易结束,不可退款) + trading.setTradingState(TradingStateEnum.YJS); + } else { + //非最终状态不处理,当前交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)不处理 + return false; + } + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } + + @Override + public Boolean closeTrading(Trading trading) throws CommonException { + //查询配置 + Config config = AlipayConfig.getConfig(trading.getEnterpriseId()); + //Factory使用配置 + Factory.setOptions(config); + try { + //调用支付宝API:通用查询支付情况 + AlipayTradeCloseResponse closeResponse = Factory + .Payment + .Common() + .close(String.valueOf(trading.getTradingOrderNo())); + boolean success = ResponseChecker.success(closeResponse); + if (success) { + trading.setTradingState(TradingStateEnum.QXDD); + return true; + } + return false; + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CLOSE_FAIL.getValue()); + } + } + + @Override + public Boolean refundTrading(RefundRecord refundRecord) throws CommonException { + //查询配置 + Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId()); + //Factory使用配置 + Factory.setOptions(config); + //调用支付宝API:通用查询支付情况 + AlipayTradeRefundResponse refundResponse; + try { + // 支付宝easy sdk + refundResponse = Factory + .Payment + .Common() + //扩展参数:退款单号 + .optional("out_request_no", refundRecord.getRefundNo()) + .refund(Convert.toStr(refundRecord.getTradingOrderNo()), + Convert.toStr(refundRecord.getRefundAmount())); + } catch (Exception e) { + String msg = StrUtil.format("调用支付宝退款接口出错!refundRecord = {}", refundRecord); + log.error(msg, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, msg); + } + refundRecord.setRefundId(null); + refundRecord.setRefundCode(refundResponse.getSubCode()); + refundRecord.setRefundMsg(JSONUtil.toJsonStr(refundResponse)); + boolean success = ResponseChecker.success(refundResponse); + if (success) { + refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS); + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_REFUND_FAIL.getValue()); + } + + @Override + public Boolean queryRefundTrading(RefundRecord refundRecord) throws CommonException { + //查询配置 + Config config = AlipayConfig.getConfig(refundRecord.getEnterpriseId()); + //Factory使用配置 + Factory.setOptions(config); + AlipayTradeFastpayRefundQueryResponse response; + try { + response = Factory.Payment.Common().queryRefund( + Convert.toStr(refundRecord.getTradingOrderNo()), + Convert.toStr(refundRecord.getRefundNo())); + } catch (Exception e) { + log.error("调用支付宝查询退款接口出错!refundRecord = {}", refundRecord, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_REFUND_FAIL.getValue()); + } + + refundRecord.setRefundCode(response.getSubCode()); + refundRecord.setRefundMsg(JSONUtil.toJsonStr(response)); + boolean success = ResponseChecker.success(response); + if (success) { + refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS); + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_REFUND_FAIL.getValue()); + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliNativePayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliNativePayHandler.java new file mode 100644 index 0000000..55b118f --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AliNativePayHandler.java @@ -0,0 +1,63 @@ +package com.jzo2o.trade.handler.alipay; + +import cn.hutool.core.convert.Convert; +import cn.hutool.json.JSONUtil; +import com.alipay.easysdk.factory.Factory; +import com.alipay.easysdk.kernel.Config; +import com.alipay.easysdk.kernel.util.ResponseChecker; +import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.NativePayHandler; +import com.jzo2o.trade.model.domain.Trading; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 支付宝的扫描支付的具体实现 + */ +@Slf4j +@Component("aliNativePayHandler") +@PayChannel(type = PayChannelEnum.ALI_PAY) +public class AliNativePayHandler implements NativePayHandler { + + @Override + public void createDownLineTrading(Trading tradingEntity) throws CommonException { + //查询配置 + Config config = AlipayConfig.getConfig(tradingEntity.getEnterpriseId()); + //Factory使用配置 + Factory.setOptions(config); + AlipayTradePrecreateResponse response; + try { + //调用支付宝API面对面支付 + response = Factory + .Payment + .FaceToFace() + .preCreate(tradingEntity.getMemo(), //订单描述 + Convert.toStr(tradingEntity.getTradingOrderNo()), //业务订单号 + Convert.toStr(tradingEntity.getTradingAmount())); //金额 + } catch (Exception e) { + log.error("支付宝统一下单创建失败:tradingEntity = {}", tradingEntity, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + + //受理结果【只表示请求是否成功,而不是支付是否成功】 + boolean isSuccess = ResponseChecker.success(response); + //6.1、受理成功:修改交易单 + if (isSuccess) { + String subCode = response.getSubCode(); + String subMsg = response.getQrCode(); + tradingEntity.setPlaceOrderCode(subCode); //返回的编码 + tradingEntity.setPlaceOrderMsg(subMsg); //二维码需要展现的信息 + tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response)); + tradingEntity.setTradingState(TradingStateEnum.FKZ); + return; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AlipayConfig.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AlipayConfig.java new file mode 100644 index 0000000..bc1df33 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/alipay/AlipayConfig.java @@ -0,0 +1,50 @@ +package com.jzo2o.trade.handler.alipay; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.alipay.easysdk.kernel.Config; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.model.domain.PayChannel; +import com.jzo2o.trade.service.PayChannelService; + +/** + * @author zzj + * @version 1.0 + */ +public class AlipayConfig { + + /** + * 将支付渠道配置转化为支付宝的配置 + * + * @param enterpriseId 商户ID + * @return 支付宝的配置 + */ + public static Config getConfig(Long enterpriseId) { + // 查询配置 + PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class); + PayChannel payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_ALI_PAY); + + if (ObjectUtil.isEmpty(payChannel)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CONFIG_EMPTY.getValue()); + } + + Config config = new Config(); + config.protocol = "https"; + config.gatewayHost = payChannel.getDomain(); + config.signType = "RSA2"; + config.appId = payChannel.getAppId(); + //配置应用私钥 + config.merchantPrivateKey = payChannel.getMerchantPrivateKey(); + //配置支付宝公钥 + config.alipayPublicKey = payChannel.getPublicKey(); + //可设置异步通知接收服务地址(可选) + config.notifyUrl = payChannel.getNotifyUrl(); + //设置AES密钥,调用AES加解密相关接口时需要(可选) + config.encryptKey = payChannel.getEncryptKey(); + return config; + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/impl/BeforePayHandlerImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/impl/BeforePayHandlerImpl.java new file mode 100644 index 0000000..16d974e --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/impl/BeforePayHandlerImpl.java @@ -0,0 +1,126 @@ +package com.jzo2o.trade.handler.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +import com.jzo2o.api.trade.dto.response.ExecutionResultResDTO; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.handler.BeforePayHandler; +import com.jzo2o.trade.handler.HandlerFactory; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; +import com.jzo2o.trade.service.RefundRecordService; +import com.jzo2o.trade.service.TradingService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 交易前置处理接口 + * + * @author zzj + * @version 1.0 + */ +@Component +public class BeforePayHandlerImpl implements BeforePayHandler { + + @Resource + private TradingService tradingService; + @Resource + private IdentifierGenerator identifierGenerator; + @Resource + private RefundRecordService refundRecordService; + + + @Override + public void checkCreateTrading(Trading tradingEntity) { + //校验不为为空,订单备注、订单号、企业号、交易金额、支付渠道 + boolean flag = ObjectUtil.isAllNotEmpty(tradingEntity, + tradingEntity.getMemo(), + tradingEntity.getProductOrderNo(), + tradingEntity.getEnterpriseId(), + tradingEntity.getTradingAmount(), + tradingEntity.getTradingChannel()); + //金额不能小于等于0 + boolean flag2 = !NumberUtil.isLessOrEqual(tradingEntity.getTradingAmount(), BigDecimal.valueOf(0)); + if (!flag || !flag2) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CONFIG_ERROR.getValue()); + } + + List tradings = tradingService.queryByProductOrder(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo()); + if (ObjectUtil.isEmpty(tradings)) { + //新交易单,生成交易号 + tradingEntity.setTradingOrderNo((Long) identifierGenerator.nextId(tradingEntity)); + return ; + } + //找到已付款的记录 + Trading finishedTrading = tradingService.findFinishedTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo()); + if (ObjectUtil.isNotEmpty(finishedTrading)) { + //存在已付款单子直接抛出重复支付异常 + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.TRADING_STATE_SUCCEED.getValue()); + } + //找到该支付渠道支付中的单子 + Trading trading = tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel()); + if (ObjectUtil.isNotEmpty(trading)) { + //存在相同支付渠道的付款中单子 + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.TRADING_STATE_PAYING.getValue()); + } + //新交易单,生成交易号 + tradingEntity.setTradingOrderNo((Long) identifierGenerator.nextId(tradingEntity)); + } + + @Override + public void checkQueryTrading(Trading trading) { + if (ObjectUtil.isEmpty(trading)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NOT_FOUND.getValue()); + } + + } + + + @Override + public void checkRefundTrading(Trading trading,BigDecimal refundAmount) { + if (ObjectUtil.isEmpty(trading)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NOT_FOUND.getValue()); + } + + if (trading.getTradingState() != TradingStateEnum.YJS) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_REFUND_FAIL.getValue()); + } + + //退款总金额不可超实付总金额 + if (NumberUtil.isGreater(refundAmount, trading.getTradingAmount())) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.BASIC_REFUND_OUT_FAIL.getValue()); + } + + } + + @Override + public void checkQueryRefundTrading(RefundRecord refundRecord) { + if (ObjectUtil.isEmpty(refundRecord)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.REFUND_NOT_FOUND.getValue()); + } + + if (ObjectUtil.equals(refundRecord.getRefundStatus(), RefundStatusEnum.SUCCESS)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.REFUND_ALREADY_COMPLETED.getValue()); + } + } + + @Override + public void checkCloseTrading(Trading trading) { + if (ObjectUtil.notEqual(TradingStateEnum.FKZ, trading.getTradingState())) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CLOSE_FAIL.getValue()); + } + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WeChatBasicPayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WeChatBasicPayHandler.java new file mode 100644 index 0000000..9edc16f --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WeChatBasicPayHandler.java @@ -0,0 +1,198 @@ +package com.jzo2o.trade.handler.wechat; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.handler.wechat.response.WeChatResponse; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import static com.jzo2o.trade.enums.TradingEnum.NATIVE_REFUND_FAIL; + +/** + * 微信基础支付功能的实现 + * + * @author zzj + * @version 1.0 + */ +@Slf4j +@Component("weChatBasicPayHandler") +@PayChannel(type = PayChannelEnum.WECHAT_PAY) +public class WeChatBasicPayHandler implements BasicPayHandler { + + @Override + public Boolean queryTrading(Trading trading) throws CommonException { + // 获取微信支付的client对象 + WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId()); + + //请求地址 + String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}", trading.getTradingOrderNo()); + + //请求参数 + Map params = MapUtil.builder() + .put("mchid", client.getMchId()) + .build(); + + WeChatResponse response; + try { + response = client.doGet(apiPath, params); + } catch (Exception e) { + log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, NATIVE_REFUND_FAIL.getValue()); + } + if (response.isOk()) { + JSONObject jsonObject = JSONUtil.parseObj(response.getBody()); + // 交易状态,枚举值: + // SUCCESS:支付成功 + // REFUND:转入退款 + // NOTPAY:未支付 + // CLOSED:已关闭 + // REVOKED:已撤销(仅付款码支付会返回) + // USERPAYING:用户支付中(仅付款码支付会返回) + // PAYERROR:支付失败(仅付款码支付会返回) + String tradeStatus = jsonObject.getStr("trade_state"); + //已关闭或已撤单的更新状态为取消订单 + if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) { + trading.setTradingState(TradingStateEnum.QXDD); + //支付成功或转入退款的更新为已付款 + } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) { + trading.setTradingState(TradingStateEnum.YJS); + } else if (StrUtil.equalsAny(tradeStatus, TradingConstant.WECHAT_TRADE_PAYERROR)) { + trading.setTradingState(TradingStateEnum.FKSB); + } else { + //非最终状态不处理 + return false; + } + //修改交易单状态 + trading.setTransactionId(jsonObject.getStr("transaction_id")); + trading.setResultCode(tradeStatus); + trading.setResultMsg(jsonObject.getStr("trade_state_desc")); + trading.setResultJson(response.getBody()); + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, NATIVE_REFUND_FAIL.getValue()); + } + + @Override + public Boolean closeTrading(Trading trading) throws CommonException { + // 获取微信支付的client对象 + WechatPayHttpClient client = WechatPayHttpClient.get(trading.getEnterpriseId()); + //请求地址 + String apiPath = StrUtil.format("/v3/pay/transactions/out-trade-no/{}/close", trading.getTradingOrderNo()); + //请求参数 + Map params = MapUtil.builder() + .put("mchid", client.getMchId()) + .build(); + try { + WeChatResponse response = client.doPost(apiPath, params); + if (response.getStatus() == 204) { + trading.setTradingState(TradingStateEnum.QXDD); + return true; + } + return false; + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CLOSE_FAIL.getValue()); + } + } + + @Override + public Boolean refundTrading(RefundRecord refundRecord) throws CommonException { + // 获取微信支付的client对象 + WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId()); + //请求地址 + String apiPath = "/v3/refund/domestic/refunds"; + //请求参数 + Map params = MapUtil.builder() + .put("out_refund_no", Convert.toStr(refundRecord.getRefundNo())) + .put("out_trade_no", Convert.toStr(refundRecord.getTradingOrderNo())) + .put("amount", MapUtil.builder() + .put("refund", NumberUtil.mul(refundRecord.getRefundAmount(), 100)) //本次退款金额 + .put("total", NumberUtil.mul(refundRecord.getTotal(), 100)) //原订单金额 + .put("currency", "CNY") //币种 + .build()) + .build(); + WeChatResponse response; + try { + response = client.doPost(apiPath, params); + } catch (Exception e) { + log.error("调用微信接口出错!apiPath = {}, params = {}", apiPath, JSONUtil.toJsonStr(params), e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, NATIVE_REFUND_FAIL.getValue()); + } + refundRecord.setRefundCode(Convert.toStr(response.getStatus())); + refundRecord.setRefundMsg(response.getBody()); + if (response.isOk()) { + JSONObject jsonObject = JSONUtil.parseObj(response.getBody()); + refundRecord.setRefundId(jsonObject.getStr("refund_id")); + // SUCCESS:退款成功 + // CLOSED:退款关闭 + // PROCESSING:退款处理中 + // ABNORMAL:退款异常 + String status = jsonObject.getStr("status"); + if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) { + refundRecord.setRefundStatus(RefundStatusEnum.SENDING); + } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) { + refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS); + } else { + refundRecord.setRefundStatus(RefundStatusEnum.FAIL); + } + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, NATIVE_REFUND_FAIL.getValue()); + } + + @Override + public Boolean queryRefundTrading(RefundRecord refundRecord) throws CommonException { + // 获取微信支付的client对象 + WechatPayHttpClient client = WechatPayHttpClient.get(refundRecord.getEnterpriseId()); + + //请求地址 + String apiPath = StrUtil.format("/v3/refund/domestic/refunds/{}", refundRecord.getRefundNo()); + + WeChatResponse response; + try { + response = client.doGet(apiPath); + } catch (Exception e) { + log.error("调用微信接口出错!apiPath = {}", apiPath, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_REFUND_FAIL.getValue()); + } + + refundRecord.setRefundCode(Convert.toStr(response.getStatus())); + refundRecord.setRefundMsg(response.getBody()); + if (response.isOk()) { + JSONObject jsonObject = JSONUtil.parseObj(response.getBody()); + refundRecord.setRefundId(jsonObject.getStr("refund_id")); + // SUCCESS:退款成功 + // CLOSED:退款关闭 + // PROCESSING:退款处理中 + // ABNORMAL:退款异常 + String status = jsonObject.getStr("status"); + if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_PROCESSING)) { + refundRecord.setRefundStatus(RefundStatusEnum.SENDING); + } else if (StrUtil.equals(status, TradingConstant.WECHAT_REFUND_SUCCESS)) { + refundRecord.setRefundStatus(RefundStatusEnum.SUCCESS); + } else { + refundRecord.setRefundStatus(RefundStatusEnum.FAIL); + } + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_REFUND_FAIL.getValue()); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatJsapiPayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatJsapiPayHandler.java new file mode 100644 index 0000000..d8a6362 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatJsapiPayHandler.java @@ -0,0 +1,117 @@ +package com.jzo2o.trade.handler.wechat; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.JsapiPayHandler; +import com.jzo2o.trade.handler.wechat.bean.JsapiPayParam; +import com.jzo2o.trade.handler.wechat.response.WeChatResponse; +import com.jzo2o.trade.model.domain.Trading; +import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; +import java.security.PrivateKey; +import java.security.Signature; +import java.util.Base64; +import java.util.Map; + +/** + * 微信jsapi的实现 + * + * @author zzj + * @version 1.0 + */ +@Component("wechatJsapiPayHandler") +@PayChannel(type = PayChannelEnum.WECHAT_PAY) +public class WechatJsapiPayHandler implements JsapiPayHandler { + + @Override + public void createJsapiTrading(Trading tradingEntity) { + // 查询配置 + WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId()); + //请求地址 + String apiPath = "/v3/pay/transactions/jsapi"; + //请求参数 + Map params = MapUtil.builder() + .put("mchid", client.getMchId()) + .put("appid", client.getAppId()) + .put("description", tradingEntity.getMemo()) + .put("notify_url", client.getNotifyUrl()) + .put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo())) + .put("amount", MapUtil.builder() + .put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分 + .put("currency", "CNY") //人民币 + .build()) + .put("payer", MapUtil.builder() + .put("openid", tradingEntity.getOpenId()) //用户识别标识 + .build()) + .build(); + try { + WeChatResponse response = client.doPost(apiPath, params); + if (!response.isOk()) { + //下单失败 + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + //指定统一下单code + tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus())); + //jsapi发起支付需要的预支付id + tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("prepay_id")); + + //指定交易状态 + tradingEntity.setTradingState(TradingStateEnum.FKZ); + + //封装JSAPI调起支付的参数(给前端使用) + Long timeStamp = System.currentTimeMillis() / 1000; + String nonceStr = IdUtil.simpleUUID(); + String packages = "prepay_id=" + tradingEntity.getPlaceOrderMsg(); + JsapiPayParam jsapiPayParam = JsapiPayParam.builder() + .timeStamp(timeStamp) + .appId(client.getAppId()) + .nonceStr(nonceStr) + .packages(packages) + .paySign(this.createPaySign(client, timeStamp, nonceStr, packages)) + .build(); + + //设置jsapi调起支付的参数 + tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(jsapiPayParam)); + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + } + + /** + * 生成 + * + * @param client 微信client对象 + * @param timeStamp 时间戳 + * @param nonceStr 随机数 + * @param packages 预支付字符串 + * @return 签名字符串 + * @throws Exception 不处理异常,全部抛出 + */ + private String createPaySign(WechatPayHttpClient client, Long timeStamp, String nonceStr, String packages) throws Exception { + Signature sign = Signature.getInstance("SHA256withRSA"); + // 加载商户私钥 + PrivateKey privateKey = PemUtil + .loadPrivateKey(new ByteArrayInputStream(client.getPrivateKey().getBytes(CharsetUtil.CHARSET_UTF_8))); + sign.initSign(privateKey); + String message = StrUtil.format("{}\n{}\n{}\n{}\n", + client.getAppId(), + timeStamp, + nonceStr, + packages); + sign.update(message.getBytes()); + return Base64.getEncoder().encodeToString(sign.sign()); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatNativePayHandler.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatNativePayHandler.java new file mode 100644 index 0000000..9668d1b --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatNativePayHandler.java @@ -0,0 +1,67 @@ +package com.jzo2o.trade.handler.wechat; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.annotation.PayChannel; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.NativePayHandler; +import com.jzo2o.trade.handler.wechat.response.WeChatResponse; +import com.jzo2o.trade.model.domain.Trading; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 微信二维码支付 + * + * @author zzj + * @version 1.0 + */ +@Component("wechatNativePayHandler") +@PayChannel(type = PayChannelEnum.WECHAT_PAY) +public class WechatNativePayHandler implements NativePayHandler { + + @Override + public void createDownLineTrading(Trading tradingEntity) throws CommonException { + // 查询配置 + WechatPayHttpClient client = WechatPayHttpClient.get(tradingEntity.getEnterpriseId()); + //请求地址 + String apiPath = "/v3/pay/transactions/native"; + + //请求参数 + Map params = MapUtil.builder() + .put("mchid", client.getMchId()) + .put("appid", client.getAppId()) + .put("description", tradingEntity.getMemo()) + .put("notify_url", client.getNotifyUrl()) + .put("out_trade_no", Convert.toStr(tradingEntity.getTradingOrderNo())) + .put("amount", MapUtil.builder() + .put("total", Convert.toInt(NumberUtil.mul(tradingEntity.getTradingAmount(), 100))) //金额,单位:分 + .put("currency", "CNY") //人民币 + .build()) + .build(); + + try { + WeChatResponse response = client.doPost(apiPath, params); + if (!response.isOk()) { + //下单失败 + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + //指定统一下单code + tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus())); + //二维码需要展现的信息 + tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url")); + //指定统一下单json字符串 + tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response)); + + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatPayHttpClient.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatPayHttpClient.java new file mode 100644 index 0000000..17d7250 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/WechatPayHttpClient.java @@ -0,0 +1,161 @@ +package com.jzo2o.trade.handler.wechat; + +import cn.hutool.core.net.url.UrlBuilder; +import cn.hutool.core.net.url.UrlPath; +import cn.hutool.core.net.url.UrlQuery; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.handler.wechat.response.WeChatResponse; +import com.jzo2o.trade.model.domain.PayChannel; +import com.jzo2o.trade.service.PayChannelService; +import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; +import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; +import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.security.PrivateKey; +import java.util.Map; + +/** + * 微信支付远程调用对象 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatPayHttpClient { + + private String mchId; //商户号 + private String appId; //商户号 + private String privateKey; //私钥字符串 + private String mchSerialNo; //商户证书序列号 + private String apiV3Key; //V3密钥 + private String domain; //请求域名 + private String notifyUrl; //请求地址 + + public static WechatPayHttpClient get(Long enterpriseId) { + // 查询配置 + PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class); + PayChannel payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_WECHAT_PAY); + + if (ObjectUtil.isEmpty(payChannel)) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.CONFIG_EMPTY.getValue()); + } + + //通过渠道对象转化成微信支付的client对象 + JSONObject otherConfig = JSONUtil.parseObj(payChannel.getOtherConfig()); + return WechatPayHttpClient.builder() + .appId(payChannel.getAppId()) + .domain(payChannel.getDomain()) + .privateKey(payChannel.getMerchantPrivateKey()) + .mchId(otherConfig.getStr("mchId")) + .mchSerialNo(otherConfig.getStr("mchSerialNo")) + .apiV3Key(otherConfig.getStr("apiV3Key")) + .notifyUrl(payChannel.getNotifyUrl()) + .build(); + } + + /*** + * 构建CloseableHttpClient远程请求对象 + * @return {@link CloseableHttpClient} + */ + public CloseableHttpClient createHttpClient() throws Exception { + // 加载商户私钥(privateKey:私钥字符串) + PrivateKey merchantPrivateKey = PemUtil + .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8"))); + + // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥) + PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey); + WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials( + mchId, privateKeySigner); + + // 向证书管理器增加需要自动更新平台证书的商户信息 + CertificatesManager certificatesManager = CertificatesManager.getInstance(); + certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes("utf-8")); + + // 初始化httpClient + return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create() + .withMerchant(mchId, mchSerialNo, merchantPrivateKey) + .withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId))) + .build(); + } + + /*** + * 支持post请求的远程调用 + * @param apiPath api地址 + * @param params 携带请求参数 + * @return 返回字符串 + */ + public WeChatResponse doPost(String apiPath, Map params) throws Exception { + String url = StrUtil.format("https://{}{}", this.domain, apiPath); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-type", "application/json; charset=utf-8"); + + String body = JSONUtil.toJsonStr(params); + httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8)); + + CloseableHttpResponse response = this.createHttpClient().execute(httpPost); + return new WeChatResponse(response); + } + + /*** + * 支持get请求的远程调用 + * @param apiPath api地址 + * @param params 在路径中请求的参数 + * @return 返回字符串 + */ + public WeChatResponse doGet(String apiPath, Map params) throws Exception { + URI uri = UrlBuilder.create() + .setHost(this.domain) + .setScheme("https") + .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8)) + .setQuery(UrlQuery.of(params)) + .setCharset(CharsetUtil.CHARSET_UTF_8) + .toURI(); + return this.doGet(uri); + } + + /*** + * 支持get请求的远程调用 + * @param apiPath api地址 + * @return 返回字符串 + */ + public WeChatResponse doGet(String apiPath) throws Exception { + URI uri = UrlBuilder.create() + .setHost(this.domain) + .setScheme("https") + .setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8)) + .setCharset(CharsetUtil.CHARSET_UTF_8) + .toURI(); + return this.doGet(uri); + } + + private WeChatResponse doGet(URI uri) throws Exception { + HttpGet httpGet = new HttpGet(uri); + httpGet.addHeader("Accept", "application/json"); + CloseableHttpResponse response = this.createHttpClient().execute(httpGet); + return new WeChatResponse(response); + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/bean/JsapiPayParam.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/bean/JsapiPayParam.java new file mode 100644 index 0000000..e9f4b9a --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/bean/JsapiPayParam.java @@ -0,0 +1,50 @@ +package com.jzo2o.trade.handler.wechat.bean; + +import cn.hutool.core.annotation.Alias; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JsapiPayParam { + + /** + * 由微信生成的应用ID,全局唯一。 + * 请求基础下单接口时请注意APPID的应用属性,例如公众号场景下,需使用应用属性为公众号的服务号APPID + */ + private String appId; + + /** + * 时间戳,标准北京时间,时区为东八区, + * 自1970年1月1日 0点0分0秒以来的秒数。 + * 注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。 + */ + private Long timeStamp; + + /** + * 随机字符串,不长于32位。 + */ + private String nonceStr; + + /** + * JSAPI下单接口返回的prepay_id参数值,提交格式如: + * prepay_id=wx201410272009395522657a690389285100 + */ + @Alias("package") + private String packages; + + /** + * 签名类型,默认为RSA,仅支持RSA。 + */ + private String signType = "RSA"; + + /** + * 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 + */ + private String paySign; + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/response/WeChatResponse.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/response/WeChatResponse.java new file mode 100644 index 0000000..0e53d70 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/handler/wechat/response/WeChatResponse.java @@ -0,0 +1,35 @@ +package com.jzo2o.trade.handler.wechat.response; + +import cn.hutool.core.util.CharsetUtil; +import lombok.Data; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; + +/** + * @author zzj + * @version 1.0 + */ +@Data +public class WeChatResponse { + + private int status; //响应状态码 + private String body; //响应体数据 + + public WeChatResponse() { + + } + + public WeChatResponse(CloseableHttpResponse response) { + this.status = response.getStatusLine().getStatusCode(); + try { + this.body = EntityUtils.toString(response.getEntity(), CharsetUtil.UTF_8); + } catch (Exception e) { + // 如果出现异常,响应体为null + } + } + + public Boolean isOk() { + return this.status == 200; + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/job/TradeJob.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/job/TradeJob.java new file mode 100644 index 0000000..f8a7d90 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/job/TradeJob.java @@ -0,0 +1,163 @@ +package com.jzo2o.trade.job; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.constants.MqConstants; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.model.msg.TradeStatusMsg; +import com.jzo2o.rabbitmq.client.RabbitClient; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.handler.HandlerFactory; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; +import com.jzo2o.trade.service.BasicPayService; +import com.jzo2o.trade.service.RefundRecordService; +import com.jzo2o.trade.service.TradingService; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +/** + * 交易任务,主要是查询订单的支付状态 和 退款的成功状态 + * + * @author zzj + * @version 1.0 + */ +@Slf4j +@Component +public class TradeJob { + + @Value("${jzo2o.job.trading.count:100}") + private Integer tradingCount; + @Value("${jzo2o.job.refund.count:100}") + private Integer refundCount; + @Resource + private TradingService tradingService; + @Resource + private RefundRecordService refundRecordService; + @Resource + private BasicPayService basicPayService; + @Resource + private RabbitClient rabbitClient; + + /** + * 分片广播方式查询支付状态 + * 逻辑:每次最多查询{tradingCount}个未完成的交易单,交易单id与shardTotal取模,值等于shardIndex进行处理 + */ + @XxlJob("tradingJob") + public void tradingJob() { + // 分片参数 + int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0); + int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1); + + List list = this.tradingService.findListByTradingState(TradingStateEnum.FKZ, tradingCount); + if (CollUtil.isEmpty(list)) { + XxlJobHelper.log("查询到交易单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal); + return; + } + + //定义消息通知列表,只要是状态不为【付款中】就需要通知其他系统 + List tradeMsgList = new ArrayList<>(); + for (Trading trading : list) { + if (trading.getTradingOrderNo() % shardTotal != shardIndex) { + continue; + } + try { + //查询交易单 + TradingDTO tradingDTO = this.basicPayService.queryTradingResult(trading.getTradingOrderNo()); + if (TradingStateEnum.FKZ != tradingDTO.getTradingState()) { + TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder() + .tradingOrderNo(trading.getTradingOrderNo()) + .productOrderNo(trading.getProductOrderNo()) + .productAppId(trading.getProductAppId()) + .transactionId(tradingDTO.getTransactionId()) + .tradingChannel(tradingDTO.getTradingChannel()) + .statusCode(tradingDTO.getTradingState().getCode()) + .statusName(tradingDTO.getTradingState().name()) + .info(tradingDTO.getMemo())//备注信息 + .build(); + tradeMsgList.add(tradeStatusMsg); + }else{ + //如果是未支付,需要判断下时间,超过20分钟未支付的订单需要关闭订单以及设置状态为QXDD + long between = LocalDateTimeUtil.between(trading.getCreateTime(), LocalDateTimeUtil.now(), ChronoUnit.MINUTES); + if (between >= 20) { + try { + basicPayService.closeTrading(trading.getTradingOrderNo()); + } catch (Exception e) { + log.error("超过20分钟未支付自动关单出现异常,交易单号:{}",trading.getTradingOrderNo()); + } + } + } + } catch (Exception e) { + XxlJobHelper.log("查询交易单出错!shardIndex = {}, shardTotal = {}, trading = {}", shardIndex, shardTotal, trading, e); + } + } + + if (CollUtil.isEmpty(tradeMsgList)) { + return; + } + + //发送消息通知其他系统 + String msg = JSONUtil.toJsonStr(tradeMsgList); + rabbitClient.sendMsg(MqConstants.Exchanges.TRADE, MqConstants.RoutingKeys.TRADE_UPDATE_STATUS, msg); + } + + /** + * 分片广播方式查询退款状态 + */ +// @XxlJob("refundJob") +// public void refundJob() { +// // 分片参数 +// int shardIndex = NumberUtil.max(XxlJobHelper.getShardIndex(), 0); +// int shardTotal = NumberUtil.max(XxlJobHelper.getShardTotal(), 1); +// +// List list = this.refundRecordService.findListByRefundStatus(RefundStatusEnum.SENDING, refundCount); +// if (CollUtil.isEmpty(list)) { +// XxlJobHelper.log("查询到退款单列表为空!shardIndex = {}, shardTotal = {}", shardIndex, shardTotal); +// return; +// } +// +// //定义消息通知列表,只要是状态不为【退款中】就需要通知其他系统 +// List tradeMsgList = new ArrayList<>(); +// +// for (RefundRecordEntity refundRecord : list) { +// if (refundRecord.getRefundNo() % shardTotal != shardIndex) { +// continue; +// } +// try { +// //查询退款单 +// RefundRecordDTO refundRecordDTO = this.basicPayService.queryRefundTrading(refundRecord.getRefundNo()); +// if (RefundStatusEnum.SENDING != refundRecordDTO.getRefundStatus()) { +// TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder() +// .tradingOrderNo(refundRecord.getTradingOrderNo()) +// .productOrderNo(refundRecord.getProductOrderNo()) +// .refundNo(refundRecord.getRefundNo()) +// .statusCode(refundRecord.getRefundStatus().getCode()) +// .statusName(refundRecord.getRefundStatus().name()) +// .build(); +// tradeMsgList.add(tradeStatusMsg); +// } +// } catch (Exception e) { +// XxlJobHelper.log("查询退款单出错!shardIndex = {}, shardTotal = {}, refundRecord = {}", shardIndex, shardTotal, refundRecord, e); +// } +// } +// +// if (CollUtil.isEmpty(tradeMsgList)) { +// return; +// } +// +// //发送消息通知其他系统 +// String msg = JSONUtil.toJsonStr(tradeMsgList); +// this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRADE, Constants.MQ.RoutingKeys.REFUND_UPDATE_STATUS, msg); +// } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/PayChannelMapper.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/PayChannelMapper.java new file mode 100644 index 0000000..e27514e --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/PayChannelMapper.java @@ -0,0 +1,13 @@ +package com.jzo2o.trade.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jzo2o.trade.model.domain.PayChannel; +import org.apache.ibatis.annotations.Mapper; + +/** + * 交易渠道表Mapper接口 + */ +@Mapper +public interface PayChannelMapper extends BaseMapper { + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/RefundRecordMapper.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/RefundRecordMapper.java new file mode 100644 index 0000000..fab7980 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/RefundRecordMapper.java @@ -0,0 +1,13 @@ +package com.jzo2o.trade.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jzo2o.trade.model.domain.RefundRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 退款记录表Mapper接口 + */ +@Mapper +public interface RefundRecordMapper extends BaseMapper { + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/TradingMapper.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/TradingMapper.java new file mode 100644 index 0000000..cb2e2a5 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/mapper/TradingMapper.java @@ -0,0 +1,13 @@ +package com.jzo2o.trade.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jzo2o.trade.model.domain.Trading; +import org.apache.ibatis.annotations.Mapper; + +/** + * 交易订单表Mapper接口 + */ +@Mapper +public interface TradingMapper extends BaseMapper { + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/PayChannel.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/PayChannel.java new file mode 100644 index 0000000..c31eb0f --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/PayChannel.java @@ -0,0 +1,96 @@ +package com.jzo2o.trade.model.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author itcast + * @Description:交易渠道表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("pay_channel") +public class PayChannel implements Serializable { + private static final long serialVersionUID = -1452774366739615656L; + + /** + * 主键 + */ + private Long id; + + /** + * 通道名称 + */ + private String channelName; + + /** + * 通道唯一标记 + */ + private String channelLabel; + + /** + * 域名 + */ + private String domain; + + /** + * 商户appid + */ + private String appId; + + /** + * 支付公钥 + */ + private String publicKey; + + /** + * 商户私钥 + */ + private String merchantPrivateKey; + + /** + * 其他配置 + */ + private String otherConfig; + + /** + * AES混淆密钥 + */ + private String encryptKey; + + /** + * 说明 + */ + private String remark; + + /** + * 回调地址 + */ + private String notifyUrl; + + /** + * 是否有效 + */ + protected String enableFlag; + + /** + * 商户号 + */ + private Long enterpriseId; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/RefundRecord.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/RefundRecord.java new file mode 100644 index 0000000..3fff2bf --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/RefundRecord.java @@ -0,0 +1,103 @@ +package com.jzo2o.trade.model.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.jzo2o.trade.enums.RefundStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author itcast + * @Description:退款记录表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("refund_record") +public class RefundRecord implements Serializable { + private static final long serialVersionUID = -3998253241655800061L; + + /** + * 主键 + */ + private Long id; + + /** + * 交易系统订单号【对于三方来说:商户订单】 + */ + private Long tradingOrderNo; + + /** + * 业务系统应用标识 + */ + private String productAppId; + + /** + * 业务系统订单号 + */ + private Long productOrderNo; + + /** + * 本次退款订单号 + */ + private Long refundNo; + + /** + * 第三方支付的退款单号 + */ + private String refundId; + + /** + * 商户号 + */ + private Long enterpriseId; + + /** + * 退款渠道【支付宝、微信、现金】 + */ + private String tradingChannel; + + /** + * 退款状态 + */ + private RefundStatusEnum refundStatus; + + /** + * 返回编码 + */ + private String refundCode; + + /** + * 返回信息 + */ + private String refundMsg; + + /** + * 备注【订单门店,桌台信息】 + */ + private String memo; + + /** + * 本次退款金额 + */ + private BigDecimal refundAmount; + + /** + * 原订单金额 + */ + private BigDecimal total; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/Trading.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/Trading.java new file mode 100644 index 0000000..d027592 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/domain/Trading.java @@ -0,0 +1,162 @@ +package com.jzo2o.trade.model.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.jzo2o.trade.enums.TradingStateEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author itcast + * @Description:交易订单表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("trading") +public class Trading implements Serializable { + private static final long serialVersionUID = -3427581867070559590L; + + /** + * 主键 + */ + private Long id; + + /** + * openId标识 + */ + private String openId; + + /** + * 业务系统应用标识 + */ + private String productAppId; + /** + * 业务系统订单号 + */ + private Long productOrderNo; + + /** + * 交易系统订单号【对于三方来说:商户订单】 + */ + private Long tradingOrderNo; + + /** + * 第三方支付的交易号 + */ + private String transactionId; + + /** + * 支付渠道【支付宝、微信、现金、免单挂账】 + */ + private String tradingChannel; + + /** + * 交易类型【付款、退款、免单、挂账】 + */ + private String tradingType; + + /** + * 交易单状态【DFK待付款,FKZ付款中,QXDD取消订单,YJS已结算,MD免单,GZ挂账】 + */ + private TradingStateEnum tradingState; + + /** + * 收款人姓名 + */ + private String payeeName; + + /** + * 收款人账户ID + */ + private Long payeeId; + + /** + * 付款人姓名 + */ + private String payerName; + + /** + * 付款人Id + */ + private Long payerId; + + /** + * 交易金额 + */ + private BigDecimal tradingAmount; + + /** + * 退款金额【付款后】 + */ + private BigDecimal refund; + + /** + * 是否有退款:YES,NO + */ + private String isRefund; + + /** + * 第三方交易返回编码【最终确认交易结果】 + */ + private String resultCode; + + /** + * 第三方交易返回提示消息【最终确认交易信息】 + */ + private String resultMsg; + + /** + * 第三方交易返回信息json【分析交易最终信息】 + */ + private String resultJson; + + /** + * 统一下单返回编码 + */ + private String placeOrderCode; + + /** + * 统一下单返回信息 + */ + private String placeOrderMsg; + + /** + * 统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】 + */ + private String placeOrderJson; + + /** + * 商户号 + */ + private Long enterpriseId; + + /** + * 备注【订单门店,桌台信息】 + */ + private String memo; + + /** + * 二维码base64数据 + */ + private String qrCode; + + /** + * 是否有效 + */ + protected String enableFlag; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/PayChannelDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/PayChannelDTO.java new file mode 100644 index 0000000..6ba8f2e --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/PayChannelDTO.java @@ -0,0 +1,55 @@ +package com.jzo2o.trade.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @ClassName PayChannelVo.java + * @Description 支付通道 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("支付通道对象") +public class PayChannelDTO { + + @ApiModelProperty(value = "主键") + private Long id; + @ApiModelProperty(value = "通道名称") + private String channelName; + @ApiModelProperty(value = "通道唯一标记") + private String channelLabel; + @ApiModelProperty(value = "域名") + private String domain; + @ApiModelProperty(value = "商户appid") + private String appId; + @ApiModelProperty(value = "公钥") + private String publicKey; + @ApiModelProperty(value = "商户私钥") + private String merchantPrivateKey; + @ApiModelProperty(value = "其他配置") + private String otherConfig; + @ApiModelProperty(value = "AES混淆密钥") + private String encryptKey; + @ApiModelProperty(value = "说明") + private String remark; + @ApiModelProperty(value = "选中节点") + private String[] checkedIds; + @ApiModelProperty(value = "回调地址") + private String notifyUrl; + @ApiModelProperty(value = "是否有效") + protected String enableFlag; + @ApiModelProperty(value = "商户号") + private Long enterpriseId; + @ApiModelProperty(value = "创建时间") + protected LocalDateTime createdTime; + @ApiModelProperty(value = "更新时间") + protected LocalDateTime updatedTime; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/RefundRecordDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/RefundRecordDTO.java new file mode 100644 index 0000000..699835e --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/RefundRecordDTO.java @@ -0,0 +1,53 @@ +package com.jzo2o.trade.model.dto; + +import com.jzo2o.trade.enums.RefundStatusEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @Description: + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("退款对象") +public class RefundRecordDTO { + + @ApiModelProperty(value = "主键") + private Long id; + @ApiModelProperty(value = "交易系统订单号【对于三方来说:商户订单】") + private Long tradingOrderNo; + @ApiModelProperty(value = "业务系统应用标识") + private String productAppId; + @ApiModelProperty(value = "业务系统订单号") + private Long productOrderNo; + @ApiModelProperty(value = "本次退款订单号") + private String refundNo; + @ApiModelProperty(value = "商户号") + private Long enterpriseId; + @ApiModelProperty(value = "退款渠道【支付宝、微信、现金】") + private String tradingChannel; + @ApiModelProperty(value = "退款状态") + private RefundStatusEnum refundStatus; + @ApiModelProperty(value = "返回编码") + private String refundCode; + @ApiModelProperty(value = "返回信息") + private String refundMsg; + @ApiModelProperty(value = "备注【订单门店,桌台信息】") + private String memo; + @ApiModelProperty(value = "原订单金额") + private BigDecimal total; + @ApiModelProperty(value = "创建时间") + protected LocalDateTime created; + @ApiModelProperty(value = "更新时间") + protected LocalDateTime updated; + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/TradingDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/TradingDTO.java new file mode 100644 index 0000000..3911868 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/TradingDTO.java @@ -0,0 +1,82 @@ +package com.jzo2o.trade.model.dto; + +import com.jzo2o.trade.enums.TradingStateEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("交易数据对象") +public class TradingDTO { + + @ApiModelProperty(value = "主键") + private Long id; + @ApiModelProperty(value = "openId标识") + private String openId; + @ApiModelProperty(value = "业务系统应用标识") + private String productAppId; + @ApiModelProperty(value = "业务系统订单号") + private Long productOrderNo; + @ApiModelProperty(value = "交易系统订单号【对于三方来说:商户订单】") + private Long tradingOrderNo; + + @ApiModelProperty(value = "第三方支付的交易号") + private String transactionId; + @ApiModelProperty(value = "支付渠道【支付宝、微信、现金、免单挂账】") + private String tradingChannel; + @ApiModelProperty(value = "交易类型【付款、退款、免单、挂账】") + private String tradingType; + @ApiModelProperty(value = "交易单状态【DFK待付款,FKZ付款中,QXDD取消订单,YJS已结算,MD免单,GZ挂账】") + private TradingStateEnum tradingState; + @ApiModelProperty(value = "收款人姓名") + private String payeeName; + @ApiModelProperty(value = "收款人账户ID") + private Long payeeId; + @ApiModelProperty(value = "付款人姓名") + private String payerName; + @ApiModelProperty(value = "付款人Id") + private Long payerId; + @ApiModelProperty(value = "交易金额,单位:元") + private BigDecimal tradingAmount; + @ApiModelProperty(value = "退款金额【付款后】,单位:元") + private BigDecimal refund; + @ApiModelProperty(value = "是否有退款:YES,NO") + private String isRefund; + @ApiModelProperty(value = "第三方交易返回编码【最终确认交易结果】") + private String resultCode; + @ApiModelProperty(value = "第三方交易返回提示消息【最终确认交易信息】") + private String resultMsg; + @ApiModelProperty(value = "第三方交易返回信息json【分析交易最终信息】") + private String resultJson; + @ApiModelProperty(value = "统一下单返回编码") + private String placeOrderCode; + @ApiModelProperty(value = "统一下单返回信息") + private String placeOrderMsg; + @ApiModelProperty(value = "统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】") + private String placeOrderJson; + @ApiModelProperty(value = "商户号") + private Long enterpriseId; + @ApiModelProperty(value = "备注,如:运费") + private String memo; + @ApiModelProperty(value = "二维码base64数据") + private String qrCode; + @ApiModelProperty(value = "是否有效") + protected String enableFlag; + @ApiModelProperty(value = "退款请求号") + private String outRequestNo; + @ApiModelProperty(value = "操作退款金额") + private BigDecimal operTionRefund; + @ApiModelProperty(value = "创建时间") + protected LocalDateTime created; + @ApiModelProperty(value = "更新时间") + protected LocalDateTime updated; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/request/JsapiPayReqDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/request/JsapiPayReqDTO.java new file mode 100644 index 0000000..77a7069 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/request/JsapiPayReqDTO.java @@ -0,0 +1,32 @@ +package com.jzo2o.trade.model.dto.request; + +import com.jzo2o.api.trade.enums.PayChannelEnum; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * jsapi提交支付请求对象 + * + * @author zzj + * @version 1.0 + */ +@Data +public class JsapiPayReqDTO { + + @ApiModelProperty(value = "openId标识", required = true) + private String openId; + @ApiModelProperty(value = "商户号", required = true) + private Long enterpriseId; + @ApiModelProperty(value = "业务系统应用标识", required = true) + private String productAppId; + @ApiModelProperty(value = "业务系统订单号", required = true) + private Long productOrderNo; + @ApiModelProperty(value = "支付渠道", required = true) + private PayChannelEnum tradingChannel; + @ApiModelProperty(value = "交易金额,单位:元", required = true) + private BigDecimal tradingAmount; + @ApiModelProperty(value = "备注,如:运费", required = true) + private String memo; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/JsapiPayResDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/JsapiPayResDTO.java new file mode 100644 index 0000000..74a620a --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/JsapiPayResDTO.java @@ -0,0 +1,26 @@ +package com.jzo2o.trade.model.dto.response; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 扫码支付响应数据 + * + * @author zzj + * @version 1.0 + */ +@Data +public class JsapiPayResDTO { + + @ApiModelProperty(value = "业务系统订单号") + private Long productOrderNo; + @ApiModelProperty(value = "交易系统订单号【对于三方来说:商户订单】") + private Long tradingOrderNo; + @ApiModelProperty(value = "支付渠道【支付宝、微信、现金、免单挂账】") + private String tradingChannel; + @ApiModelProperty(value = "统一下单返回信息,预支付编号") + private String placeOrderMsg; + @ApiModelProperty(value = "统一下单返回信息json【用于生产二维码、Android ios唤醒支付等】") + private String placeOrderJson; + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/TradingStateResDTO.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/TradingStateResDTO.java new file mode 100644 index 0000000..3f3fe56 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/model/dto/response/TradingStateResDTO.java @@ -0,0 +1,23 @@ +package com.jzo2o.trade.model.dto.response; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 交易单状态响应数据 + * + * @author itcast + * @create 2023/9/11 10:41 + **/ +@Data +@NoArgsConstructor +@AllArgsConstructor +@ApiModel("交易单状态响应数据") +public class TradingStateResDTO { + + @ApiModelProperty(value = "交易单状态,2:付款中,4:已结算") + private Integer tradingState; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/BasicPayService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/BasicPayService.java new file mode 100644 index 0000000..503fede --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/BasicPayService.java @@ -0,0 +1,68 @@ +package com.jzo2o.trade.service; + +import com.jzo2o.api.trade.dto.response.ExecutionResultResDTO; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.dto.RefundRecordDTO; +import com.jzo2o.trade.model.dto.TradingDTO; + +import java.math.BigDecimal; + +/** + * 支付的基础功能 + * + * @author zzj + * @version 1.0 + */ +public interface BasicPayService { + + /*** + * 统一收单线下交易查询 + * 该接口提供所有支付订单的查询,商户可以通过该接口主动查询订单状态,完成下一步的业务逻辑。 + * @param tradingOrderNo 交易单号 + * @return 交易数据对象 + */ + TradingDTO queryTradingResult(Long tradingOrderNo) throws CommonException; + + /*** + * 统一收单交易退款接口 + * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家, + * 将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 + * + * + * @param tradingOrderNo 交易单号 + * @param refundAmount 退款金额,不能大于总支付的总金额 + * @return 是否成功 + */ + RefundRecord refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws CommonException; + +// /** +// * 通过业务订单号进行退款,方便业务系统接入 +// * +// * @param tradingOrderNo 交易单号 +// * @param refundAmount 退款金额,不能大于总支付的总金额 +// * @return +// * @throws CommonException +// */ +// ExecutionResultResDTO refundTradingByTradingOrderNo(Long tradingOrderNo, BigDecimal refundAmount) throws CommonException; + + /*** + * 统一收单交易退款查询接口 + * @param refundNo 退款单号 + * @return 退款记录数据 + */ + RefundRecordDTO queryRefundTrading(Long refundNo) throws CommonException; + + /*** + * 对于退款中的记录需要同步退款状态 + * @param tradingOrderNo 交易单号 + */ + void syncRefundResult(Long tradingOrderNo) throws CommonException; + + /*** + * 关闭交易单 + * @param tradingOrderNo 交易单号 + * @return 是否成功 + */ + Boolean closeTrading(Long tradingOrderNo) throws CommonException; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/JsapiPayService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/JsapiPayService.java new file mode 100644 index 0000000..03c7a89 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/JsapiPayService.java @@ -0,0 +1,22 @@ +package com.jzo2o.trade.service; + +import com.jzo2o.trade.model.domain.Trading; + +/** + * jsapi支付、微信小程序支付 + * + * @author zzj + * @version 1.0 + */ +public interface JsapiPayService { + + /*** + * 统一jsapi交易预创建 + * 商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、 + * JSAPI、APP等不同场景生成交易串调起支付。 + * @param tradingEntity 交易单 + * + * @return 交易单,支付串码 + */ + Trading createJsapiTrading(Trading tradingEntity); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NativePayService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NativePayService.java new file mode 100644 index 0000000..7b7cbe8 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NativePayService.java @@ -0,0 +1,19 @@ +package com.jzo2o.trade.service; + +import com.jzo2o.api.trade.dto.response.NativePayResDTO; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.model.domain.Trading; + +/** + * 二维码支付 + */ +public interface NativePayService { + /*** + * 扫码支付,收银员通过收银台或商户后台调用此接口,生成二维码后,展示给用户,由用户扫描二维码完成订单支付。 + * + * @param changeChannel 是否切换二维码 + * @param tradingEntity 扫码支付提交参数 + * @return 交易数据 + */ + Trading createDownLineTrading(boolean changeChannel, Trading tradingEntity); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NotifyService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NotifyService.java new file mode 100644 index 0000000..9812837 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/NotifyService.java @@ -0,0 +1,34 @@ +package com.jzo2o.trade.service; + +import com.jzo2o.common.expcetions.CommonException; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; + +import javax.servlet.http.HttpServletRequest; + +/** + * 支付通知 + * + * @author zzj + * @version 1.0 + */ +public interface NotifyService { + + + /** + * 微信支付通知,官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml + * + * @param request 微信请求对象 + * @param enterpriseId 商户id + * @throws CommonException 抛出异常,通过异常决定是否响应200 + */ + void wxPayNotify(NotificationRequest request, Long enterpriseId) throws CommonException; + + /** + * 支付宝支付通知,官方文档:https://opendocs.alipay.com/open/194/103296?ref=api + * + * @param request 请求对象 + * @param enterpriseId 商户id + * @throws CommonException 抛出异常,通过异常决定是否响应200 + */ + void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws CommonException; +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/PayChannelService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/PayChannelService.java new file mode 100644 index 0000000..3f7fa43 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/PayChannelService.java @@ -0,0 +1,60 @@ +package com.jzo2o.trade.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.jzo2o.trade.model.domain.PayChannel; +import com.jzo2o.trade.model.dto.PayChannelDTO; + +import java.util.List; + +/** + * @Description: 支付通道服务类 + */ +public interface PayChannelService extends IService { + + /** + * @param payChannelDTO 查询条件 + * @param pageNum 当前页 + * @param pageSize 当前页 + * @return Page 分页对象 + * @Description 支付通道列表 + */ + Page findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize); + + /** + * 根据商户id查询渠道配置,该配置会被缓存10分钟 + * + * @param enterpriseId 商户id + * @param channelLabel 通道唯一标记 + * @return PayChannelEntity 交易渠道对象 + */ + PayChannel findByEnterpriseId(Long enterpriseId, String channelLabel); + + /** + * @param payChannelDTO 对象信息 + * @return PayChannelEntity 交易渠道对象 + * @Description 创建支付通道 + */ + PayChannel createPayChannel(PayChannelDTO payChannelDTO); + + /** + * @param payChannelDTO 对象信息 + * @return Boolean 是否成功 + * @Description 修改支付通道 + */ + Boolean updatePayChannel(PayChannelDTO payChannelDTO); + + /** + * @param checkedIds 选择的支付通道ID + * @return Boolean 是否成功 + * @Description 删除支付通道 + */ + Boolean deletePayChannel(String[] checkedIds); + + /** + * @param channelLabel 支付通道标识 + * @return 支付通道列表 + * @Description 查找渠道标识 + */ + List findPayChannelList(String channelLabel); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/QRCodeService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/QRCodeService.java new file mode 100644 index 0000000..d962fa5 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/QRCodeService.java @@ -0,0 +1,23 @@ +package com.jzo2o.trade.service; + +import com.jzo2o.api.trade.enums.PayChannelEnum; + +public interface QRCodeService { + + /** + * 生成二维码 + * + * @param content 二维码中的内容 + * @return 图片base64数据 + */ + String generate(String content); + + /** + * 生成二维码,带logo + * + * @param content 二维码中的内容 + * @param payChannel 付款渠道 + * @return 图片base64数据 + */ + String generate(String content, PayChannelEnum payChannel); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/RefundRecordService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/RefundRecordService.java new file mode 100644 index 0000000..2ae570f --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/RefundRecordService.java @@ -0,0 +1,47 @@ +package com.jzo2o.trade.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.model.domain.RefundRecord; + +import java.util.List; + +/** + * @Description: 退款记录表服务类 + */ +public interface RefundRecordService extends IService { + + /** + * 根据退款单号查询退款记录 + * + * @param refundNo 退款单号 + * @return 退款记录数据 + */ + RefundRecord findByRefundNo(Long refundNo); + + /** + * 根据交易单号查询退款单 + * + * @param tradingOrderNo 交易单号 + * @return 退款列表 + */ + List findByTradingOrderNo(Long tradingOrderNo); + + /** + * 根据订单号查询退款列表 + * + * @param productAppId 业务系统标识 + * @param productOrderNo 订单号 + * @return 退款列表 + */ + List findListByProductOrderNo(String productAppId,Long productOrderNo); + + /*** + * 按状态查询退款单,按照时间正序排序 + * + * @param refundStatus 状态 + * @param count 查询数量,默认查询10条 + * @return 退款单数据列表 + */ + List findListByRefundStatus(RefundStatusEnum refundStatus, Integer count); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/TradingService.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/TradingService.java new file mode 100644 index 0000000..5fe2755 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/TradingService.java @@ -0,0 +1,60 @@ +package com.jzo2o.trade.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; + +import java.util.List; + +/** + * @Description:交易订单表 服务类 + */ +public interface TradingService extends IService { + + /*** + * 按交易单号查询交易单 + * + * @param tradingOrderNo 交易单号 + * @return 交易单数据 + */ + Trading findTradByTradingOrderNo(Long tradingOrderNo); + + + + /*** + * 按交易状态查询交易单,按照时间正序排序 + * @param tradingState 状态 + * @param count 查询数量,默认查询10条 + * @return 交易单数据列表 + */ + List findListByTradingState(TradingStateEnum tradingState, Integer count); + + + /** + * 根据订单id和支付方式查询付款中的交易单 + * @param productAppId 业务系统标识 + * @param productOrderNo 订单号 + * @param tradingChannel 支付渠道代码 + * @return 交易单 + */ + Trading queryDuringTrading(String productAppId,Long productOrderNo, String tradingChannel); + + /** + * 根据订单id查询交易单 + * + * @param productAppId 业务系统标识 + * @param productOrderNo 订单号 + * @return 交易单 + */ + List queryByProductOrder(String productAppId,Long productOrderNo); + + /** + * 根据订单id查询已付款的交易单 + * @param productAppId 业务系统标识 + * @param productOrderNo 订单id + * @return 交易单 + */ + Trading findFinishedTrading(String productAppId,Long productOrderNo); +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/BasicPayServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/BasicPayServiceImpl.java new file mode 100644 index 0000000..6cd442d --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/BasicPayServiceImpl.java @@ -0,0 +1,286 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +import com.jzo2o.api.trade.dto.response.ExecutionResultResDTO; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.constant.Constants; +import com.jzo2o.trade.constant.TradingCacheConstant; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.handler.BeforePayHandler; +import com.jzo2o.trade.handler.HandlerFactory; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.RefundRecordDTO; +import com.jzo2o.trade.model.dto.TradingDTO; +import com.jzo2o.trade.service.BasicPayService; +import com.jzo2o.trade.service.RefundRecordService; +import com.jzo2o.trade.service.TradingService; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 支付的基础功能 + */ +@Slf4j +@Service +public class BasicPayServiceImpl implements BasicPayService { + + @Resource + private BeforePayHandler beforePayHandler; + @Resource + private RedissonClient redissonClient; + @Resource + private TradingService tradingService; + @Resource + private RefundRecordService refundRecordService; + @Resource + private IdentifierGenerator identifierGenerator; + + @Override + public TradingDTO queryTradingResult(Long tradingOrderNo) throws CommonException { + //通过单号查询交易单数据 + Trading trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo); + if(ObjectUtil.isNull(trading)){ + return null; + } + //如果已付款或已取消直接返回 + if(StrUtil.equalsAny(trading.getTradingState().getValue(),TradingStateEnum.YJS.getValue(),TradingStateEnum.QXDD.getValue())){ + return BeanUtil.toBean(trading,TradingDTO.class); + } + //查询前置处理:检测交易单参数 + this.beforePayHandler.checkQueryTrading(trading); + //支付状态 + TradingStateEnum tradingState = trading.getTradingState(); + //如果支付成功或支付取消就直接返回 + if (ObjectUtil.equal(tradingState, TradingStateEnum.YJS) || ObjectUtil.equal(tradingState, TradingStateEnum.QXDD)) { + return BeanUtil.toBean(trading, TradingDTO.class); + } + String key = TradingCacheConstant.QUERY_PAY + tradingOrderNo; + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + //选取不同的支付渠道实现 + BasicPayHandler handler = HandlerFactory.get(trading.getTradingChannel(), BasicPayHandler.class); + Boolean result = handler.queryTrading(trading); + if (result) { + //如果交易单已经完成,需要将二维码数据删除,节省数据库空间,如果有需要可以再次生成 + if (ObjectUtil.equal(trading.getTradingState(), TradingStateEnum.YJS) || ObjectUtil.equal(trading.getTradingState(), TradingStateEnum.QXDD)) { + trading.setQrCode(""); + } + //更新数据 + this.tradingService.saveOrUpdate(trading); + } + return BeanUtil.toBean(trading, TradingDTO.class); + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("查询交易单数据异常: trading = {}", trading, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } finally { + lock.unlock(); + } + } + +// @Override +// @Transactional +// public ExecutionResultResDTO refundTradingByTradingOrderNo(Long tradingOrderNo, BigDecimal refundAmount) throws CommonException { +// //根据业务订单号查看交易单信息 +// Trading trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo); +// if(ObjectUtil.isEmpty(trading)){ +// throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NOT_FOUND.getValue()); +// } +// //只有已付款的交易单方可退款 +// if(ObjectUtil.notEqual(TradingStateEnum.YJS,trading.getTradingState())){ +// throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.REFUND_FAIL.getValue()); +// } +// +// ExecutionResultResDTO executionResultResDTO = refundTrading(trading.getTradingOrderNo(), refundAmount); +// return executionResultResDTO; +// } + + @Override + @Transactional + public RefundRecord refundTrading(Long tradingOrderNo, BigDecimal refundAmount) throws CommonException { + //通过单号查询交易单数据 + Trading trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo); + //入库前置检查 + this.beforePayHandler.checkRefundTrading(trading,refundAmount); + String key = TradingCacheConstant.REFUND_PAY + trading.getTradingOrderNo(); + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + + //对于退款中的记录需要同步退款状态 + syncRefundResult(tradingOrderNo); + //查询退款记录 + List refundRecordList = this.refundRecordService.findByTradingOrderNo(trading.getTradingOrderNo()); + //取出退款成功或退款中的记录 + List collect = refundRecordList.stream().filter(r -> StrUtil.equalsAny(r.getRefundStatus().getValue(),RefundStatusEnum.SENDING.getValue(),RefundStatusEnum.SUCCESS.getValue())).collect(Collectors.toList()); + //当没有退款成功和退款中的记录时方可继续退款 + if(ObjectUtil.isEmpty(collect)){ + //设置退款金额 + trading.setRefund(refundAmount); + + RefundRecord refundRecord = new RefundRecord(); + //退款单号 + refundRecord.setRefundNo(Convert.toLong(this.identifierGenerator.nextId(refundRecord))); + refundRecord.setTradingOrderNo(trading.getTradingOrderNo()); + refundRecord.setProductOrderNo(trading.getProductOrderNo()); + refundRecord.setProductAppId(trading.getProductAppId()); + refundRecord.setRefundAmount(refundAmount); + refundRecord.setEnterpriseId(trading.getEnterpriseId()); + refundRecord.setTradingChannel(trading.getTradingChannel()); + refundRecord.setTotal(trading.getTradingAmount()); + //初始状态为退款中 + refundRecord.setRefundStatus(RefundStatusEnum.APPLY_REFUND); + this.refundRecordService.save(refundRecord); + //设置交易单是退款订单 + trading.setIsRefund(Constants.YES); + this.tradingService.saveOrUpdate(trading); + + //请求第三方退款 + //选取不同的支付渠道实现 + BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class); + Boolean result = handler.refundTrading(refundRecord); + if (result) { + //更新退款记录数据 + this.refundRecordService.saveOrUpdate(refundRecord); + } + return refundRecord; + } + //取出第一条记录返回 + RefundRecord first = CollectionUtil.getFirst(refundRecordList); + if(ObjectUtil.isNotNull(first)){ + return first; + } + + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("查询交易单数据异常:{}", ExceptionUtil.stacktraceToString(e)); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } finally { + lock.unlock(); + } + } + + /*** + * 对于退款中的记录需要同步退款状态 + * @param tradingOrderNo 交易单号 + */ + @Override + public void syncRefundResult(Long tradingOrderNo) throws CommonException{ + //查询退款记录 + List refundRecordList = this.refundRecordService.findByTradingOrderNo(tradingOrderNo); + //存在退款中记录 + List collect = refundRecordList.stream().filter(r -> r.getRefundStatus().equals(RefundStatusEnum.SENDING)).collect(Collectors.toList()); + + if (ObjectUtil.isNotEmpty(collect)) { + collect.forEach(v->{ + queryRefundTrading(v.getRefundNo()); + }); + } + + } + @Override + public RefundRecordDTO queryRefundTrading(Long refundNo) throws CommonException { + //通过单号查询交易单数据 + RefundRecord refundRecord = this.refundRecordService.findByRefundNo(refundNo); + //查询前置处理 + this.beforePayHandler.checkQueryRefundTrading(refundRecord); + + String key = TradingCacheConstant.REFUND_QUERY_PAY + refundNo; + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + + //选取不同的支付渠道实现 + BasicPayHandler handler = HandlerFactory.get(refundRecord.getTradingChannel(), BasicPayHandler.class); + Boolean result = handler.queryRefundTrading(refundRecord); + if (result) { + //更新数据 + this.refundRecordService.saveOrUpdate(refundRecord); + } + return BeanUtil.toBean(refundRecord, RefundRecordDTO.class); + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.REFUND_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("查询退款交易单数据异常: refundRecord = {}", refundRecord, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.REFUND_FAIL.getValue()); + } finally { + lock.unlock(); + } + } + + /*** + * 关闭交易单 + * @param tradingOrderNo 交易单号 + * @return 是否成功 + */ + @Override + public Boolean closeTrading(Long tradingOrderNo) throws CommonException { + //通过单号查询交易单数据 + Trading trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo); + if (ObjectUtil.isEmpty(trading)) { + return true; + } + + //入库前置检查 + this.beforePayHandler.checkCloseTrading(trading); + + String key = TradingCacheConstant.CLOSE_PAY + trading.getTradingOrderNo(); + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + + //选取不同的支付渠道实现 + BasicPayHandler handler = HandlerFactory.get(trading.getTradingChannel(), BasicPayHandler.class); + Boolean result = handler.closeTrading(trading); + if (result) { + trading.setQrCode(""); + this.tradingService.saveOrUpdate(trading); + } + return true; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("查询交易单数据异常:{}", ExceptionUtil.stacktraceToString(e)); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_QUERY_FAIL.getValue()); + } finally { + lock.unlock(); + } + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/JsapiPayServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/JsapiPayServiceImpl.java new file mode 100644 index 0000000..5b6c813 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/JsapiPayServiceImpl.java @@ -0,0 +1,84 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.constant.Constants; +import com.jzo2o.trade.constant.TradingCacheConstant; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.handler.BeforePayHandler; +import com.jzo2o.trade.handler.HandlerFactory; +import com.jzo2o.trade.handler.JsapiPayHandler; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.service.JsapiPayService; +import com.jzo2o.trade.service.TradingService; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * @author itcast + */ +@Slf4j +@Service +public class JsapiPayServiceImpl implements JsapiPayService { + + @Resource + private RedissonClient redissonClient; + @Resource + private TradingService tradingService; + @Resource + private BeforePayHandler beforePayHandler; + + @Override + public Trading createJsapiTrading(Trading tradingEntity) { + //获取付款中的记录 + Trading trading = tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel()); + //付款中的记录直接返回 + if (ObjectUtil.isNotNull(trading)){ + return trading; + } + //交易前置处理:检测交易单参数 + beforePayHandler.checkCreateTrading(tradingEntity); + tradingEntity.setEnableFlag(Constants.YES); + tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK); + + //对交易订单加锁 + Long productOrderNo = tradingEntity.getProductOrderNo(); + String key = TradingCacheConstant.CREATE_PAY + productOrderNo; + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + + //交易前置处理:幂等性处理 +// this.beforePayHandler.idempotentCreateTrading(tradingEntity); + + //调用不同的支付渠道进行处理 + JsapiPayHandler jsapiPayHandler = HandlerFactory.get(tradingEntity.getTradingChannel(), JsapiPayHandler.class); + jsapiPayHandler.createJsapiTrading(tradingEntity); + + //新增或更新交易数据 + boolean flag = this.tradingService.saveOrUpdate(tradingEntity); + if (!flag) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.SAVE_OR_UPDATE_FAIL.getValue()); + } + + return tradingEntity; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("Jsapi预创建异常: tradingDTO = {}", tradingEntity, e); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } finally { + lock.unlock(); + } + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NativePayServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NativePayServiceImpl.java new file mode 100644 index 0000000..9aa87fd --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NativePayServiceImpl.java @@ -0,0 +1,132 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.jzo2o.api.trade.dto.response.NativePayResDTO; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.trade.constant.Constants; +import com.jzo2o.trade.constant.TradingCacheConstant; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.trade.enums.TradingEnum; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.BasicPayHandler; +import com.jzo2o.trade.handler.BeforePayHandler; +import com.jzo2o.trade.handler.HandlerFactory; +import com.jzo2o.trade.handler.NativePayHandler; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.service.BasicPayService; +import com.jzo2o.trade.service.NativePayService; +import com.jzo2o.trade.service.QRCodeService; +import com.jzo2o.trade.service.TradingService; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Native支付方式Face接口:商户生成二维码,用户扫描支付 + * + * @author itcast + */ +@Service +@Slf4j +public class NativePayServiceImpl implements NativePayService { + + @Resource + private RedissonClient redissonClient; + @Resource + private TradingService tradingService; + @Resource + private BeforePayHandler beforePayHandler; + @Resource + private QRCodeService qrCodeService; + @Resource + private BasicPayService basicPayService; + /** + * 切换支付渠道自动关单 + * @param tradingChannel 要切换的目标支付渠道 + * @param productAppId 业务系统标识 + * @param productOrderNo 业务订单号 + */ + private void changeChannelAndCloseTrading(String productAppId,Long productOrderNo,String tradingChannel){ + //根据订单号查询交易单 + List yjsTradByProductOrderNo = tradingService.queryByProductOrder(productAppId,productOrderNo); + + yjsTradByProductOrderNo.forEach(v->{ + //与目标支付渠道不同的渠道且支付中的进行关单 + if(ObjectUtil.notEqual(v.getTradingChannel(),tradingChannel) && + v.getTradingState().equals(TradingStateEnum.FKZ)){ + //关单 + Boolean result = basicPayService.closeTrading(v.getTradingOrderNo()); + log.info("业务系统:{},业务订单:{},切换交易订单:{}的支付渠道为:{},关闭其它支付渠道:{}",productAppId,productOrderNo,v.getTradingOrderNo(),tradingChannel,v.getTradingChannel()); + } + }); + + } + + @Override + public Trading createDownLineTrading(boolean changeChannel,Trading tradingEntity) { + //获取付款中的记录 + Trading trading = tradingService.queryDuringTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(), tradingEntity.getTradingChannel()); + //如果切换二维码需要查询其它支付渠道付款中的交易单进行退款操作 + if(changeChannel){ + changeChannelAndCloseTrading(tradingEntity.getProductAppId(),tradingEntity.getProductOrderNo(),tradingEntity.getTradingChannel()); + } + //付款中的记录直接返回无需生成二维码 + if (ObjectUtil.isNotNull(trading)){ + return trading; + } + //交易前置处理:检测交易单参数 + beforePayHandler.checkCreateTrading(tradingEntity); + + tradingEntity.setTradingType(TradingConstant.TRADING_TYPE_FK); + tradingEntity.setEnableFlag(Constants.YES); + + //对交易订单加锁 + Long productOrderNo = tradingEntity.getProductOrderNo(); + String key = TradingCacheConstant.CREATE_PAY + productOrderNo; + RLock lock = redissonClient.getFairLock(key); + try { + //获取锁 + if (lock.tryLock(TradingCacheConstant.REDIS_WAIT_TIME, TimeUnit.SECONDS)) { + //交易前置处理:幂等性处理 +// this.beforePayHandler.idempotentCreateTrading(tradingEntity); + + //调用不同的支付渠道进行处理 + PayChannelEnum payChannel = PayChannelEnum.valueOf(tradingEntity.getTradingChannel()); + NativePayHandler nativePayHandler = HandlerFactory.get(payChannel, NativePayHandler.class); + nativePayHandler.createDownLineTrading(tradingEntity); + + //生成统一收款二维码 + String placeOrderMsg = tradingEntity.getPlaceOrderMsg(); + String qrCode = this.qrCodeService.generate(placeOrderMsg, payChannel); + tradingEntity.setQrCode(qrCode); + //指定交易状态为付款中 + tradingEntity.setTradingState(TradingStateEnum.FKZ); + //新增交易数据 + boolean flag = this.tradingService.save(tradingEntity); + if (!flag) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.SAVE_OR_UPDATE_FAIL.getValue()); + } + + return tradingEntity; + } + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } catch (CommonException e) { + throw e; + } catch (Exception e) { + log.error("统一收单线下交易预创建异常:{}", ExceptionUtil.stacktraceToString(e)); + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, TradingEnum.NATIVE_PAY_FAIL.getValue()); + } finally { + lock.unlock(); + } + } + +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NotifyServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NotifyServiceImpl.java new file mode 100644 index 0000000..8941fcf --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/NotifyServiceImpl.java @@ -0,0 +1,188 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alipay.easysdk.factory.Factory; +import com.alipay.easysdk.kernel.Config; +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.constants.MqConstants; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.model.msg.TradeStatusMsg; +import com.jzo2o.rabbitmq.client.RabbitClient; +import com.jzo2o.trade.constant.TradingCacheConstant; +import com.jzo2o.trade.constant.TradingConstant; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.handler.alipay.AlipayConfig; +import com.jzo2o.trade.handler.wechat.WechatPayHttpClient; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.service.NotifyService; +import com.jzo2o.trade.service.TradingService; +import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; +import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; +import com.wechat.pay.contrib.apache.httpclient.notification.Notification; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 支付成功的通知处理 + * + * @author itcast + */ +@Slf4j +@Service +public class NotifyServiceImpl implements NotifyService { + + @Resource + private TradingService tradingService; + @Resource + private RedissonClient redissonClient; + // @Resource +// private MQFeign mqFeign; + @Resource + private RabbitClient rabbitClient; + + @Override + public void wxPayNotify(NotificationRequest request, Long enterpriseId) throws CommonException { + // 查询配置 + WechatPayHttpClient client = WechatPayHttpClient.get(enterpriseId); + + JSONObject jsonData; + + //验证签名,确保请求来自微信 + try { + //确保在管理器中存在自动更新的商户证书 + client.createHttpClient(); + + CertificatesManager certificatesManager = CertificatesManager.getInstance(); + Verifier verifier = certificatesManager.getVerifier(client.getMchId()); + + //验签和解析请求数据 + NotificationHandler notificationHandler = new NotificationHandler(verifier, client.getApiV3Key().getBytes(StandardCharsets.UTF_8)); + Notification notification = notificationHandler.parse(request); + //通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS,只处理此类通知 + if (!StrUtil.equals("TRANSACTION.SUCCESS", notification.getEventType())) { + return; + } + + //获取解密后的数据 + jsonData = JSONUtil.parseObj(notification.getDecryptData()); + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, "验签失败"); + } + + //交易状态 + String tradeStateFromWX = jsonData.getStr("trade_state"); + /** + 交易状态,枚举值: + SUCCESS:支付成功 + REFUND:转入退款 + NOTPAY:未支付 + CLOSED:已关闭 + REVOKED:已撤销(付款码支付) + USERPAYING:用户支付中(付款码支付) + PAYERROR:支付失败(其他原因,如银行返回失败) + */ + String tradeStatus = TradingStateEnum.FKZ.getValue(); + if (StrUtil.equalsAny(tradeStateFromWX, TradingConstant.WECHAT_TRADE_CLOSED, TradingConstant.WECHAT_TRADE_REVOKED)) { + tradeStatus = TradingStateEnum.QXDD.getValue(); + //支付成功或转入退款的更新为已付款 + } else if (StrUtil.equalsAny(tradeStateFromWX, TradingConstant.WECHAT_TRADE_SUCCESS, TradingConstant.WECHAT_TRADE_REFUND)) { + tradeStatus = TradingStateEnum.YJS.getValue(); + } else if (StrUtil.equalsAny(tradeStateFromWX, TradingConstant.WECHAT_TRADE_PAYERROR)) { + tradeStatus = TradingStateEnum.FKSB.getValue(); + } + + //交易单号 + Long tradingOrderNo = jsonData.getLong("out_trade_no"); + log.info("微信支付通知:tradingOrderNo = {}, data = {}", tradingOrderNo, jsonData); + + //更新交易单 + this.updateTrading(tradingOrderNo, jsonData.getStr("transaction_id"),tradeStatus, jsonData.getStr("trade_state_desc"), jsonData.toString()); + } + + private void updateTrading(Long tradingOrderNo, String transactionId,String tradeStatus, String resultMsg, String resultJson) { + try { + Trading trading = this.tradingService.findTradByTradingOrderNo(tradingOrderNo); + trading.setTradingState(TradingStateEnum.valueOf(tradeStatus)); + //清空二维码数据 + trading.setQrCode(""); + trading.setTransactionId(transactionId); + trading.setResultMsg(resultMsg); + trading.setResultJson(resultJson); + this.tradingService.saveOrUpdate(trading); + + // 发消息通知其他系统 + TradeStatusMsg tradeStatusMsg = TradeStatusMsg.builder() + .tradingOrderNo(trading.getTradingOrderNo()) + .productOrderNo(trading.getProductOrderNo()) + .productAppId(trading.getProductAppId()) + .transactionId(trading.getTransactionId()) + .tradingChannel(trading.getTradingChannel()) + .statusCode(TradingStateEnum.YJS.getCode()) + .statusName(TradingStateEnum.YJS.name()) + .info(trading.getMemo())//备注信息 + .build(); + + String msg = JSONUtil.toJsonStr(Collections.singletonList(tradeStatusMsg)); + rabbitClient.sendMsg(MqConstants.Exchanges.TRADE, MqConstants.RoutingKeys.TRADE_UPDATE_STATUS, msg); + return; + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, "处理业务失败"); + } + } + + @Override + public void aliPayNotify(HttpServletRequest request, Long enterpriseId) throws CommonException { + //获取参数 + Map parameterMap = request.getParameterMap(); + Map param = new HashMap<>(); + for (Map.Entry entry : parameterMap.entrySet()) { + param.put(entry.getKey(), StrUtil.join(",", entry.getValue())); + } + + String tradeStatusFromAli = param.get("trade_status"); + String tradeStatus = TradingStateEnum.FKZ.getValue(); + if (StrUtil.equals(TradingConstant.ALI_TRADE_CLOSED, tradeStatusFromAli)) { + //支付取消:TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款) + tradeStatus = TradingStateEnum.QXDD.getValue(); + } else if (StrUtil.equalsAny(tradeStatusFromAli, TradingConstant.ALI_TRADE_SUCCESS, TradingConstant.ALI_TRADE_FINISHED)) { + // TRADE_SUCCESS(交易支付成功) + // TRADE_FINISHED(交易结束,不可退款) + tradeStatus = TradingStateEnum.YJS.getValue(); + } + + //查询配置 + Config config = AlipayConfig.getConfig(enterpriseId); + Factory.setOptions(config); + try { + Boolean result = Factory + .Payment + .Common().verifyNotify(param); + if (!result) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, "验签失败"); + } + } catch (Exception e) { + throw new CommonException(ErrorInfo.Code.TRADE_FAILED, "验签失败"); + } + + //获取交易单号 + Long tradingOrderNo = Convert.toLong(param.get("out_trade_no")); + String transactionId = param.get("trade_no"); + //更新交易单 + this.updateTrading(tradingOrderNo, transactionId, tradeStatus,"", JSONUtil.toJsonStr(param)); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/PayChannelServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/PayChannelServiceImpl.java new file mode 100644 index 0000000..014c1a0 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/PayChannelServiceImpl.java @@ -0,0 +1,78 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jzo2o.trade.constant.Constants; +import com.jzo2o.trade.mapper.PayChannelMapper; +import com.jzo2o.trade.model.domain.PayChannel; +import com.jzo2o.trade.model.dto.PayChannelDTO; +import com.jzo2o.trade.service.PayChannelService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * @author itcast + * @Description: 服务实现类 + */ +@Service +public class PayChannelServiceImpl extends ServiceImpl implements PayChannelService { + + @Override + public Page findPayChannelPage(PayChannelDTO payChannelDTO, int pageNum, int pageSize) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + //设置条件 + queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getChannelLabel()), PayChannel::getChannelLabel, payChannelDTO.getChannelLabel()); + queryWrapper.likeRight(StrUtil.isNotEmpty(payChannelDTO.getChannelName()), PayChannel::getChannelName, payChannelDTO.getChannelName()); + queryWrapper.eq(StrUtil.isNotEmpty(payChannelDTO.getEnableFlag()), PayChannel::getEnableFlag, payChannelDTO.getEnableFlag()); + //设置排序 + queryWrapper.orderByAsc(PayChannel::getCreateTime); + + return super.page(page, queryWrapper); + } + + @Override + public PayChannel findByEnterpriseId(Long enterpriseId, String channelLabel) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(PayChannel::getEnterpriseId, enterpriseId) + .eq(PayChannel::getChannelLabel, channelLabel) + .eq(PayChannel::getEnableFlag, Constants.YES); + return super.getOne(queryWrapper); + } + + @Override + public PayChannel createPayChannel(PayChannelDTO payChannelDTO) { + PayChannel payChannel = BeanUtil.toBean(payChannelDTO, PayChannel.class); + boolean flag = super.save(payChannel); + if (flag) { + return payChannel; + } + return null; + } + + @Override + public Boolean updatePayChannel(PayChannelDTO payChannelDTO) { + PayChannel payChannel = BeanUtil.toBean(payChannelDTO, PayChannel.class); + return super.updateById(payChannel); + } + + @Override + public Boolean deletePayChannel(String[] checkedIds) { + List ids = Arrays.asList(checkedIds); + return super.removeByIds(ids); + } + + @Override + public List findPayChannelList(String channelLabel) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(PayChannel::getChannelLabel, channelLabel) + .eq(PayChannel::getEnableFlag, Constants.YES); + return list(queryWrapper); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/QRCodeServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/QRCodeServiceImpl.java new file mode 100644 index 0000000..ce0a366 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/QRCodeServiceImpl.java @@ -0,0 +1,51 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.qrcode.QrCodeUtil; +import cn.hutool.extra.qrcode.QrConfig; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.jzo2o.trade.config.QRCodeConfig; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.service.QRCodeService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @author itcast + */ +@Service +public class QRCodeServiceImpl implements QRCodeService { + + @Resource + private QRCodeConfig qrCodeConfig; + + @Override + public String generate(String content, PayChannelEnum payChannel) { + QrConfig qrConfig = new QrConfig(); + //设置边距 + qrConfig.setMargin(this.qrCodeConfig.getMargin()); + //二维码颜色 + qrConfig.setForeColor(HexUtil.decodeColor(this.qrCodeConfig.getForeColor())); + //设置背景色 + qrConfig.setBackColor(HexUtil.decodeColor(this.qrCodeConfig.getBackColor())); + //纠错级别 + qrConfig.setErrorCorrection(ErrorCorrectionLevel.valueOf(this.qrCodeConfig.getErrorCorrectionLevel())); + //设置宽 + qrConfig.setWidth(this.qrCodeConfig.getWidth()); + //设置高 + qrConfig.setHeight(this.qrCodeConfig.getHeight()); + if (ObjectUtil.isNotEmpty(payChannel)) { + //设置logo + qrConfig.setImg(this.qrCodeConfig.getLogo(payChannel)); + } + return QrCodeUtil.generateAsBase64(content, qrConfig, ImgUtil.IMAGE_TYPE_PNG); + } + + @Override + public String generate(String content) { + return generate(content, null); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/RefundRecordServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/RefundRecordServiceImpl.java new file mode 100644 index 0000000..3b26ab7 --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/RefundRecordServiceImpl.java @@ -0,0 +1,54 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.util.NumberUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jzo2o.trade.enums.RefundStatusEnum; +import com.jzo2o.trade.mapper.RefundRecordMapper; +import com.jzo2o.trade.model.domain.RefundRecord; +import com.jzo2o.trade.service.RefundRecordService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author itcast + * @Description: 退款记录服务实现类 + */ +@Service +public class RefundRecordServiceImpl extends ServiceImpl implements RefundRecordService { + + @Override + public RefundRecord findByRefundNo(Long refundNo) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(RefundRecord::getRefundNo, refundNo); + return super.getOne(queryWrapper); + } + + @Override + public List findByTradingOrderNo(Long tradingOrderNo) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(RefundRecord::getTradingOrderNo, tradingOrderNo); + queryWrapper.orderByDesc(RefundRecord::getCreateTime); + return super.list(queryWrapper); + } + + @Override + public List findListByProductOrderNo(String productAppId,Long productOrderNo) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(RefundRecord::getProductOrderNo, productOrderNo) + .eq(RefundRecord::getProductAppId, productAppId); + queryWrapper.orderByDesc(RefundRecord::getCreateTime); + return super.list(queryWrapper); + } + + @Override + public List findListByRefundStatus(RefundStatusEnum refundStatus, Integer count) { + count = NumberUtil.max(count, 10); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(RefundRecord::getRefundStatus, refundStatus) + .orderByAsc(RefundRecord::getCreateTime) + .last("LIMIT " + count); + return list(queryWrapper); + } +} diff --git a/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/TradingServiceImpl.java b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/TradingServiceImpl.java new file mode 100644 index 0000000..353ac7a --- /dev/null +++ b/jzo2o-trade/src/main/java/com/jzo2o/trade/service/impl/TradingServiceImpl.java @@ -0,0 +1,98 @@ +package com.jzo2o.trade.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.NumberUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jzo2o.api.trade.enums.PayChannelEnum; +import com.jzo2o.trade.constant.Constants; +import com.jzo2o.trade.enums.TradingStateEnum; +import com.jzo2o.trade.mapper.TradingMapper; +import com.jzo2o.trade.model.domain.Trading; +import com.jzo2o.trade.model.dto.TradingDTO; +import com.jzo2o.trade.service.BasicPayService; +import com.jzo2o.trade.service.TradingService; +import io.swagger.annotations.ApiModelProperty; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author itcast + * @Description:交易订单表 服务实现类 + */ +@Service +public class TradingServiceImpl extends ServiceImpl implements TradingService { + @Resource + private BasicPayService basicPayService; + @Override + public Trading findTradByTradingOrderNo(Long tradingOrderNo) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Trading::getTradingOrderNo, tradingOrderNo); + Trading one = super.getOne(queryWrapper); + return one; + } + + + + @Override + public List findListByTradingState(TradingStateEnum tradingState, Integer count) { + count = NumberUtil.max(count, 10); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Trading::getTradingState, tradingState) + .eq(Trading::getEnableFlag, Constants.YES) + .orderByAsc(Trading::getCreateTime) + .last("LIMIT " + count); + return list(queryWrapper); + } + + /** + * 根据订单id和支付方式查询付款中的交易单 + * + * @param productOrderNo 订单号 + * @param tradingChannel 支付渠道代码 + * @return 交易单 + */ + @Override + public Trading queryDuringTrading(String productAppId,Long productOrderNo, String tradingChannel){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Trading::getProductOrderNo, productOrderNo) + .eq(Trading::getProductAppId,productAppId) + .eq(Trading::getTradingState,TradingStateEnum.FKZ) + .eq(Trading::getTradingChannel, tradingChannel); + List list = list(queryWrapper); + Trading trading = CollectionUtil.getFirst(list); + return trading; + } + /** + * 根据订单id查询交易单 + * + * @param productOrderNo 订单号 + * @return 交易单 + */ + @Override + public List queryByProductOrder(String productAppId,Long productOrderNo){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Trading::getProductOrderNo, productOrderNo) + .eq(Trading::getProductAppId, productAppId); + return list(queryWrapper); + } + /** + * 根据订单id查询已付款的交易单 + * + * @param productOrderNo 订单id + * @return 交易单 + */ + @Override + public Trading findFinishedTrading(String productAppId,Long productOrderNo){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Trading::getProductOrderNo, productOrderNo) + .eq(Trading::getProductAppId,productAppId) + .eq(Trading::getTradingState,TradingStateEnum.YJS); + List list = list(queryWrapper); + Trading first = CollectionUtil.getFirst(list); + return first; + } +} diff --git a/jzo2o-trade/src/main/resources/bootstrap-dev.yml b/jzo2o-trade/src/main/resources/bootstrap-dev.yml new file mode 100644 index 0000000..9e68052 --- /dev/null +++ b/jzo2o-trade/src/main/resources/bootstrap-dev.yml @@ -0,0 +1,17 @@ +spring: + cloud: + nacos: + username: nacos + password: nacos + server-addr: 192.168.122.135:8848 + config: + namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63 + file-extension: yaml + discovery: + namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63 + ip: ${ACCESS_IP:} + +################# 日志配置 ################# +logging: + level: + com.jzo2o: debug \ No newline at end of file diff --git a/jzo2o-trade/src/main/resources/bootstrap-prod.yml b/jzo2o-trade/src/main/resources/bootstrap-prod.yml new file mode 100644 index 0000000..06e37e6 --- /dev/null +++ b/jzo2o-trade/src/main/resources/bootstrap-prod.yml @@ -0,0 +1,16 @@ +spring: + cloud: + nacos: + username: ${NACOS_USERNAME} + password: ${NACOS_PASSWORD} + server-addr: ${NACOS_ADDR} + config: + namespace: ${NACOS_NAMESPACE} + file-extension: yaml + discovery: + namespace: ${NACOS_NAMESPACE} +logging: + level: + com.jzo2o: debug +server: + port: 8080 \ No newline at end of file diff --git a/jzo2o-trade/src/main/resources/bootstrap-test.yml b/jzo2o-trade/src/main/resources/bootstrap-test.yml new file mode 100644 index 0000000..06e37e6 --- /dev/null +++ b/jzo2o-trade/src/main/resources/bootstrap-test.yml @@ -0,0 +1,16 @@ +spring: + cloud: + nacos: + username: ${NACOS_USERNAME} + password: ${NACOS_PASSWORD} + server-addr: ${NACOS_ADDR} + config: + namespace: ${NACOS_NAMESPACE} + file-extension: yaml + discovery: + namespace: ${NACOS_NAMESPACE} +logging: + level: + com.jzo2o: debug +server: + port: 8080 \ No newline at end of file diff --git a/jzo2o-trade/src/main/resources/bootstrap.yml b/jzo2o-trade/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..0ad235f --- /dev/null +++ b/jzo2o-trade/src/main/resources/bootstrap.yml @@ -0,0 +1,88 @@ +################# 服务器配置 ################# +server: + port: 11505 + undertow: + accesslog: + enabled: true + pattern: "%t %a "%r" %s (%D ms)" + dir: /data/logs/undertow/${spring.application.name}/access-logs/ + servlet: + context-path: /trade + +################# spring公共配置 ################# +spring: + mvc: + path-match: + matching-strategy: ant_path_matcher + format: + date: yyyy-MM-dd HH:mm:ss + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + profiles: + active: dev + application: + name: jzo2o-trade + main: + # 支持循环依赖注入 + allow-circular-references: true + # bean名相同覆盖 + allow-bean-definition-overriding: true + cloud: + nacos: + username: ${NACOS_USERNAME} + password: ${NACOS_PASSWORD} + server-addr: ${NACOS_ADDR} + discovery: + namespace: ${NACOS_NAMESPACE} + config: + namespace: ${NACOS_NAMESPACE} + file-extension: yaml + shared-configs: # 共享配置 + - data-id: shared-redis-cluster.yaml # 共享redis配置 + refresh: false + - data-id: shared-xxl-job.yaml # xxl-job配置 + refresh: false + - data-id: shared-rabbitmq.yaml # rabbitmq配置 + refresh: false + - data-id: shared-mysql.yaml # mysql配置 + refresh: false + +jzo2o: + job: + trading: + count: ${job.trading.count} #每次查询交易单的数量 + refund: + count: ${job.refund.count} #每次查询退款单的数量 + qrcode: #二维码生成参数 + margin: ${qrcode.margin} #边距,二维码和背景之间的边距 + fore-color: ${qrcode.fore-color} #二维码颜色,默认黑色 + back-color: ${qrcode.back-color} #背景色,默认白色 + #低级别的像素块更大,可以远距离识别,但是遮挡就会造成无法识别。高级别则相反,像素块小,允许遮挡一定范围,但是像素块更密集。 + error-correction-level: ${qrcode.error-correction-level} #纠错级别,可选参数:L、M、Q、H,默认:M + width: ${qrcode.width} #宽 + height: ${qrcode.height} #高 + +################# 项目独有配置 ################# +mysql: + db-name: jzo2o-trade +mybatis: + mapper-locations: mapper/*.xml + type-aliases-package: com.jzo2o.trade.mapper +swagger: + enable: true + package-path: com.jzo2o.trade.controller + title: 家政服务-支付服务接口文档 + description: 该微服务完记录交易、对接支付宝微信支付平台。 + contact-name: 传智教育·研究院 + contact-url: http://www.itcast.cn/ + contact-email: yjy@itcast.cn + version: v1.0 + +################# 日志配置 ################# +logging: + level: + com.jzo2o: debug + org.apache.http: info #es请求日志 +feign: + enable: true \ No newline at end of file diff --git a/jzo2o-trade/src/main/resources/logos/alipay.png b/jzo2o-trade/src/main/resources/logos/alipay.png new file mode 100644 index 0000000000000000000000000000000000000000..07b9a1d46d1dac47ebb5cb38e71c85e3adce44d5 GIT binary patch literal 1614 zcmX9-3pkW%6rQqEVzmaDXhTb-k=iZWZlo)jai3`tT2bvP*(JMjof(;NZ74>gqIA)a zP>HOBGI>-;*)DRA5WEK2!dWXtRz^%$G2h72?afbG?oak zJq0U-_%<91L$Ev&i^8xWk(8E+(C3kEgwzd%ZW!sH_zb&LGCpx{90}_b(k8MH4n#&G z5#d8TMVH`dH=evfWea{f2CX16U_}!M`Y?vgNzgokvR4@Xgv}?&+K77YG-Bg%&;rRC z%;K;%4nK=g)rR+<;d%}~#Nv24-i@Mf1brXyWfIdf7#v0NT?F5NyajigF*J&aDb&73 zSiyg-@loDHwyY5ddN4{G$-ch%3p>(rARFFUICKTidN4W;VKIJ_!CC@+KJK?-d&fS~|x85o-&=kt3G;!2TPiBvff$`D$By_v8{ z!K!Fv)necyG=t!E35rjc0j8$W_70^l5qlfXXRz!j)C18!f-7|};3MT8K2Ly?hVu_l z+lhUbkzGfQomf*$3As~#Ax6f)Jc-0|IG#pEHM-wJd}v!j>fLJPU>pi`{Qp6M$YC$Z_03=>Lhz zy4pLP!=ln93i=IdGL<#V%*pAU>d8^isj3OK{S=ClF3V~Q_vhL~>A1xL_QIn!soF;2 zT2k{jrQJc-I#1qiY7Y4Aw)MdRsfO(9CXE$t$D*{04zZJ6hKjO_4mDKx!}`GRFTsoS z#-{gXyq)aj*Yn<{7ph0KCv_SoIM`PnJ)M?dwn%o}Mtx39uXd`J`byI}H8o{JlXsO? z+P)@+@@f~lCZ(kz>YU|jyL}<7$sej*_x$5#pkAUJ>N8i)w&)pH*VN`=Mm_X)@w`2! zs`Rw1L&KKrd7|7NTeYLW{Be5rY?w0eih3^4#rY+wRNPThUylrth7j_Nw06X%OG7ZezO7H*r{OxU|fD zR{TC!-hO)FfJ8Q8ygi@B*m-m1s*I&eXt}J~T@OVi-2(2PJq#Ze@$W|%+fyZyv}IC` zuX9A^H3!cJ1$v_Mor9jWeNNOk_JdtX@h|e!jGUIzL$8+xVG{ z`azLQK27dE5a-)5bAeX#hk00n$=Q)QbHSx!-Up*?U;g^zd2R>GNSN)o<7%nu&D|6w zH>sbG<+H4UwNKrGi|Mu&Us}gi>F0D@%AX6|%6Wl33_-`?p|EI9R!zFUR@dxiIY+!u z#=M`9{>8(_SWU^?KPM*3ctgIVh1<0_CRcln@XpvP6@@B`(jeNvK3A$D*5`Pf(6ciT z*0;@bdZo{v<}&}Bj;j%9U*yc<%?KqGgQ1@$9Gs%3#QxzvoTmA2tAq1x7u*}u=eDsX zzw{SYheqBzRd283(leKqH$IV{Yqq?^@bdKJ2|uQigTaIje9v};7$(0TJKuiSVIz#b zR(S0fdpB2W&aYAKeBrTGG|OPQdXVGht2h+B;=b ZEe;`dC*7F=SBA*H#Ikm@y0+Cz_%EuVlF0x7 literal 0 HcmV?d00001 diff --git a/jzo2o-trade/src/main/resources/logos/wechat.png b/jzo2o-trade/src/main/resources/logos/wechat.png new file mode 100644 index 0000000000000000000000000000000000000000..036893733bcc648fff9e4a8f6c915df9b906f931 GIT binary patch literal 2702 zcmV;93UT#`P)2rqh4Anc2C?-95ka zec$ifMv_c?`S6blNY zV@E$%zfr-FbFyYm8#E>0N)|O!_~nXsDLnjYSrk5K8iD)Xs^ayBKiGAyxYY{l$f;{Y zBtwBxLM2#YQE|g?S0P%3S(X{+ag6kwz|g|0?*4h+T~5s@@&IpeCzMT#|;?s#KgzO3>X2ASk%H8 zcV=P&p0|@t5KTp2pF6ms_{oyG@}MW6sSebBDSf**g>q2DL}i-d4L;qweEg#ehZO=g zg{I;hK3odVDP;&4tbTEF=dv$X)WsT;+yM;5f(L+smv5f#7w`rN(lbGQgd#@7;%xD? z_y21218(eA1fx)Qcn08yE|eART6(7CtY7ytLZO|I($}E7*F)Zz#nAb-YSS>+=D%>? z@InY*?SO~=g0urARiYkkI8n57$zy4oSQo0 zqe7$ST|GBHZCc3NALs?i0OqPHMwAM8f(SgnVB4b4PHqo*Ly`i5+M-d!PHjrr{OX3< zp^hSl9P()?)UXVTSt+iR$pa@W$elkiZH)c>?y4_y0+|{9!GeYfcuI+;?Wo$bVCyeE zx?4vC;e>d}`iW%0yDF11pmsqPMP`Cz@(YkjtJMt0Oe-&teN)EN{EQh>(yz03cdY5e zhRP3LJ@nRsVTDg$ztU#9fCtuNOz!{U*W0Rg`W*pa0i;EIEqMX5g;f9$9E3t7WVM}Q zat9m(HIBS%bFycH*+4kqZSp6YYlna%kPt2mk0HUQDEV z04V1%Gd3x7?V{m}-Z--U^V6TZiQlQ|C_w^2|LX=$x-)zBjYFn)B!)6` z{}123S@G_c6I(SE`!p{S-7BgWHp4fjPk(XxGd_n8L6V{PE5CYX!{LoCr;o^}03Wo^ zFr&HtoL6W5$?xzz@%6Ktk8d@2%&YsLz)>1AESiy&dSgbx9a*=I4~>G{BuI#q>Tu1Q zrR&~3@?isLj^@{i1|^bJ6HRED;Y|P4YYU#c%AYOZTVgFs_paQ2b~lVpq7vDyAY71r zv(1b@JgR8v#Kl0=Pfu=pspR$VYReod$w|(>E$il4LkfoX&lT+elMPw~E8%$CmQ&mR zTJc`#g))ukPSuG7D?z=Glzb2uZW^&xR}&x(ELzjEnbJNfMmB{uRy%ehwyUMS3+ z2Q>l_?9f09I--3Jl-E_PE8no`*p?WL`gEU)H32La64>oy)^0YC1h?jXaq81QNuMl} zZ?-n zlBZxpgkG7{{OHK1ua~bYsV{RAw@Y)&;tH)s9Q8P#MD4j1jT?Ev>_6u}pXJX4%?D1R z^`#qE>|f<_y6qx$%s0DQEmn9==B$_VU+7qe+6&s?b9#^vCF$#wD_K!4 z#iZ4Y&d$tVeZwk`=1IuTvBsnGK6@Zyv=SAXM8)k@mC;kkxmR;Pcm0z$W#*q~KC$|n zS9YA<6QNNzc3`5xRkL0U$STU`$zts;`Bpr;C6E; z9k-ZbF*Ba(4oyrQxAWAl^R2ahPcrBLkiBJlpLnh8Er^m34IaH%M=tyeM;%@dNmg)G?FBwDi-E9QIMk*{ zLuPW;=G)&3x&oGHV^uIeu_QvZZFP%x|E9S5K){uh7@%Cv7F!1{5{q|?r7Jry-fm$b z_(;6X?eM-Y0NE#Lql>Y0`^zu5jzNkc`l!!J)KyicUMEZ4wv!V0+A zZ1ob)AFg|B-Zi&FDnYUk$RVtM(d)?;huR;k{@1eNrz#r``<=<4Eqyn*J;B40W~&i( ztFGmf9=>nbd?b9GwDvNuouij6pg`-PV~r;s-?yr`=0K9}lP!pP>F7kM8S7dPgPP6J zrlCoL9-p*)cJ@qBLS_lR^kB4QokSZk1XI5u+W6Bg_nvO6@~9qBfKKlC^06jH6)3Z1 z8BrE@sm_8y(^gJiHrSUYXA|sCwTs>bzVoRV@}OeUYDDIIe9x)oli)#MV484vl4lf{ z$g0@PrK#YOXYd%4`Vzn(PEojNvU^sKBIQ_`l0gCK=% z8IV4n(9$j`>;?}rfm4l;x69sHy6^WoM9a`^*|$A3x(J%JKH5-Gcet|YculmvF