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