diff --git a/daxpay-common/pom.xml b/daxpay-common/pom.xml index 940243ab..18d4625a 100644 --- a/daxpay-common/pom.xml +++ b/daxpay-common/pom.xml @@ -25,18 +25,6 @@ lombok provided - - - org.projectlombok - lombok-mapstruct-binding - provided - - - - org.mapstruct - mapstruct-processor - provided - diff --git a/daxpay-core/pom.xml b/daxpay-core/pom.xml index 81a8f506..a1bd4966 100644 --- a/daxpay-core/pom.xml +++ b/daxpay-core/pom.xml @@ -24,17 +24,5 @@ lombok provided - - - org.projectlombok - lombok-mapstruct-binding - provided - - - - org.mapstruct - mapstruct-processor - provided - diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayRefundStatusEnum.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayRefundStatusEnum.java new file mode 100644 index 00000000..7941f302 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayRefundStatusEnum.java @@ -0,0 +1,23 @@ +package cn.bootx.platform.daxpay.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 退款状态枚举 + * @author xxm + * @since 2023/12/18 + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusEnum { + + SUCCESS("success","成功"), + FAIL("fail","失败"); + + /** 编码 */ + private final String code; + + /** 名称 */ + private final String name; +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayStatusEnum.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayStatusEnum.java index f7f93752..5e9e6f55 100644 --- a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayStatusEnum.java +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/code/PayStatusEnum.java @@ -22,10 +22,10 @@ public enum PayStatusEnum { PARTIAL_REFUND("partial_refund","部分退款"), REFUNDED("REFUNDED","已退款"); - /** 支付方式字符编码 */ + /** 编码 */ private final String code; /** 名称 */ private final String name; - } +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CancelParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CancelParam.java index 41e87da9..6c2b47ef 100644 --- a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CancelParam.java +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CancelParam.java @@ -11,4 +11,11 @@ import lombok.Data; @Data @Schema(title = "支付取消参数") public class CancelParam { + + @Schema(description = "支付单ID") + private Long paymentId; + + @Schema(description = "业务号") + private String businessNo; + } diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CloseParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CloseParam.java new file mode 100644 index 00000000..0c575903 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/CloseParam.java @@ -0,0 +1,21 @@ +package cn.bootx.platform.daxpay.param.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 支付关闭参数 + * @author xxm + * @since 2023/12/17 + */ +@Data +@Schema(title = "支付关闭参数") +public class CloseParam { + + @Schema(description = "支付单ID") + private Long paymentId; + + @Schema(description = "业务号") + private String businessNo; + +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayParam.java index 5b16f75e..103b907f 100644 --- a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayParam.java +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayParam.java @@ -20,7 +20,6 @@ import java.util.List; @Schema(title = "支付参数") public class PayParam extends PayCommonParam{ - @Schema(description = "业务号") @NotBlank(message = "业务号不可为空") private String businessNo; diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PaySyncParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PaySyncParam.java new file mode 100644 index 00000000..df9eb453 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PaySyncParam.java @@ -0,0 +1,21 @@ +package cn.bootx.platform.daxpay.param.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 支付状态同步参数 + * @author xxm + * @since 2023/12/17 + */ +@Data +@Schema(title = "支付状态同步参数") +public class PaySyncParam { + + @Schema(description = "支付单ID") + private Long paymentId; + + @Schema(description = "业务号") + private String businessNo; + +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayWayParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayWayParam.java index 774b9028..706b80b2 100644 --- a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayWayParam.java +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/PayWayParam.java @@ -11,7 +11,6 @@ import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import java.math.BigDecimal; /** * 同意下单支付方式参数 @@ -39,7 +38,7 @@ public class PayWayParam { @Schema(description = "支付金额") @NotNull(message = "支付金额不可为空") - private BigDecimal amount; + private Integer amount; /** * @see AliPayParam diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundModeParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundModeParam.java new file mode 100644 index 00000000..77edfc68 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundModeParam.java @@ -0,0 +1,47 @@ +package cn.bootx.platform.daxpay.param.pay; + +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.param.channel.AliPayParam; +import cn.bootx.platform.daxpay.param.channel.VoucherPayParam; +import cn.bootx.platform.daxpay.param.channel.WalletPayParam; +import cn.bootx.platform.daxpay.param.channel.WeChatPayParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 分通道退款参数 + * @author xxm + * @since 2023/12/18 + */ +@Data +@Accessors(chain = true) +@Schema(title = "分通道退款参数") +public class RefundModeParam { + + /** + * @see PayChannelEnum#getCode() + */ + @Schema(description = "支付通道编码") + @NotBlank(message = "支付通道编码不可为空") + private String payChannel; + + @Schema(description = "退款金额") + @NotNull(message = "退款金额不可为空") + private Integer amount; + + /** + * 预留的扩展参数, 暂时未使用 + * @see AliPayParam + * @see WeChatPayParam + * @see VoucherPayParam + * @see WalletPayParam + */ + @Schema(description = "附加退款参数") + private String channelExtra; + + +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundParam.java new file mode 100644 index 00000000..f7b7f32e --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundParam.java @@ -0,0 +1,47 @@ +package cn.bootx.platform.daxpay.param.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import java.util.List; + +/** + * 退款参数,适用于组合支付的订单退款操作中, + * @author xxm + * @since 2023/12/18 + */ +@Data +@Schema(title = "退款参数") +public class RefundParam { + + @Schema(description = "支付单ID") + private Long paymentId; + + @Schema(description = "业务号") + private String businessNo; + + /** + * 部分退款需要传输refundModes参数 + */ + @Schema(description = "是否全部退款") + private boolean refundAll; + + /** + * 部分退款时此项必填 + */ + @Schema(description = "退款订单号") + private String refundNo; + + /** + * 部分退款时必传 + */ + @Valid + @Schema(description = "退款参数列表") + private List refundModes; + + @Schema(description = "退款原因") + private String reason; + + +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundSyncParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundSyncParam.java new file mode 100644 index 00000000..0235fc82 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/RefundSyncParam.java @@ -0,0 +1,43 @@ +package cn.bootx.platform.daxpay.param.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 退款状态同步参数 + * @author xxm + * @since 2023/12/17 + */ +@Data +@Schema(title = "退款状态同步参数") +public class RefundSyncParam { + + /** + * 部分退款时 refundId 和 refundNo 必传一个 + * 如果与 businessNo同时传输,以本参数为准 + */ + @Schema(description = "退款订单ID") + private Long refundId; + + /** + * 退款订单号,部分退款时 refundId 和 refundNo 必传一个, + * 如果与 paymentId 和 businessNo 同时传输,以本参数为准 + */ + @Schema(description = "退款订单号") + private String refundNo; + + /** + * 支付单ID, 通常为商户的业务订单号,全部退款时, + * paymentId 和 businessNo可传其中一个,同时传输以前者为准,部分退款可以不进行传输 + */ + @Schema(description = "支付单ID") + private Long paymentId; + + /** + * 支付订单的业务号, 通常为商户的业务订单号,全部退款时, + * paymentId 和 businessNo可传其中一个,同时传输以前者为准,部分退款可以不进行传输 + */ + @Schema(description = "业务号") + private String businessNo; + +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/SimpleRefundParam.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/SimpleRefundParam.java new file mode 100644 index 00000000..752c9024 --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/param/pay/SimpleRefundParam.java @@ -0,0 +1,63 @@ +package cn.bootx.platform.daxpay.param.pay; + +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.param.channel.AliPayParam; +import cn.bootx.platform.daxpay.param.channel.VoucherPayParam; +import cn.bootx.platform.daxpay.param.channel.WalletPayParam; +import cn.bootx.platform.daxpay.param.channel.WeChatPayParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 简单退款参数,只可以用于非组合的支付订单 + * @author xxm + * @since 2023/12/18 + */ +@Data +@Schema(title = "简单退款参数") +public class SimpleRefundParam { + + @Schema(description = "支付单ID") + private Long paymentId; + + @Schema(description = "业务号") + private String businessNo; + + /** + * 部分退款需要传输refundModes参数 + */ + @Schema(description = "是否全部退款") + private boolean refundAll; + + /** + * 部分退款时此项必填 + */ + @Schema(description = "退款订单号") + private String refundNo; + /** + * @see PayChannelEnum#getCode() + */ + @Schema(description = "支付通道编码") + @NotBlank(message = "支付通道编码不可为空") + private String payChannel; + + @Schema(description = "退款金额") + @NotNull(message = "退款金额不可为空") + private Integer amount; + + /** + * 预留的扩展参数, 暂时未使用 + * @see AliPayParam + * @see WeChatPayParam + * @see VoucherPayParam + * @see WalletPayParam + */ + @Schema(description = "附加退款参数") + private String channelExtra; + + @Schema(description = "退款原因") + private String reason; +} diff --git a/daxpay-core/src/main/java/cn/bootx/platform/daxpay/result/pay/RefundResult.java b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/result/pay/RefundResult.java new file mode 100644 index 00000000..7a304fbb --- /dev/null +++ b/daxpay-core/src/main/java/cn/bootx/platform/daxpay/result/pay/RefundResult.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.result.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 退款响应参数 + * @author xxm + * @since 2023/12/18 + */ +@Data +@Accessors(chain = true) +@Schema(title = "退款响应参数") +public class RefundResult { + + @Schema(description = "退款ID") + private Long refundId; + + @Schema(description = "退款订单号") + private String refundNo; + + @Schema(description = "退款状态") + private String state; + +} diff --git a/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/DaxPaySingleGatewayApp.java b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/DaxPaySingleGatewayApp.java index 771e2e10..3066d20c 100644 --- a/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/DaxPaySingleGatewayApp.java +++ b/daxpay-single/daxpay-single-admin/src/main/java/cn/bootx/platform/daxpay/DaxPaySingleGatewayApp.java @@ -3,6 +3,7 @@ package cn.bootx.platform.daxpay; import org.apache.ibatis.annotations.Mapper; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.ComponentScan; /** * 管理端 @@ -11,5 +12,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; */ @ConfigurationPropertiesScan @MapperScan(annotationClass = Mapper.class) +@ComponentScan public class DaxPaySingleGatewayApp { } diff --git a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleGatewayApp.java b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleGatewayApp.java index a4dc4c68..61f4e3f3 100644 --- a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleGatewayApp.java +++ b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleGatewayApp.java @@ -3,6 +3,8 @@ package cn.bootx.platform.daxpay; import org.apache.ibatis.annotations.Mapper; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.ComponentScan; + /** * 网关端 * @author xxm @@ -10,5 +12,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; */ @ConfigurationPropertiesScan @MapperScan(annotationClass = Mapper.class) +@ComponentScan public class DaxpaySingleGatewayApp { } diff --git a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/PayCallbackController.java b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/PayCallbackController.java new file mode 100644 index 00000000..8d0ac07c --- /dev/null +++ b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/PayCallbackController.java @@ -0,0 +1,54 @@ +package cn.bootx.platform.daxpay.openapi.controller; + +import cn.bootx.platform.common.core.annotation.IgnoreAuth; +import cn.bootx.platform.daxpay.core.channel.alipay.service.AliPayCallbackService; +import cn.bootx.platform.daxpay.core.channel.wechat.service.WeChatPayCallbackService; +import com.ijpay.alipay.AliPayApi; +import com.ijpay.core.kit.HttpKit; +import com.ijpay.core.kit.WxPayKit; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author xxm + * @since 2021/2/27 + */ +@IgnoreAuth +@Slf4j +@Tag(name = "支付回调") +@RestController +@RequestMapping("/pay/callback") +@AllArgsConstructor +public class PayCallbackController { + + private final AliPayCallbackService aliPayCallbackService; + + private final WeChatPayCallbackService weChatPayCallbackService; + + @SneakyThrows + @Operation(summary = "支付宝回调") + @PostMapping("/alipay") + public String aliPay(HttpServletRequest request) { + Map stringStringMap = AliPayApi.toMap(request); + return aliPayCallbackService.payCallback(stringStringMap); + } + + @SneakyThrows + @Operation(summary = "微信支付回调") + @PostMapping("/wechat") + public String wechat(HttpServletRequest request) { + String xmlMsg = HttpKit.readData(request); + Map params = WxPayKit.xmlToMap(xmlMsg); + return weChatPayCallbackService.payCallback(params); + } + +} diff --git a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/UniPayController.java b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/UniPayController.java index 517cf7e9..4e2c2020 100644 --- a/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/UniPayController.java +++ b/daxpay-single/daxpay-single-gateway/src/main/java/cn/bootx/platform/daxpay/openapi/controller/UniPayController.java @@ -1,9 +1,10 @@ package cn.bootx.platform.daxpay.openapi.controller; import cn.bootx.platform.common.core.annotation.IgnoreAuth; -import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.*; import cn.bootx.platform.daxpay.result.DaxResult; import cn.bootx.platform.daxpay.result.pay.PayResult; +import cn.bootx.platform.daxpay.result.pay.RefundResult; import cn.bootx.platform.daxpay.util.DaxRes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -37,22 +38,22 @@ public class UniPayController { } @Operation(summary = "订单撤销") @PostMapping("/cancel") - public DaxResult cancel(){ + public DaxResult cancel(@RequestBody CancelParam param){ return DaxRes.ok(); } @Operation(summary = "订单关闭") @PostMapping("/close") - public DaxResult close(){ + public DaxResult close(@RequestBody CloseParam param){ return DaxRes.ok(); } @Operation(summary = "统一退款") @PostMapping("/refund") - public DaxResult refund(){ + public DaxResult refund(@RequestBody RefundParam param){ return DaxRes.ok(); } @Operation(summary = "简单退款") @PostMapping("/simpleRefund") - public DaxResult simpleRefund(){ + public DaxResult simpleRefund(@RequestBody SimpleRefundParam param){ return DaxRes.ok(); } @Operation(summary = "支付状态同步") @@ -62,7 +63,7 @@ public class UniPayController { } @Operation(summary = "退款状态同步") @PostMapping("/syncRefund") - public DaxResult syncRefund(){ + public DaxResult syncRefund(@RequestBody RefundSyncParam param){ return DaxRes.ok(); } } diff --git a/daxpay-single/daxpay-single-service/pom.xml b/daxpay-single/daxpay-single-service/pom.xml index a3181c1c..95ac9295 100644 --- a/daxpay-single/daxpay-single-service/pom.xml +++ b/daxpay-single/daxpay-single-service/pom.xml @@ -12,6 +12,68 @@ daxpay-single-service + + + org.projectlombok + lombok + provided + + + + org.projectlombok + lombok-mapstruct-binding + provided + + + + org.mapstruct + mapstruct-processor + provided + + + + org.mapstruct + mapstruct + provided + + + + com.baomidou + mybatis-plus-boot-starter + + + + cn.bootx.platform + common-mybatis-plus + + + + com.alibaba + easyexcel + + + + cn.bootx + table-modify-mysql + ${table-modify.version} + + + + cn.bootx.platform + common-redis-client + + + + cn.bootx.platform + common-spring + + + + cn.bootx.platform + common-super-query + ${bootx-platform.version} + + cn.bootx.platform @@ -24,10 +86,31 @@ daxpay-common ${dax.version} - + + - com.baomidou - mybatis-plus-boot-starter + com.github.javen205 + IJPay-AliPay + ${IJPay.version} + + + fastjson + com.alibaba + + + + + + + com.github.javen205 + IJPay-WxPay + ${IJPay.version} + + + + com.github.binarywang + weixin-java-pay + ${wxjava.version} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleServiceApp.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleServiceApp.java index 2981cfd6..8b9ded75 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleServiceApp.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/DaxpaySingleServiceApp.java @@ -3,6 +3,7 @@ package cn.bootx.platform.daxpay; import org.apache.ibatis.annotations.Mapper; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.ComponentScan; /** * 业务服务 @@ -11,5 +12,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; */ @ConfigurationPropertiesScan @MapperScan(annotationClass = Mapper.class) +@ComponentScan public class DaxpaySingleServiceApp { } diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayCode.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayCode.java new file mode 100644 index 00000000..39f37ff8 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayCode.java @@ -0,0 +1,86 @@ +package cn.bootx.platform.daxpay.code; + +/** + * 支付宝支付参数 + * + * @author xxm + * @since 2021/2/27 + */ +public interface AliPayCode { + + // 认证类型 + /** 公钥 */ + String AUTH_TYPE_KEY = "key"; + + /** 证书 */ + String AUTH_TYPE_CART = "cart"; + + // 渠道枚举 + /** 目前PC支付必填 */ + String FAST_INSTANT_TRADE_PAY = "FAST_INSTANT_TRADE_PAY"; + + /** WAP支付必填 手机网站支付产品 */ + String QUICK_WAP_PAY = "QUICK_WAP_WAY"; + + /** APP支付必填 APP支付产品 */ + String QUICK_MSECURITY_PAY = "QUICK_MSECURITY_PAY"; + + /** 付款码支付 */ + String BAR_CODE = "bar_code"; + + // 响应字段 + /** 支付状态 */ + String TRADE_STATUS = "trade_status"; + + /** 公用回传参数 */ + String PASS_BACK_PARAMS = "passback_params"; + + /** 对交易或商品的描述(在没有公用回传参数的时候, 这个作为公用回传参数) */ + String BODY = "body"; + + /** 外部订单号-paymentId */ + String OUT_TRADE_NO = "out_trade_no"; + + /** 支付宝流水号 */ + String TRADE_NO = "trade_no"; + + /** appId */ + String APP_ID = "app_id"; + + // 交易状态说明 + /** 交易创建,等待买家付款 */ + String PAYMENT_WAIT_BUYER_PAY = "WAIT_BUYER_PAY"; + + /** 未付款交易超时关闭,或支付完成后全额退款 */ + String PAYMENT_TRADE_CLOSED = "TRADE_CLOSED"; + + /** 交易支付成功 */ + String PAYMENT_TRADE_SUCCESS = "TRADE_SUCCESS"; + + /** 交易结束,不可退款 */ + String PAYMENT_TRADE_FINISHED = "TRADE_FINISHED"; + + // 通知触发条件 + /** 交易完成 */ + String NOTIFY_TRADE_FINISHED = "TRADE_FINISHED"; + + /** 支付成功 */ + String NOTIFY_TRADE_SUCCESS = "TRADE_SUCCESS"; + + /** 交易创建,不触发通知 */ + String NOTIFY_WAIT_BUYER_PAY = "WAIT_BUYER_PAY"; + + /** 交易关闭 */ + String NOTIFY_TRADE_CLOSED = "TRADE_CLOSED"; + + // 错误提示 + /** 交易不存在 */ + String ACQ_TRADE_NOT_EXIST = "ACQ.TRADE_NOT_EXIST"; + + // 网关返回码 + String SUCCESS = "10000"; + + // 网关返回码 支付进行中 order success pay inprocess + String INPROCESS = "10003"; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayWay.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayWay.java new file mode 100644 index 00000000..caec9c8d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/AliPayWay.java @@ -0,0 +1,39 @@ +package cn.bootx.platform.daxpay.code; + +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 支付宝支付方式 + * + * @author xxm + * @since 2021/7/2 + */ +@UtilityClass +public class AliPayWay { + + // 支付方式 + private static final List PAY_WAYS = Arrays.asList(PayWayEnum.WAP, PayWayEnum.APP, PayWayEnum.WEB, + PayWayEnum.QRCODE, PayWayEnum.BARCODE); + + /** + * 根据编码获取 + */ + public PayWayEnum findByCode(String code) { + return PAY_WAYS.stream() + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 获取支持的支付方式 + */ + public List getPayWays() { + return PAY_WAYS; + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatus.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatus.java new file mode 100644 index 00000000..1cda0e6c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/PaySyncStatus.java @@ -0,0 +1,32 @@ +package cn.bootx.platform.daxpay.code; + +/** + * 支付网关同步状态 + * + * @author xxm + * @since 2021/4/21 + */ +public interface PaySyncStatus { + + /** 不需要同步 */ + String NOT_SYNC = "not_sync"; + + /** 远程支付成功 */ + String TRADE_SUCCESS = "trade_success"; + + /** 交易创建,等待买家付款 */ + String WAIT_BUYER_PAY = "wait_buyer_pay"; + + /** 已关闭 */ + String TRADE_CLOSED = "trade_closed"; + + /** 已退款 */ + String TRADE_REFUND = "trade_refund"; + + /** 查询不到订单 */ + String NOT_FOUND = "not_found"; + + /** 查询失败 */ + String FAIL = "fail"; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/VoucherCode.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/VoucherCode.java new file mode 100644 index 00000000..b0668ed5 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/VoucherCode.java @@ -0,0 +1,55 @@ +package cn.bootx.platform.daxpay.code; + +/** + * 储值卡常量 + * + * @author xxm + * @since 2022/3/14 + */ +public interface VoucherCode { + + /** + * 状态-正常 + */ + String STATUS_NORMAL = "normal"; + + /** + * 状态-停用 + */ + String STATUS_FORBIDDEN = "forbidden"; + + /** + * 储值卡日志-开通 + */ + String LOG_ACTIVE = "active"; + + /** + * 储值卡日志-导入 + */ + String LOG_IMPORT = "import"; + + /** + * 储值卡日志-预冻结额度 + */ + String LOG_FREEZE_BALANCE = "freeze"; + + /** + * 储值卡日志-扣减并解冻余额 + */ + String LOG_REDUCE_AND_UNFREEZE_BALANCE = "reduceAndUnfreeze"; + + /** + * 储值卡日志-直接支付 + */ + String LOG_PAY = "pay"; + /** + * 储值卡日志-取消支付 + */ + String LOG_CLOSE_PAY = "closePay"; + + /** + * 储值卡日志-退款到本卡中 + */ + String LOG_REFUND_SELF = "refundSelf"; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayCode.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayCode.java new file mode 100644 index 00000000..4b645922 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayCode.java @@ -0,0 +1,88 @@ +package cn.bootx.platform.daxpay.code; + +/** + * 微信参数 + * + * @author xxm + * @since 2021/6/21 + */ +public interface WeChatPayCode { + + // 版本 + String API_V2 = "apiV2"; + + String API_V3 = "apiV3"; + + // 请求参数 + /** jsapi发起获取AuthCode时的重定向参数 */ + String JSAPI_REDIRECT_URL = "JsapiRedirectUrl"; + + // 返回参数 + /** 二维码链接 */ + String CODE_URL = "code_url"; + + /** 支付跳转链接 */ + String MWEB_URL = "mweb_url"; + + /** 预支付交易会话ID */ + String PREPAY_ID = "prepay_id"; + + /** 返回状态码 */ + String RETURN_CODE = "return_code"; + + /** 返回信息 */ + String RETURN_MSG = "return_msg"; + + /** 返回错误代码(例如付款码返回的支付中状态就在这里面) */ + String ERR_CODE = "err_code"; + + /** 返回错误信息 */ + String ERR_CODE_DES = "err_code_des"; + + /** 业务结果(部分结果不在这个参数里, 例如付款码的响应码) */ + String RESULT_CODE = "result_code"; + + /** 交易类型 */ + String TRADE_TYPE = "trade_type"; + + /** appid */ + String APPID = "appid"; + + /** 交易状态 */ + String TRADE_STATE = "trade_state"; + + /** 商户订单号 */ + String OUT_TRADE_NO = "out_trade_no"; + + /** 微信交易单号 */ + String TRANSACTION_ID = "transaction_id"; + + // 交易状态 + /** 支付成功 */ + String TRADE_SUCCESS = "SUCCESS"; + + /** 支付失败 */ + String TRADE_FAIL = "FAIL"; + + /** 退款 */ + String TRADE_REFUND = "REFUND"; + + /** 未支付 */ + String TRADE_NOTPAY = "NOTPAY"; + + /** 已关闭 */ + String TRADE_CLOSED = "CLOSED"; + + /** 已接收,等待扣款 */ + String TRADE_ACCEPT = "ACCEPT"; + + /** 已撤销(刷卡支付) */ + String TRADE_REVOKED = "REVOKED"; + + /** 用户支付中(刷卡支付) */ + String TRADE_USERPAYING = "USERPAYING"; + + /** 支付失败(刷卡支付) */ + String TRADE_PAYERROR = "PAYERROR"; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayWay.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayWay.java new file mode 100644 index 00000000..b90f85f6 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/code/WeChatPayWay.java @@ -0,0 +1,39 @@ +package cn.bootx.platform.daxpay.code; + +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 微信支付方式 + * + * @author xxm + * @since 2021/7/2 + */ +@UtilityClass +public class WeChatPayWay { + + private static final List PAY_WAYS = Arrays.asList(PayWayEnum.WAP, PayWayEnum.APP, PayWayEnum.JSAPI, + PayWayEnum.QRCODE, PayWayEnum.BARCODE); + + /** + * 根据数字编号获取 + */ + public PayWayEnum findByCode(String code) { + return PAY_WAYS.stream() + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 获取支持的支付方式 + */ + public List getPayWays() { + return PAY_WAYS; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/entity/BasePayOrder.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/entity/BasePayOrder.java new file mode 100644 index 00000000..489d0e02 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/entity/BasePayOrder.java @@ -0,0 +1,47 @@ +package cn.bootx.platform.daxpay.common.entity; + +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 基础支付订单信息类 + * @author xxm + * @since 2023/12/18 + */ +@Data +@Accessors(chain = true) +public class BasePayOrder { + + /** 交易记录ID */ + @DbMySqlIndex(comment = "交易记录ID") + private Long paymentId; + + /** 交易金额 */ + @DbColumn(comment = "交易金额") + private Integer amount; + + /** 可退款金额 */ + @DbColumn(comment = "可退款金额") + private Integer refundableBalance; + + /** 关联的业务号 */ + @DbMySqlIndex(comment = "业务号索引") + @DbColumn(comment = "关联的业务号") + private String businessNo; + + /** + * 支付状态 + * @see PayStatusEnum + */ + @DbColumn(comment = "支付状态") + private String status; + + /** 支付时间 */ + @DbColumn(comment = "支付时间") + private LocalDateTime payTime; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/exception/ExceptionInfo.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/exception/ExceptionInfo.java new file mode 100644 index 00000000..63639167 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/common/exception/ExceptionInfo.java @@ -0,0 +1,27 @@ +package cn.bootx.platform.daxpay.common.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2021/5/24 + */ +@Data +@Accessors(chain = true) +@Schema(title = "异常信息") +public class ExceptionInfo { + + /** 错误码 */ + private String errorCode; + + /** 错误信息 */ + private String errorMsg; + + public ExceptionInfo(String errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/convert/CallbackNotifyMConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/convert/CallbackNotifyMConvert.java new file mode 100644 index 00000000..c5ef6785 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/convert/CallbackNotifyMConvert.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.callback.convert; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 回调通知转换 + * @author xxm + * @since 2023/12/18 + */ +@Mapper +public interface CallbackNotifyMConvert { + CallbackNotifyMConvert CONVERT = Mappers.getMapper(CallbackNotifyMConvert.class); +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyManager.java new file mode 100644 index 00000000..f03670b3 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyManager.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.callback.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.callback.entity.CallbackNotify; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * 支付回调通知 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Repository +public class CallbackNotifyManager extends BaseManager { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyMapper.java new file mode 100644 index 00000000..17ffa6cd --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/dao/CallbackNotifyMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.callback.dao; + +import cn.bootx.platform.daxpay.core.callback.entity.CallbackNotify; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * + * @author xxm + * @since 2023/12/18 + */ +@Mapper +public interface CallbackNotifyMapper extends BaseMapper { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/entity/CallbackNotify.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/entity/CallbackNotify.java new file mode 100644 index 00000000..905073d7 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/entity/CallbackNotify.java @@ -0,0 +1,54 @@ +package cn.bootx.platform.daxpay.core.callback.entity; + +import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import cn.bootx.table.modify.annotation.DbComment; +import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType; +import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 回调通知 + * @author xxm + * @since 2023/12/18 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class CallbackNotify extends MpCreateEntity { + /** 支付记录id */ + @DbComment("支付记录id") + private Long paymentId; + + /** + * 支付渠道 + * @see PayChannelEnum#getCode() + */ + @DbComment("支付渠道") + private String payChannel; + + /** 通知消息 */ + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbComment("通知消息") + private String notifyInfo; + + /** + * 处理状态 + * @see PayStatusEnum + */ + @DbComment("处理状态") + private String status; + + /** 提示信息 */ + @DbComment("提示信息") + private String msg; + + /** 回调时间 */ + @DbComment("回调时间") + private LocalDateTime notifyTime; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/service/CallbackNotifyService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/service/CallbackNotifyService.java new file mode 100644 index 00000000..65e8ed08 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/callback/service/CallbackNotifyService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.callback.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 接收到的支付回调通知 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CallbackNotifyService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/convert/AlipayConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/convert/AlipayConvert.java new file mode 100644 index 00000000..3b2f6e38 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/convert/AlipayConvert.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.convert; + +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import cn.bootx.platform.daxpay.dto.channel.alipay.AlipayConfigDto; +import cn.bootx.platform.daxpay.param.channel.alipay.AlipayConfigParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 支付宝转换 + * + * @author xxm + * @since 2021/7/5 + */ +@Mapper +public interface AlipayConvert { + + AlipayConvert CONVERT = Mappers.getMapper(AlipayConvert.class); + + AlipayConfig convert(AlipayConfigDto in); + + AlipayConfig convert(AlipayConfigParam in); + + AlipayConfigDto convert(AlipayConfig in); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderManager.java new file mode 100644 index 00000000..978e2beb --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderManager.java @@ -0,0 +1,25 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.common.entity.BasePayOrder; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AliPayOrder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 支付宝支付订单 + * + * @author xxm + * @since 2021/2/26 + */ +@Repository +@RequiredArgsConstructor +public class AliPayOrderManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return this.findByField(BasePayOrder::getPaymentId, paymentId); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderMapper.java new file mode 100644 index 00000000..b47a4315 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AliPayOrderMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.dao; + +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AliPayOrder; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 支付宝支付 + * + * @author xxm + * @since 2021/2/26 + */ +@Mapper +public interface AliPayOrderMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigManager.java new file mode 100644 index 00000000..2c336fcd --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigManager.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +/** + * 支付宝配置 + * + * @author xxm + * @since 2021/2/26 + */ +@Repository +@RequiredArgsConstructor +public class AlipayConfigManager extends BaseManager { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigMapper.java new file mode 100644 index 00000000..cecfe0a4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/dao/AlipayConfigMapper.java @@ -0,0 +1,15 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.dao; + +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 支付宝配置 + * @author xxm + * @since 2023/12/18 + */ +@Mapper +public interface AlipayConfigMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AliPayOrder.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AliPayOrder.java new file mode 100644 index 00000000..720d9859 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AliPayOrder.java @@ -0,0 +1,34 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.daxpay.common.entity.BasePayOrder; +import cn.bootx.platform.daxpay.dto.channel.alipay.AliPaymentDto; +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付宝支付记录 + * + * @author xxm + * @since 2021/2/26 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_ali_payment") +public class AliPayOrder extends BasePayOrder implements EntityBaseFunction { + + /** 支付宝交易号 */ + private String tradeNo; + + @Override + public AliPaymentDto toDto() { + AliPaymentDto dto = new AliPaymentDto(); + BeanUtil.copyProperties(this, dto); + return dto; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AlipayConfig.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AlipayConfig.java new file mode 100644 index 00000000..1c7856cc --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/entity/AlipayConfig.java @@ -0,0 +1,130 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.entity; + +import cn.bootx.platform.common.core.annotation.BigField; +import cn.bootx.platform.common.core.annotation.EncryptionField; +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.common.mybatisplus.handler.StringListTypeHandler; +import cn.bootx.platform.daxpay.code.AliPayCode; +import cn.bootx.platform.daxpay.core.channel.alipay.convert.AlipayConvert; +import cn.bootx.platform.daxpay.dto.channel.alipay.AlipayConfigDto; +import cn.bootx.platform.daxpay.param.channel.alipay.AlipayConfigParam; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.annotation.DbTable; +import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType; +import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 支付宝支付配置 + * + * @author xxm + * @since 2020/12/15 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@DbTable(comment = "支付宝支付配置") +@TableName("pay_alipay_config") +public class AlipayConfig extends MpBaseEntity implements EntityBaseFunction { + + /** 支付宝商户appId */ + @DbColumn(comment = "支付宝商户appId") + private String appId; + + /** 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 */ + @DbColumn(comment = "异步通知页面路径") + private String notifyUrl; + + /** + * 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址 + */ + @DbColumn(comment = "同步通知页面路径") + private String returnUrl; + + /** 请求网关地址 */ + @DbColumn(comment = "") + private String serverUrl; + + /** + * 认证类型 证书/公钥 + * @see AliPayCode#AUTH_TYPE_KEY + */ + @DbColumn(comment = "认证类型") + private String authType; + + /** 签名类型 RSA/RSA2 */ + @DbColumn(comment = "签名类型 RSA/RSA2") + public String signType; + + /** 支付宝公钥 */ + @BigField + @EncryptionField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "支付宝公钥") + public String alipayPublicKey; + + /** 私钥 */ + @BigField + @EncryptionField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "私钥") + private String privateKey; + + /** 应用公钥证书 */ + @BigField + @EncryptionField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "应用公钥证书") + private String appCert; + + /** 支付宝公钥证书 */ + @BigField + @EncryptionField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "支付宝公钥证书") + private String alipayCert; + + /** 支付宝CA根证书 */ + @BigField + @EncryptionField + @DbColumn(comment = "支付宝CA根证书") + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + private String alipayRootCert; + + /** 是否沙箱环境 */ + @DbColumn(comment = "是否沙箱环境") + private boolean sandbox; + + /** 超时配置 */ + @DbColumn(comment = "超时配置") + private Integer expireTime; + + /** 可用支付方式 */ + @DbColumn(comment = "可用支付方式") + @TableField(typeHandler = StringListTypeHandler.class) + private List payWays; + + @DbColumn(comment = "状态") + private String status; + + /** 备注 */ + @DbColumn(comment = "备注") + private String remark; + + @Override + public AlipayConfigDto toDto() { + return AlipayConvert.CONVERT.convert(this); + } + + public static AlipayConfig init(AlipayConfigParam in) { + return AlipayConvert.CONVERT.convert(in); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCallbackService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCallbackService.java new file mode 100644 index 00000000..0a3222db --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCallbackService.java @@ -0,0 +1,105 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.common.core.util.CertUtil; +import cn.bootx.platform.common.redis.RedisClient; +import cn.bootx.platform.daxpay.code.AliPayCode; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import cn.bootx.platform.daxpay.core.callback.dao.CallbackNotifyManager; +import cn.bootx.platform.daxpay.core.channel.alipay.dao.AlipayConfigManager; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import cn.bootx.platform.daxpay.core.payment.callback.service.PayCallbackService; +import cn.bootx.platform.daxpay.func.AbsPayCallbackStrategy; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConstants; +import com.alipay.api.internal.util.AlipaySignature; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * 支付宝回调处理 + * + * @author xxm + * @since 2021/2/28 + */ +@Slf4j +@Service +public class AliPayCallbackService extends AbsPayCallbackStrategy { + + private final AlipayConfigManager alipayConfigManager; + + public AliPayCallbackService(RedisClient redisClient, CallbackNotifyManager callbackNotifyManager, + PayCallbackService payCallbackService, AlipayConfigManager alipayConfigManager) { + super(redisClient, callbackNotifyManager, payCallbackService); + this.alipayConfigManager = alipayConfigManager; + } + + @Override + public PayChannelEnum getPayChannel() { + return PayChannelEnum.ALI; + } + + @Override + public String getTradeStatus() { + Map params = PARAMS.get(); + String tradeStatus = params.get(AliPayCode.TRADE_STATUS); + if (Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_SUCCESS)) { + return PayStatusEnum.SUCCESS.getCode(); + } + return PayStatusEnum.FAIL.getCode(); + } + + /** + * 验证信息格式 + */ + @SneakyThrows + @Override + public boolean verifyNotify() { + Map params = PARAMS.get(); + String callReq = JSONUtil.toJsonStr(params); + String appId = params.get(AliPayCode.APP_ID); + if (StrUtil.isBlank(appId)) { + log.error("支付宝回调报文 appId 为空 {}", callReq); + return false; + } + AlipayConfig alipayConfig = null; + if (alipayConfig == null) { + log.error("支付宝支付配置不存在: {}", callReq); + return false; + } + + try { + if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_KEY)) { + return AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(), CharsetUtil.UTF_8, + AlipayConstants.SIGN_TYPE_RSA2); + } + else { + return AlipaySignature.verifyV1(params, CertUtil.getCertByContent(alipayConfig.getAlipayCert()), + CharsetUtil.UTF_8, AlipayConstants.SIGN_TYPE_RSA2); + } + } + catch (AlipayApiException e) { + log.error("支付宝验签失败", e); + return false; + } + } + + @Override + public Long getPaymentId() { + Map params = PARAMS.get(); + return Long.valueOf(params.get(AliPayCode.OUT_TRADE_NO)); + } + + @Override + public String getReturnMsg() { + return "success"; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCloseService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCloseService.java new file mode 100644 index 00000000..599112db --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayCloseService.java @@ -0,0 +1,84 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.common.spring.exception.RetryableException; +import cn.bootx.platform.daxpay.code.paymodel.AliPayCode; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.core.refund.local.AsyncRefundLocal; +import cn.bootx.platform.daxpay.exception.payment.PayFailureException; +import cn.hutool.core.util.IdUtil; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeCancelModel; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.response.AlipayTradeCancelResponse; +import com.alipay.api.response.AlipayTradeRefundResponse; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Objects; + +/** + * 支付宝支付取消和支付关闭 + * + * @author xxm + * @since 2021/4/20 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayCloseService { + + /** + * 关闭支付 + */ + @Retryable(value = RetryableException.class) + public void cancelRemote(Payment payment) { + // 只有部分需要调用支付宝网关进行关闭 + AlipayTradeCancelModel model = new AlipayTradeCancelModel(); + model.setOutTradeNo(String.valueOf(payment.getId())); + + try { + AlipayTradeCancelResponse response = AliPayApi.tradeCancelToResponse(model); + if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) { + log.error("网关返回撤销失败: {}", response.getSubMsg()); + throw new PayFailureException(response.getSubMsg()); + } + } + catch (AlipayApiException e) { + log.error("关闭订单失败:", e); + throw new PayFailureException("关闭订单失败"); + } + } + + /** + * 退款 + */ + public void refund(Payment payment, BigDecimal amount) { + AlipayTradeRefundModel refundModel = new AlipayTradeRefundModel(); + refundModel.setOutTradeNo(String.valueOf(payment.getId())); + refundModel.setRefundAmount(amount.toPlainString()); + + // 设置退款号 + AsyncRefundLocal.set(IdUtil.getSnowflakeNextIdStr()); + refundModel.setOutRequestNo(AsyncRefundLocal.get()); + try { + AlipayTradeRefundResponse response = AliPayApi.tradeRefundToResponse(refundModel); + if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) { + AsyncRefundLocal.setErrorMsg(response.getSubMsg()); + AsyncRefundLocal.setErrorCode(response.getCode()); + log.error("网关返回退款失败: {}", response.getSubMsg()); + throw new PayFailureException(response.getSubMsg()); + } + } + catch (AlipayApiException e) { + log.error("订单退款失败:", e); + AsyncRefundLocal.setErrorMsg(e.getErrMsg()); + AsyncRefundLocal.setErrorCode(e.getErrCode()); + throw new PayFailureException("订单退款失败"); + } + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayOrderService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayOrderService.java new file mode 100644 index 00000000..e34e0f78 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayOrderService.java @@ -0,0 +1,126 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.core.channel.alipay.dao.AliPayOrderManager; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AliPayOrder; +import cn.bootx.platform.daxpay.core.order.pay.dao.PayOrderManager; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrderChannel; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayRefundableInfo; +import cn.bootx.platform.daxpay.core.payment.pay.local.AsyncPayInfo; +import cn.bootx.platform.daxpay.core.payment.pay.local.AsyncPayInfoLocal; +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * 支付宝支付订单 + * 1.创建: 支付调起并支付成功后才会创建 + * 2.撤销: 关闭本地支付记录 + * 3.退款: 发起退款时记录 + * + * @author xxm + * @since 2021/2/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayOrderService { + + private final AliPayOrderManager aliPayOrderManager; + + private final PayOrderManager payOrderManager; + + /** + * 支付调起成功 更新payment中异步支付类型信息, 如果支付完成, 创建支付宝支付单 + */ + public void updatePaySuccess(PayOrder payOrder, PayWayParam payWayParam) { + AsyncPayInfo asyncPayInfo = AsyncPayInfoLocal.get(); + payOrder.setAsyncPayMode(true).setAsyncPayChannel(PayChannelEnum.ALI.getCode()); + + // TODO 支付 + List payChannelInfo = new ArrayList<>(); + List refundableInfos = new ArrayList<>(); + // 清除已有的异步支付类型信息 + payChannelInfo.removeIf(payTypeInfo -> PayChannelEnum.ASYNC_TYPE_CODE.contains(payTypeInfo.getPayChannel())); + refundableInfos.removeIf(payTypeInfo -> PayChannelEnum.ASYNC_TYPE_CODE.contains(payTypeInfo.getPayChannel())); + // 更新支付宝支付类型信息 + payChannelInfo.add(new PayChannelInfo().setPayChannel(PayChannelEnum.ALI.getCode()) + .setPayWay(payWayParam.getPayWay()) + .setAmount(payWayParam.getAmount()) + .setExtraParamsJson(payWayParam.getExtraParamsJson())); + payOrder.setPayChannelInfo(payChannelInfo); + // 更新支付宝可退款类型信息 + refundableInfos + .add(new RefundableInfo().setPayChannel(PayChannelEnum.ALI.getCode()).setAmount(payWayParam.getAmount())); + payOrder.setRefundableInfo(refundableInfos); + // 如果支付完成(付款码情况) 调用 updateSyncSuccess 创建支付宝支付记录 + if (Objects.equals(payOrder.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + this.createAliPayment(payOrder, payWayParam, asyncPayInfo.getTradeNo()); + } + } + + /** + * 更新异步支付记录成功状态, 并创建支付宝支付记录 + */ + public void updateAsyncSuccess(Long id, PayWayParam payWayParam, String tradeNo) { + // 更新支付记录 + Payment payment = payOrderManager.findById(id).orElseThrow(() -> new PayFailureException("支付记录不存在")); + this.createAliPayment(payment,payWayParam,tradeNo); + } + + /** + * 创建支付宝支付记录(支付调起成功后才会创建) + */ + private void createAliPayment(Payment payment, PayWayParam payWayParam, String tradeNo) { + // 创建支付宝支付记录 + AliPayOrder aliPayOrder = new AliPayOrder(); + aliPayOrder.setTradeNo(tradeNo) + .setPaymentId(payment.getId()) + .setAmount(payWayParam.getAmount()) + .setRefundableBalance(payWayParam.getAmount()) + .setBusinessNo(payment.getBusinessId()) + .setStatus(PayStatusCode.TRADE_SUCCESS) + .setPayTime(LocalDateTime.now()); + aliPayOrderManager.save(aliPayOrder); + } + + /** + * 取消状态 + */ + public void updateClose(Long paymentId) { + Optional aliPaymentOptional = aliPayOrderManager.findByPaymentId(paymentId); + aliPaymentOptional.ifPresent(aliPayOrder -> { + aliPayOrder.setStatus(PayStatusCode.TRADE_CANCEL); + aliPayOrderManager.updateById(aliPayOrder); + }); + } + + /** + * 更新退款 + */ + public void updatePayRefund(Long paymentId, BigDecimal amount) { + Optional aliPaymentOptional = aliPayOrderManager.findByPaymentId(paymentId); + aliPaymentOptional.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setStatus(PayStatusCode.TRADE_REFUNDING); + } + aliPayOrderManager.updateById(payment); + }); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayService.java new file mode 100644 index 00000000..840fe1b5 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AliPayService.java @@ -0,0 +1,259 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.code.pay.PayWayEnum; +import cn.bootx.platform.daxpay.code.paymodel.AliPayCode; +import cn.bootx.platform.daxpay.code.paymodel.AliPayWay; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import cn.bootx.platform.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.platform.daxpay.exception.payment.PayFailureException; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import cn.bootx.platform.daxpay.util.PayWayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Method; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayResponse; +import com.alipay.api.domain.*; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.*; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static cn.bootx.platform.daxpay.code.paymodel.AliPayCode.BAR_CODE; +import static cn.bootx.platform.daxpay.code.paymodel.AliPayCode.QUICK_MSECURITY_PAY; + +/** + * 支付宝支付service + * + * @author xxm + * @since 2021/2/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayService { + + /** + * 支付前检查支付方式是否可用 + */ + public void validation(PayWayParam payWayParam, AlipayConfig alipayConfig) { + List payWays = Optional.ofNullable(alipayConfig.getPayWays()) + .filter(StrUtil::isNotBlank) + .map(s -> StrUtil.split(s, ',')) + .orElse(new ArrayList<>(1)); + // 发起的支付类型是否在支持的范围内 + PayWayEnum payWayEnum = Optional.ofNullable(AliPayWay.findByCode(payWayParam.getPayWay())) + .orElseThrow(() -> new PayFailureException("非法的支付宝支付类型")); + if (!payWays.contains(payWayEnum.getCode())) { + throw new PayFailureException("该支付宝支付方式不可用"); + } + } + + /** + * 调起支付 + */ + public void pay(BigDecimal amount, Payment payment, AliPayParam aliPayParam, PayWayParam payWayParam, + AlipayConfig alipayConfig) { + String payBody = null; + // 线程存储 + AsyncPayInfo asyncPayInfo = Optional.ofNullable(AsyncPayInfoLocal.get()).orElse(new AsyncPayInfo()); + // wap支付 + if (Objects.equals(payWayParam.getPayWay(), PayWayEnum.WAP.getCode())) { + payBody = this.wapPay(amount, payment, alipayConfig, aliPayParam); + } + // 程序支付 + else if (Objects.equals(payWayParam.getPayWay(), PayWayEnum.APP.getCode())) { + payBody = this.appPay(amount, payment, alipayConfig); + } + // pc支付 + else if (Objects.equals(payWayParam.getPayWay(), PayWayEnum.WEB.getCode())) { + payBody = this.webPay(amount, payment, alipayConfig, aliPayParam); + } + // 二维码支付 + else if (Objects.equals(payWayParam.getPayWay(), PayWayEnum.QRCODE.getCode())) { + payBody = this.qrCodePay(amount, payment, alipayConfig); + } + // 付款码支付 + else if (Objects.equals(payWayParam.getPayWay(), PayWayEnum.BARCODE.getCode())) { + String tradeNo = this.barCode(amount, payment, aliPayParam, alipayConfig); + asyncPayInfo.setExpiredTime(false).setTradeNo(tradeNo); + } + // 通常是发起支付的参数 + asyncPayInfo.setPayBody(payBody); + AsyncPayInfoLocal.set(asyncPayInfo); + } + + /** + * wap支付 + */ + public String wapPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig, AliPayParam aliPayParam) { + + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setTotalAmount(amount.toPlainString()); + // 过期时间 + model.setTimeoutExpress(PayWayUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setProductCode(AliPayCode.QUICK_WAP_PAY); + model.setQuitUrl(aliPayParam.getReturnUrl()); + + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(alipayConfig.getNotifyUrl()); + request.setReturnUrl(aliPayParam.getReturnUrl()); + + try { + // 通过GET方式的请求, 返回URL的响应, 默认是POST方式的请求, 返回的是表单响应 + AlipayTradeWapPayResponse response = AliPayApi.pageExecute(request, Method.GET.name()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝手机支付失败", e); + throw new PayFailureException("支付宝手机支付失败"); + } + } + + /** + * app支付 + */ + public String appPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig) { + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + + model.setSubject(payment.getTitle()); + model.setProductCode(QUICK_MSECURITY_PAY); + model.setOutTradeNo(String.valueOf(payment.getId())); + // 过期时间 + model.setTimeoutExpress(PayWayUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + + try { + AlipayTradeAppPayResponse response = AliPayApi.appPayToResponse(model, alipayConfig.getNotifyUrl()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝APP支付失败", e); + throw new PayFailureException("支付宝APP支付失败"); + } + } + + /** + * PC支付 + */ + public String webPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig, AliPayParam aliPayParam) { + + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + // 过期时间 + model.setTimeoutExpress(PayWayUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + // 目前仅支持FAST_INSTANT_TRADE_PAY + model.setProductCode(AliPayCode.FAST_INSTANT_TRADE_PAY); + + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(alipayConfig.getNotifyUrl()); + request.setReturnUrl(aliPayParam.getReturnUrl()); + try { + // 通过GET方式的请求, 返回URL的响应, 默认是POST方式的请求, 返回的是表单响应 + AlipayTradePagePayResponse response = AliPayApi.pageExecute(request, Method.GET.name()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝PC支付失败", e); + throw new PayFailureException("支付宝PC支付失败"); + } + } + + /** + * 二维码支付(扫码支付) + */ + public String qrCodePay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig) { + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setTotalAmount(amount.toPlainString()); + + // 过期时间 + model.setTimeoutExpress(PayWayUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + + try { + AlipayTradePrecreateResponse response = AliPayApi.tradePrecreatePayToResponse(model, + alipayConfig.getNotifyUrl()); + this.verifyErrorMsg(response); + return response.getQrCode(); + } + catch (AlipayApiException e) { + log.error("支付宝手机支付失败", e); + throw new PayFailureException("支付宝手机支付失败"); + } + } + + /** + * 付款码支付 + */ + public String barCode(BigDecimal amount, Payment payment, AliPayParam aliPayParam, AlipayConfig alipayConfig) { + AlipayTradePayModel model = new AlipayTradePayModel(); + + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setScene(BAR_CODE); + model.setAuthCode(aliPayParam.getAuthCode()); + + // 过期时间 + model.setTimeoutExpress(PayWayUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + + try { + AlipayTradePayResponse response = AliPayApi.tradePayToResponse(model, alipayConfig.getNotifyUrl()); + + // 支付成功处理 金额2000以下免密支付 + if (Objects.equals(response.getCode(), AliPayCode.SUCCESS)) { + payment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + return response.getTradeNo(); + } + // 非支付中响应码, 进行错误处理 + if (!Objects.equals(response.getCode(), AliPayCode.INPROCESS)) { + this.verifyErrorMsg(response); + } + } + catch (AlipayApiException e) { + log.error("主动扫码支付失败", e); + throw new PayFailureException("主动扫码支付失败"); + } + return null; + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(AlipayResponse alipayResponse) { + if (!Objects.equals(alipayResponse.getCode(), AliPayCode.SUCCESS)) { + String errorMsg = alipayResponse.getSubMsg(); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = alipayResponse.getMsg(); + } + log.error("支付失败 {}", errorMsg); + throw new PayFailureException(errorMsg); + } + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipayConfigService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipayConfigService.java new file mode 100644 index 00000000..4b838890 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipayConfigService.java @@ -0,0 +1,131 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.dto.LabelValue; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.daxpay.code.AliPayCode; +import cn.bootx.platform.daxpay.code.AliPayWay; +import cn.bootx.platform.daxpay.core.channel.alipay.dao.AlipayConfigManager; +import cn.bootx.platform.daxpay.core.channel.alipay.entity.AlipayConfig; +import cn.bootx.platform.daxpay.dto.channel.alipay.AlipayConfigDto; +import cn.bootx.platform.daxpay.param.channel.alipay.AlipayConfigParam; +import cn.bootx.platform.daxpay.param.channel.alipay.AlipayConfigQuery; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.util.CharsetUtil; +import com.ijpay.alipay.AliPayApiConfig; +import com.ijpay.alipay.AliPayApiConfigKit; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付宝支付 + * + * @author xxm + * @since 2020/12/15 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AlipayConfigService { + + private final AlipayConfigManager alipayConfigManager; + + /** + * 添加支付宝配置 + */ + @Transactional(rollbackFor = Exception.class) + public void add(AlipayConfigParam param) { + AlipayConfig alipayConfig = AlipayConfig.init(param); + alipayConfigManager.save(alipayConfig); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void update(AlipayConfigParam param) { + AlipayConfig alipayConfig = alipayConfigManager.findById(param.getId()).orElseThrow(DataNotExistException::new); + BeanUtil.copyProperties(param, alipayConfig, CopyOptions.create().ignoreNullValue()); + alipayConfigManager.updateById(alipayConfig); + } + + /** + * 获取 + */ + public AlipayConfigDto findById(Long id) { + return alipayConfigManager.findById(id).map(AlipayConfig::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, AlipayConfigQuery param) { + return null; + } + + /** + * 支付宝支持支付方式 + */ + public List findPayWayList() { + return AliPayWay.getPayWays() + .stream() + .map(e -> new LabelValue(e.getName(),e.getCode())) + .collect(Collectors.toList()); + } + + /** + * 初始化IJPay服务,通过商户应用AppCode获取支付配置 + */ + public void initApiConfigByMchAppCode(String mchAppCode){ + AlipayConfig alipayConfig = null; + this.initApiConfig(alipayConfig); + } + + /** + * 初始化IJPay服务 + */ + @SneakyThrows + public void initApiConfig(AlipayConfig alipayConfig) { + + AliPayApiConfig aliPayApiConfig; + // 公钥 + if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_KEY)) { + aliPayApiConfig = AliPayApiConfig.builder() + .setAppId(alipayConfig.getAppId()) + .setPrivateKey(alipayConfig.getPrivateKey()) + .setAliPayPublicKey(alipayConfig.getAlipayPublicKey()) + .setCharset(CharsetUtil.UTF_8) + .setServiceUrl(alipayConfig.getServerUrl()) + .setSignType(alipayConfig.getSignType()) + .build(); + } + // 证书 + else if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_CART)) { + aliPayApiConfig = AliPayApiConfig.builder() + .setAppId(alipayConfig.getAppId()) + .setPrivateKey(alipayConfig.getPrivateKey()) + .setAppCertContent(alipayConfig.getAppCert()) + .setAliPayCertContent(alipayConfig.getAlipayCert()) + .setAliPayRootCertContent(alipayConfig.getAlipayRootCert()) + .setCharset(CharsetUtil.UTF_8) + .setServiceUrl(alipayConfig.getServerUrl()) + .setSignType(alipayConfig.getSignType()) + .buildByCertContent(); + } + else { + throw new BizException("支付宝认证方式不可为空"); + } + AliPayApiConfigKit.setThreadLocalAliPayApiConfig(aliPayApiConfig); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipaySyncService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipaySyncService.java new file mode 100644 index 00000000..29540980 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/alipay/service/AlipaySyncService.java @@ -0,0 +1,73 @@ +package cn.bootx.platform.daxpay.core.channel.alipay.service; + +import cn.bootx.platform.daxpay.code.AliPayCode; +import cn.bootx.platform.daxpay.code.PaySyncStatus; +import cn.bootx.platform.daxpay.core.payment.sync.result.PaySyncResult; +import cn.hutool.json.JSONUtil; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeQueryModel; +import com.alipay.api.response.AlipayTradeQueryResponse; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Objects; + +/** + * 支付宝同步 + * + * @author xxm + * @since 2021/5/17 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AlipaySyncService { + + /** + * 与支付宝网关同步状态 1 远程支付成功 2 交易创建,等待买家付款 3 超时关闭 4 查询不到 5 查询失败 + */ + public PaySyncResult syncPayStatus(Long paymentId) { + PaySyncResult paySyncResult = new PaySyncResult().setPaySyncStatus(PaySyncStatus.FAIL); + + // 查询 + try { + AlipayTradeQueryModel queryModel = new AlipayTradeQueryModel(); + queryModel.setOutTradeNo(String.valueOf(paymentId)); + // 查询退款参数 + AlipayTradeQueryResponse response = AliPayApi.tradeQueryToResponse(queryModel); + String tradeStatus = response.getTradeStatus(); + paySyncResult.setJson(JSONUtil.toJsonStr(response)); + // 支付完成 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_SUCCESS) + || Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_FINISHED)) { + + HashMap map = new HashMap<>(1); + map.put(AliPayCode.TRADE_NO, response.getTradeNo()); + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_SUCCESS).setMap(map); + } + // 待支付 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_WAIT_BUYER_PAY)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.WAIT_BUYER_PAY); + } + // 已关闭 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_CLOSED)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_CLOSED); + } + // 未找到 + if (Objects.equals(response.getSubCode(), AliPayCode.ACQ_TRADE_NOT_EXIST)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.NOT_FOUND); + } + // 退款 支付宝查不到 + + } + catch (AlipayApiException e) { + log.error("查询订单失败:", e); + paySyncResult.setMsg(e.getErrMsg()); + } + return paySyncResult; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentManager.java new file mode 100644 index 00000000..36ba9fbe --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentManager.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.core.channel.cash.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.cash.entity.CashPayOrder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 现金支付 + * + * @author xxm + * @since 2021/6/23 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class CashPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(CashPayOrder::getPaymentId, paymentId); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentMapper.java new file mode 100644 index 00000000..823b849d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/dao/CashPaymentMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.cash.dao; + +import cn.bootx.platform.daxpay.core.channel.cash.entity.CashPayOrder; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 现金支付 + * + * @author xxm + * @since 2021/6/23 + */ +@Mapper +public interface CashPaymentMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/entity/CashPayOrder.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/entity/CashPayOrder.java new file mode 100644 index 00000000..47e05640 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/entity/CashPayOrder.java @@ -0,0 +1,21 @@ +package cn.bootx.platform.daxpay.core.channel.cash.entity; + +import cn.bootx.platform.daxpay.common.entity.BasePayOrder; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 现金支付记录 + * + * @author xxm + * @since 2021/6/23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("pay_cash_payment") +@Accessors(chain = true) +public class CashPayOrder extends BasePayOrder { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/service/CashService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/service/CashService.java new file mode 100644 index 00000000..7c075470 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/cash/service/CashService.java @@ -0,0 +1,72 @@ +package cn.bootx.platform.daxpay.core.channel.cash.service; + +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.core.channel.cash.dao.CashPaymentManager; +import cn.bootx.platform.daxpay.core.channel.cash.entity.CashPayOrder; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * 现金支付 + * + * @author xxm + * @since 2021/6/23 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CashService { + + private final CashPaymentManager cashPaymentManager; + + /** + * 支付 + */ + public void pay(PayWayParam payMode, PayOrder payment, PayParam payParam) { + CashPayOrder walletPayment = new CashPayOrder(); + walletPayment.setPaymentId(payment.getId()) + .setBusinessNo(payParam.getBusinessNo()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setStatus(payment.getStatus()); + cashPaymentManager.save(walletPayment); + } + + /** + * 关闭 + */ + public void close(Long paymentId) { + Optional cashPaymentOpt = cashPaymentManager.findByPaymentId(paymentId); + cashPaymentOpt.ifPresent(cashPayOrder -> { + cashPayOrder.setStatus(PayStatusCode.TRADE_CANCEL); + cashPaymentManager.updateById(cashPayOrder); + }); + } + + /** + * 退款 + */ + public void refund(Long paymentId, BigDecimal amount) { + Optional cashPayment = cashPaymentManager.findByPaymentId(paymentId); + cashPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setStatus(PayStatusCode.TRADE_REFUNDING); + } + cashPaymentManager.updateById(payment); + }); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/convert/UnionPayConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/convert/UnionPayConvert.java new file mode 100644 index 00000000..4f697cb3 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/convert/UnionPayConvert.java @@ -0,0 +1,19 @@ +package cn.bootx.platform.daxpay.core.channel.union.convert; + +import cn.bootx.platform.daxpay.core.channel.union.entity.UnionPayConfig; +import cn.bootx.platform.daxpay.dto.channel.union.UnionPayConfigDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Mapper +public interface UnionPayConvert { + + UnionPayConvert CONVERT = Mappers.getMapper(UnionPayConvert.class); + + UnionPayConfigDto convert(UnionPayConfig in); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigManager.java new file mode 100644 index 00000000..6bded1d8 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigManager.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.core.channel.union.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.union.entity.UnionPayConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class UnionPayConfigManager extends BaseManager { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigMapper.java new file mode 100644 index 00000000..9a65f795 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayConfigMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.union.dao; + +import cn.bootx.platform.daxpay.core.channel.union.entity.UnionPayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Mapper +public interface UnionPayConfigMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderManager.java new file mode 100644 index 00000000..7ca5a176 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderManager.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.core.channel.union.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.union.entity.UnionPayOrder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class UnionPayOrderManager extends BaseManager { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderMapper.java new file mode 100644 index 00000000..3e0c6434 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/dao/UnionPayOrderMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.union.dao; + +import cn.bootx.platform.daxpay.core.channel.union.entity.UnionPayOrder; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Mapper +public interface UnionPayOrderMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayConfig.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayConfig.java new file mode 100644 index 00000000..449d3979 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayConfig.java @@ -0,0 +1,29 @@ +package cn.bootx.platform.daxpay.core.channel.union.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.core.channel.union.convert.UnionPayConvert; +import cn.bootx.platform.daxpay.dto.channel.union.UnionPayConfigDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 云闪付支付配置 + * + * @author xxm + * @since 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_union_pay_config") +public class UnionPayConfig extends MpBaseEntity implements EntityBaseFunction { + + @Override + public UnionPayConfigDto toDto() { + return UnionPayConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayOrder.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayOrder.java new file mode 100644 index 00000000..906c7f71 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/entity/UnionPayOrder.java @@ -0,0 +1,20 @@ +package cn.bootx.platform.daxpay.core.channel.union.entity; + +import cn.bootx.platform.daxpay.common.entity.BasePayOrder; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 云闪付支付订单 + * @author xxm + * @since 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_union_payment") +public class UnionPayOrder extends BasePayOrder { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayConfigService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayConfigService.java new file mode 100644 index 00000000..27d60b82 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayConfigService.java @@ -0,0 +1,19 @@ +package cn.bootx.platform.daxpay.core.channel.union.service; + +import cn.bootx.platform.daxpay.core.channel.union.dao.UnionPayConfigManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPayConfigService { + + private final UnionPayConfigManager unionPayConfigManager; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayService.java new file mode 100644 index 00000000..12b11f15 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPayService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.union.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPayService { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPaymentService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPaymentService.java new file mode 100644 index 00000000..0305898f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/union/service/UnionPaymentService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.union.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPaymentService { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/convert/VoucherConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/convert/VoucherConvert.java new file mode 100644 index 00000000..ae311033 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/convert/VoucherConvert.java @@ -0,0 +1,27 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.convert; + +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherLog; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherPayment; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherDto; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherLogDto; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherPaymentDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Mapper +public interface VoucherConvert { + + VoucherConvert CONVERT = Mappers.getMapper(VoucherConvert.class); + + VoucherDto convert(Voucher in); + + VoucherLogDto convert(VoucherLog in); + + VoucherPaymentDto convert(VoucherPayment in); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogManager.java new file mode 100644 index 00000000..927c120d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogManager.java @@ -0,0 +1,33 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.base.MpIdEntity; +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherLog; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * @author xxm + * @since 2022/3/19 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherLogManager extends BaseManager { + + /** + * 根据储值卡id进行分页 + */ + public Page pageByVoucherId(PageParam pageParam, Long voucherId) { + Page mpPage = MpUtil.getMpPage(pageParam,VoucherLog.class); + return lambdaQuery() + .eq(VoucherLog::getVoucherId, voucherId) + .orderByDesc(MpIdEntity::getId) + .page(mpPage); + + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogMapper.java new file mode 100644 index 00000000..7c89a04c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherLogMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @since 2022/3/19 + */ +@Mapper +public interface VoucherLogMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherManager.java new file mode 100644 index 00000000..82452839 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherManager.java @@ -0,0 +1,83 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.base.MpDelEntity; +import cn.bootx.platform.common.mybatisplus.base.MpIdEntity; +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import cn.bootx.platform.daxpay.param.channel.voucher.VoucherParam; +import cn.bootx.platform.starter.auth.util.SecurityUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherManager extends BaseManager { + + /** + * 分页 + */ + public Page page(PageParam pageParam, VoucherParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, Voucher.class); + return this.lambdaQuery() + .ge(Objects.nonNull(param.getStartTime()), Voucher::getStartTime, param.getStartTime()) + .le(Objects.nonNull(param.getEndTime()), Voucher::getEndTime, param.getEndTime()) + .eq(Objects.nonNull(param.getEnduring()), Voucher::isEnduring, param.getEnduring()) + .like(StrUtil.isNotBlank(param.getCardNo()), Voucher::getCardNo, param.getCardNo()) + .like(Objects.nonNull(param.getBatchNo()), Voucher::getBatchNo, param.getBatchNo()) + .orderByDesc(MpIdEntity::getId) + .page(mpPage); + } + + /** + * 根据卡号查询 + */ + public Optional findByCardNo(String cardNo) { + return this.findByField(Voucher::getCardNo, cardNo); + } + + /** + * 根据卡号查询 + */ + public List findByCardNoList(List cardNos) { + return this.findAllByFields(Voucher::getCardNo, cardNos); + } + + /** + * 更改状态 + */ + public void changeStatus(Long id, String status) { + Long userIdOrDefaultId = SecurityUtil.getUserIdOrDefaultId(); + this.lambdaUpdate().eq(MpIdEntity::getId, id) + .set(MpDelEntity::getLastModifiedTime, LocalDateTime.now()) + .set(MpDelEntity::getLastModifier,userIdOrDefaultId) + .set(Voucher::getStatus, status).update(); + + } + + /** + * 批量更改状态 + */ + public void changeStatusBatch(List ids, String status) { + Long userIdOrDefaultId = SecurityUtil.getUserIdOrDefaultId(); + this.lambdaUpdate().in(MpIdEntity::getId, ids) + .set(MpDelEntity::getLastModifiedTime, LocalDateTime.now()) + .set(MpDelEntity::getLastModifier,userIdOrDefaultId) + .set(Voucher::getStatus, status).update(); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherMapper.java new file mode 100644 index 00000000..63e52bc7 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Mapper +public interface VoucherMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentManager.java new file mode 100644 index 00000000..d7fb54e9 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentManager.java @@ -0,0 +1,27 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherPaymentManager extends BaseManager { + + /** + * 根据支付id + */ + public Optional findByPaymentId(Long paymentId) { + return this.findByField(VoucherPayment::getPaymentId, paymentId); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentMapper.java new file mode 100644 index 00000000..dc337a69 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/dao/VoucherPaymentMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.dao; + +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Mapper +public interface VoucherPaymentMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/Voucher.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/Voucher.java new file mode 100644 index 00000000..7743de48 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/Voucher.java @@ -0,0 +1,91 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import cn.bootx.platform.daxpay.core.channel.voucher.convert.VoucherConvert; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherDto; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.annotation.DbComment; +import cn.bootx.table.modify.annotation.DbTable; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 储值卡 + * + * @author xxm + * @since 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@DbTable(comment = "储值卡") +@Accessors(chain = true) +@TableName("pay_voucher") +public class Voucher extends MpBaseEntity implements EntityBaseFunction { + + /** 商户编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户编码") + private String mchCode; + + /** 商户应用编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户应用编码") + private String mchAppCode; + + /** 卡号 */ + @DbComment("卡号") + @DbMySqlIndex(comment = "卡号索引") + private String cardNo; + + /** 生成批次号 */ + @DbComment("生成批次号") + private Long batchNo; + + /** 面值 */ + @DbComment("面值") + private BigDecimal faceValue; + + /** 余额 */ + @DbComment("余额") + private BigDecimal balance; + + /** 预冻结额度 */ + @DbColumn(comment = "预冻结额度") + private BigDecimal freezeBalance; + + + /** 是否长期有效 */ + @DbComment("是否长期有效") + private boolean enduring; + + /** 开始时间 */ + @DbComment("开始时间") + private LocalDateTime startTime; + + /** 结束时间 */ + @DbComment("结束时间") + private LocalDateTime endTime; + + /** + * 状态 + * @see VoucherCode#STATUS_FORBIDDEN + */ + @DbComment("状态") + private String status; + + @Override + public VoucherDto toDto() { + return VoucherConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherLog.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherLog.java new file mode 100644 index 00000000..d39da81c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherLog.java @@ -0,0 +1,69 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import cn.bootx.platform.daxpay.core.channel.voucher.convert.VoucherConvert; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherLogDto; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 储值卡日志 + * + * @author xxm + * @since 2022/3/17 + */ +@EqualsAndHashCode(callSuper = true) +@Data +//@DbTable(comment = "储值卡日志") +@Accessors(chain = true) +@TableName("pay_voucher_log") +public class VoucherLog extends MpBaseEntity implements EntityBaseFunction { + + /** 储值卡id */ + @DbMySqlIndex(comment = "储值卡ID") + @DbColumn(comment = "储值卡id") + private Long voucherId; + + /** 储值卡号 */ + @DbColumn(comment = "储值卡号") + private String voucherNo; + + /** 金额 */ + @DbColumn(comment = "金额") + private BigDecimal amount; + + /** + * 类型 + * @see VoucherCode#LOG_PAY + */ + @DbColumn(comment = "类型") + private String type; + + /** 交易记录ID */ + @DbColumn(comment = "交易记录ID") + private Long paymentId; + + /** 业务ID */ + @DbColumn(comment = "业务ID") + private String businessId; + + /** 备注 */ + @DbColumn(comment = "备注") + private String remark; + + /** + * 转换 + */ + @Override + public VoucherLogDto toDto() { + return VoucherConvert.CONVERT.convert(this); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherPayment.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherPayment.java new file mode 100644 index 00000000..64e41756 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherPayment.java @@ -0,0 +1,44 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.handler.JacksonRawTypeHandler; +import cn.bootx.platform.daxpay.core.channel.base.entity.BasePayment; +import cn.bootx.platform.daxpay.core.channel.voucher.convert.VoucherConvert; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherPaymentDto; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType; +import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 储值卡支付记录 + * + * @author xxm + * @since 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +//@DbTable(comment = "储值卡支付记录") +@Accessors(chain = true) +@TableName(value = "pay_voucher_payment",autoResultMap = true) +public class VoucherPayment extends BasePayment implements EntityBaseFunction { + + /** 储值卡扣款记录列表 */ + @DbColumn(comment = "储值卡扣款列表") + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @TableField(typeHandler = JacksonRawTypeHandler.class) + private List voucherRecords; + + + @Override + public VoucherPaymentDto toDto() { + return VoucherConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherRecord.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherRecord.java new file mode 100644 index 00000000..fec3c3c5 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/entity/VoucherRecord.java @@ -0,0 +1,22 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.entity; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 储值卡扣款记录列表 + * @author xxm + * @since 2023/6/29 + */ +@Data +@Accessors(chain = true) +public class VoucherRecord { + + /** 卡号 */ + private String cardNo; + + /** 扣款金额 */ + private BigDecimal amount; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherLogService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherLogService.java new file mode 100644 index 00000000..378b8bea --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherLogService.java @@ -0,0 +1,32 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.service; + +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherLogManager; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherLogDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 储值卡日志 + * + * @author xxm + * @since 2022/3/19 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherLogService { + + private final VoucherLogManager voucherLogManager; + + /** + * 储值卡日志分页 + */ + public PageResult pageByVoucherId(PageParam pageParam, Long voucherId){ + return MpUtil.convert2DtoPageResult(voucherLogManager.pageByVoucherId(pageParam,voucherId)); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPayService.java new file mode 100644 index 00000000..09af755b --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPayService.java @@ -0,0 +1,386 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.service; + +import cn.bootx.platform.common.core.function.CollectorsFunction; +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherLogManager; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherManager; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherPaymentManager; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherLog; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherPayment; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherRecord; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.exception.payment.PayFailureException; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 储值卡支付 + * + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherPayService { + + private final VoucherManager voucherManager; + + private final VoucherPaymentManager voucherPaymentManager; + + private final VoucherLogManager voucherLogManager; + + private final VoucherQueryService voucherQueryService; + + /** + * 获取并检查储值卡 + */ + public List getAndCheckVoucher(PayWayParam payWayParam) { + VoucherPayParam voucherPayParam; + try { + // 储值卡参数验证 + String extraParamsJson = payWayParam.getExtraParamsJson(); + if (StrUtil.isNotBlank(extraParamsJson)) { + voucherPayParam = JSONUtil.toBean(extraParamsJson, VoucherPayParam.class); + } else { + throw new PayFailureException("储值卡支付参数错误"); + } + } + catch (JSONException e) { + throw new PayFailureException("储值卡支付参数错误"); + } + + List cardNoList = voucherPayParam.getCardNoList(); + List vouchers = voucherManager.findByCardNoList(cardNoList); + + + // 判断是否有重复or无效的储值卡 + if (vouchers.size() != cardNoList.size()) { + throw new PayFailureException("储值卡支付参数错误"); + } + // 卡信息校验 + String timeCheck = voucherQueryService.check(vouchers); + if (StrUtil.isNotBlank(timeCheck)) { + throw new PayFailureException(timeCheck); + } + // 金额是否满足 + BigDecimal amount = vouchers.stream().map(Voucher::getBalance).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + if (BigDecimalUtil.compareTo(amount, payWayParam.getAmount()) < 0) { + throw new PayFailureException("储值卡余额不足"); + } + + return sort(vouchers); + } + + /** + * 支付前冻结余额 (异步组合支付环境下) + */ + public List freezeBalance(BigDecimal amount, Payment payment, List vouchers){ + List voucherLogs = new ArrayList<>(); + List voucherRecords = new ArrayList<>(); + for (Voucher voucher : vouchers) { + // 待支付余额为零, 不在处理后面的储值卡 + if (BigDecimalUtil.compareTo(amount, BigDecimal.ZERO) < 1) { + break; + } + BigDecimal balance = voucher.getBalance(); + // 日志 + VoucherLog voucherLog = new VoucherLog() + .setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()) + .setType(VoucherCode.LOG_FREEZE_BALANCE) + .setRemark(String.format("预冻结金额 %.2f ", amount)) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()); + + // 待支付额大于储值卡余额. 储值卡的金额全冻结 + if (BigDecimalUtil.compareTo(amount, balance) == 1) { + voucher.setFreezeBalance(balance); + voucherLog.setAmount(balance); + // 剩余待支付的金额 + amount = amount.subtract(balance); + } + else { + // 待支付额小于或储值卡余额, 冻结待支付的金额 + voucher.setFreezeBalance(amount); + voucherLog.setAmount(amount); + // 待支付的金额扣减完毕, 值设置为零 + amount = BigDecimal.ZERO; + } + voucherRecords.add(new VoucherRecord().setCardNo(voucher.getCardNo()).setAmount(voucher.getFreezeBalance())); + voucherLogs.add(voucherLog); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(voucherLogs); + return voucherRecords; + } + + /** + * 支付成功, 对冻结的金额进行扣款 (异步组合支付环境下) + */ + public void paySuccess(Long paymentId){ + voucherPaymentManager.findByPaymentId(paymentId).ifPresent(voucherPayment -> { + List voucherRecords = voucherPayment.getVoucherRecords(); + + List cardNoList = voucherRecords.stream() + .map(VoucherRecord::getCardNo) + .collect(Collectors.toList()); + List vouchers = voucherManager.findByCardNoList(cardNoList); + List voucherLogs = new ArrayList<>(); + for (Voucher voucher : vouchers) { + // 余额扣减 + voucher.setBalance(voucher.getBalance().subtract(voucher.getFreezeBalance())) + .setFreezeBalance(BigDecimal.ZERO); + // 日志 + VoucherLog voucherLog = new VoucherLog() + .setPaymentId(voucherPayment.getPaymentId()) + .setBusinessId(voucherPayment.getBusinessId()) + .setType(VoucherCode.LOG_REDUCE_AND_UNFREEZE_BALANCE) + .setRemark(String.format("扣款金额 %.2f ", voucherPayment.getAmount())) + .setVoucherId(voucher.getId()) + .setAmount(voucher.getFreezeBalance()) + .setVoucherNo(voucher.getCardNo()); + voucherLogs.add(voucherLog); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(voucherLogs); + }); + } + + /** + * 直接支付 (同步支付方式下) + */ + @Transactional(rollbackFor = Exception.class) + public List pay(BigDecimal amount, Payment payment, List vouchers) { + List voucherLogs = new ArrayList<>(); + List voucherRecords = new ArrayList<>(); + for (Voucher voucher : vouchers) { + // 待支付余额为零, 不在处理后面的储值卡 + if (BigDecimalUtil.compareTo(amount, BigDecimal.ZERO) < 1) { + break; + } + BigDecimal balance = voucher.getBalance(); + // 日志 + VoucherLog voucherLog = new VoucherLog() + .setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()) + .setType(VoucherCode.LOG_PAY) + .setRemark(String.format("支付金额 %.2f ", amount)) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()); + + // 记录当前卡扣了多少钱 + BigDecimal amountRecord; + // 待支付额大于储值卡余额. 储值卡金额全扣光 + if (BigDecimalUtil.compareTo(amount, balance) == 1) { + amountRecord = voucher.getBalance(); + voucher.setBalance(BigDecimal.ZERO); + voucherLog.setAmount(balance); + amount = amount.subtract(balance); + } + else { + // 待支付额小于或储值卡余额, 储值卡余额-待支付额 + amountRecord = balance.subtract(amount); + voucher.setBalance(balance.subtract(amount)); + voucherLog.setAmount(amount); + // 支付完毕, 待支付金额归零 + amount = BigDecimal.ZERO; + } + voucherLogs.add(voucherLog); + voucherRecords.add(new VoucherRecord().setCardNo(voucher.getCardNo()).setAmount(amountRecord)); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(voucherLogs); + return voucherRecords; + } + + + /** + * 取消支付,解除冻结的额度, 只有在异步支付中才会发生取消操作 + */ + public void close(Long paymentId) { + voucherPaymentManager.findByPaymentId(paymentId).ifPresent(voucherPayment -> { + List cardNoList = voucherPayment.getVoucherRecords().stream() + .map(VoucherRecord::getCardNo) + .collect(Collectors.toList()); + List vouchers = voucherManager.findByCardNoList(cardNoList); + // 解冻额度和记录日志 + List logs = new ArrayList<>(); + for (Voucher voucher : vouchers) { + VoucherLog log = new VoucherLog().setAmount(voucher.getFreezeBalance()) + .setPaymentId(paymentId) + .setBusinessId(voucherPayment.getBusinessId()) + .setVoucherId(voucher.getId()) + .setRemark(String.format("取消支付金额 %.2f ", voucher.getFreezeBalance())) + .setVoucherNo(voucher.getCardNo()) + .setType(VoucherCode.LOG_CLOSE_PAY); + logs.add(log); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(logs); + }); + } + + /** + * 退款 + * 全额退款支持分别退回到原有卡和统一退回到某张卡中, 默认退回到原有卡中 + * 部分退款, 会退到指定的一张卡上, 如果不指定, 则自动退到有效期最久的卡上 + */ + @Transactional(rollbackFor = Exception.class) + public void refund(Long paymentId, BigDecimal amount, VoucherRefundParam voucherRefundParam) { + VoucherPayment voucherPayment = voucherPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new PayFailureException("储值卡支付记录不存在")); + // 全部退款还是部分退款 + if (BigDecimalUtil.compareTo(amount,voucherPayment.getAmount())==0){ + // 是否全部退到一张卡中 + if (voucherRefundParam.isRefundToOne()){ + this.refundToOne(voucherPayment, voucherRefundParam.getRefundVoucherNo()); + } else { + // 退回到原有卡中 + this.refundToRaw(voucherPayment); + } + } else { + this.refundToOne(voucherPayment, voucherRefundParam.getRefundVoucherNo()); + } + } + + /** + * 全部退款到一张卡中 + */ + private void refundToOne(VoucherPayment voucherPayment, String refundVoucherNo){ + // 获取储值卡扣款信息 + List voucherRecords = voucherPayment.getVoucherRecords(); + Map voucherRecordMap = voucherRecords.stream() + .collect(Collectors.toMap(VoucherRecord::getCardNo, VoucherRecord::getAmount, CollectorsFunction::retainLatest)); + List cardNoList = voucherRecords.stream() + .map(VoucherRecord::getCardNo) + .collect(Collectors.toList()); + List vouchers = voucherManager.findByCardNoList(cardNoList); + // 如果未传入卡号, 默认退到最抗用的一张卡上 + if (StrUtil.isBlank(refundVoucherNo)){ + List sort = this.sort(vouchers); + refundVoucherNo = sort.get(sort.size()-1).getCardNo(); + } + + // 筛选出来要进行退款的卡 + String finalRefundVoucherNo = refundVoucherNo; + Voucher voucher = vouchers.stream() + .filter(vr -> Objects.equals(vr.getCardNo(), finalRefundVoucherNo)) + .findFirst() + .orElseThrow(() -> new PayFailureException("退款卡号不存在")); + // 将金额全部推到指定的卡上 + voucher.setBalance(voucher.getBalance().add(voucherPayment.getAmount())); + // 记录日志 + List voucherLogs = new ArrayList<>(); + for (Voucher v : vouchers) { + BigDecimal voucherAmount = voucherRecordMap.get(v.getCardNo()); + VoucherLog voucherLog = new VoucherLog() + .setType(VoucherCode.LOG_REFUND_SELF) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()) + .setAmount(voucherAmount) + .setPaymentId(voucherPayment.getPaymentId()) + .setBusinessId(voucherPayment.getBusinessId()) + .setRemark(String.format("退款金额 %.2f, 退款到卡号: %s 储值卡中", voucherAmount, voucher.getCardNo())); + // 接收退款的卡 + if (Objects.equals(v,voucher)){ + voucherLog.setAmount(voucherPayment.getAmount()) + .setType(VoucherCode.LOG_REFUND_SELF) + .setRemark(String.format("退款金额 %.2f, 退款到卡号: %s 储值卡中", voucherAmount, voucher.getCardNo())); + } + } + voucherManager.updateById(voucher); + voucherLogManager.saveAll(voucherLogs); + } + + /** + * 退款到原有的卡中 + */ + private void refundToRaw(VoucherPayment voucherPayment){ + // 获取储值卡扣款信息 + List voucherRecords = voucherPayment.getVoucherRecords(); + List cardNoList = voucherRecords.stream() + .map(VoucherRecord::getCardNo) + .collect(Collectors.toList()); + Map voucherRecordMap = voucherRecords.stream() + .collect(Collectors.toMap(VoucherRecord::getCardNo, VoucherRecord::getAmount, CollectorsFunction::retainLatest)); + List vouchers = voucherManager.findByCardNoList(cardNoList); + + // 退款 和 记录日志 + List voucherLogs = new ArrayList<>(); + for (Voucher voucher : vouchers) { + BigDecimal voucherAmount = voucherRecordMap.get(voucher.getCardNo()); + voucher.setBalance(voucherAmount.add(voucher.getBalance())); + + voucherLogs.add(new VoucherLog() + .setType(VoucherCode.LOG_REFUND_SELF) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()) + .setAmount(voucherAmount) + .setPaymentId(voucherPayment.getPaymentId()) + .setBusinessId(voucherPayment.getBusinessId()) + .setRemark(String.format("退款金额 %.2f ", voucherAmount))); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(voucherLogs); + } + + + /** + * 对储值卡进行排序 + * 有期限的在前面, 同样有期限到期时间短的在前面, 同样到期日余额小的在前面, 金额一样id小的前面 + */ + private List sort(List vouchers){ + vouchers.sort(this::compareTime); + return vouchers; + } + + /** + * 比较储值卡的期限 + */ + private int compareTime(Voucher v1,Voucher v2){ + // 期限对比, 都为长期 + if (v1.isEnduring()&&v2.isEnduring()){ + // 比较余额 + return compareBalance(v1,v2); + } + // 都不为长期, 且金额一致 + if (Objects.equals(v1.getEndTime(),v2.getEndTime())){ + // 比较余额 + return compareBalance(v1,v2); + } + // 期限对比 其中一个为长期 + if (v1.isEnduring()^v2.isEnduring()){ + return v1.isEnduring()?1:-1; + } + // 比较期限 + return v1.getEndTime().compareTo(v2.getEndTime()); + } + + /** + * 比较储值卡的余额, 余额一致比较主键 + */ + private int compareBalance(Voucher v1,Voucher v2) { + int i = BigDecimalUtil.compareTo(v1.getBalance(), v2.getBalance()); + if (i==0){ + return Long.compare(v1.getId(),v2.getId()); + } + return i; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPaymentService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPaymentService.java new file mode 100644 index 00000000..6db4bbb4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherPaymentService.java @@ -0,0 +1,87 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherPaymentManager; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherPayment; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherRecord; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 储值卡支付记录 + * + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherPaymentService { + + private final VoucherPaymentManager voucherPaymentManager; + + /** + * 添加支付记录 + */ + public void savePayment(Payment payment, PayParam payParam, PayWayParam payMode, List voucherRecords) { + VoucherPayment voucherPayment = new VoucherPayment().setVoucherRecords(voucherRecords); + voucherPayment.setPaymentId(payment.getId()) + .setBusinessId(payParam.getBusinessId()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setPayStatus(payment.getPayStatus()); + voucherPaymentManager.save(voucherPayment); + } + + /** + * 更新成功状态 + */ + public void updateSuccess(Long paymentId) { + Optional payment = voucherPaymentManager.findByPaymentId(paymentId); + if (payment.isPresent()) { + VoucherPayment voucherPayment = payment.get(); + voucherPayment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + voucherPaymentManager.updateById(voucherPayment); + } + } + + /** + * 关闭操作 + */ + public void updateClose(Long paymentId) { + VoucherPayment payment = voucherPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("未查询到查询交易记录")); + payment.setPayStatus(PayStatusCode.TRADE_CANCEL); + voucherPaymentManager.updateById(payment); + } + + /** + * 更新退款 + */ + public void updateRefund(Long paymentId, BigDecimal amount) { + Optional voucherPayment = voucherPaymentManager.findByPaymentId(paymentId); + voucherPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + voucherPaymentManager.updateById(payment); + }); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherQueryService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherQueryService.java new file mode 100644 index 00000000..63480e0d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherQueryService.java @@ -0,0 +1,117 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.service; + +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.core.util.LocalDateTimeUtil; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherManager; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import cn.bootx.platform.daxpay.core.merchant.service.MchAppService; +import cn.bootx.platform.daxpay.dto.channel.voucher.VoucherDto; +import cn.bootx.platform.daxpay.exception.payment.PayFailureException; +import cn.bootx.platform.daxpay.param.channel.voucher.VoucherParam; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 储值卡查询 + * + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherQueryService { + + private final VoucherManager voucherManager; + + private final MchAppService mchAppService; + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, VoucherParam param) { + return MpUtil.convert2DtoPageResult(voucherManager.page(pageParam, param)); + } + + /** + * 根据id查询 + */ + public VoucherDto findById(Long id) { + return voucherManager.findById(id).map(Voucher::toDto).orElseThrow(() -> new DataNotExistException("储值卡不存在")); + } + + /** + * 根据卡号查询 + */ + public VoucherDto findByCardNo(String cardNo) { + return voucherManager.findByCardNo(cardNo) + .map(Voucher::toDto) + .orElseThrow(() -> new DataNotExistException("储值卡不存在")); + } + + /** + * 获取并判断卡状态 + */ + public VoucherDto getAndJudgeVoucher(String cardNo){ + Voucher voucher = voucherManager.findByCardNo(cardNo) + .orElseThrow(() -> new DataNotExistException("储值卡不存在")); + // 过期 + String checkMsg = check(Collections.singletonList(voucher)); + if (StrUtil.isNotBlank(checkMsg)){ + throw new PayFailureException(checkMsg); + } + return voucher.toDto(); + } + + /** + * 卡信息检查 + */ + public String check(List vouchers) { + // 判断有效期 + boolean timeCheck = vouchers.stream() + .filter(voucher -> !Objects.equals(voucher.isEnduring(), true)) + .allMatch(voucher -> LocalDateTimeUtil.between(LocalDateTime.now(), voucher.getStartTime(), + voucher.getEndTime())); + if (!timeCheck) { + return "储值卡不再有效期内"; + } + // 判断状态 + boolean statusCheck = vouchers.stream() + .allMatch(voucher -> Objects.equals(voucher.getStatus(), VoucherCode.STATUS_NORMAL)); + if (!statusCheck){ + return "储值卡不是启用状态"; + } + // 判断是否是同一个商户应用下的储值卡 + List mchCodes = vouchers.stream() + .map(Voucher::getMchCode) + .distinct() + .collect(Collectors.toList()); + List mchAppCodes = vouchers.stream() + .map(Voucher::getMchAppCode) + .distinct() + .collect(Collectors.toList()); + if (mchAppCodes.size()!=1 || mchCodes.size()!=1){ + return "这些储值卡不止属于一个商户应用"; + } + String mchCode = mchCodes.get(0); + String mchAppCode = mchAppCodes.get(0); + // 是否有关联关系判断 + if (!mchAppService.checkMatch(mchCode, mchAppCode)) { + return "应用信息与商户信息不匹配"; + } + return null; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherService.java new file mode 100644 index 00000000..7a2e5aad --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/voucher/service/VoucherService.java @@ -0,0 +1,164 @@ +package cn.bootx.platform.daxpay.core.channel.voucher.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherLogManager; +import cn.bootx.platform.daxpay.core.channel.voucher.dao.VoucherManager; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.Voucher; +import cn.bootx.platform.daxpay.core.channel.voucher.entity.VoucherLog; +import cn.bootx.platform.daxpay.core.merchant.service.MchAppService; +import cn.bootx.platform.daxpay.param.channel.voucher.VoucherImportParam; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 储值卡 + * + * @author xxm + * @since 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherService { + + private final VoucherManager voucherManager; + + private final VoucherLogManager voucherLogManager; + + private final MchAppService mchAppService; + + /** + * 批量生成 + */ + @Transactional(rollbackFor = Exception.class) + public void generationBatch(VoucherGenerationParam param) { + // 是否有管理关系判断 + if (!mchAppService.checkMatch(param.getMchCode(), param.getMchAppCode())) { + throw new BizException("应用信息与商户信息不匹配"); + } + + Integer count = param.getCount(); + List vouchers = new ArrayList<>(count); + long batchNo = IdUtil.getSnowflakeNextId(); + for (int i = 0; i < count; i++) { + Voucher voucher = new Voucher() + .setCardNo('V' + IdUtil.getSnowflakeNextIdStr()) + .setMchCode(param.getMchCode()) + .setMchAppCode(param.getMchAppCode()) + .setBatchNo(batchNo) + .setBalance(param.getFaceValue()) + .setFaceValue(param.getFaceValue()) + .setEnduring(param.getEnduring()) + .setStatus(param.getStatus()); + if (Objects.equals(param.getEnduring(), Boolean.FALSE)) { + voucher.setStartTime(param.getStartTime()).setEndTime(param.getEndTime()); + } + vouchers.add(voucher); + } + voucherManager.saveAll(vouchers); + // 日志 + List voucherLogs = vouchers.stream() + .map(voucher -> new VoucherLog().setType(VoucherCode.LOG_ACTIVE) + .setAmount(voucher.getBalance()) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo())) + .collect(Collectors.toList()); + voucherLogManager.saveAll(voucherLogs); + } + + /** + * 批量导入 + * @param skip 是否跳过已经导入的储值卡,false时将会异常 + */ + @Transactional(rollbackFor = Exception.class) + public void importBatch(Boolean skip, String mchCode, String mchAppCode,List voucherImports) { + // 是否有关联关系判断 + if (!mchAppService.checkMatch(mchCode, mchAppCode)) { + throw new BizException("应用信息与商户信息不匹配"); + } + List cardNoList = voucherImports.stream() + .map(VoucherImportParam::getCardNo) + .distinct() + .collect(Collectors.toList()); + // 卡号不能重复 + if (voucherImports.size()!=cardNoList.size()){ + throw new BizException("卡号不能重复"); + } + // 查询库中是否已经有对应的储值卡号 + List vouchersByDB = voucherManager.findByCardNoList(cardNoList); + // 不跳过已经导入的储值卡且存在数据, 抛出异常 + if (Objects.equals(skip,true)&& CollUtil.isNotEmpty(vouchersByDB)){ + log.warn("数据库中已经存在的卡号:{}",vouchersByDB.stream().map(Voucher::getCardNo).collect(Collectors.toList())); + throw new BizException("要导入的卡号在数据中已经存在"); + } + // 导入对应储值卡 + List cardNoListByDb = vouchersByDB.stream() + .map(Voucher::getCardNo) + .distinct() + .collect(Collectors.toList()); + long batchNo = IdUtil.getSnowflakeNextId(); + List vouchers = voucherImports.stream() + .filter(o -> !cardNoListByDb.contains(o.getCardNo())) + .map(o -> { + Voucher voucher = new Voucher(); + BeanUtil.copyProperties(o, voucher); + return voucher.setMchCode(mchCode) + .setMchAppCode(mchAppCode) + .setBatchNo(batchNo); + }) + .collect(Collectors.toList()); + voucherManager.saveAll(vouchers); + // TODO 记录日志 + + } + + /** + * 启用 + */ + public void unlock(Long id) { + voucherManager.changeStatus(id, VoucherCode.STATUS_NORMAL); + } + + /** + * 冻结 + */ + public void lock(Long id) { + voucherManager.changeStatus(id, VoucherCode.STATUS_FORBIDDEN); + } + + /** + * 批量启用 + */ + public void unlockBatch(List ids) { + voucherManager.changeStatusBatch(ids, VoucherCode.STATUS_NORMAL); + } + + /** + * 批量冻结 + */ + public void lockBatch(List ids) { + voucherManager.changeStatusBatch(ids, VoucherCode.STATUS_FORBIDDEN); + } + + /** + * 更改储值卡信息 + */ + public void changeInfo(VoucherChangeParam voucherChangeParam) { + // 查询对应的卡 + Voucher voucher = voucherManager.findByCardNo(voucherChangeParam.getCardNo()) + .orElseThrow(DataNotExistException::new); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/convert/WalletConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/convert/WalletConvert.java new file mode 100644 index 00000000..f91ebfd0 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/convert/WalletConvert.java @@ -0,0 +1,36 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.convert; + +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletConfig; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletLog; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletPayment; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletConfigDto; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletDto; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletLogDto; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletPaymentDto; +import cn.bootx.platform.daxpay.param.channel.wechat.WalletConfigParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 转换 + * + * @author xxm + * @since 2021/8/20 + */ +@Mapper +public interface WalletConvert { + + WalletConvert CONVERT = Mappers.getMapper(WalletConvert.class); + + WalletDto convert(Wallet in); + + WalletPaymentDto convert(WalletPayment in); + + WalletLogDto convert(WalletLog in); + + WalletConfigDto convert(WalletConfig in); + + WalletConfig convert(WalletConfigParam in); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigManager.java new file mode 100644 index 00000000..1867aeaa --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigManager.java @@ -0,0 +1,24 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 钱包配置 + * @author xxm + * @since 2023/7/14 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class WalletConfigManager extends BaseManager { + + public Optional findByMchCode(String mchCode){ + return this.findByField(WalletConfig::getMchCode,mchCode); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigMapper.java new file mode 100644 index 00000000..27ada2c2 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletConfigMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钱包配置 + * @author xxm + * @since 2023/7/14 + */ +@Mapper +public interface WalletConfigMapper extends BaseMapper { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogManager.java new file mode 100644 index 00000000..53f28e5f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogManager.java @@ -0,0 +1,56 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.base.MpIdEntity; +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletLog; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletLogQueryParam; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Objects; + +/** + * 钱包日志 + * + * @author xxm + * @since 2020/12/8 + */ +@Repository +@RequiredArgsConstructor +public class WalletLogManager extends BaseManager { + + /** + * 分页查询指定用户的钱包日志 + */ + public Page pageByUserId(PageParam pageParam, WalletLogQueryParam param, Long userId) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + return this.lambdaQuery().orderByDesc(MpIdEntity::getId).eq(WalletLog::getUserId, userId).page(mpPage); + } + + /** + * 分页查询 + */ + public Page page(PageParam pageParam, WalletLogQueryParam query) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + return this.lambdaQuery() + .orderByDesc(MpIdEntity::getId) + .like(Objects.nonNull(query.getUserId()), WalletLog::getUserId, query.getUserId()) + .like(Objects.nonNull(query.getWalletId()), WalletLog::getWalletId, query.getWalletId()) + .page(mpPage); + } + + /** + * 分页查询 根据钱包id + */ + public Page pageByWalletId(PageParam pageParam, WalletLogQueryParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + return this.lambdaQuery() + .orderByDesc(MpIdEntity::getId) + .eq(WalletLog::getWalletId, param.getWalletId()) + .page(mpPage); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogMapper.java new file mode 100644 index 00000000..9fabe6ed --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletLogMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钱包日志 + * + * @author xxm + * @since 2020/12/8 + */ +@Mapper +public interface WalletLogMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletManager.java new file mode 100644 index 00000000..10cfc89d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletManager.java @@ -0,0 +1,161 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.common.query.generator.QueryGenerator; +import cn.bootx.platform.daxpay.core.channel.config.entity.PayChannelConfig; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletQueryParam; +import cn.bootx.platform.iam.core.user.entity.UserInfo; +import cn.bootx.platform.iam.param.user.UserInfoParam; +import cn.bootx.platform.starter.auth.util.SecurityUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 钱包管理 + * + * @author xxm + * @since 2020/12/8 + */ +@Repository +@RequiredArgsConstructor +public class WalletManager extends BaseManager { + + private final WalletMapper walletMapper; + + /** + * 预占额度 + * @param walletId 钱包ID + * @param amount 金额 + * @return 更新数量 + */ + public int freezeBalance(Long walletId, BigDecimal amount){ + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.freezeBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 解冻金额 + * @param walletId 钱包ID + * @param amount 金额 + * @return 更新数量 + */ + public int unfreezeBalance(Long walletId, BigDecimal amount){ + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.unfreezeBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 扣减余额 + * @param walletId 钱包ID + * @param amount 扣减金额 + * @return 操作条数 + */ + public int reduceBalance(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.reduceBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 扣减余额同时解除预冻结的额度 + * @param walletId 钱包ID + * @param amount 扣减金额 + * @return 操作条数 + */ + public int reduceAndUnfreezeBalance(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.reduceAndUnfreezeBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 增加余额 + * @param walletId 钱包 + * @param amount 金额 + * @return 更新数量 + */ + public int increaseBalance(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.increaseBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 更改余额-允许扣成负数 + * @param walletId 钱包ID + * @param amount 更改的额度, 正数增加,负数减少 + * @return 剩余条数 + */ + public int reduceBalanceUnlimited(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.reduceBalanceUnlimited(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 用户钱包是否存在 + */ + public boolean existsByUser(Long userId,String mchAppCode) { + return lambdaQuery() + .eq(Wallet::getUserId,userId) + .eq(Wallet::getMchAppCode,mchAppCode) + .exists(); + } + + /** + * 查询用户的钱包 + */ + public Optional findByUser(Long userId,String mchAppCode) { + return lambdaQuery() + .eq(Wallet::getUserId,userId) + .eq(Wallet::getMchAppCode,mchAppCode) + .oneOpt(); + } + + /** + * 分页查询 + */ + public Page page(PageParam pageParam, WalletQueryParam param) { + QueryWrapper wrapper = QueryGenerator.generator(param); + Page mpPage = MpUtil.getMpPage(pageParam, Wallet.class); + wrapper.select(this.getEntityClass(), MpUtil::excludeBigField) + .orderByDesc(MpUtil.getColumnName(PayChannelConfig::getId)); + return this.page(mpPage, wrapper); + } + + /** + * 待开通钱包的用户列表 + */ + public Page pageByNotWallet(PageParam pageParam, String mchCode, UserInfoParam userInfoParam) { + Page mpPage = MpUtil.getMpPage(pageParam, UserInfo.class); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.orderByDesc("w.id") + .and(o->o.ne("w.mch_code",mchCode).or().isNull("w.mch_code")) + .like(StrUtil.isNotBlank(userInfoParam.getUsername()), "u.username", userInfoParam.getUsername()) + .like(StrUtil.isNotBlank(userInfoParam.getName()), "u.name", userInfoParam.getName()); + return walletMapper.pageByNotWallet(mpPage, wrapper); + } + + /** + * 查询已经存在钱包的用户id + */ + public List findExistUserIds(List userIds) { + return this.lambdaQuery() + .select(Wallet::getUserId) + .in(Wallet::getUserId, userIds) + .list() + .stream() + .map(Wallet::getUserId) + .collect(Collectors.toList()); + + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletMapper.java new file mode 100644 index 00000000..20958eec --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletMapper.java @@ -0,0 +1,96 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.iam.core.user.entity.UserInfo; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 钱包 + * + * @author xxm + * @since 2021/2/24 + */ +@Mapper +public interface WalletMapper extends BaseMapper { + + /** + * 预冻结余额 + * @param walletId 钱包ID + * @param amount 冻结的额度 + * @param operator 操作人 + * @param date 时间 + * @return 更新数量 + */ + int freezeBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 解冻金额 + * @param walletId 钱包ID + * @param amount 冻结的额度 + * @param operator 操作人 + * @param date 时间 + * @return 更新数量 + */ + int unfreezeBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 释放预占额度 + * @param walletId 钱包ID + * @param amount 要释放的额度 + * @param operator 操作人 + * @param date 操作时间 + * @return 操作条数 + */ + int reduceBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 扣减余额同时解除预冻结的额度 + * @param walletId 钱包ID + * @param amount 减少的额度 + * @param operator 操作人 + * @param date 操作时间 + * @return 操作条数 + */ + int reduceAndUnfreezeBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + + /** + * 增加余额 + * @param walletId 钱包ID + * @param amount 增加的额度 + * @param operator 操作人 + * @param date 时间 + * @return 更新数量 + */ + int increaseBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 扣减余额,允许扣成负数 + * @param walletId 钱包ID + * @param amount 更改的额度, 正数增加,负数减少 + * @param operator 操作人 + * @param date 操作时间 + * @return 操作条数 + */ + int reduceBalanceUnlimited(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 待开通钱包的用户列表 + */ + Page pageByNotWallet(Page mpPage, @Param(Constants.WRAPPER) Wrapper wrapper); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentManager.java new file mode 100644 index 00000000..718c9ef3 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentManager.java @@ -0,0 +1,19 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.base.entity.BasePayment; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletPayment; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class WalletPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(BasePayment::getPaymentId, paymentId); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentMapper.java new file mode 100644 index 00000000..291064b5 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/dao/WalletPaymentMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.dao; + +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WalletPaymentMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/Wallet.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/Wallet.java new file mode 100644 index 00000000..50445b98 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/Wallet.java @@ -0,0 +1,75 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.code.paymodel.WalletCode; +import cn.bootx.platform.daxpay.core.channel.wallet.convert.WalletConvert; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletDto; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.annotation.DbTable; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import cn.bootx.table.modify.mysql.constants.MySqlIndexType; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import lombok.experimental.FieldNameConstants; + +import java.math.BigDecimal; + +import static cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet.Fields.mchAppCode; +import static cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet.Fields.userId; + +/** + * 钱包 + * + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@DbMySqlIndex(comment = "用户和应用编码联合唯一索引",fields = {userId,mchAppCode},type = MySqlIndexType.UNIQUE) +@DbTable(comment = "钱包") +@Accessors(chain = true) +@TableName("pay_wallet") +@FieldNameConstants +public class Wallet extends MpBaseEntity implements EntityBaseFunction { + + /** 商户编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户编码") + private String mchCode; + + /** 商户应用编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户应用编码") + private String mchAppCode; + + /** 关联用户id */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "关联用户id") + private Long userId; + + /** 余额 */ + @DbColumn(comment = "余额") + private BigDecimal balance; + + /** 预冻结额度 */ + @DbColumn(comment = "预冻结额度") + private BigDecimal freezeBalance; + + /** + * 状态 + * @see WalletCode#STATUS_FORBIDDEN + */ + @DbColumn(comment = "状态") + private String status; + + @Override + public WalletDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletConfig.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletConfig.java new file mode 100644 index 00000000..971fc3d1 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletConfig.java @@ -0,0 +1,66 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.core.channel.wallet.convert.WalletConvert; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletConfigDto; +import cn.bootx.platform.daxpay.param.channel.wechat.WalletConfigParam; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.annotation.DbTable; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 钱包配置 + * @author xxm + * @since 2023/7/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@DbTable(comment = "钱包配置") +@Accessors(chain = true) +@TableName("pay_wallet_config") +public class WalletConfig extends MpBaseEntity implements EntityBaseFunction { + + /** 商户编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户编码") + private String mchCode; + + /** 商户应用编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbMySqlIndex(comment = "商户应用编码唯一索引") + @DbColumn(comment = "商户应用编码") + private String mchAppCode; + + /** 默认余额 */ + @DbColumn(comment = "默认余额") + private BigDecimal defaultBalance; + + /** 状态 */ + @DbColumn(comment = "状态") + private String state; + + /** 备注 */ + @DbColumn(comment = "备注") + private String remark; + + /** + * 转换 + */ + @Override + public WalletConfigDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + + public static WalletConfig init(WalletConfigParam in){ + return WalletConvert.CONVERT.convert(in); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletLog.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletLog.java new file mode 100644 index 00000000..d02743a6 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletLog.java @@ -0,0 +1,72 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.entity; + + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.daxpay.core.channel.wallet.convert.WalletConvert; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletLogDto; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 钱包日志 + * + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +//@DbTable(comment = "钱包日志") +@Accessors(chain = true) +@TableName("pay_wallet_log") +public class WalletLog extends MpBaseEntity implements EntityBaseFunction { + + /** 钱包id */ + @DbMySqlIndex(comment = "钱包索引ID") + @DbColumn(comment = "钱包id") + private Long walletId; + + /** 用户id */ + @DbColumn(comment = "用户id") + private Long userId; + + /** 类型 */ + @DbColumn(comment = "类型") + private String type; + + /** 交易记录ID */ + @DbColumn(comment = "交易记录ID") + private Long paymentId; + + /** 业务ID */ + @DbColumn(comment = "业务ID") + private String businessId; + + /** + * 操作类型 + * @see cn.bootx.platform.daxpay.code.paymodel.WalletCode#OPERATION_SOURCE_USER + */ + @DbColumn(comment = "操作类型") + private String operationSource; + + /** 金额 */ + @DbColumn(comment = "金额") + private BigDecimal amount; + + /** 备注 */ + @DbColumn(comment = "备注") + private String remark; + + + @Override + public WalletLogDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletPayment.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletPayment.java new file mode 100644 index 00000000..51a6f235 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/entity/WalletPayment.java @@ -0,0 +1,33 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.daxpay.core.channel.base.entity.BasePayment; +import cn.bootx.platform.daxpay.core.channel.wallet.convert.WalletConvert; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletPaymentDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 钱包交易记录表 + * + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +//@DbTable(comment = "钱包交易记录") +@Accessors(chain = true) +@TableName("pay_wallet_payment") +public class WalletPayment extends BasePayment implements EntityBaseFunction { + + /** 钱包ID */ + private Long walletId; + + @Override + public WalletPaymentDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletConfigService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletConfigService.java new file mode 100644 index 00000000..fd195b69 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletConfigService.java @@ -0,0 +1,70 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.daxpay.code.MchAndAppCode; +import cn.bootx.platform.daxpay.code.pay.PayChannelEnum; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletConfigManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletConfig; +import cn.bootx.platform.daxpay.core.merchant.entity.MchAppPayConfig; +import cn.bootx.platform.daxpay.core.merchant.service.MchAppPayConfigService; +import cn.bootx.platform.daxpay.core.merchant.service.MchAppService; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletConfigDto; +import cn.bootx.platform.daxpay.param.channel.wechat.WalletConfigParam; +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钱包配置 + * @author xxm + * @since 2023/7/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletConfigService { + private final WalletConfigManager walletConfigManager; + private final MchAppPayConfigService mchAppPayConfigService; + private final MchAppService mchAppService; + + /** + * 根据应用编码获取钱包配置 + */ + public WalletConfigDto findByMchCode(String mchCode){ + return walletConfigManager.findByMchCode(mchCode) + .map(WalletConfig::toDto) + .orElse(new WalletConfigDto()); + } + + /** + * 新增或更新 + */ + public void add(WalletConfigParam param){ + // 是否有管理关系判断 + if (!mchAppService.checkMatch(param.getMchCode(), param.getMchAppCode())) { + throw new BizException("应用信息与商户信息不匹配"); + } + WalletConfig walletConfig = WalletConfig.init(param); + walletConfig.setState(MchAndAppCode.PAY_CONFIG_STATE_NORMAL); + walletConfigManager.save(walletConfig); + // 保存关联关系 + MchAppPayConfig mchAppPayConfig = new MchAppPayConfig().setAppCode(walletConfig.getMchAppCode()) + .setConfigId(walletConfig.getId()) + .setChannel(PayChannelEnum.WALLET.getCode()) + .setState(walletConfig.getState()); + mchAppPayConfigService.add(mchAppPayConfig); + } + + /** + * 更新 + */ + public void update(WalletConfigParam param){ + WalletConfig walletConfig = walletConfigManager.findById(param.getId()) + .orElseThrow(DataNotExistException::new); + BeanUtil.copyProperties(param,walletConfig); + walletConfigManager.updateById(walletConfig); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletLogService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletLogService.java new file mode 100644 index 00000000..f813aa8e --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletLogService.java @@ -0,0 +1,49 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletLogManager; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletLogDto; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletLogQueryParam; +import cn.bootx.platform.starter.auth.util.SecurityUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钱包日志 + * + * @author xxm + * @since 2020/12/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletLogService { + + private final WalletLogManager walletLogManager; + + /** + * 个人钱包日志分页 + */ + public PageResult pageByPersonal(PageParam pageParam, WalletLogQueryParam param) { + Long userId = SecurityUtil.getUserId(); + return MpUtil.convert2DtoPageResult(walletLogManager.pageByUserId(pageParam, param, userId)); + } + + /** + * 钱包日志分页 + */ + public PageResult page(PageParam pageParam, WalletLogQueryParam param) { + return MpUtil.convert2DtoPageResult(walletLogManager.page(pageParam, param)); + } + + /** + * 根据钱包id查询钱包日志(分页) + */ + public PageResult pageByWalletId(PageParam pageParam, WalletLogQueryParam param) { + return MpUtil.convert2DtoPageResult(walletLogManager.pageByWalletId(pageParam, param)); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPayService.java new file mode 100644 index 00000000..c1a17c40 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPayService.java @@ -0,0 +1,176 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.code.paymodel.WalletCode; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletLogManager; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletManager; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletPaymentManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletLog; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletPayment; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.exception.waller.WalletLackOfBalanceException; +import cn.bootx.platform.daxpay.exception.waller.WalletNotExistsException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * 钱包支付操作 + * + * @author xxm + * @since 2021/2/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletPayService { + + private final WalletManager walletManager; + + private final WalletPaymentManager walletPaymentManager; + + private final WalletLogManager walletLogManager; + + /** + * 支付前冻结余额 + * @param amount 付款金额 + * @param payment 支付记录 + * @param wallet 钱包 + */ + public void freezeBalance(BigDecimal amount, Payment payment, Wallet wallet) { + // 冻结余额 + int i = walletManager.freezeBalance(wallet.getId(), amount); + // 判断操作结果 + if (i < 1) { + throw new WalletLackOfBalanceException(); + } + // 日志 + WalletLog walletLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setPaymentId(payment.getId()) + .setAmount(amount) + .setType(WalletCode.LOG_FREEZE_BALANCE) + .setRemark(String.format("钱包预冻结金额 %.2f ", amount)) + .setOperationSource(WalletCode.OPERATION_SOURCE_USER) + .setBusinessId(payment.getBusinessId()); + walletLogManager.save(walletLog); + } + + /** + * 支付成功, 进行扣款 + */ + public void paySuccess(Long paymentId){ + // 钱包支付记录 + walletPaymentManager.findByPaymentId(paymentId).ifPresent(walletPayment -> { + Optional walletOpt = walletManager.findById(walletPayment.getWalletId()); + if (!walletOpt.isPresent()) { + log.error("钱包出现恶性问题,需要人工排查"); + return; + } + Wallet wallet = walletOpt.get(); + walletPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + walletPaymentManager.save(walletPayment); + // 支付余额 + int i = walletManager.reduceAndUnfreezeBalance(wallet.getId(), walletPayment.getAmount()); + // 判断操作结果 + if (i < 1) { + throw new WalletLackOfBalanceException(); + } + // 日志 + WalletLog walletLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setPaymentId(walletPayment.getPaymentId()) + .setAmount(walletPayment.getAmount()) + .setType(WalletCode.LOG_REDUCE_AND_UNFREEZE_BALANCE) + .setRemark(String.format("钱包扣款金额 %.2f ", walletPayment.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_USER) + .setBusinessId(walletPayment.getBusinessId()); + walletLogManager.save(walletLog); + }); + } + + /** + * 直接进行支付 + */ + public void pay(BigDecimal amount, Payment payment, Wallet wallet){ + // 直接扣减余额 + int i = walletManager.reduceBalance(wallet.getId(), amount); + + // 判断操作结果 + if (i < 1) { + throw new WalletLackOfBalanceException(); + } + // 日志 + WalletLog walletLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setPaymentId(payment.getId()) + .setAmount(amount) + .setType(WalletCode.LOG_PAY) + .setRemark(String.format("钱包支付金额 %.2f ", amount)) + .setOperationSource(WalletCode.OPERATION_SOURCE_USER) + .setBusinessId(payment.getBusinessId()); + walletLogManager.save(walletLog); + } + + /** + * 取消支付,解除冻结的额度, 只有在异步支付中才会发生取消操作 + */ + public void close(Long paymentId) { + // 钱包支付记录 + walletPaymentManager.findByPaymentId(paymentId).ifPresent(walletPayment -> { + Optional walletOpt = walletManager.findById(walletPayment.getWalletId()); + if (!walletOpt.isPresent()) { + log.error("钱包出现恶性问题,需要人工排查"); + return; + } + Wallet wallet = walletOpt.get(); + walletPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + walletPaymentManager.save(walletPayment); + + // 金额返还 + walletManager.increaseBalance(wallet.getId(), walletPayment.getAmount()); + // 记录日志 + WalletLog walletLog = new WalletLog().setAmount(walletPayment.getAmount()) + .setPaymentId(walletPayment.getPaymentId()) + .setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_CLOSE_PAY) + .setRemark(String.format("取消支付金额 %.2f ", walletPayment.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_SYSTEM) + .setBusinessId(walletPayment.getBusinessId()); + // save log + walletLogManager.save(walletLog); + }); + } + + /** + * 退款 + */ + @Transactional(rollbackFor = Exception.class) + public void refund(Long paymentId, BigDecimal amount) { + // 钱包支付记录 + WalletPayment walletPayment = walletPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("钱包支付记录不存在")); + // 获取钱包 + Wallet wallet = walletManager.findById(walletPayment.getWalletId()).orElseThrow(WalletNotExistsException::new); + walletManager.increaseBalance(wallet.getId(), amount); + + WalletLog walletLog = new WalletLog().setAmount(amount) + .setPaymentId(walletPayment.getPaymentId()) + .setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_REFUND) + .setRemark(String.format("钱包退款金额 %.2f ", amount)) + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN) + .setBusinessId(walletPayment.getBusinessId()); + // save log + walletLogManager.save(walletLog); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPaymentService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPaymentService.java new file mode 100644 index 00000000..f4f3d2c4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletPaymentService.java @@ -0,0 +1,86 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletPaymentManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletPayment; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 钱包交易记录的相关操作 + * + * @author xxm + * @since 2020/12/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletPaymentService { + + private final WalletPaymentManager walletPaymentManager; + + /** + * 保存钱包支付记录 + */ + public void savePayment(Payment payment, PayParam payParam, PayWayParam payMode, Wallet wallet) { + WalletPayment walletPayment = new WalletPayment().setWalletId(wallet.getId()); + walletPayment.setPaymentId(payment.getId()) + .setBusinessId(payParam.getBusinessId()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setPayStatus(payment.getPayStatus()); + walletPaymentManager.save(walletPayment); + } + + /** + * 更新成功状态 + */ + public void updateSuccess(Long paymentId) { + Optional payment = walletPaymentManager.findByPaymentId(paymentId); + if (payment.isPresent()) { + WalletPayment walletPayment = payment.get(); + walletPayment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + walletPaymentManager.updateById(walletPayment); + } + } + + /** + * 关闭操作 + */ + public void updateClose(Long paymentId) { + WalletPayment walletPayment = walletPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("未查询到查询交易记录")); + walletPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + walletPaymentManager.updateById(walletPayment); + } + + /** + * 更新退款 + */ + public void updateRefund(Long paymentId, BigDecimal amount) { + Optional walletPayment = walletPaymentManager.findByPaymentId(paymentId); + walletPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + walletPaymentManager.updateById(payment); + }); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletQueryService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletQueryService.java new file mode 100644 index 00000000..0c95d25d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletQueryService.java @@ -0,0 +1,106 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.entity.UserDetail; +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletDto; +import cn.bootx.platform.daxpay.dto.channel.wallet.WalletInfoDto; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletPayParam; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletQueryParam; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.iam.core.user.service.UserQueryService; +import cn.bootx.platform.iam.dto.user.UserInfoDto; +import cn.bootx.platform.iam.param.user.UserInfoParam; +import cn.bootx.platform.starter.auth.util.SecurityUtil; +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/** + * 钱包 + * + * @author xxm + * @since 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletQueryService { + + private final WalletManager walletManager; + + private final UserQueryService userQueryService; + + /** + * 根据钱包ID查询Wallet + */ + public WalletDto findById(Long walletId) { + return walletManager.findById(walletId).map(Wallet::toDto).orElseThrow(DataNotExistException::new); + } + + + /** + * 根据用户ID查询钱包 + */ + public WalletDto findByUser(String mchAppCode) { + Long userId = SecurityUtil.getUserId(); + return walletManager.findByUser(userId,mchAppCode).map(Wallet::toDto).orElse(null); + } + + /** + * 获取钱包综合信息 + */ + public WalletInfoDto getWalletInfo(Long walletId) { + Wallet wallet = walletManager.findById(walletId).orElseThrow(DataNotExistException::new); + UserInfoDto userInfoDto = userQueryService.findById(wallet.getUserId()); + WalletInfoDto walletInfoDto = new WalletInfoDto(); + BeanUtil.copyProperties(wallet, walletInfoDto); + walletInfoDto.setUserName(userInfoDto.getName()); + return walletInfoDto; + } + + /** + * 查询用户 分页 + */ + public PageResult page(PageParam pageParam, WalletQueryParam param) { + return MpUtil.convert2DtoPageResult(walletManager.page(pageParam, param)); + } + + /** + * 待开通钱包的用户列表 + */ + public PageResult pageByNotWallet(PageParam pageParam,String mchCode, UserInfoParam userInfoParam) { + return MpUtil.convert2DtoPageResult(walletManager.pageByNotWallet(pageParam,mchCode ,userInfoParam)); + } + + + /** + * 获取钱包, 获取顺序: 1. 显式传入的钱包ID 2. 显式传入的用户ID 3. 从系统中获取到的用户ID + * + */ + public Wallet getWallet(WalletPayParam walletPayParam, PayParam payParam){ + + Wallet wallet = null; + Long userId = null; + // 首先根据钱包ID查询 + if (Objects.nonNull(walletPayParam.getWalletId())) { + wallet = walletManager.findById(walletPayParam.getWalletId()).orElseThrow(null); + } + if (Objects.nonNull(wallet)){ + return wallet; + } + // 根据用户id查询 + if (Objects.isNull(walletPayParam.getUserId())){ + userId = SecurityUtil.getCurrentUser().map(UserDetail::getId).orElse(null); + } + return walletManager.findByUser(userId,payParam.getMchAppCode()).orElse(null); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletService.java new file mode 100644 index 00000000..e566759e --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wallet/service/WalletService.java @@ -0,0 +1,170 @@ +package cn.bootx.platform.daxpay.core.channel.wallet.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.paymodel.WalletCode; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletConfigManager; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletLogManager; +import cn.bootx.platform.daxpay.core.channel.wallet.dao.WalletManager; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.Wallet; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletConfig; +import cn.bootx.platform.daxpay.core.channel.wallet.entity.WalletLog; +import cn.bootx.platform.daxpay.core.merchant.service.MchAppService; +import cn.bootx.platform.daxpay.param.channel.wallet.WalletRechargeParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + * @author xxm + * @since 2023/6/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletService { + + private final WalletManager walletManager; + + private final WalletConfigManager walletConfigManager; + + private final WalletLogManager walletLogManager; + + private final MchAppService mchAppService; + + /** + * 开通操作 创建 + */ + @Transactional(rollbackFor = Exception.class) + public void createWallet(Long userId, String mchCode, String mchAppCode) { + + // 是否有管理关系判断 + if (!mchAppService.checkMatch(mchCode, mchAppCode)) { + throw new BizException("应用信息与商户信息不匹配"); + } + + // 钱包配置 + BigDecimal defaultBalance = walletConfigManager.findByMchCode(mchAppCode) + .map(WalletConfig::getDefaultBalance) + .orElse(BigDecimal.ZERO); + + // 判断钱包是否已开通 + if (walletManager.existsByUser(userId, mchAppCode)) { + throw new BizException("钱包已经开通"); + } + Wallet wallet = new Wallet().setUserId(userId) + .setBalance(defaultBalance) + .setMchCode(mchCode) + .setMchAppCode(mchAppCode) + .setFreezeBalance(BigDecimal.ZERO) + .setStatus(WalletCode.STATUS_NORMAL); + walletManager.save(wallet); + // 激活 log + WalletLog activeLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_ACTIVE) + .setAmount(defaultBalance) + .setRemark("激活钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_USER); + walletLogManager.save(activeLog); + } + + /** + * 批量开通 + */ + @Transactional(rollbackFor = Exception.class) + public void createWalletBatch(List userIds,String mchCode, String mchAppCode) { + // 是否有管理关系判断 + if (!mchAppService.checkMatch(mchCode, mchAppCode)) { + throw new BizException("应用信息与商户信息不匹配"); + } + // 钱包配置 + BigDecimal defaultBalance = walletConfigManager.findByMchCode(mchAppCode) + .map(WalletConfig::getDefaultBalance) + .orElse(BigDecimal.ZERO); + // 查询出已经开通钱包的id + List existUserIds = walletManager.findExistUserIds(userIds); + userIds.removeAll(existUserIds); + List wallets = userIds.stream() + .map(userId -> new Wallet().setUserId(userId) + .setStatus(WalletCode.STATUS_NORMAL) + .setFreezeBalance(BigDecimal.ZERO) + .setBalance(defaultBalance)) + .collect(Collectors.toList()); + walletManager.saveAll(wallets); + List walletLogs = wallets.stream() + .map(wallet -> new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setAmount(defaultBalance) + .setType(WalletCode.LOG_ACTIVE) + .setRemark("批量开通钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN)) + .collect(Collectors.toList()); + walletLogManager.saveAll(walletLogs); + } + + /** + * 锁定钱包 + */ + @Transactional(rollbackFor = Exception.class) + public void lock(Long walletId) { + Wallet wallet = walletManager.findById(walletId).orElseThrow(DataNotExistException::new); + wallet.setStatus(WalletCode.STATUS_FORBIDDEN); + walletManager.updateById(wallet); + // 激活 log + WalletLog log = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_LOCK) + .setRemark("锁定钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN); + walletLogManager.save(log); + } + + /** + * 解锁钱包 + */ + @Transactional(rollbackFor = Exception.class) + public void unlock(Long walletId) { + Wallet wallet = walletManager.findById(walletId).orElseThrow(DataNotExistException::new); + wallet.setStatus(WalletCode.STATUS_NORMAL); + walletManager.updateById(wallet); + // 激活 log + WalletLog log = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_UNLOCK) + .setRemark("解锁钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN); + walletLogManager.save(log); + } + + /** + * 更改余额 也可以扣款 + */ + @Transactional(rollbackFor = Exception.class) + public void changerBalance(WalletRechargeParam param) { + if (BigDecimalUtil.compareTo(param.getAmount(), BigDecimal.ZERO) == 1) { + walletManager.increaseBalance(param.getWalletId(), param.getAmount()); + } + else if (BigDecimalUtil.compareTo(param.getAmount(), BigDecimal.ZERO) == -1) { + walletManager.reduceBalanceUnlimited(param.getWalletId(), param.getAmount()); + } + else { + return; + } + Wallet wallet = walletManager.findById(param.getWalletId()).orElseThrow(DataNotExistException::new); + WalletLog walletLog = new WalletLog().setAmount(param.getAmount()) + .setWalletId(wallet.getId()) + .setType(WalletCode.LOG_ADMIN_CHANGER) + .setUserId(wallet.getUserId()) + .setRemark(String.format("系统变动余额 %.2f ", param.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN); + walletLogManager.save(walletLog); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/convert/WeChatConvert.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/convert/WeChatConvert.java new file mode 100644 index 00000000..d6e19e03 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/convert/WeChatConvert.java @@ -0,0 +1,30 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.convert; + +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayment; +import cn.bootx.platform.daxpay.dto.channel.wechat.WeChatPayConfigDto; +import cn.bootx.platform.daxpay.dto.channel.wechat.WeChatPaymentDto; +import cn.bootx.platform.daxpay.param.channel.wechat.WeChatPayConfigParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 微信转换类 + * + * @author xxm + * @since 2021/6/21 + */ +@Mapper +public interface WeChatConvert { + + WeChatConvert CONVERT = Mappers.getMapper(WeChatConvert.class); + + WeChatPayConfig convert(WeChatPayConfigParam in); + + WeChatPayConfigDto convert(WeChatPayConfig in); + + WeChatPaymentDto convert(WeChatPayment in); + + WeChatPayment convert(WeChatPaymentDto in); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigManager.java new file mode 100644 index 00000000..a25580fe --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigManager.java @@ -0,0 +1,34 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.dao; + +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.base.MpIdEntity; +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.common.query.generator.QueryGenerator; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.param.channel.wechat.WeChatPayConfigParam; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +/** + * 微信支付配置 + * + * @author xxm + * @since 2021/3/19 + */ +@Repository +@RequiredArgsConstructor +public class WeChatPayConfigManager extends BaseManager { + + /** + * 分页 + */ + public Page page(PageParam pageParam, WeChatPayConfigParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, WeChatPayConfig.class); + QueryWrapper wrapper = QueryGenerator.generator(param); + wrapper.orderByDesc(MpIdEntity.Id.id); + return this.page(mpPage,wrapper); + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigMapper.java new file mode 100644 index 00000000..fb61adec --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPayConfigMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.dao; + +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 微信支付配置 + * + * @author xxm + * @since 2021/3/19 + */ +@Mapper +public interface WeChatPayConfigMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentManager.java new file mode 100644 index 00000000..c1503af0 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentManager.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 微信支付记录 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class WeChatPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(WeChatPayment::getPaymentId, paymentId); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentMapper.java new file mode 100644 index 00000000..113f9af1 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/dao/WeChatPaymentMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.dao; + +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WeChatPaymentMapper extends BaseMapper { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayConfig.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayConfig.java new file mode 100644 index 00000000..7be5865c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayConfig.java @@ -0,0 +1,131 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.entity; + +import cn.bootx.platform.common.core.annotation.BigField; +import cn.bootx.platform.common.core.annotation.EncryptionField; +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.common.mybatisplus.handler.StringListTypeHandler; +import cn.bootx.platform.daxpay.code.WeChatPayCode; +import cn.bootx.platform.daxpay.core.channel.wechat.convert.WeChatConvert; +import cn.bootx.platform.daxpay.dto.channel.wechat.WeChatPayConfigDto; +import cn.bootx.platform.daxpay.param.channel.wechat.WeChatPayConfigParam; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.annotation.DbTable; +import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 微信支付配置 + * + * @author xxm + * @since 2021/3/1 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@DbTable(comment = "微信支付配置") +@Accessors(chain = true) +@TableName("pay_wechat_pay_config") +public class WeChatPayConfig extends MpBaseEntity implements EntityBaseFunction { + + /** 名称 */ + @DbColumn(comment = "名称") + private String name; + + /** 商户编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbColumn(comment = "商户编码") + private String mchCode; + + /** 商户应用编码 */ + @TableField(updateStrategy = FieldStrategy.NEVER) + @DbMySqlIndex(comment = "商户应用编码唯一索引") + @DbColumn(comment = "商户应用编码") + private String mchAppCode; + + /** 微信商户Id */ + @DbColumn(comment = "微信商户号") + private String wxMchId; + + /** 微信应用appId */ + @DbColumn(comment = "微信应用appId") + private String wxAppId; + + /** + * api版本 + * @see WeChatPayCode#API_V2 + */ + @DbColumn(comment = "api版本") + private String apiVersion; + + /** 商户平台「API安全」中的 APIv2 密钥 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @BigField + @EncryptionField + @DbColumn(comment = "APIv2 密钥") + private String apiKeyV2; + + /** 商户平台「API安全」中的 APIv3 密钥 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @BigField + @EncryptionField + @DbColumn(comment = "APIv3 密钥") + private String apiKeyV3; + + /** APPID对应的接口密码,用于获取微信公众号jsapi支付时使用 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @EncryptionField + @DbColumn(comment = "APPID对应的接口密码") + private String appSecret; + + /** API证书中p12证书Base64 */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + @BigField + @EncryptionField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "API证书中p12证书Base64") + private String p12; + + /** 服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https */ + @DbColumn(comment = "异步通知页面") + private String notifyUrl; + + /** 页面跳转同步通知页面路径 */ + @DbColumn(comment = "同步通知页面") + private String returnUrl; + + /** 是否沙箱环境 */ + @DbColumn(comment = "是否沙箱环境") + private boolean sandbox; + + /** 超时时间(分钟) */ + @DbColumn(comment = "超时时间(分钟)") + private Integer expireTime; + + /** 可用支付方式 */ + @DbColumn(comment = "可用支付方式") + @TableField(typeHandler = StringListTypeHandler.class) + private List payWays; + + /** 备注 */ + @DbColumn(comment = "备注") + private String remark; + + @Override + public WeChatPayConfigDto toDto() { + return WeChatConvert.CONVERT.convert(this); + } + + public static WeChatPayConfig init(WeChatPayConfigParam in) { + return WeChatConvert.CONVERT.convert(in); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayment.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayment.java new file mode 100644 index 00000000..a748f2e8 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/entity/WeChatPayment.java @@ -0,0 +1,32 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.entity; + +import cn.bootx.platform.common.core.function.EntityBaseFunction; +import cn.bootx.platform.daxpay.core.channel.base.entity.BasePayment; +import cn.bootx.platform.daxpay.core.channel.wechat.convert.WeChatConvert; +import cn.bootx.platform.daxpay.dto.channel.wechat.WeChatPaymentDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2021/6/21 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wechat_payment") +public class WeChatPayment extends BasePayment implements EntityBaseFunction { + + /** + * 微信交易号 + */ + private String tradeNo; + + @Override + public WeChatPaymentDto toDto() { + return WeChatConvert.CONVERT.convert(this); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCallbackService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCallbackService.java new file mode 100644 index 00000000..f38c063d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCallbackService.java @@ -0,0 +1,103 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.common.redis.RedisClient; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import cn.bootx.platform.daxpay.code.WeChatPayCode; +import cn.bootx.platform.daxpay.core.callback.dao.CallbackNotifyManager; +import cn.bootx.platform.daxpay.core.channel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.payment.callback.service.PayCallbackService; +import cn.bootx.platform.daxpay.func.AbsPayCallbackStrategy; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +import static cn.bootx.platform.daxpay.code.WeChatPayCode.APPID; + + +/** + * 微信支付回调 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@Service +public class WeChatPayCallbackService extends AbsPayCallbackStrategy { + + private final WeChatPayConfigManager weChatPayConfigManager; + + public WeChatPayCallbackService(RedisClient redisClient, CallbackNotifyManager callbackNotifyManager, + PayCallbackService payCallbackService, WeChatPayConfigManager weChatPayConfigManager) { + super(redisClient, callbackNotifyManager, payCallbackService); + this.weChatPayConfigManager = weChatPayConfigManager; + } + + @Override + public PayChannelEnum getPayChannel() { + return PayChannelEnum.WECHAT; + } + + /** + * 获取支付单id + */ + @Override + public Long getPaymentId() { + Map params = PARAMS.get(); + String paymentId = params.get(WeChatPayCode.OUT_TRADE_NO); + return Long.valueOf(paymentId); + } + + /** + * 获取支付状态 + */ + @Override + public String getTradeStatus() { + Map params = PARAMS.get(); + if (WxPayKit.codeIsOk(params.get(WeChatPayCode.RESULT_CODE))) { + return PayStatusEnum.SUCCESS.getCode(); + } + else { + return PayStatusEnum.FAIL.getCode(); + } + } + + /** + * 验证回调消息 + */ + @Override + public boolean verifyNotify() { + Map params = PARAMS.get(); + String callReq = JSONUtil.toJsonStr(params); + log.info("微信发起回调 报文: {}", callReq); + String appId = params.get(APPID); + + if (StrUtil.isBlank(appId)) { + log.warn("微信回调报文 appId 为空 {}", callReq); + return false; + } + + WeChatPayConfig weChatPayConfig = null; + if (weChatPayConfig == null) { + log.warn("微信支付配置不存在: {}", callReq); + return false; + } + return WxPayKit.verifyNotify(params, weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256, null); + } + + @Override + public String getReturnMsg() { + Map xml = new HashMap<>(4); + xml.put(WeChatPayCode.RETURN_CODE, "SUCCESS"); + xml.put(WeChatPayCode.RETURN_MSG, "OK"); + return WxPayKit.toXml(xml); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCloseService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCloseService.java new file mode 100644 index 00000000..9cbdb77f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayCloseService.java @@ -0,0 +1,117 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.common.spring.exception.RetryableException; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.code.PayRefundStatusEnum; +import cn.bootx.platform.daxpay.code.WeChatPayCode; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayRefundableInfo; +import cn.bootx.platform.daxpay.core.payment.refund.local.AsyncRefundLocal; +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.CloseOrderModel; +import com.ijpay.wxpay.model.RefundModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.math.BigDecimal; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * 微信支付关闭和退款 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayCloseService { + + /** + * 关闭支付 + */ + @Retryable(value = RetryableException.class) + public void cancelRemote(PayOrder payOrder, WeChatPayConfig weChatPayConfig) { + // 只有部分需要调用微信网关进行关闭 + Map params = CloseOrderModel.builder() + .appid(weChatPayConfig.getWxAppId()) + .mch_id(weChatPayConfig.getWxMchId()) + .out_trade_no(String.valueOf(payOrder.getId())) + .nonce_str(WxPayKit.generateStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + String xmlResult = WxPayApi.closeOrder(params); + + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + } + + /** + * 退款 + */ + public void refund(PayOrder payOrder, BigDecimal amount, + WeChatPayConfig weChatPayConfig) { + PayRefundableInfo refundableInfo = payOrder.getRefundableInfos().stream() + .filter(o -> Objects.equals(o.getChannel(), PayChannelEnum.WECHAT.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("未找到微信支付的详细信息")); + String refundFee = amount.multiply(BigDecimal.valueOf(100)).toBigInteger().toString(); + String totalFee = refundableInfo.getAmount().toString(); + // 设置退款号 + AsyncRefundLocal.set(IdUtil.getSnowflakeNextIdStr()); + Map params = RefundModel.builder() + .appid(weChatPayConfig.getWxAppId()) + .mch_id(weChatPayConfig.getWxMchId()) + .out_trade_no(String.valueOf(payOrder.getId())) + .out_refund_no(AsyncRefundLocal.get()) + .total_fee(totalFee) + .refund_fee(refundFee) + .nonce_str(WxPayKit.generateStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + // 获取证书文件 + if (StrUtil.isBlank(weChatPayConfig.getP12())){ + String errorMsg = "微信p.12证书未配置,无法进行退款"; + AsyncRefundLocal.setErrorMsg(errorMsg); + AsyncRefundLocal.setErrorCode(PayRefundStatusEnum.SUCCESS.getCode()); + throw new PayFailureException(errorMsg); + } + byte[] fileBytes = Base64.decode(weChatPayConfig.getP12()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(fileBytes); + // 证书密码为 微信商户号 + String xmlResult = WxPayApi.orderRefund(false, params, inputStream, weChatPayConfig.getWxMchId()); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(Map result) { + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + if (!WxPayKit.codeIsOk(returnCode) || !WxPayKit.codeIsOk(resultCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = result.get(WeChatPayCode.RETURN_MSG); + } + log.error("订单退款/关闭失败 {}", errorMsg); + AsyncRefundLocal.setErrorMsg(errorMsg); + AsyncRefundLocal.setErrorCode(Optional.ofNullable(resultCode).orElse(returnCode)); + throw new PayFailureException(errorMsg); + } + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayConfigService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayConfigService.java new file mode 100644 index 00000000..a2eea75a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayConfigService.java @@ -0,0 +1,83 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.common.core.exception.DataNotExistException; +import cn.bootx.platform.common.core.rest.PageResult; +import cn.bootx.platform.common.core.rest.dto.LabelValue; +import cn.bootx.platform.common.core.rest.param.PageParam; +import cn.bootx.platform.common.mybatisplus.util.MpUtil; +import cn.bootx.platform.daxpay.code.WeChatPayWay; +import cn.bootx.platform.daxpay.core.channel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.dto.channel.wechat.WeChatPayConfigDto; +import cn.bootx.platform.daxpay.exception.pay.PayFailureException; +import cn.bootx.platform.daxpay.param.channel.wechat.WeChatPayConfigParam; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 微信支付配置 + * + * @author xxm + * @since 2021/3/5 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayConfigService { + + private final WeChatPayConfigManager weChatPayConfigManager; + + /** + * 添加微信支付配置 + */ + @Transactional(rollbackFor = Exception.class) + public void add(WeChatPayConfigParam param) { + + WeChatPayConfig weChatPayConfig = WeChatPayConfig.init(param); + weChatPayConfigManager.save(weChatPayConfig); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void update(WeChatPayConfigParam param) { + WeChatPayConfig weChatPayConfig = weChatPayConfigManager.findById(param.getId()) + .orElseThrow(() -> new PayFailureException("微信支付配置不存在")); + param.setActivity(null); + BeanUtil.copyProperties(param, weChatPayConfig, CopyOptions.create().ignoreNullValue()); + weChatPayConfigManager.updateById(weChatPayConfig); + } + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, WeChatPayConfigParam param) { + return MpUtil.convert2DtoPageResult(weChatPayConfigManager.page(pageParam, param)); + } + + /** + * 获取 + */ + public WeChatPayConfigDto findById(Long id) { + return weChatPayConfigManager.findById(id).map(WeChatPayConfig::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 微信支持支付方式 + */ + public List findPayWayList() { + return WeChatPayWay.getPayWays() + .stream() + .map(e -> new LabelValue(e.getName(),e.getCode())) + .collect(Collectors.toList()); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayService.java new file mode 100644 index 00000000..70d2939a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPayService.java @@ -0,0 +1,281 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.common.core.util.LocalDateTimeUtil; +import cn.bootx.platform.common.jackson.util.JacksonUtil; +import cn.bootx.platform.common.spring.exception.RetryableException; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.code.pay.PayWayEnum; +import cn.bootx.platform.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.platform.daxpay.code.paymodel.WeChatPayWay; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.core.sync.result.PaySyncResult; +import cn.bootx.platform.daxpay.core.sync.service.PaySyncService; +import cn.bootx.platform.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.platform.daxpay.exception.payment.PayFailureException; +import cn.bootx.platform.daxpay.param.channel.wechat.WeChatPayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import cn.bootx.platform.daxpay.util.PayWayUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.enums.TradeType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.MicroPayModel; +import com.ijpay.wxpay.model.UnifiedOrderModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +import static cn.bootx.platform.daxpay.code.pay.PaySyncStatus.WAIT_BUYER_PAY; + +/** + * 微信支付 + * + * @author xxm + * @since 2021/3/2 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayService { + + private final PaySyncService paySyncService; + + private final WeChatPaySyncService weChatPaySyncService; + + /** + * 校验 + */ + public void validation(PayWayParam payWayParam, WeChatPayConfig weChatPayConfig) { + List payWays = Optional.ofNullable(weChatPayConfig.getPayWays()) + .filter(StrUtil::isNotBlank) + .map(s -> StrUtil.split(s, ',')) + .orElse(new ArrayList<>(1)); + + PayWayEnum payWayEnum = Optional.ofNullable(WeChatPayWay.findByCode(payWayParam.getPayWay())) + .orElseThrow(() -> new PayFailureException("非法的微信支付类型")); + if (!payWays.contains(payWayEnum.getCode())) { + throw new PayFailureException("该微信支付方式不可用"); + } + } + + /** + * 支付 + */ + public void pay(BigDecimal amount, Payment payment, WeChatPayParam weChatPayParam, PayWayParam payWayParam, + WeChatPayConfig weChatPayConfig) { + // 微信传入的是分, 将元转换为分 + String totalFee = String.valueOf(amount.multiply(new BigDecimal(100)).longValue()); + AsyncPayInfo asyncPayInfo = Optional.ofNullable(AsyncPayInfoLocal.get()).orElse(new AsyncPayInfo()); + String payBody = null; + PayWayEnum payWayEnum = PayWayEnum.findByCode(payWayParam.getPayWay()); + + // wap支付 + if (payWayEnum == PayWayEnum.WAP) { + payBody = this.wapPay(totalFee, payment, weChatPayConfig); + } + // APP支付 + else if (payWayEnum == PayWayEnum.APP) { + payBody = this.appPay(totalFee, payment, weChatPayConfig); + } + // 微信公众号支付或者小程序支付 + else if (payWayEnum == PayWayEnum.JSAPI) { + payBody = this.jsPay(totalFee, payment, weChatPayParam.getOpenId(), weChatPayConfig); + } + // 二维码支付 + else if (payWayEnum == PayWayEnum.QRCODE) { + payBody = this.qrCodePay(totalFee, payment, weChatPayConfig); + } + // 付款码支付 + else if (payWayEnum == PayWayEnum.BARCODE) { + String tradeNo = this.barCode(totalFee, payment, weChatPayParam.getAuthCode(), weChatPayConfig); + asyncPayInfo.setTradeNo(tradeNo).setExpiredTime(false); + } + asyncPayInfo.setPayBody(payBody); + AsyncPayInfoLocal.set(asyncPayInfo); + } + + /** + * wap支付 + */ + private String wapPay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.MWEB.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.MWEB_URL); + } + + /** + * 程序支付 + */ + private String appPay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.APP.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.PREPAY_ID); + } + + /** + * 微信公众号支付或者小程序支付 + */ + private String jsPay(String amount, Payment payment, String openId, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.JSAPI.getTradeType()) + .openid(openId) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + String prepayId = result.get(WeChatPayCode.PREPAY_ID); + Map packageParams = WxPayKit.miniAppPrepayIdCreateSign(weChatPayConfig.getWxAppId(), prepayId, + weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + String jsonStr = JacksonUtil.toJson(packageParams); + log.info("小程序支付的参数:" + jsonStr); + return jsonStr ; + } + + /** + * 二维码支付 + */ + private String qrCodePay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.NATIVE.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.CODE_URL); + } + + /** + * 付款码支付 + */ + private String barCode(String amount, Payment payment, String authCode, WeChatPayConfig weChatPayConfig) { + Map params = MicroPayModel.builder() + .appid(weChatPayConfig.getWxAppId()) + .mch_id(weChatPayConfig.getWxMchId()) + .nonce_str(WxPayKit.generateStr()) + .body(payment.getTitle()) + .auth_code(authCode) + .out_trade_no(String.valueOf(payment.getId())) + .total_fee(amount) + .spbill_create_ip(NetUtil.getLocalhostStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.microPay(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + // 支付失败 + if (!WxPayKit.codeIsOk(returnCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + throw new PayFailureException(errorMsg); + } + + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + String errCode = result.get(WeChatPayCode.ERR_CODE); + // 支付成功处理 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_SUCCESS)) { + payment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + return result.get(WeChatPayCode.TRANSACTION_ID); + } + // 支付中, 发起轮训同步 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_FAIL) + && Objects.equals(errCode, WeChatPayCode.TRADE_USERPAYING)) { + SpringUtil.getBean(this.getClass()).rotationSync(payment, weChatPayConfig); + return result.get(WeChatPayCode.TRANSACTION_ID); + } + + // 支付撤销 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_REVOKED)) { + throw new PayFailureException("用户已撤销支付"); + } + + // 支付失败 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_PAYERROR) + || Objects.equals(resultCode, WeChatPayCode.TRADE_FAIL)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + throw new PayFailureException(errorMsg); + } + return null; + } + + /** + * 构建参数 + */ + private UnifiedOrderModel.UnifiedOrderModelBuilder buildParams(String amount, Payment payment, + WeChatPayConfig weChatPayConfig, String tradeType) { + // 过期时间 + payment.setExpiredTime(PayWayUtil.getPaymentExpiredTime(weChatPayConfig.getExpireTime())); + return UnifiedOrderModel.builder() + .appid(weChatPayConfig.getWxAppId()) + .mch_id(weChatPayConfig.getWxMchId()) + .nonce_str(WxPayKit.generateStr()) + .time_start(LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)) + // 反正v2版本的超时时间无效 + .time_expire(PayWayUtil.getWxExpiredTime(weChatPayConfig.getExpireTime())) + .body(payment.getTitle()) + .out_trade_no(String.valueOf(payment.getId())) + .total_fee(amount) + .spbill_create_ip(NetUtil.getLocalhostStr()) + .notify_url(weChatPayConfig.getNotifyUrl()) + .trade_type(tradeType); + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(Map result) { + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + if (!WxPayKit.codeIsOk(returnCode) || !WxPayKit.codeIsOk(resultCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = result.get(WeChatPayCode.RETURN_MSG); + } + log.error("支付失败 {}", errorMsg); + throw new PayFailureException(errorMsg); + } + } + + /** + * 重试同步支付状态, 最多10次, 30秒不操作微信会自动关闭 + */ + @Async("bigExecutor") + @Retryable(value = RetryableException.class, maxAttempts = 10, backoff = @Backoff(value = 5000L)) + public void rotationSync(Payment payment, WeChatPayConfig weChatPayConfig) { + PaySyncResult paySyncResult = weChatPaySyncService.syncPayStatus(payment.getId(), weChatPayConfig); + // 不为支付中状态后, 调用系统同步更新状态, 支付状态则继续重试 + if (Objects.equals(WAIT_BUYER_PAY, paySyncResult.getPaySyncStatus())) { + throw new RetryableException(); + } + else { + paySyncService.syncPayment(payment); + } + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaySyncService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaySyncService.java new file mode 100644 index 00000000..9fcefda1 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaySyncService.java @@ -0,0 +1,88 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.daxpay.code.PaySyncStatus; +import cn.bootx.platform.daxpay.code.WeChatPayCode; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.payment.sync.result.PaySyncResult; +import cn.hutool.json.JSONUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.UnifiedOrderModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * 微信支付同步服务 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPaySyncService { + + /** + * 同步查询 + */ + public PaySyncResult syncPayStatus(Long paymentId, WeChatPayConfig weChatPayConfig) { + PaySyncResult paySyncResult = new PaySyncResult().setPaySyncStatus(PaySyncStatus.FAIL); + Map params = UnifiedOrderModel.builder() + .appid(weChatPayConfig.getWxAppId()) + .mch_id(weChatPayConfig.getWxMchId()) + .nonce_str(WxPayKit.generateStr()) + .out_trade_no(String.valueOf(paymentId)) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + try { + String xmlResult = WxPayApi.orderQuery(params); + Map result = WxPayKit.xmlToMap(xmlResult); + paySyncResult.setJson(JSONUtil.toJsonStr(result)); + // 查询失败 + if (!WxPayKit.codeIsOk(result.get(WeChatPayCode.RETURN_CODE))) { + log.warn("查询微信订单失败:{}", result); + return paySyncResult; + } + + // 未查到订单 + if (!WxPayKit.codeIsOk(result.get(WeChatPayCode.RESULT_CODE))) { + log.warn("疑似未查询到订单:{}", result); + return paySyncResult.setPaySyncStatus(PaySyncStatus.NOT_FOUND); + } + String tradeStatus = result.get(WeChatPayCode.TRADE_STATE); + // 支付完成 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_SUCCESS) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_ACCEPT)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_SUCCESS).setMap(result); + } + // 待支付 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_NOTPAY) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_USERPAYING)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.WAIT_BUYER_PAY); + } + + // 已退款/退款中 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_REFUND)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_REFUND); + } + // 已关闭 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_CLOSED) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_REVOKED) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_PAYERROR)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_CLOSED); + } + + } + catch (RuntimeException e) { + log.error("查询订单失败:", e); + paySyncResult.setMsg(e.getMessage()); + } + return paySyncResult; + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaymentService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaymentService.java new file mode 100644 index 00000000..6d0bdec4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/channel/wechat/service/WeChatPaymentService.java @@ -0,0 +1,128 @@ +package cn.bootx.platform.daxpay.core.channel.wechat.service; + +import cn.bootx.platform.common.core.exception.BizException; +import cn.bootx.platform.common.core.util.BigDecimalUtil; +import cn.bootx.platform.daxpay.code.pay.PayChannelEnum; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.core.channel.wechat.dao.WeChatPaymentManager; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayment; +import cn.bootx.platform.daxpay.core.order.pay.dao.PayOrderManager; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.order.pay.service.PayOrderService; +import cn.bootx.platform.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.core.payment.pay.local.AsyncPayInfo; +import cn.bootx.platform.daxpay.core.payment.pay.local.AsyncPayInfoLocal; +import cn.bootx.platform.daxpay.core.payment.service.PaymentService; +import cn.bootx.platform.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.platform.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.platform.daxpay.dto.payment.RefundableInfo; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * 微信支付记录单 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPaymentService { + + private final PayOrderManager paymentService; + + private final WeChatPaymentManager weChatPaymentManager; + + /** + * 支付调起成功 更新payment中异步支付类型信息, 如果支付完成, 创建微信支付单 + */ + public void updatePaySuccess(PayOrder payOrder, PayWayParam payWayParam) { + AsyncPayInfo asyncPayInfo = AsyncPayInfoLocal.get(); + payOrder.setAsyncPayMode(true).setAsyncPayChannel(PayChannelEnum.WECHAT.getCode()); + + List payTypeInfos = new ArrayList<>(); + List refundableInfos = new ArrayList<>(); + // 清除已有的异步支付类型信息 + payTypeInfos.removeIf(payTypeInfo -> PayChannelEnum.ASYNC_TYPE_CODE.contains(payTypeInfo.getPayChannel())); + refundableInfos.removeIf(payTypeInfo -> PayChannelEnum.ASYNC_TYPE_CODE.contains(payTypeInfo.getPayChannel())); + // 添加微信支付类型信息 + payTypeInfos.add(new PayChannelInfo().setPayChannel(PayChannelEnum.WECHAT.getCode()) + .setPayWay(payWayParam.getPayWay()) + .setAmount(payWayParam.getAmount()) + .setExtraParamsJson(payWayParam.getExtraParamsJson())); + payOrder.setPayChannelInfo(payTypeInfos); + // 更新微信可退款类型信息 + refundableInfos.add( + new RefundableInfo().setPayChannel(PayChannelEnum.WECHAT.getCode()).setAmount(payWayParam.getAmount())); + payOrder.setRefundableInfo(refundableInfos); + // 如果支付完成(付款码情况) 调用 updateSyncSuccess 创建微信支付记录 + if (Objects.equals(payOrder.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + this.createWeChatPayment(payOrder, payWayParam, asyncPayInfo.getTradeNo()); + } + } + + /** + * 异步支付成功, 更新支付记录成功状态, 并创建微信支付记录 + */ + public void updateAsyncSuccess(Long id, PayWayParam payWayParam, String tradeNo) { + Payment payment = paymentService.findById(id).orElseThrow(() -> new BizException("支付记录不存在")); + this.createWeChatPayment(payment, payWayParam, tradeNo); + } + + /** + * 并创建微信支付记录 + */ + private void createWeChatPayment(Payment payment, PayWayParam payWayParam, String tradeNo) { + // 创建微信支付记录 + WeChatPayment wechatPayment = new WeChatPayment(); + wechatPayment.setTradeNo(tradeNo) + .setPaymentId(payment.getId()) + .setAmount(payWayParam.getAmount()) + .setRefundableBalance(payWayParam.getAmount()) + .setBusinessId(payment.getBusinessId()) + .setPayStatus(PayStatusCode.TRADE_SUCCESS) + .setPayTime(LocalDateTime.now()); + weChatPaymentManager.save(wechatPayment); + } + + /** + * 取消状态 + */ + public void updateClose(Long paymentId) { + Optional weChatPaymentOptional = weChatPaymentManager.findByPaymentId(paymentId); + weChatPaymentOptional.ifPresent(weChatPayment -> { + weChatPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + weChatPaymentManager.updateById(weChatPayment); + }); + } + + /** + * 更新退款 + */ + public void updatePayRefund(Long paymentId, BigDecimal amount) { + Optional weChatPayment = weChatPaymentManager.findByPaymentId(paymentId); + weChatPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + weChatPaymentManager.updateById(payment); + }); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderManager.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderManager.java new file mode 100644 index 00000000..537c96b1 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderManager.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.core.order.pay.dao; + +import cn.bootx.platform.common.mybatisplus.impl.BaseManager; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * 支付订单 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class PayOrderManager extends BaseManager { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderMapper.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderMapper.java new file mode 100644 index 00000000..87fb7514 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/dao/PayOrderMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.platform.daxpay.core.order.pay.dao; + +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 支付订单 + * @author xxm + * @since 2023/12/18 + */ +@Mapper +public interface PayOrderMapper extends BaseMapper { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrder.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrder.java new file mode 100644 index 00000000..a9f91e6a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrder.java @@ -0,0 +1,90 @@ +package cn.bootx.platform.daxpay.core.order.pay.entity; + +import cn.bootx.platform.common.core.annotation.BigField; +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.platform.common.mybatisplus.handler.JacksonRawTypeHandler; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import cn.bootx.table.modify.annotation.DbColumn; +import cn.bootx.table.modify.mysql.annotation.DbMySqlFieldType; +import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex; +import cn.bootx.table.modify.mysql.constants.MySqlFieldTypeEnum; +import cn.bootx.table.modify.mysql.constants.MySqlIndexType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 支付订单 + * @author xxm + * @since 2023/12/18 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_order") +public class PayOrder extends MpBaseEntity { + + /** 关联的业务id */ + @DbMySqlIndex(comment = "业务业务号索引",type = MySqlIndexType.UNIQUE) + @DbColumn(comment = "关联的业务id") + private String businessNo; + + /** 标题 */ + @DbColumn(comment = "标题") + private String title; + + /** 是否是异步支付 */ + @DbColumn(comment = "是否是异步支付") + private boolean asyncPayMode; + + /** 是否是组合支付 */ + @DbColumn(comment = "是否是组合支付") + private boolean combinationPayMode; + + /** + * 异步支付渠道 + * @see PayChannelEnum#ASYNC_TYPE_CODE + */ + @DbColumn(comment = "异步支付渠道") + private String asyncPayChannel; + + /** 金额 */ + @DbColumn(comment = "金额") + private BigDecimal amount; + + /** 可退款余额 */ + @DbColumn(comment = "可退款余额") + private BigDecimal refundableBalance; + + /** + * 退款信息列表 + * @see PayRefundableInfo + */ + @TableField(typeHandler = JacksonRawTypeHandler.class) + @BigField + @DbMySqlFieldType(MySqlFieldTypeEnum.LONGTEXT) + @DbColumn(comment = "退款信息列表") + private List refundableInfos; + + /** + * 支付状态 + * @see PayStatusEnum + */ + @DbColumn(comment = "支付状态") + private String status; + + /** 支付时间 */ + @DbColumn(comment = "支付时间") + private LocalDateTime payTime; + + /** 过期时间 */ + @DbColumn(comment = "过期时间") + private LocalDateTime expiredTime; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderChannel.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderChannel.java new file mode 100644 index 00000000..7db1ed7c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderChannel.java @@ -0,0 +1,30 @@ +package cn.bootx.platform.daxpay.core.order.pay.entity; + +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.table.modify.annotation.DbColumn; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付订单通道信息 + * @author xxm + * @since 2023/12/18 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_order_channel") +public class PayOrderChannel extends MpBaseEntity { + + @DbColumn(comment = "支付id") + private Long paymentId; + + @DbColumn(comment = "通道") + private String channel; + + @DbColumn(comment = "金额") + private Integer amount; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderExtra.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderExtra.java new file mode 100644 index 00000000..9fe41253 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayOrderExtra.java @@ -0,0 +1,38 @@ +package cn.bootx.platform.daxpay.core.order.pay.entity; + + +import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.table.modify.annotation.DbColumn; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付订单扩展信息 + * @author xxm + * @since 2023/12/18 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_order_extra") +public class PayOrderExtra extends MpBaseEntity { + + /** 支付终端ip */ + @DbColumn(comment = "支付终端ip") + private String clientIp; + + /** 描述 */ + @DbColumn(comment = "描述") + private String description; + + /** 错误码 */ + @DbColumn(comment = "错误码") + private String errorCode; + + /** 错误信息 */ + @DbColumn(comment = "错误信息") + private String errorMsg; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayRefundableInfo.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayRefundableInfo.java new file mode 100644 index 00000000..97efbe55 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/entity/PayRefundableInfo.java @@ -0,0 +1,24 @@ +package cn.bootx.platform.daxpay.core.order.pay.entity; + +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付订单可退款信息 + * @author xxm + * @since 2023/12/18 + */ +@Data +@Accessors(chain = true) +public class PayRefundableInfo { + /** + * @see PayChannelEnum#getCode() + */ + private String channel; + + /** + * 可退款金额 + */ + private Integer amount; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/service/PayOrderService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/service/PayOrderService.java new file mode 100644 index 00000000..b6c15d7c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/order/pay/service/PayOrderService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.order.pay.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 支付订单服务 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayOrderService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/result/PayCallbackResult.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/result/PayCallbackResult.java new file mode 100644 index 00000000..a44d516d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/result/PayCallbackResult.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.core.payment.callback.result; + +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付回调处理结果 + * + * @author xxm + * @since 2021/6/22 + */ +@Data +@Accessors(chain = true) +public class PayCallbackResult { + + /** + * 处理状态 + * @see PayStatusEnum + */ + private String code; + + /** 提示信息 */ + private String msg; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/service/PayCallbackService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/service/PayCallbackService.java new file mode 100644 index 00000000..7f91cfa6 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/callback/service/PayCallbackService.java @@ -0,0 +1,226 @@ +package cn.bootx.platform.daxpay.core.payment.callback.service; + +import cn.bootx.platform.common.core.exception.ErrorCodeRuntimeException; +import cn.bootx.platform.common.core.util.LocalDateTimeUtil; +import cn.bootx.platform.daxpay.code.pay.PayChannelEnum; +import cn.bootx.platform.daxpay.code.pay.PayStatusCode; +import cn.bootx.platform.daxpay.core.order.pay.dao.PayOrderManager; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.order.pay.service.PayOrderService; +import cn.bootx.platform.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.platform.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.platform.daxpay.core.pay.exception.BaseException; +import cn.bootx.platform.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.platform.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.platform.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.platform.daxpay.core.pay.func.PayStrategyConsumer; +import cn.bootx.platform.daxpay.core.pay.result.PayCallbackResult; +import cn.bootx.platform.daxpay.core.payment.callback.result.PayCallbackResult; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.core.payment.service.PaymentService; +import cn.bootx.platform.daxpay.event.PayEventSender; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付回调处理 + * + * @author xxm + * @since 2021/2/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayCallbackService { + + private final PayOrderManager payOrderService; + + /** + * 统一回调处理 + * @param tradeStatus 支付状态 + * @see PayStatusCode + */ + public PayCallbackResult callback(Long paymentId, String tradeStatus, Map map) { + + // 获取payment和paymentParam数据 + PayOrder payment = payOrderService.findById(paymentId).orElse(null); + + // 支付单不存在,记录回调记录 + if (Objects.isNull(payment)) { + return new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单不存在,记录回调记录"); + } + + // 回调时间超出了支付单超时时间, 记录一下, 不做处理 + if (Objects.nonNull(payment.getExpiredTime()) + && LocalDateTimeUtil.ge(LocalDateTime.now(), payment.getExpiredTime())) { + return new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调时间超出了支付单支付有效时间"); + } + + // 成功状态 + if (Objects.equals(PayStatusCode.NOTIFY_TRADE_SUCCESS, tradeStatus)) { + return this.success(payment, map); + } + else { + // 失败状态 + return this.fail(payment, map); + } + } + + /** + * 成功处理 + */ + private PayCallbackResult success(Payment payment, Map map) { + PayCallbackResult result = new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_SUCCESS); + + // payment已经被支付,不需要重复处理 + if (Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_IGNORE).setMsg("支付单已经是支付成功状态,不进行处理"); + } + + // payment已被取消,记录回调记录 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_PROGRESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单不是待支付状态,记录回调记录"); + } + + // 2.通过工厂生成对应的策略组 + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayWayList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单数据非法,未找到对应的支付方式"); + } + + // 3.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + // 4.处理方法, 支付时只有一种payModel(异步支付), 失败时payment的所有payModel都会生效 + boolean handlerFlag = this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 执行异步支付方式的成功回调(不会有同步payModel) + strategyList.forEach(absPaymentStrategy -> absPaymentStrategy.doAsyncSuccessHandler(map)); + + // 修改payment支付状态为成功 + paymentObj.setPayStatus(PayStatusCode.TRADE_SUCCESS); + paymentObj.setPayTime(LocalDateTime.now()); + payOrderService.updateById(paymentObj); + }); + + if (handlerFlag) { + // 5. 发送成功事件 + eventSender.sendPayComplete(PayEventBuilder.buildPayComplete(payment)); + } + else { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调处理过程报错"); + } + return result; + } + + /** + * 失败处理, 关闭并退款 按说这块不会发生 + */ + private PayCallbackResult fail(Payment payment, Map map) { + PayCallbackResult result = new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_SUCCESS); + + // payment已被取消,记录回调记录,后期处理 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_PROGRESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_IGNORE).setMsg("支付单已经取消,记录回调记录"); + } + + // payment支付成功, 状态非法 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单状态非法,支付网关状态为失败,但支付单状态为已完成"); + } + + // 2.通过工厂生成对应的策略组 + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayWayList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单数据非法,未找到对应的支付方式"); + } + // 3.初始化支付关闭的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + // 4.处理方法, 支付时只有一种payModel(异步支付), 失败时payment的所有payModel都会生效 + boolean handlerFlag = this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 执行异步支付方式的成功回调(不会有同步payModel) + strategyList.forEach(AbsPayStrategy::doCancelHandler); + + // 修改payment支付状态为成功 + paymentObj.setPayStatus(PayStatusCode.TRADE_CANCEL); + payOrderService.updateById(paymentObj); + }); + + if (handlerFlag) { + // 5. 发送退款事件 + eventSender.sendPayRefund(PayEventBuilder.buildPayRefund(payment)); + } + else { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调处理过程报错"); + } + + return result; + } + + /** + * 处理方法 + * @param payOrder 支付订单 + * @param strategyList 支付策略 + * @param successCallback 成功操作 + */ + private boolean doHandler(PayOrder payOrder, List strategyList, + PayStrategyConsumer, Payment> successCallback) { + + try { + // 1.获取异步支付方式,通过工厂生成对应的策略组 + List syncPaymentStrategyList = strategyList.stream() + .filter(paymentStrategy -> PayChannelEnum.ASYNC_TYPE_CODE.contains(paymentStrategy.getType())) + .collect(Collectors.toList()); + // 执行成功方法 + successCallback.accept(syncPaymentStrategyList, payOrder); + } + catch (Exception e) { + // error事件的处理 + this.asyncErrorHandler(payOrder, strategyList, e); + return false; + } + return true; + } + + /** + * 对Error的处理 + */ + private void asyncErrorHandler(Payment payment, List strategyList, Exception e) { + + // 默认的错误信息 + ExceptionInfo exceptionInfo = new ExceptionInfo(PayStatusCode.TRADE_FAIL, e.getMessage()); + if (e instanceof BaseException) { + exceptionInfo = ((BaseException) e).getExceptionInfo(); + } + else if (e instanceof ErrorCodeRuntimeException) { + ErrorCodeRuntimeException ex = (ErrorCodeRuntimeException) e; + exceptionInfo = new ExceptionInfo(String.valueOf(ex.getCode()), ex.getMessage()); + } + + // 更新Payment的状态 + payment.setErrorCode(String.valueOf(exceptionInfo.getErrorCode())); + payment.setErrorMsg(String.valueOf(exceptionInfo.getErrorMsg())); + payment.setPayStatus(PayStatusCode.TRADE_FAIL); + payOrderService.updateById(payment); + + // 调用失败处理 + for (AbsPayStrategy paymentStrategy : strategyList) { + paymentStrategy.doAsyncErrorHandler(exceptionInfo); + } + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/close/service/PayCloseService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/close/service/PayCloseService.java new file mode 100644 index 00000000..bc1fa870 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/close/service/PayCloseService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.payment.close.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 支付关闭和撤销服务 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayCloseService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/func/AbsPayStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/func/AbsPayStrategy.java new file mode 100644 index 00000000..97ff2567 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/func/AbsPayStrategy.java @@ -0,0 +1,94 @@ +package cn.bootx.platform.daxpay.core.payment.pay.func; + +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.common.exception.ExceptionInfo; +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * 抽象支付策略基类 同步支付 异步支付 错误处理 关闭支付 撤销支付 支付网关同步 退款 + * + * @author xxm + * @since 2020/12/11 + */ +@Getter +@Setter +public abstract class AbsPayStrategy { + + /** 支付对象 */ + private PayOrder order = null; + + /** 支付参数 */ + private PayParam payParam = null; + + /** 支付方式参数 支付参数中的与这个不一致, 以这个为准 */ + private PayWayParam payWayParam = null; + + /** + * 策略标示 + * @see PayChannelEnum + */ + public abstract PayChannelEnum getType(); + + /** + * 初始化支付的参数 + */ + public void initPayParam(PayOrder order, PayParam payParam) { + this.order = order; + this.payParam = payParam; + } + + /** + * 支付前对处理 包含必要的校验以及对Payment对象的创建和保存操作 + */ + public void doBeforePayHandler() { + } + + /** + * 支付操作 + */ + public abstract void doPayHandler(); + + /** + * 支付成功的处理方式 + */ + public void doSuccessHandler() { + } + + /** + * 支付失败的处理方式 + */ + public void doErrorHandler(ExceptionInfo exceptionInfo) { + } + + /** + * 异步支付成功的处理方式 + */ + public void doAsyncSuccessHandler(Map map) { + } + + /** + * 异步支付失败的处理方式, 默认使用支付失败的处理方式 同步支付方式调用时同 this#doErrorHandler + */ + public void doAsyncErrorHandler(ExceptionInfo exceptionInfo) { + this.doErrorHandler(exceptionInfo); + } + + /** + * 撤销支付操作,支付交易返回失败或支付系统超时,调用该接口撤销交易 默认为关闭本地支付记录 + */ + public void doCancelHandler() { + this.doCloseHandler(); + } + + /** + * 关闭本地支付记录 + */ + public abstract void doCloseHandler(); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfo.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfo.java new file mode 100644 index 00000000..921c762f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfo.java @@ -0,0 +1,29 @@ +package cn.bootx.platform.daxpay.core.payment.pay.local; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @since 2021/2/28 + */ +@Data +@Accessors(chain = true) +@Schema(title = "异步支付线程信息") +public class AsyncPayInfo implements Serializable { + + private static final long serialVersionUID = 8239742916705144905L; + + /** 支付参数体(通常用于发起支付的参数) */ + private String payBody; + + /** 第三方支付平台订单号(付款码支付直接成功时会出现) */ + private String tradeNo; + + /** 是否记录超时时间,默认记录 */ + private boolean expiredTime = true; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfoLocal.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfoLocal.java new file mode 100644 index 00000000..f2a20e81 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/local/AsyncPayInfoLocal.java @@ -0,0 +1,36 @@ +package cn.bootx.platform.daxpay.core.payment.pay.local; + +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 异步支付线程变量 + * + * @author xxm + * @since 2021/4/21 + */ +public final class AsyncPayInfoLocal { + + private static final ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); + + /** + * 设置 + */ + public static void set(AsyncPayInfo asyncPayInfo) { + THREAD_LOCAL.set(asyncPayInfo); + } + + /** + * 获取 + */ + public static AsyncPayInfo get() { + return THREAD_LOCAL.get(); + } + + /** + * 清除 + */ + public static void clear() { + THREAD_LOCAL.remove(); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/pay/service/PayService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/service/PayService.java similarity index 81% rename from daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/pay/service/PayService.java rename to daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/service/PayService.java index 0541abd3..5c9c7ea5 100644 --- a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/pay/service/PayService.java +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/pay/service/PayService.java @@ -1,4 +1,4 @@ -package cn.bootx.platform.daxpay.core.pay.service; +package cn.bootx.platform.daxpay.core.payment.pay.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -6,7 +6,6 @@ import org.springframework.stereotype.Service; /** * 支付流程服务 - * * @author xxm * @since 2020/12/9 */ diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/local/AsyncRefundLocal.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/local/AsyncRefundLocal.java new file mode 100644 index 00000000..edc378ac --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/local/AsyncRefundLocal.java @@ -0,0 +1,72 @@ +package cn.bootx.platform.daxpay.core.payment.refund.local; + +import cn.bootx.platform.daxpay.code.PayStatusEnum; +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 异步退款线程变量 + * + * @author xxm + * @since 2022/3/9 + */ +public final class AsyncRefundLocal { + + private static final ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); + + private static final ThreadLocal ERROR_MSG = new TransmittableThreadLocal<>(); + + private static final ThreadLocal ERROR_CODE = new TransmittableThreadLocal<>(); + + /** + * 设置 退款号 + */ + public static void set(String refundId) { + THREAD_LOCAL.set(refundId); + } + + /** + * 获取 退款号 + */ + public static String get() { + return THREAD_LOCAL.get(); + } + + /** + * 设置 错误内容 + */ + public static void setErrorMsg(String errorMsg) { + ERROR_MSG.set(errorMsg); + } + + /** + * 获取 错误内容 + */ + public static String getErrorMsg() { + return ERROR_MSG.get(); + } + + /** + * 设置 错误码 + * @see PayStatusEnum 或者其他支付渠道返回的错误码 + */ + public static void setErrorCode(String errorCode) { + ERROR_CODE.set(errorCode); + } + + /** + * 获取 错误码 + */ + public static String getErrorCode() { + return ERROR_CODE.get(); + } + + /** + * 清除 + */ + public static void clear() { + THREAD_LOCAL.remove(); + ERROR_MSG.remove(); + ERROR_CODE.remove(); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/service/PayRefundService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/service/PayRefundService.java new file mode 100644 index 00000000..986f6543 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/refund/service/PayRefundService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.payment.refund.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 支付退款服务 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayRefundService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/factory/PaySyncStrategyFactory.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/factory/PaySyncStrategyFactory.java new file mode 100644 index 00000000..b880e7aa --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/factory/PaySyncStrategyFactory.java @@ -0,0 +1,38 @@ +package cn.bootx.platform.daxpay.core.payment.sync.factory; + + +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.core.payment.sync.func.AbsPaySyncStrategy; +import cn.bootx.platform.daxpay.core.payment.sync.strategy.AliPaySyncStrategy; +import cn.bootx.platform.daxpay.core.payment.sync.strategy.WeChatPaySyncStrategy; +import cn.bootx.platform.daxpay.exception.pay.PayUnsupportedMethodException; +import cn.hutool.extra.spring.SpringUtil; + +/** + * 支付同步策略工厂类 + * @author xxm + * @since 2023/7/14 + */ +public class PaySyncStrategyFactory { + /** + * 获取支付同步策略 + * @param payChannelCode 支付通道编码 + * @return 支付同步策略类 + */ + public static AbsPaySyncStrategy create(String payChannelCode) { + AbsPaySyncStrategy strategy; + PayChannelEnum channelEnum = PayChannelEnum.findByCode(payChannelCode); + switch (channelEnum) { + case ALI: + strategy = SpringUtil.getBean(AliPaySyncStrategy.class); + break; + case WECHAT: + strategy = SpringUtil.getBean(WeChatPaySyncStrategy.class); + break; + default: + throw new PayUnsupportedMethodException(); + } + // noinspection ConstantConditions + return strategy; + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/func/AbsPaySyncStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/func/AbsPaySyncStrategy.java new file mode 100644 index 00000000..5865eb83 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/func/AbsPaySyncStrategy.java @@ -0,0 +1,34 @@ +package cn.bootx.platform.daxpay.core.payment.sync.func; + +import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder; +import cn.bootx.platform.daxpay.core.payment.sync.result.PaySyncResult; +import lombok.Getter; +import lombok.Setter; + +/** + * 支付同步抽象类 + * @author xxm + * @since 2023/7/14 + */ +@Getter +@Setter +public abstract class AbsPaySyncStrategy { + + /** 支付对象 */ + private PayOrder order = null; + + /** + * 初始化支付的参数 + */ + public void initPayParam(PayOrder order) { + this.order = order; + } + + + /** + * 异步支付单与支付网关进行状态比对 + * @see cn.bootx.platform.daxpay.code.PaySyncStatus + */ + public abstract PaySyncResult doSyncPayStatusHandler(); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/result/PaySyncResult.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/result/PaySyncResult.java new file mode 100644 index 00000000..09f7e9b4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/result/PaySyncResult.java @@ -0,0 +1,36 @@ +package cn.bootx.platform.daxpay.core.payment.sync.result; + +import cn.bootx.platform.daxpay.code.PaySyncStatus; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Map; + +import static cn.bootx.platform.daxpay.code.PaySyncStatus.NOT_SYNC; + +/** + * 支付网关通知状态对象 + * + * @author xxm + * @since 2021/4/21 + */ +@Data +@Accessors(chain = true) +public class PaySyncResult { + + /** + * 支付网关同步状态 + * @see PaySyncStatus#NOT_SYNC + */ + private String paySyncStatus = NOT_SYNC; + + /** 网关返回参数(会被用到的参数) */ + private Map map; + + /** 网关返回对象的json字符串 */ + private String json; + + /** 错误提示 */ + private String msg; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/PaySyncService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/PaySyncService.java new file mode 100644 index 00000000..3e463d5f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/PaySyncService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.payment.sync.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 支付同步服务 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PaySyncService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/RefundSyncService.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/RefundSyncService.java new file mode 100644 index 00000000..d0ec0f8d --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/service/RefundSyncService.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.core.payment.sync.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 退款状态同步服务 + * @author xxm + * @since 2023/12/18 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RefundSyncService { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/AliPaySyncStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/AliPaySyncStrategy.java new file mode 100644 index 00000000..0aa4100a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/AliPaySyncStrategy.java @@ -0,0 +1,43 @@ +package cn.bootx.platform.daxpay.core.payment.sync.strategy; + + +import cn.bootx.platform.daxpay.core.channel.alipay.dao.AlipayConfigManager; +import cn.bootx.platform.daxpay.core.channel.alipay.service.AlipaySyncService; +import cn.bootx.platform.daxpay.core.payment.sync.func.AbsPaySyncStrategy; +import cn.bootx.platform.daxpay.core.payment.sync.result.PaySyncResult; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 支付宝支付同步 + * @author xxm + * @since 2023/7/14 + */ +@Scope(SCOPE_PROTOTYPE) +@Component +@RequiredArgsConstructor +public class AliPaySyncStrategy extends AbsPaySyncStrategy { + + private final AlipayConfigManager alipayConfigManager; + + private final AlipaySyncService alipaySyncService; + + /** + * 异步支付单与支付网关进行状态比对 + */ + @Override + public PaySyncResult doSyncPayStatusHandler() { + this.initAlipayConfig(); + return alipaySyncService.syncPayStatus(this.getOrder().getId()); + } + + /** + * 初始化支付宝配置信息 + */ + private void initAlipayConfig() { + // 检查并获取支付宝支付配置 + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/WeChatPaySyncStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/WeChatPaySyncStrategy.java new file mode 100644 index 00000000..848f28fe --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/core/payment/sync/strategy/WeChatPaySyncStrategy.java @@ -0,0 +1,45 @@ +package cn.bootx.platform.daxpay.core.payment.sync.strategy; + +import cn.bootx.platform.daxpay.core.channel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.platform.daxpay.core.channel.wechat.entity.WeChatPayConfig; +import cn.bootx.platform.daxpay.core.channel.wechat.service.WeChatPaySyncService; +import cn.bootx.platform.daxpay.core.payment.sync.func.AbsPaySyncStrategy; +import cn.bootx.platform.daxpay.core.payment.sync.result.PaySyncResult; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 微信支付同步 + * @author xxm + * @since 2023/7/14 + */ +@Scope(SCOPE_PROTOTYPE) +@Component +@RequiredArgsConstructor +public class WeChatPaySyncStrategy extends AbsPaySyncStrategy { + + private final WeChatPayConfigManager weChatPayConfigManager; + + private final WeChatPaySyncService weChatPaySyncService; + + private WeChatPayConfig weChatPayConfig; + + /** + * 异步支付单与支付网关进行状态比对 + */ + @Override + public PaySyncResult doSyncPayStatusHandler() { + // 检查并获取微信支付配置 + this.initWeChatPayConfig(); + return weChatPaySyncService.syncPayStatus(this.getOrder().getId(), this.weChatPayConfig); + } + + /** + * 初始化微信支付 + */ + private void initWeChatPayConfig() { + } +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AliPaymentDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AliPaymentDto.java new file mode 100644 index 00000000..1ce82d89 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AliPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.dto.channel.alipay; + +import cn.bootx.platform.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @since 2021/2/27 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付宝支付记录") +public class AliPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = 6883103229754466130L; + + @Schema(description = "支付宝交易号") + private String tradeNo; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AlipayConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AlipayConfigDto.java new file mode 100644 index 00000000..5924206e --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/alipay/AlipayConfigDto.java @@ -0,0 +1,91 @@ +package cn.bootx.platform.daxpay.dto.channel.alipay; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.starter.data.perm.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @since 2021/2/26 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置") +public class AlipayConfigDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = 6641158663606363171L; + + @Schema(description = "名称") + private String name; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "支付宝商户appId") + @SensitiveInfo + private String appId; + + @Schema(description = "服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址") + private String returnUrl; + + @Schema(description = "请求网关地址") + private String serverUrl; + + @Schema(description = "认证类型 证书/公钥") + private String authType; + + @Schema(description = "签名类型") + private String signType; + + @Schema(description = "支付宝公钥") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayPublicKey; + + @Schema(description = "私钥") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String privateKey; + + @Schema(description = "应用公钥证书") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String appCert; + + @Schema(description = "支付宝公钥证书文件") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayCert; + + @Schema(description = "支付宝CA根证书文件") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayRootCert; + + @Schema(description = "超时配置") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/config/PayChannelConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/config/PayChannelConfigDto.java new file mode 100644 index 00000000..2f4e1fd7 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/config/PayChannelConfigDto.java @@ -0,0 +1,33 @@ +package cn.bootx.platform.daxpay.dto.channel.config; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付渠道配置 + * + * @author xxm + * @since 2023-05-24 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Schema(title = "支付渠道配置") +@Accessors(chain = true) +public class PayChannelConfigDto extends BaseDto { + + @Schema(description = "渠道编码") + private String code; + + @Schema(description = "渠道名称") + private String name; + + @Schema(description = "图片") + private Long image; + + @Schema(description = "排序") + private Double sortNo; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/union/UnionPayConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/union/UnionPayConfigDto.java new file mode 100644 index 00000000..cb01c084 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/union/UnionPayConfigDto.java @@ -0,0 +1,19 @@ +package cn.bootx.platform.daxpay.dto.channel.union; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "云闪付配置") +public class UnionPayConfigDto extends BaseDto { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherDto.java new file mode 100644 index 00000000..727e8f50 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherDto.java @@ -0,0 +1,56 @@ +package cn.bootx.platform.daxpay.dto.channel.voucher; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @since 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "储值卡") +public class VoucherDto extends BaseDto { + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "卡号") + private String cardNo; + + @Schema(description = "生成批次号") + private Long batchNo; + + @Schema(description = "面值") + private BigDecimal faceValue; + + @Schema(description = "余额") + private BigDecimal balance; + + @Schema(description = "是否长期有效") + private Boolean enduring; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + /** + * @see VoucherCode#STATUS_FORBIDDEN + */ + @Schema(description = "状态") + private String status; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherLogDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherLogDto.java new file mode 100644 index 00000000..51adc28c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherLogDto.java @@ -0,0 +1,61 @@ +package cn.bootx.platform.daxpay.dto.channel.voucher; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.daxpay.code.paymodel.VoucherCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 储值卡日志 + * + * @author xxm + * @since 2022/3/17 + */ +@Schema(title = "储值卡日志") +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class VoucherLogDto extends BaseDto { + + /** 储值卡id */ + @Schema(description = "储值卡id") + private Long voucherId; + + /** 储值卡号 */ + @Schema(description = "储值卡号") + private String voucherNo; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + /** 金额 */ + @Schema(description = "金额") + private BigDecimal amount; + + /** + * 类型 + * @see VoucherCode#LOG_PAY + */ + @Schema(description = "类型") + private String type; + + /** 交易记录ID */ + @Schema(description = "交易记录ID") + private Long paymentId; + + /** 业务ID */ + @Schema(description = "业务ID") + private String businessId; + + /** 备注 */ + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherPaymentDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherPaymentDto.java new file mode 100644 index 00000000..878fd1b4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/voucher/VoucherPaymentDto.java @@ -0,0 +1,19 @@ +package cn.bootx.platform.daxpay.dto.channel.voucher; + +import cn.bootx.platform.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "储值卡支付记录") +public class VoucherPaymentDto extends BasePaymentDto { + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletConfigDto.java new file mode 100644 index 00000000..639de5d4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletConfigDto.java @@ -0,0 +1,37 @@ +package cn.bootx.platform.daxpay.dto.channel.wallet; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * + * @author xxm + * @since 2023/7/20 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包配置") +public class WalletConfigDto extends BaseDto { + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "默认余额") + private BigDecimal defaultBalance; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletDto.java new file mode 100644 index 00000000..627ba931 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletDto.java @@ -0,0 +1,45 @@ +package cn.bootx.platform.daxpay.dto.channel.wallet; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包") +public class WalletDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = -1563719305334334625L; + + @Schema(description = "ID,钱包的唯一标识") + private Long id; + + @Schema(description = "钱包关联的账号ID") + private Long userId; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "钱包余额") + private BigDecimal balance; + + @Schema(description = "预冻结额度") + private BigDecimal freezeBalance; + + @Schema(description = "状态") + private String status; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletInfoDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletInfoDto.java new file mode 100644 index 00000000..804ad755 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletInfoDto.java @@ -0,0 +1,23 @@ +package cn.bootx.platform.daxpay.dto.channel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 钱包综合信息 + * + * @author xxm + * @since 2022/3/13 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包综合信息") +public class WalletInfoDto extends WalletDto { + + @Schema(description = "钱包关联的账号名称") + private String userName; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletLogDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletLogDto.java new file mode 100644 index 00000000..d2fa9d44 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletLogDto.java @@ -0,0 +1,59 @@ +package cn.bootx.platform.daxpay.dto.channel.wallet; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.daxpay.code.paymodel.WalletCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包日志") +public class WalletLogDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = -2553004953931903738L; + + @Schema(description = "钱包ID") + private Long walletId; + + @Schema(description = "用户ID") + private Long userId; + + /** + * @see WalletCode + */ + @Schema(description = "类型") + private String type; + + @Schema(description = "交易记录ID") + private Long paymentId; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "业务ID") + private String businessId; + + /** + * @see WalletCode#OPERATION_SOURCE_SYSTEM + */ + @Schema(description = " 1 系统操作 2管理员操作 3用户操作") + private String operationSource; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletPaymentDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletPaymentDto.java new file mode 100644 index 00000000..af23b29c --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wallet/WalletPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.dto.channel.wallet; + +import cn.bootx.platform.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @since 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包支付记录") +public class WalletPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = 8238920331255597223L; + + @Schema(description = "钱包ID") + private Long walletId; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPayConfigDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPayConfigDto.java new file mode 100644 index 00000000..a8060f19 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPayConfigDto.java @@ -0,0 +1,81 @@ +package cn.bootx.platform.daxpay.dto.channel.wechat; + +import cn.bootx.platform.common.core.rest.dto.BaseDto; +import cn.bootx.platform.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.platform.starter.data.perm.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @since 2021/3/19 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "微信支付配置") +public class WeChatPayConfigDto extends BaseDto implements Serializable { + + @Schema(description = "名称") + private String name; + + @Schema(description = "微信商户号") + @SensitiveInfo + private String wxMchId; + + @Schema(description = "微信应用appId") + @SensitiveInfo + private String wxAppId; + + /** + * @see WeChatPayCode#API_V2 + */ + @Schema(description = "api版本") + private String apiVersion; + + @Schema(description = "商户平台「API安全」中的 APIv2 密钥") + @SensitiveInfo + private String apiKeyV2; + + @Schema(description = "商户平台「API安全」中的 APIv3 密钥") + @SensitiveInfo + private String apiKeyV3; + + @Schema(description = "APPID对应的接口密码,用于获取接口调用凭证access_token时使用") + @SensitiveInfo + private String appSecret; + + @Schema(description = "API 证书中的 p12 文件id") + @SensitiveInfo + private String p12; + + @Schema(description = "服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https ") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径") + private String returnUrl; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "超时时间(分钟)") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPaymentDto.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPaymentDto.java new file mode 100644 index 00000000..706dc4e6 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/dto/channel/wechat/WeChatPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.platform.daxpay.dto.channel.wechat; + +import cn.bootx.platform.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @since 2021/6/21 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "微信支付记录") +public class WeChatPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = -2400358210732595795L; + + @Schema(description = "微信交易号") + private String tradeNo; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayCallbackStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayCallbackStrategy.java new file mode 100644 index 00000000..d2c21833 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayCallbackStrategy.java @@ -0,0 +1,112 @@ +package cn.bootx.platform.daxpay.func; + +import cn.bootx.platform.common.redis.RedisClient; +import cn.bootx.platform.daxpay.code.PayChannelEnum; +import cn.bootx.platform.daxpay.core.callback.dao.CallbackNotifyManager; +import cn.bootx.platform.daxpay.core.callback.entity.CallbackNotify; +import cn.bootx.platform.daxpay.core.payment.callback.result.PayCallbackResult; +import cn.bootx.platform.daxpay.core.payment.callback.service.PayCallbackService; +import cn.hutool.json.JSONUtil; +import com.alibaba.ttl.TransmittableThreadLocal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 支付回调处理抽象接口 + * + * @author xxm + * @since 2021/6/21 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbsPayCallbackStrategy { + + protected static final ThreadLocal> PARAMS = new TransmittableThreadLocal<>(); + + private final RedisClient redisClient; + + private final CallbackNotifyManager callbackNotifyManager; + + private final PayCallbackService payCallbackService; + + /** + * 支付回调 + */ + public String payCallback(Map params) { + PARAMS.set(params); + try { + log.info("支付回调处理: {}", params); + // 验证消息 + if (!this.verifyNotify()) { + return null; + } + // 去重处理 + if (!this.duplicateChecker()) { + return this.getReturnMsg(); + } + // 调用统一回调处理 + PayCallbackResult result = payCallbackService.callback(this.getPaymentId(), this.getTradeStatus(), + params); + // 记录回调记录 + this.saveNotifyRecord(result); + } + finally { + PARAMS.remove(); + } + return this.getReturnMsg(); + } + + /** + * 支付类型 + * @see PayChannelEnum + */ + public abstract PayChannelEnum getPayChannel(); + + /** + * 去重处理 + */ + public boolean duplicateChecker() { + // 判断10秒内是否已经回调处理 + String key = "payment:callback:duplicate:" + this.getPaymentId(); + return redisClient.setIfAbsent(key, "", 10 * 1000); + } + + /** + * 验证信息格式 + */ + public abstract boolean verifyNotify(); + + /** + * 获取paymentId + */ + public abstract Long getPaymentId(); + + /** + * 获取支付状态 + * @see cn.bootx.platform.daxpay.code.PayStatusEnum + */ + public abstract String getTradeStatus(); + + /** + * 返回响应结果 + */ + public abstract String getReturnMsg(); + + /** + * 保存回调记录 + */ + public void saveNotifyRecord(PayCallbackResult result) { + CallbackNotify payNotifyRecord = new CallbackNotify() + .setNotifyInfo(JSONUtil.toJsonStr(PARAMS.get())) + .setNotifyTime(LocalDateTime.now()) + .setPaymentId(this.getPaymentId()) + .setPayChannel(this.getPayChannel().getCode()) + .setStatus(result.getCode()) + .setMsg(result.getMsg()); + callbackNotifyManager.save(payNotifyRecord); + } + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayStrategy.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayStrategy.java new file mode 100644 index 00000000..c2180719 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/AbsPayStrategy.java @@ -0,0 +1,94 @@ +package cn.bootx.platform.daxpay.func; + +import cn.bootx.platform.daxpay.code.pay.PayChannelEnum; +import cn.bootx.platform.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.platform.daxpay.core.payment.entity.Payment; +import cn.bootx.platform.daxpay.param.pay.PayParam; +import cn.bootx.platform.daxpay.param.pay.PayWayParam; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * 抽象支付策略基类 同步支付 异步支付 错误处理 关闭支付 撤销支付 支付网关同步 退款 + * + * @author xxm + * @since 2020/12/11 + */ +@Getter +@Setter +public abstract class AbsPayStrategy { + + /** 支付对象 */ + private Payment payment = null; + + /** 支付参数 */ + private PayParam payParam = null; + + /** 支付方式参数 支付参数中的与这个不一致, 以这个为准 */ + private PayWayParam payWayParam = null; + + /** + * 策略标示 + * @see PayChannelEnum + */ + public abstract PayChannelEnum getType(); + + /** + * 初始化支付的参数 + */ + public void initPayParam(Payment payment, PayParam payParam) { + this.payment = payment; + this.payParam = payParam; + } + + /** + * 支付前对处理 包含必要的校验以及对Payment对象的创建和保存操作 + */ + public void doBeforePayHandler() { + } + + /** + * 支付操作 + */ + public abstract void doPayHandler(); + + /** + * 支付成功的处理方式 + */ + public void doSuccessHandler() { + } + + /** + * 支付失败的处理方式 + */ + public void doErrorHandler(ExceptionInfo exceptionInfo) { + } + + /** + * 异步支付成功的处理方式 + */ + public void doAsyncSuccessHandler(Map map) { + } + + /** + * 异步支付失败的处理方式, 默认使用支付失败的处理方式 同步支付方式调用时同 this#doErrorHandler + */ + public void doAsyncErrorHandler(ExceptionInfo exceptionInfo) { + this.doErrorHandler(exceptionInfo); + } + + /** + * 撤销支付操作,支付交易返回失败或支付系统超时,调用该接口撤销交易 默认为关闭本地支付记录 + */ + public void doCancelHandler() { + this.doCloseHandler(); + } + + /** + * 关闭本地支付记录 + */ + public abstract void doCloseHandler(); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/PayStrategyConsumer.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/PayStrategyConsumer.java new file mode 100644 index 00000000..33c2c30b --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/func/PayStrategyConsumer.java @@ -0,0 +1,18 @@ +package cn.bootx.platform.daxpay.func; + +import cn.bootx.platform.daxpay.core.payment.entity.Payment; + +import java.util.List; + +/** + * 支付策略接口 + * + * @author xxm + * @since 2020/12/9 + */ +@FunctionalInterface +public interface PayStrategyConsumer, S extends Payment> { + + void accept(T t, S s); + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigParam.java new file mode 100644 index 00000000..5867b142 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigParam.java @@ -0,0 +1,79 @@ +package cn.bootx.platform.daxpay.param.channel.alipay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @since 2021/2/26 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置参数") +public class AlipayConfigParam implements Serializable { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "支付宝商户appId") + private String appId; + + @Schema(description = "服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址") + private String returnUrl; + + @Schema(description = "请求网关地址") + private String serverUrl; + + @Schema(description = "认证类型 证书/公钥") + private String authType; + + @Schema(description = "签名类型") + public String signType; + + @Schema(description = "支付宝公钥") + public String alipayPublicKey; + + @Schema(description = "私钥") + private String privateKey; + + @Schema(description = "应用公钥证书") + private String appCert; + + @Schema(description = "支付宝公钥证书文件") + private String alipayCert; + + @Schema(description = "支付宝CA根证书文件") + private String alipayRootCert; + + @Schema(description = "超时配置") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigQuery.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigQuery.java new file mode 100644 index 00000000..94cbf0e9 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/alipay/AlipayConfigQuery.java @@ -0,0 +1,29 @@ +package cn.bootx.platform.daxpay.param.channel.alipay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @since 2021/7/22 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置搜索参数") +public class AlipayConfigQuery implements Serializable { + + private static final long serialVersionUID = -173325268481050362L; + + /** 名称 */ + private String name; + + /** 状态 */ + private Integer state; + + /** 支付宝商户appId */ + private String appId; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherImportParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherImportParam.java new file mode 100644 index 00000000..87e3e3dc --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherImportParam.java @@ -0,0 +1,54 @@ +package cn.bootx.platform.daxpay.param.channel.voucher; + +import cn.bootx.platform.daxpay.code.VoucherCode; +import cn.hutool.core.date.DatePattern; +import com.alibaba.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Data +@Schema(title = "储值卡导入参数") +public class VoucherImportParam { + + @ExcelProperty("卡号") + @Schema(description = "卡号") + private String cardNo; + + @ExcelProperty("面值") + @Schema(description = "面值") + private BigDecimal faceValue; + + @ExcelProperty("余额") + @Schema(description = "余额") + private BigDecimal balance; + + @ExcelProperty("是否长期有效") + @Schema(description = "是否长期有效") + private Boolean enduring; + + @ExcelProperty("开始时间") + @Schema(description = "开始时间") + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime startTime; + + @ExcelProperty("结束时间") + @Schema(description = "结束时间") + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime endTime; + + /** + * @see VoucherCode#STATUS_NORMAL + */ + @ExcelProperty("默认状态") + @Schema(description = "默认状态") + private String status; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherParam.java new file mode 100644 index 00000000..d7767108 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/voucher/VoucherParam.java @@ -0,0 +1,54 @@ +package cn.bootx.platform.daxpay.param.channel.voucher; + +import cn.bootx.platform.daxpay.code.VoucherCode; +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @since 2022/3/14 + */ +@Data +@Accessors(chain = true) +@Schema(title = "储值卡查询参数") +public class VoucherParam { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "卡号") + private String cardNo; + + @Schema(description = "生成批次号") + private Long batchNo; + + @Schema(description = "面值") + private BigDecimal faceValue; + + @Schema(description = "余额") + private BigDecimal balance; + + @Schema(description = "是否长期有效") + private Boolean enduring; + + @Schema(description = "开始时间") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime startTime; + + @Schema(description = "结束时间") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime endTime; + + /** + * @see VoucherCode + */ + @Schema(description = "状态") + private Integer status; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletLogQueryParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletLogQueryParam.java new file mode 100644 index 00000000..43867ec4 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletLogQueryParam.java @@ -0,0 +1,43 @@ +package cn.bootx.platform.daxpay.param.channel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author xxm + * @since 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包日志查询参数") +public class WalletLogQueryParam implements Serializable { + + private static final long serialVersionUID = -4046664021959786637L; + + @Schema(description = "钱包ID (与userId至少存在一个)") + private Long walletId; + + @Schema(description = "用户ID (钱包至少存在一个)") + private Long userId; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "开始日期") + private LocalDateTime startDate; + + @Schema(description = "结束日期") + private LocalDateTime endDate; + + @Schema(description = "日志类型,不传则查询全部") + private List type; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletPayParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletPayParam.java new file mode 100644 index 00000000..8b86502a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletPayParam.java @@ -0,0 +1,28 @@ +package cn.bootx.platform.daxpay.param.channel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 钱包支付参数 + * + * @author xxm + * @since 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包支付参数") +public class WalletPayParam implements Serializable { + + private static final long serialVersionUID = 3255160458016870367L; + + @Schema(description = "钱包ID") + private Long walletId; + + @Schema(description = "用户ID") + private Long userId; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletQueryParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletQueryParam.java new file mode 100644 index 00000000..b95d0892 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletQueryParam.java @@ -0,0 +1,29 @@ +package cn.bootx.platform.daxpay.param.channel.wallet; + +import cn.bootx.platform.common.core.annotation.QueryParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2020/12/8 + */ +@Data +@QueryParam(type = QueryParam.CompareTypeEnum.LIKE) +@Accessors(chain = true) +@Schema(title = "钱包查询参数") +public class WalletQueryParam { + + @Schema(description = "钱包ID") + private Long walletId; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRechargeParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRechargeParam.java new file mode 100644 index 00000000..b4ae2cd9 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRechargeParam.java @@ -0,0 +1,29 @@ +package cn.bootx.platform.daxpay.param.channel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @author xxm + * @since 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包充值参数") +public class WalletRechargeParam implements Serializable { + + private static final long serialVersionUID = 73058709379178254L; + + @NotNull(message = "钱包ID不可为空") + @Schema(description = "钱包ID") + private Long walletId; + + @NotNull(message = "充值金额不可为空") + @Schema(description = "充值金额") + private BigDecimal amount; +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRefundParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRefundParam.java new file mode 100644 index 00000000..02ab736a --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wallet/WalletRefundParam.java @@ -0,0 +1,16 @@ +package cn.bootx.platform.daxpay.param.channel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 钱包退款参数 + * @author xxm + * @since 2023/6/29 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包退款参数") +public class WalletRefundParam { +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WalletConfigParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WalletConfigParam.java new file mode 100644 index 00000000..2bccd293 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WalletConfigParam.java @@ -0,0 +1,37 @@ +package cn.bootx.platform.daxpay.param.channel.wechat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 钱包配置 + * @author xxm + * @since 2023/7/20 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包配置") +public class WalletConfigParam { + @Schema(description = "主键") + private Long id; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "默认余额") + private BigDecimal defaultBalance; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayConfigParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayConfigParam.java new file mode 100644 index 00000000..0e8a5525 --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayConfigParam.java @@ -0,0 +1,83 @@ +package cn.bootx.platform.daxpay.param.channel.wechat; + +import cn.bootx.platform.common.core.annotation.QueryParam; +import cn.bootx.platform.daxpay.code.paymodel.WeChatPayCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 微信支付配置参数 + * + * @author xxm + * @since 2022/7/7 + */ +@Data +@QueryParam(type = QueryParam.CompareTypeEnum.LIKE) +@Accessors(chain = true) +@Schema(title = "微信支付配置参数") +public class WeChatPayConfigParam { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "商户编码") + private String mchCode; + + @Schema(description = "商户应用编码") + private String mchAppCode; + + @Schema(description = "微信商户号") + private String wxMchId; + + @Schema(description = "微信应用appId") + private String wxAppId; + + /** + * @see WeChatPayCode#API_V2 + */ + @Schema(description = "api版本") + private String apiVersion; + + @Schema(description = "商户平台「API安全」中的 APIv2 密钥") + private String apiKeyV2; + + @Schema(description = "商户平台「API安全」中的 APIv3 密钥") + private String apiKeyV3; + + @Schema(description = "APPID对应的接口密码,用于获取接口调用凭证access_token时使用") + private String appSecret; + + @Schema(description = "API 证书中的 p12 文件") + private String p12; + + @Schema(description = "服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https ") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径") + private String returnUrl; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "超时时间(分钟)") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private String state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayParam.java b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayParam.java new file mode 100644 index 00000000..4040e34f --- /dev/null +++ b/daxpay-single/daxpay-single-service/src/main/java/cn/bootx/platform/daxpay/param/channel/wechat/WeChatPayParam.java @@ -0,0 +1,22 @@ +package cn.bootx.platform.daxpay.param.channel.wechat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @since 2021/6/21 + */ +@Data +@Accessors(chain = true) +@Schema(title = "微信支付参数") +public class WeChatPayParam { + + @Schema(description = "微信openId") + private String openId; + + @Schema(description = "授权码(主动扫描用户的付款码)") + private String authCode; + +} diff --git a/pom.xml b/pom.xml index e57e8130..64c6d288 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 4.0.0 1.5.3.Final 0.2.0 - 1.5.3 + 1.5.4 4.5.2.B 2.2.3 2.2.4